Go app framework
This commit is contained in:
parent
41036b3c3a
commit
d0a430505c
58
README.md
58
README.md
@ -1,3 +1,61 @@
|
||||
# go-app
|
||||
|
||||
A simple app framework for bootstrapping logging, otel, and servers from config and environment
|
||||
|
||||
## Usage
|
||||
|
||||
- Use the helper function to load config file (if given with -config) and apply
|
||||
configuration through environment. This will prepare your app config and logging.
|
||||
|
||||
- Load up HTTP routes and health checks.
|
||||
|
||||
- Start the app.
|
||||
|
||||
- Wait for it to be done and do any extra cleanup.
|
||||
|
||||
Here is a reference main.go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/app"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cncl := signal.NotifyContext(context.Background(), os.Kill, unix.SIGTERM)
|
||||
defer cncl()
|
||||
|
||||
ctx = app.MustSetupConfigAndLogging(ctx)
|
||||
|
||||
awApp := app.App{
|
||||
AppContext: ctx,
|
||||
HTTP: &app.AppHTTP{
|
||||
// Load up your HTTP methods here
|
||||
Funcs: []srv.HTTPFunc{
|
||||
{
|
||||
Path: "/test",
|
||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("unimplemented test method"))
|
||||
},
|
||||
},
|
||||
},
|
||||
// Load up health checks here
|
||||
HealthChecks: []srv.HealthCheckFunc{
|
||||
func(ctx context.Context) error {
|
||||
return errors.New("health check unimplemented")
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
awApp.MustRun()
|
||||
<-awApp.Done()
|
||||
}
|
||||
```
|
||||
|
49
go.mod
Normal file
49
go.mod
Normal file
@ -0,0 +1,49 @@
|
||||
module gitea.libretechconsulting.com/rmcguire/go-app
|
||||
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v9 v9.0.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/rs/zerolog v1.33.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
|
||||
go.opentelemetry.io/otel v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0
|
||||
go.opentelemetry.io/otel/metric v1.33.0
|
||||
go.opentelemetry.io/otel/sdk v1.33.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0
|
||||
go.opentelemetry.io/otel/trace v1.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.61.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/grpc v1.68.1 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
)
|
112
go.sum
Normal file
112
go.sum
Normal file
@ -0,0 +1,112 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
|
||||
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0 h1:sSPw658Lk2NWAv74lkD3B/RSDb+xRFx46GjkrL3VUZo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.55.0/go.mod h1:nC00vyCmQixoeaxF6KNyP42II/RHa9UdruK02qBmHvI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
93
pkg/app/app.go
Normal file
93
pkg/app/app.go
Normal file
@ -0,0 +1,93 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
AppContext context.Context
|
||||
HTTP *AppHTTP
|
||||
cfg *config.AppConfig
|
||||
l *zerolog.Logger
|
||||
tracer trace.Tracer
|
||||
shutdownFuncs []shutdownFunc
|
||||
appDone chan interface{}
|
||||
}
|
||||
|
||||
type AppHTTP struct {
|
||||
Funcs []srv.HTTPFunc
|
||||
HealthChecks []srv.HealthCheckFunc
|
||||
httpDone <-chan interface{}
|
||||
}
|
||||
|
||||
type (
|
||||
healthCheckFunc func(context.Context) error
|
||||
shutdownFunc func(context.Context) error
|
||||
)
|
||||
|
||||
func (a *App) Done() <-chan interface{} {
|
||||
return a.appDone
|
||||
}
|
||||
|
||||
func (a *App) MustRun() {
|
||||
if a.cfg != nil {
|
||||
panic(errors.New("already ran app trying to run"))
|
||||
}
|
||||
|
||||
// Set up app
|
||||
a.cfg = config.MustFromCtx(a.AppContext)
|
||||
a.l = zerolog.Ctx(a.AppContext)
|
||||
a.shutdownFuncs = make([]shutdownFunc, 0)
|
||||
a.appDone = make(chan interface{})
|
||||
a.HTTP.httpDone = make(chan interface{})
|
||||
|
||||
if len(a.HTTP.Funcs) < 1 {
|
||||
a.l.Warn().Msg("no http funcs provided, only serving health and metrics")
|
||||
}
|
||||
|
||||
// Start OTEL
|
||||
a.initOTEL()
|
||||
var initSpan trace.Span
|
||||
_, initSpan = a.tracer.Start(a.AppContext, "init")
|
||||
|
||||
// Start HTTP
|
||||
a.initHTTP()
|
||||
|
||||
// Monitor app lifecycle
|
||||
go a.run()
|
||||
|
||||
// Startup Complete
|
||||
a.l.Info().
|
||||
Str("name", a.cfg.Name).
|
||||
Str("version", a.cfg.Version).
|
||||
Str("logLevel", a.cfg.Logging.Level).
|
||||
Msg("app initialized")
|
||||
initSpan.SetStatus(codes.Ok, "")
|
||||
initSpan.End()
|
||||
}
|
||||
|
||||
func (a *App) initHTTP() {
|
||||
var httpShutdown shutdownFunc
|
||||
httpShutdown, a.HTTP.httpDone = srv.MustInitHTTPServer(
|
||||
a.AppContext,
|
||||
a.HTTP.Funcs,
|
||||
a.HTTP.HealthChecks...,
|
||||
)
|
||||
a.shutdownFuncs = append(a.shutdownFuncs, httpShutdown)
|
||||
}
|
||||
|
||||
func (a *App) initOTEL() {
|
||||
var otelShutdown shutdownFunc
|
||||
a.AppContext, otelShutdown = otel.Init(a.AppContext)
|
||||
a.shutdownFuncs = append(a.shutdownFuncs, otelShutdown)
|
||||
a.tracer = otel.MustTracerFromCtx(a.AppContext)
|
||||
}
|
67
pkg/app/run.go
Normal file
67
pkg/app/run.go
Normal file
@ -0,0 +1,67 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
// Watches contexts and channels for the
|
||||
// app to be finished and calls shutdown once
|
||||
// the app is done
|
||||
func (a *App) run() {
|
||||
select {
|
||||
case <-a.AppContext.Done():
|
||||
a.l.Warn().Str("reason", a.AppContext.Err().Error()).
|
||||
Msg("shutting down on context done")
|
||||
case <-a.HTTP.httpDone:
|
||||
a.l.Warn().Msg("shutting down early on http server done")
|
||||
}
|
||||
a.Shutdown()
|
||||
|
||||
a.appDone <- nil
|
||||
}
|
||||
|
||||
// Typically invoked when AppContext is done
|
||||
// or Server has exited. Not intended to be called
|
||||
// manually
|
||||
func (a *App) Shutdown() {
|
||||
now := time.Now()
|
||||
|
||||
doneCtx, cncl := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer func() {
|
||||
if doneCtx.Err() == context.DeadlineExceeded {
|
||||
a.l.Err(doneCtx.Err()).
|
||||
Dur("shutdownTime", time.Since(now)).
|
||||
Msg("app shutdown aborted")
|
||||
} else {
|
||||
a.l.Info().
|
||||
Int("shutdownFuncsCalled", len(a.shutdownFuncs)).
|
||||
Dur("shutdownTime", time.Since(now)).
|
||||
Msg("app shutdown normally")
|
||||
}
|
||||
cncl()
|
||||
}()
|
||||
|
||||
doneCtx, span := a.tracer.Start(doneCtx, "shutdown")
|
||||
defer span.End()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(a.shutdownFuncs))
|
||||
|
||||
for _, f := range a.shutdownFuncs {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := f(doneCtx)
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, "shutdown failed")
|
||||
span.RecordError(err)
|
||||
a.l.Err(err).Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
25
pkg/app/setup.go
Normal file
25
pkg/app/setup.go
Normal file
@ -0,0 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/logging"
|
||||
)
|
||||
|
||||
// Helper function to return a context loaded up with
|
||||
// config.AppConfig and a logger
|
||||
func MustSetupConfigAndLogging(ctx context.Context) context.Context {
|
||||
ctx, err := config.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
ctx = logging.MustInitLogging(ctx)
|
||||
|
||||
zerolog.Ctx(ctx).Trace().Any("config", *cfg).Send()
|
||||
return ctx
|
||||
}
|
69
pkg/config/config.go
Normal file
69
pkg/config/config.go
Normal file
@ -0,0 +1,69 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/caarlos0/env/v9"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// To be set by ldflags in go build command or
|
||||
// retrieved from build meta below
|
||||
var Version = "(devel)"
|
||||
|
||||
// Calling this will try to load from config if -config is
|
||||
// provided as a file, and will apply any environment overrides
|
||||
// on-top of configuration defaults.
|
||||
// Config is stored in returned context, and can be retrieved
|
||||
// using config.FromCtx(ctx)
|
||||
func LoadConfig(ctx context.Context) (context.Context, error) {
|
||||
configPath := flag.String("config", "", "Path to the configuration file")
|
||||
flag.Parse()
|
||||
|
||||
// Start with defaults
|
||||
// Load from config if provided
|
||||
// Layer on environment
|
||||
cfg, err := loadConfig(*configPath)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Add config to context, and return
|
||||
// an updated context
|
||||
ctx = cfg.AddToCtx(ctx)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func loadConfig(configPath string) (*AppConfig, error) {
|
||||
cfg := newAppConfig()
|
||||
|
||||
if configPath != "" {
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
decoder := yaml.NewDecoder(file)
|
||||
if err := decoder.Decode(cfg); err != nil {
|
||||
return nil, fmt.Errorf("could not decode config file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := env.Parse(cfg); err != nil {
|
||||
return nil, fmt.Errorf("could not parse environment variables: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getVersion() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
|
||||
return info.Main.Version
|
||||
}
|
||||
return Version
|
||||
}
|
36
pkg/config/ctx.go
Normal file
36
pkg/config/ctx.go
Normal file
@ -0,0 +1,36 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type appConfigKey uint8
|
||||
|
||||
const appConfigCtxKey appConfigKey = iota
|
||||
|
||||
func (a *AppConfig) AddToCtx(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, appConfigCtxKey, a)
|
||||
}
|
||||
|
||||
func MustFromCtx(ctx context.Context) *AppConfig {
|
||||
cfg, err := FromCtx(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func FromCtx(ctx context.Context) (*AppConfig, error) {
|
||||
ctxData := ctx.Value(appConfigCtxKey)
|
||||
if ctxData == nil {
|
||||
return nil, errors.New("no config found in context")
|
||||
}
|
||||
|
||||
cfg, ok := ctxData.(*AppConfig)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid config stored in context")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
70
pkg/config/types.go
Normal file
70
pkg/config/types.go
Normal file
@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
func newAppConfig() *AppConfig {
|
||||
return &AppConfig{
|
||||
Version: getVersion(),
|
||||
Logging: &LogConfig{},
|
||||
HTTP: &HTTPConfig{},
|
||||
OTEL: &OTELConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `yaml:"name" env:"APP_NAME" envDefault:"go-http-server-with-otel"`
|
||||
Environment string `yaml:"environment" env:"APP_ENVIRONMENT" envDefault:"development"`
|
||||
// This should either be set by ldflags, such as with
|
||||
// go build -ldflags "-X gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version=$(VERSION)"
|
||||
// or allow this to use build meta. Will default to (devel)
|
||||
Version string `yaml:"version" env:"APP_VERSION"`
|
||||
Logging *LogConfig `yaml:"logging"`
|
||||
HTTP *HTTPConfig `yaml:"http"`
|
||||
OTEL *OTELConfig `yaml:"otel"`
|
||||
}
|
||||
|
||||
// Logging Configuration
|
||||
type LogConfig struct {
|
||||
Enabled bool `yaml:"enabled" env:"APP_LOG_ENABLED" envDefault:"true"`
|
||||
Level string `yaml:"level" env:"APP_LOG_LEVEL" envDefault:"info"`
|
||||
Format LogFormat `yaml:"format" env:"APP_LOG_FORMAT" envDefault:"json"`
|
||||
Output LogOutput `yaml:"output" env:"APP_LOG_OUTPUT" envDefault:"stderr"`
|
||||
TimeFormat TimeFormat `yaml:"timeFormat" env:"APP_LOG_TIME_FORMAT" envDefault:"short"`
|
||||
}
|
||||
|
||||
type LogFormat string
|
||||
|
||||
const (
|
||||
LogFormatConsole LogFormat = "console"
|
||||
LogFormatJSON LogFormat = "json"
|
||||
)
|
||||
|
||||
type TimeFormat string
|
||||
|
||||
const (
|
||||
TimeFormatShort TimeFormat = "short"
|
||||
TimeFormatLong TimeFormat = "long"
|
||||
TimeFormatUnix TimeFormat = "unix"
|
||||
TimeFormatRFC3339 TimeFormat = "rfc3339"
|
||||
TimeFormatOff TimeFormat = "off"
|
||||
)
|
||||
|
||||
type LogOutput string
|
||||
|
||||
const (
|
||||
LogOutputStdout LogOutput = "stdout"
|
||||
LogOutputStderr LogOutput = "stderr"
|
||||
)
|
||||
|
||||
// HTTP Configuration
|
||||
type HTTPConfig struct {
|
||||
Listen string `yaml:"listen" env:"APP_HTTP_LISTEN" envDefault:"127.0.0.1:8080"`
|
||||
RequestTimeout int `yaml:"requestTimeout" env:"APP_HTTP_REQUEST_TIMEOUT" envDefault:"30"`
|
||||
}
|
||||
|
||||
// OTEL Configuration
|
||||
type OTELConfig struct {
|
||||
Enabled bool `yaml:"enabled" env:"APP_OTEL_ENABLED" envDefault:"true"`
|
||||
PrometheusEnabled bool `yaml:"prometheusEnabled" env:"APP_OTEL_PROMETHEUS_ENABLED" envDefault:"true"`
|
||||
PrometheusPath string `yaml:"prometheusPath" env:"APP_OTEL_PROMETHEUS_PATH" envDefault:"/metrics"`
|
||||
StdoutEnabled bool `yaml:"stdoutEnabled" env:"APP_OTEL_STDOUT_ENABLED" envDefault:"false"`
|
||||
MetricIntervalSecs int `yaml:"metricIntervalSecs" env:"APP_OTEL_METRIC_INTERVAL_SECS" envDefault:"15"`
|
||||
}
|
72
pkg/logging/logging.go
Normal file
72
pkg/logging/logging.go
Normal file
@ -0,0 +1,72 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
)
|
||||
|
||||
func MustInitLogging(ctx context.Context) context.Context {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
logger, err := configureLogger(cfg.Logging)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return logger.WithContext(ctx)
|
||||
}
|
||||
|
||||
func configureLogger(cfg *config.LogConfig) (*zerolog.Logger, error) {
|
||||
setTimeFormat(cfg.TimeFormat)
|
||||
|
||||
// Default JSON logger
|
||||
logger := zerolog.New(os.Stderr)
|
||||
if cfg.TimeFormat != config.TimeFormatOff {
|
||||
logger = logger.With().Timestamp().Logger()
|
||||
}
|
||||
|
||||
// Pretty console logger
|
||||
if cfg.Format == config.LogFormatConsole {
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: zerolog.TimeFieldFormat,
|
||||
}
|
||||
if cfg.TimeFormat == config.TimeFormatOff {
|
||||
consoleWriter.FormatTimestamp = func(_ interface{}) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
logger = log.Output(consoleWriter)
|
||||
}
|
||||
|
||||
level, err := zerolog.ParseLevel(cfg.Level)
|
||||
if err != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
logger = logger.Level(level)
|
||||
zerolog.SetGlobalLevel(level)
|
||||
|
||||
return &logger, err
|
||||
}
|
||||
|
||||
func setTimeFormat(format config.TimeFormat) {
|
||||
switch format {
|
||||
case config.TimeFormatShort:
|
||||
zerolog.TimeFieldFormat = time.Kitchen
|
||||
case config.TimeFormatUnix:
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
case config.TimeFormatLong:
|
||||
zerolog.TimeFieldFormat = time.DateTime
|
||||
case config.TimeFormatRFC3339:
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
case config.TimeFormatOff:
|
||||
zerolog.TimeFieldFormat = ""
|
||||
}
|
||||
}
|
54
pkg/otel/ctx.go
Normal file
54
pkg/otel/ctx.go
Normal file
@ -0,0 +1,54 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type otelCtxKey uint8
|
||||
|
||||
const (
|
||||
ctxKeyTracer otelCtxKey = iota
|
||||
ctxKeyMeter
|
||||
)
|
||||
|
||||
func MustTracerFromCtx(ctx context.Context) trace.Tracer {
|
||||
ctxData := ctx.Value(ctxKeyTracer)
|
||||
if ctxData == nil {
|
||||
panic(errors.New("no tracer found in context"))
|
||||
}
|
||||
|
||||
tracer, ok := ctxData.(trace.Tracer)
|
||||
if !ok {
|
||||
panic(errors.New("invalid tracer found in context"))
|
||||
}
|
||||
|
||||
return tracer
|
||||
}
|
||||
|
||||
func AddTracerToCtx(ctx context.Context, tracer trace.Tracer) context.Context {
|
||||
ctx = context.WithValue(ctx, ctxKeyTracer, tracer)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func MustMeterFromCtx(ctx context.Context) metric.Meter {
|
||||
ctxData := ctx.Value(ctxKeyMeter)
|
||||
if ctxData == nil {
|
||||
panic(errors.New("no meter found in context"))
|
||||
}
|
||||
|
||||
meter, ok := ctxData.(metric.Meter)
|
||||
if !ok {
|
||||
panic(errors.New("invalid meter found in context"))
|
||||
}
|
||||
|
||||
return meter
|
||||
}
|
||||
|
||||
func AddMeterToCtx(ctx context.Context, meter metric.Meter) context.Context {
|
||||
ctx = context.WithValue(ctx, ctxKeyMeter, meter)
|
||||
return ctx
|
||||
}
|
227
pkg/otel/otel.go
Normal file
227
pkg/otel/otel.go
Normal file
@ -0,0 +1,227 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
opentelemetry "go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
|
||||
noopMetric "go.opentelemetry.io/otel/metric/noop"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
traceSDK "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
||||
trace "go.opentelemetry.io/otel/trace"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
// OTEL Options
|
||||
var (
|
||||
EnableStdoutExporter Option = enableStdoutExporter{}
|
||||
EnablePrometheusExporter Option = enablePrometheusExporter{}
|
||||
// Overide the default metric export interval
|
||||
WithMetricExportInterval = func(interval time.Duration) Option {
|
||||
return exportInterval{interval: interval}
|
||||
}
|
||||
)
|
||||
|
||||
const defMetricInterval = 15 * time.Second
|
||||
|
||||
// Context must carry config.AppConfig
|
||||
func Init(ctx context.Context) (context.Context, func(context.Context) error) {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
// Nothing to do here if not enabled
|
||||
if !cfg.OTEL.Enabled {
|
||||
opentelemetry.SetMeterProvider(noopMetric.NewMeterProvider())
|
||||
opentelemetry.SetTracerProvider(noop.NewTracerProvider())
|
||||
return ctx, func(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
metricInterval = defMetricInterval
|
||||
options = make([]Option, 0)
|
||||
s = &settings{}
|
||||
shutdownFuncs []func(context.Context) error
|
||||
)
|
||||
|
||||
// Prepare settings for OTEL from configuration
|
||||
if cfg.OTEL.StdoutEnabled {
|
||||
options = append(options, EnableStdoutExporter)
|
||||
}
|
||||
if cfg.OTEL.MetricIntervalSecs > 0 {
|
||||
metricInterval = time.Duration(cfg.OTEL.MetricIntervalSecs) * time.Second
|
||||
}
|
||||
if cfg.OTEL.PrometheusEnabled {
|
||||
options = append(options, EnablePrometheusExporter)
|
||||
}
|
||||
options = append(options,
|
||||
WithMetricExportInterval(metricInterval))
|
||||
|
||||
// Apply settings
|
||||
for _, opt := range options {
|
||||
opt.apply(s)
|
||||
}
|
||||
|
||||
// shutdown calls cleanup functions registered via shutdownFuncs.
|
||||
// The errors from the calls are joined.
|
||||
// Each registered cleanup will be invoked once.
|
||||
shutdown := func(ctx context.Context) error {
|
||||
var err error
|
||||
for _, fn := range shutdownFuncs {
|
||||
err = errors.Join(err, fn(ctx))
|
||||
}
|
||||
shutdownFuncs = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
|
||||
handleErr := func(inErr error) {
|
||||
if err := errors.Join(inErr, shutdown(ctx)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "OTEL Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up meter provider.
|
||||
meterProvider, err := s.newMeterProvider(ctx)
|
||||
if err != nil {
|
||||
handleErr(err)
|
||||
return ctx, shutdown
|
||||
}
|
||||
|
||||
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
|
||||
opentelemetry.SetMeterProvider(meterProvider)
|
||||
|
||||
meter := opentelemetry.Meter(cfg.Name)
|
||||
ctx = AddMeterToCtx(ctx, meter)
|
||||
|
||||
// Set up tracing
|
||||
opentelemetry.SetTextMapPropagator(newPropagator())
|
||||
var tracerProvider *traceSDK.TracerProvider
|
||||
tracerProvider, err = s.newTracerProvider(ctx)
|
||||
if err != nil {
|
||||
handleErr(err)
|
||||
return ctx, shutdown
|
||||
}
|
||||
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
||||
opentelemetry.SetTracerProvider(tracerProvider)
|
||||
|
||||
tracer := opentelemetry.Tracer(cfg.Name)
|
||||
ctx = AddTracerToCtx(ctx, tracer)
|
||||
|
||||
return ctx, shutdown
|
||||
}
|
||||
|
||||
func newPropagator() propagation.TextMapPropagator {
|
||||
return propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *settings) newTracerProvider(ctx context.Context) (traceProvider *traceSDK.TracerProvider, err error) {
|
||||
traceOpts := []traceSDK.TracerProviderOption{
|
||||
traceSDK.WithResource(newResource()),
|
||||
}
|
||||
|
||||
host, set := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
if set && host != "" {
|
||||
exporter, err := otlptracegrpc.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceOpts = append(traceOpts, traceSDK.WithBatcher(exporter))
|
||||
}
|
||||
|
||||
if s.EnableStdoutExporter {
|
||||
stdoutExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceOpts = append(traceOpts, traceSDK.WithBatcher(stdoutExporter))
|
||||
}
|
||||
|
||||
traceProvider = traceSDK.NewTracerProvider(traceOpts...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newResource() *resource.Resource {
|
||||
return resource.NewWithAttributes(semconv.SchemaURL)
|
||||
}
|
||||
|
||||
// Configures meter provider
|
||||
// Always provides a prometheus metrics exporter
|
||||
// Conditionally provides an OTLP metrics exporter
|
||||
func (s *settings) newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {
|
||||
// OTEL Prometheus Exporter
|
||||
exporter, err := prometheus.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricOptions := make([]metric.Option, 0, 5)
|
||||
if s.EnableStdoutExporter {
|
||||
stdoutMetricExporter, err := stdoutmetric.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(metric.NewPeriodicReader(stdoutMetricExporter)),
|
||||
)
|
||||
}
|
||||
|
||||
host, set := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
var otlpExporter *otlpmetricgrpc.Exporter
|
||||
if set && host != "" {
|
||||
if exp, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithInsecure()); err != nil {
|
||||
return nil, fmt.Errorf("otlpmetricgrpc.New: %w", err)
|
||||
} else {
|
||||
otlpExporter = exp
|
||||
}
|
||||
}
|
||||
|
||||
var meterProvider *metric.MeterProvider
|
||||
if otlpExporter != nil {
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(exporter),
|
||||
metric.WithReader(
|
||||
metric.NewPeriodicReader(
|
||||
otlpExporter,
|
||||
metric.WithInterval(s.MetricExportInterval),
|
||||
),
|
||||
),
|
||||
metric.WithResource(newResource()),
|
||||
)
|
||||
} else {
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(exporter),
|
||||
metric.WithResource(newResource()),
|
||||
)
|
||||
}
|
||||
|
||||
meterProvider = metric.NewMeterProvider(metricOptions...)
|
||||
|
||||
return meterProvider, nil
|
||||
}
|
||||
|
||||
// Creates a new tracer from the global opentelemetry provider
|
||||
func NewTracer(options ...trace.TracerOption) trace.Tracer {
|
||||
return opentelemetry.GetTracerProvider().Tracer(
|
||||
os.Getenv("OTEL_SERVICE_NAME"),
|
||||
options...,
|
||||
)
|
||||
}
|
38
pkg/otel/settings.go
Normal file
38
pkg/otel/settings.go
Normal file
@ -0,0 +1,38 @@
|
||||
package otel
|
||||
|
||||
import "time"
|
||||
|
||||
type settings struct {
|
||||
EnableStdoutExporter bool
|
||||
EnablePrometheusExporter bool
|
||||
MetricExportInterval time.Duration
|
||||
}
|
||||
|
||||
type Option interface {
|
||||
apply(*settings)
|
||||
}
|
||||
|
||||
type enableStdoutExporter struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (setting enableStdoutExporter) apply(o *settings) {
|
||||
o.EnableStdoutExporter = true
|
||||
}
|
||||
|
||||
type enablePrometheusExporter struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (setting enablePrometheusExporter) apply(o *settings) {
|
||||
o.EnablePrometheusExporter = true
|
||||
}
|
||||
|
||||
type exportInterval struct {
|
||||
Option
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func (setting exportInterval) apply(o *settings) {
|
||||
o.MetricExportInterval = setting.interval
|
||||
}
|
30
pkg/otel/util.go
Normal file
30
pkg/otel/util.go
Normal file
@ -0,0 +1,30 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
)
|
||||
|
||||
func GetTracer(ctx context.Context, components ...string) trace.Tracer {
|
||||
return otel.Tracer(getName(ctx, components...))
|
||||
}
|
||||
|
||||
func GetMeter(ctx context.Context, components ...string) metric.Meter {
|
||||
return otel.Meter(getName(ctx, components...))
|
||||
}
|
||||
|
||||
func getName(ctx context.Context, components ...string) string {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
path := make([]string, 0, len(components)+1)
|
||||
path = append(path, cfg.Name)
|
||||
path = append(path, components...)
|
||||
|
||||
return strings.Join(path, ".")
|
||||
}
|
131
pkg/srv/http.go
Normal file
131
pkg/srv/http.go
Normal file
@ -0,0 +1,131 @@
|
||||
package srv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
|
||||
)
|
||||
|
||||
var (
|
||||
httpMeter metric.Meter
|
||||
httpTracer trace.Tracer
|
||||
readTimeout = 10 * time.Second
|
||||
writeTimeout = 10 * time.Second
|
||||
idleTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type HTTPFunc struct {
|
||||
Path string
|
||||
HandlerFunc http.HandlerFunc
|
||||
}
|
||||
|
||||
func prepHTTPServer(ctx context.Context, handleFuncs []HTTPFunc, hcFuncs ...HealthCheckFunc) *http.Server {
|
||||
var (
|
||||
cfg = config.MustFromCtx(ctx)
|
||||
l = zerolog.Ctx(ctx)
|
||||
mux = &http.ServeMux{}
|
||||
)
|
||||
|
||||
// NOTE: Wraps handle func with otelhttp handler and
|
||||
// inserts route tag
|
||||
otelHandleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
|
||||
handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
|
||||
mux.Handle(pattern, handler) // Associate pattern with handler
|
||||
}
|
||||
|
||||
healthChecks := handleHealthCheckFunc(ctx, hcFuncs...)
|
||||
otelHandleFunc("/health", healthChecks)
|
||||
otelHandleFunc("/", healthChecks)
|
||||
|
||||
for _, f := range handleFuncs {
|
||||
otelHandleFunc(f.Path, f.HandlerFunc)
|
||||
}
|
||||
|
||||
// Prometheus metrics endpoint
|
||||
if cfg.OTEL.PrometheusEnabled {
|
||||
mux.Handle(cfg.OTEL.PrometheusPath, promhttp.Handler())
|
||||
l.Info().Str("prometheusPath", cfg.OTEL.PrometheusPath).
|
||||
Msg("mounted prometheus metrics endpoint")
|
||||
}
|
||||
|
||||
// Add OTEL, skip health-check spans
|
||||
// NOTE: Add any other span exclusions here
|
||||
handler := otelhttp.NewHandler(mux, "/",
|
||||
otelhttp.WithFilter(func(r *http.Request) bool {
|
||||
switch r.URL.Path {
|
||||
case "/health":
|
||||
return false
|
||||
case cfg.OTEL.PrometheusPath:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}))
|
||||
|
||||
return &http.Server{
|
||||
Addr: cfg.HTTP.Listen,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
IdleTimeout: idleTimeout,
|
||||
Handler: handler,
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a shutdown func and a done channel if the
|
||||
// server aborts abnormally. Panics on error.
|
||||
func MustInitHTTPServer(ctx context.Context, funcs []HTTPFunc, hcFuncs ...HealthCheckFunc) (
|
||||
func(context.Context) error, <-chan interface{},
|
||||
) {
|
||||
shutdownFunc, doneChan, err := InitHTTPServer(ctx, funcs, hcFuncs...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return shutdownFunc, doneChan
|
||||
}
|
||||
|
||||
// Returns a shutdown func and a done channel if the
|
||||
// server aborts abnormally. Returns error on failure to start
|
||||
func InitHTTPServer(ctx context.Context, funcs []HTTPFunc, hcFuncs ...HealthCheckFunc) (
|
||||
func(context.Context) error, <-chan interface{}, error,
|
||||
) {
|
||||
l := zerolog.Ctx(ctx)
|
||||
doneChan := make(chan interface{})
|
||||
|
||||
var server *http.Server
|
||||
|
||||
httpMeter = otel.GetMeter(ctx, "http")
|
||||
httpTracer = otel.GetTracer(ctx, "http")
|
||||
|
||||
server = prepHTTPServer(ctx, funcs, hcFuncs...)
|
||||
|
||||
go func() {
|
||||
l.Debug().Msg("HTTP Server Started")
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
l.Err(err).Msg("HTTP server error")
|
||||
} else {
|
||||
l.Info().Msg("HTTP server shut down")
|
||||
}
|
||||
doneChan <- nil
|
||||
}()
|
||||
|
||||
// Shut down http server with a deadline
|
||||
return func(shutdownCtx context.Context) error {
|
||||
l.Debug().Msg("stopping http server")
|
||||
server.Shutdown(shutdownCtx)
|
||||
return nil
|
||||
}, doneChan, nil
|
||||
}
|
67
pkg/srv/http_health.go
Normal file
67
pkg/srv/http_health.go
Normal file
@ -0,0 +1,67 @@
|
||||
package srv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type HealthCheckFunc func(context.Context) error
|
||||
|
||||
func handleHealthCheckFunc(ctx context.Context, hcFuncs ...HealthCheckFunc) func(w http.ResponseWriter, r *http.Request) {
|
||||
// Return http handle func
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
healthChecksFailed bool
|
||||
errs error
|
||||
hcWg sync.WaitGroup
|
||||
)
|
||||
|
||||
if len(hcFuncs) < 1 {
|
||||
zerolog.Ctx(ctx).Warn().Msg("no health checks given responding with dummy 200")
|
||||
hcFuncs = append(hcFuncs, dummyHealthCheck)
|
||||
}
|
||||
|
||||
// Run all health check funcs concurrently
|
||||
// log all errors
|
||||
hcWg.Add(len(hcFuncs))
|
||||
for _, hc := range hcFuncs {
|
||||
go func() {
|
||||
defer hcWg.Done()
|
||||
errs = errors.Join(errs, hc(ctx))
|
||||
}()
|
||||
}
|
||||
hcWg.Wait()
|
||||
|
||||
if errs != nil {
|
||||
healthChecksFailed = true
|
||||
}
|
||||
|
||||
if healthChecksFailed {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errs != nil {
|
||||
w.Write([]byte(errs.Error()))
|
||||
} else {
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dummyHealthCheck(ctx context.Context) error {
|
||||
workFor := rand.Intn(750)
|
||||
ticker := time.NewTicker(time.Duration(time.Duration(workFor) * time.Millisecond))
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user