Go app framework

This commit is contained in:
Ryan McGuire 2025-01-04 12:24:42 -05:00
parent 41036b3c3a
commit d0a430505c
16 changed files with 1199 additions and 1 deletions

View File

@ -1,3 +1,61 @@
# go-app # go-app
A simple app framework for bootstrapping logging, otel, and servers from config and environment 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}
}