diff --git a/bunrouterotel/.golangci.yaml b/bunrouterotel/.golangci.yaml new file mode 100644 index 0000000..d884b2f --- /dev/null +++ b/bunrouterotel/.golangci.yaml @@ -0,0 +1,141 @@ +## 基于 golangci-lint@v1.52.2 +run: + timeout: 1m + build-tags: [ ] + skip-dirs: [ ] + skip-files: [ ] +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - cyclop + - dupl + - durationcheck + - errname + - errorlint + - exhaustive + - exportloopref + - funlen + - gocheckcompilerdirectives + - gochecknoinits + - goconst + - gocritic + - gocyclo + - goimports + - gomnd + - goprintffuncname + - gosec + - lll + - loggercheck + - makezero + - nakedret + - nestif + - nilnil + - noctx + - nolintlint + - prealloc + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - stylecheck + - tenv + - testableexamples + - testpackage + - tparallel + - unconvert + - unparam + - usestdlibvars + - wastedassign + - whitespace +linters-settings: + errcheck: + check-type-assertions: true + exclude-functions: [ ] + govet: + enable-all: true + disable: [ ] + cyclop: + max-complexity: 10 + package-average: 0.0 + dupl: + threshold: 150 + exhaustive: + check: + - switch + - map + funlen: + lines: 100 + statements: 60 + gocritic: + disabled-checks: + - commentFormatting + settings: + captLocal: + paramsOnly: false + underef: + skipRecvDeref: false + gocyclo: + min-complexity: 20 + gomnd: + ignored-functions: + - os.Chmod + - os.Mkdir + - os.MkdirAll + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets + - prometheus.ExponentialBucketsRange + - prometheus.LinearBuckets + lll: + line-length: 240 + nakedret: + max-func-lines: 10 + nestif: + min-complexity: 5 + predeclared: + ignore: "" + q: false + reassign: + patterns: + - ".*" + rowserrcheck: + packages: + - github.com/jmoiron/sqlx + tenv: + all: true + usestdlibvars: + time-month: true + time-layout: true + crypto-hash: true + default-rpc-path: true + os-dev-null: true + sql-isolation-level: true + tls-signature-scheme: true + constant-kind: true + syslog-priority: true +issues: + max-same-issues: 10 + exclude-rules: + - source: "//noinspection" + linters: [ gocritic ] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx diff --git a/bunrouterotel/go.mod b/bunrouterotel/go.mod new file mode 100644 index 0000000..de5eca6 --- /dev/null +++ b/bunrouterotel/go.mod @@ -0,0 +1,15 @@ +module git.blauwelle.com/go/crate/bunrouterotel + +go 1.21.1 + +require ( + github.com/uptrace/bunrouter v1.0.20 + go.opentelemetry.io/otel v1.19.0 + go.opentelemetry.io/otel/trace v1.19.0 +) + +require ( + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect +) diff --git a/bunrouterotel/go.sum b/bunrouterotel/go.sum new file mode 100644 index 0000000..dcde191 --- /dev/null +++ b/bunrouterotel/go.sum @@ -0,0 +1,23 @@ +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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/uptrace/bunrouter v1.0.20 h1:jNvYNcJxF+lSYBQAaQjnE6I11Zs0m+3M5Ek7fq/Tp4c= +github.com/uptrace/bunrouter v1.0.20/go.mod h1:TwT7Bc0ztF2Z2q/ZzMuSVkcb/Ig/d3MQeP2cxn3e1hI= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/bunrouterotel/middleware.go b/bunrouterotel/middleware.go new file mode 100644 index 0000000..a416a05 --- /dev/null +++ b/bunrouterotel/middleware.go @@ -0,0 +1,79 @@ +package bunrouterotel + +import ( + "net/http" + + "github.com/uptrace/bunrouter" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" + "go.opentelemetry.io/otel/trace" +) + +const ( + tracerName = "git.blauwelle.com/go/crate/bunrouterotel" + version = "0.1.0" +) + +type config struct { + propagators propagation.TextMapPropagator +} + +type Option interface { + apply(cfg *config) +} + +type optionFunc func(cfg *config) + +func (o optionFunc) apply(cfg *config) { + o(cfg) +} + +func WithPropagators(propagators propagation.TextMapPropagator) Option { + return optionFunc(func(cfg *config) { + cfg.propagators = propagators + }) +} + +// Middleware create a span, which record the request, +// HTTP status code is NOT recorded. +func Middleware(serverName string, opts ...Option) bunrouter.MiddlewareFunc { + cfg := config{} + for _, opt := range opts { + opt.apply(&cfg) + } + if cfg.propagators == nil { + cfg.propagators = otel.GetTextMapPropagator() + } + tracer := otel.Tracer(tracerName, trace.WithInstrumentationVersion("semver:"+version)) + return func(next bunrouter.HandlerFunc) bunrouter.HandlerFunc { + return func(w http.ResponseWriter, req bunrouter.Request) error { + ctx := cfg.propagators.Extract(req.Context(), propagation.HeaderCarrier(req.Header)) + var spanName string + params := req.Params() + route := params.Route() + if route != "" { + spanName = req.Method + " " + route + } else { + spanName = "HTTP " + req.Method + " route not found" + } + tarcerStartOpts := []trace.SpanStartOption{ + trace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", req.Request)...), + trace.WithAttributes(semconv.EndUserAttributesFromHTTPRequest(req.Request)...), + trace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(serverName, route, req.Request)...), + trace.WithSpanKind(trace.SpanKindServer), + } + ctx, span := tracer.Start(ctx, spanName, tarcerStartOpts...) + req.Request = req.Request.WithContext(ctx) + paramSlice := params.Slice() + attrs := make([]attribute.KeyValue, 0, len(paramSlice)) + for _, param := range paramSlice { + attrs = append(attrs, attribute.String("http.route.param."+param.Key, param.Value)) + } + span.SetAttributes(attrs...) + defer span.End() + return next(w, req) + } + } +}