diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e2e751314..8f29a9fbfb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Support yaml/json struct tags for generated code in `go.opentelemetry.io/contrib/config`. (#5433) - Add support for parsing YAML configuration via `ParseYAML` in `go.opentelemetry.io/contrib/config`. (#5433) - Add support for temporality preference configuration in `go.opentelemetry.io/contrib/config`. (#5860) +- Support metrics for `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin`. (#6245) ### Changed diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go index 1affd4d6ca5..beaf53cc79a 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go @@ -6,7 +6,10 @@ package otelgin // import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" import ( + "bytes" "fmt" + "io" + "time" "github.com/gin-gonic/gin" @@ -14,6 +17,8 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.20.0" oteltrace "go.opentelemetry.io/otel/trace" @@ -43,6 +48,56 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { if cfg.Propagators == nil { cfg.Propagators = otel.GetTextMapPropagator() } + + if cfg.MeterProvider == nil { + cfg.MeterProvider = otel.GetMeterProvider() + } + meter := cfg.MeterProvider.Meter( + ScopeName, + otelmetric.WithInstrumentationVersion(Version()), + ) + + var err error + cfg.reqDuration, err = meter.Float64Histogram("http.server.request.duration", + otelmetric.WithDescription("Duration of HTTP server requests."), + otelmetric.WithUnit("ms")) + if err != nil { + otel.Handle(err) + if cfg.reqDuration == nil { + cfg.reqDuration = noop.Float64Histogram{} + } + } + + cfg.reqSize, err = meter.Int64UpDownCounter("http.server.request.body.size", + otelmetric.WithDescription("Size of HTTP server request bodies."), + otelmetric.WithUnit("By")) + if err != nil { + otel.Handle(err) + if cfg.reqSize == nil { + cfg.reqSize = noop.Int64UpDownCounter{} + } + } + + cfg.respSize, err = meter.Int64UpDownCounter("http.server.response.body.size", + otelmetric.WithDescription("Size of HTTP server response bodies."), + otelmetric.WithUnit("By")) + if err != nil { + otel.Handle(err) + if cfg.respSize == nil { + cfg.respSize = noop.Int64UpDownCounter{} + } + } + + cfg.activeReqs, err = meter.Int64UpDownCounter("http.server.active_requests", + otelmetric.WithDescription("Number of active HTTP server requests."), + otelmetric.WithUnit("{request}")) + if err != nil { + otel.Handle(err) + if cfg.activeReqs == nil { + cfg.activeReqs = noop.Int64UpDownCounter{} + } + } + return func(c *gin.Context) { for _, f := range cfg.Filters { if !f(c.Request) { @@ -71,6 +126,7 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { oteltrace.WithAttributes(semconv.HTTPRoute(c.FullPath())), oteltrace.WithSpanKind(oteltrace.SpanKindServer), } + metricAttrs := semconvutil.HTTPServerRequestMetrics(service, c.Request) var spanName string if cfg.SpanNameFormatter == nil { spanName = c.FullPath() @@ -79,24 +135,48 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { } if spanName == "" { spanName = fmt.Sprintf("HTTP %s route not found", c.Request.Method) + } else { + metricAttrs = append(metricAttrs, semconv.HTTPRoute(spanName)) } ctx, span := tracer.Start(ctx, spanName, opts...) defer span.End() // pass the span through the request context c.Request = c.Request.WithContext(ctx) + // calculate the size of the request. + reqSize := calcReqSize(c) + before := time.Now() // serve the request to the next middleware c.Next() + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedTime := float64(time.Since(before)) / float64(time.Millisecond) + respSize := c.Writer.Size() + // If nothing written in the response yet, a value of -1 may be returned. + if respSize < 0 { + respSize = 0 + } + status := c.Writer.Status() span.SetStatus(semconvutil.HTTPServerStatus(status)) + if status > 0 { - span.SetAttributes(semconv.HTTPStatusCode(status)) + statCodeAttr := semconv.HTTPStatusCode(status) + metricAttrs = append(metricAttrs, statCodeAttr) + span.SetAttributes(statCodeAttr) } if len(c.Errors) > 0 { - span.SetAttributes(attribute.String("gin.errors", c.Errors.String())) + ginErrAttr := attribute.String("gin.errors", c.Errors.String()) + metricAttrs = append(metricAttrs, ginErrAttr) + span.SetAttributes(ginErrAttr) } + + metricAttrSet := attribute.NewSet(metricAttrs...) // create a set to minimize alloc + cfg.reqSize.Add(ctx, int64(reqSize), otelmetric.WithAttributeSet(metricAttrSet)) + cfg.respSize.Add(ctx, int64(respSize), otelmetric.WithAttributeSet(metricAttrSet)) + cfg.reqDuration.Record(ctx, elapsedTime, otelmetric.WithAttributeSet(metricAttrSet)) + cfg.activeReqs.Add(ctx, 1, otelmetric.WithAttributeSet(metricAttrSet)) } } @@ -134,3 +214,30 @@ func HTML(c *gin.Context, code int, name string, obj interface{}) { }() c.HTML(code, name, obj) } + +// calcReqSize returns the total size of the request. +// It will calculate the header size by iterate all the header KVs +// and add with body size. +func calcReqSize(c *gin.Context) int { + // Calculate the size of headers + headerSize := 0 + for name, values := range c.Request.Header { + headerSize += len(name) + 2 // Colon and space + for _, value := range values { + headerSize += len(value) + } + } + + // Read the request body + body, err := io.ReadAll(c.Request.Body) + if err != nil { + // can't read the body, just return the headerSize. + return headerSize + } + + // Restore the request body for further processing + c.Request.Body = io.NopCloser(bytes.NewReader(body)) + + // Calculate the total size of the request (headers + body) + return headerSize + len(body) +} diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace_test.go index ecef39e3aeb..f2b68aeac7b 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace_test.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace_test.go @@ -6,7 +6,9 @@ package otelgin import ( + "bytes" "context" + "io" "net/http" "net/http/httptest" "testing" @@ -95,3 +97,70 @@ func TestPropagationWithCustomPropagators(t *testing.T) { router.ServeHTTP(w, r) } + +// TestCalcReqSize tests the calcReqSize function. +func TestCalcReqSize(t *testing.T) { + // Create a sample request with a body and headers + body := []byte("sample body") + req, err := http.NewRequest("POST", "/test", bytes.NewReader(body)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer token") + + // Create a Gin context with the request + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Call the function to calculate the request size + size := calcReqSize(c) + + // Calculate the expected size (body + headers + extra bytes for header formatting) + expectedSize := len(body) + len("Content-Type") + len("application/json") + len("Authorization") + len("Bearer token") + 4 // 4 extra bytes for ": " and "\r\n" + + // Check if the calculated size matches the expected size + if size != expectedSize { + t.Errorf("Expected request size %d, got %d", expectedSize, size) + } +} + +// TestCalcReqSizeWithBodyRead tests the calcReqSize function and ensures the request body can still be read afterward. +func TestCalcReqSizeWithBodyRead(t *testing.T) { + // Create a sample request with a body and headers + body := []byte("sample body") + req, err := http.NewRequest("POST", "/test", bytes.NewReader(body)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer token") + + // Create a Gin context with the request + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Call the function to calculate the request size + size := calcReqSize(c) + + // Calculate the expected size (body + headers + extra bytes for header formatting) + expectedSize := len(body) + len("Content-Type") + len("application/json") + len("Authorization") + len("Bearer token") + 4 // 4 extra bytes for ": " and "\r\n" + + // Check if the calculated size matches the expected size + if size != expectedSize { + t.Errorf("Expected request size %d, got %d", expectedSize, size) + } + + // Read the request body again + newBody, err := io.ReadAll(c.Request.Body) + if err != nil { + t.Fatalf("Failed to read request body: %v", err) + } + + // Check if the body is unchanged + if !bytes.Equal(newBody, body) { + t.Errorf("Expected request body %q, got %q", body, newBody) + } +} diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/go.mod b/instrumentation/github.com/gin-gonic/gin/otelgin/go.mod index 173ba041450..2de01495c2f 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/go.mod +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/go.mod @@ -9,39 +9,39 @@ require ( github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/propagators/b3 v1.30.0 go.opentelemetry.io/otel v1.31.0 + go.opentelemetry.io/otel/metric v1.31.0 go.opentelemetry.io/otel/trace v1.31.0 ) require ( - github.com/bytedance/sonic v1.12.3 // indirect - github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.1 // indirect - github.com/goccy/go-json v0.10.3 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/go.sum b/instrumentation/github.com/gin-gonic/gin/otelgin/go.sum index c6ec79c43b5..23cc64ac662 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/go.sum +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/go.sum @@ -1,8 +1,7 @@ -github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= -github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= -github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -10,8 +9,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= -github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= @@ -27,18 +26,18 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= @@ -49,18 +48,20 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -73,23 +74,25 @@ go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozR go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= -golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= -golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/option.go b/instrumentation/github.com/gin-gonic/gin/otelgin/option.go index 143ca8e849e..960db505552 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/option.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/option.go @@ -10,16 +10,23 @@ import ( "github.com/gin-gonic/gin" + otelmetric "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" ) type config struct { TracerProvider oteltrace.TracerProvider + MeterProvider otelmetric.MeterProvider Propagators propagation.TextMapPropagator Filters []Filter GinFilters []GinFilter SpanNameFormatter SpanNameFormatter + + reqDuration otelmetric.Float64Histogram + reqSize otelmetric.Int64UpDownCounter + respSize otelmetric.Int64UpDownCounter + activeReqs otelmetric.Int64UpDownCounter } // Filter is a predicate used to determine whether a given http.request should @@ -65,6 +72,16 @@ func WithTracerProvider(provider oteltrace.TracerProvider) Option { }) } +// WithMeterProvider specifies a meter provider to use for creating a meter. +// If none is specified, the global provider is used. +func WithMeterProvider(provider otelmetric.MeterProvider) Option { + return optionFunc(func(cfg *config) { + if provider != nil { + cfg.MeterProvider = provider + } + }) +} + // WithFilter adds a filter to the list of filters used by the handler. // If any filter indicates to exclude a request then the request will not be // traced. All gin and net/http filters must allow a request to be traced for a Span to be created. diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go index fb8698422dc..8a1bb47cbc1 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/test/gintrace_test.go @@ -6,6 +6,7 @@ package test import ( + "context" "errors" "html/template" "net/http" @@ -20,8 +21,13 @@ import ( "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" "go.opentelemetry.io/otel/attribute" oteltrace "go.opentelemetry.io/otel/trace" @@ -321,3 +327,117 @@ func TestWithGinFilter(t *testing.T) { assert.Len(t, sr.Ended(), 1) }) } + +func TestMetric(t *testing.T) { + t.Setenv("OTEL_METRICS_EXEMPLAR_FILTER", "always_off") + sr := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + + mr := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(mr)) + + router := gin.New() + router.Use(otelgin.Middleware("foobar", + otelgin.WithTracerProvider(provider), + otelgin.WithMeterProvider(mp), + ), + ) + router.GET("/user/:id", func(c *gin.Context) { + id := c.Param("id") + _, _ = c.Writer.Write([]byte(id)) + }) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + // do and verify the request + router.ServeHTTP(w, r) + response := w.Result() //nolint:bodyclose // False positive for httptest.ResponseRecorder: https://github.com/timakin/bodyclose/issues/59. + require.Equal(t, http.StatusOK, response.StatusCode) + + // verify traces look good + spans := sr.Ended() + require.Len(t, spans, 1) + span := spans[0] + assert.Equal(t, "/user/:id", span.Name()) + assert.Equal(t, oteltrace.SpanKindServer, span.SpanKind()) + attr := span.Attributes() + assert.Contains(t, attr, attribute.String("http.method", "GET")) + assert.Contains(t, attr, attribute.String("http.route", "/user/:id")) + + // verify metrics look good. + rm := metricdata.ResourceMetrics{} + err := mr.Collect(context.Background(), &rm) + assert.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + attrs := []attribute.KeyValue{ + semconv.HTTPMethod("GET"), + semconv.HTTPRoute("/user/:id"), + semconv.HTTPSchemeHTTP, + semconv.HTTPStatusCode(200), + semconv.NetHostName("foobar"), + semconv.NetProtocolName("http"), + semconv.NetProtocolVersion("1.1"), + } + want := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin", + Version: otelgin.Version(), + }, + Metrics: []metricdata.Metrics{ + { + Name: "http.server.request.duration", + Description: "Duration of HTTP server requests.", + Unit: "ms", + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + { + Attributes: attribute.NewSet(attrs...), + }, + }, + }, + }, + { + Name: "http.server.request.body.size", + Description: "Size of HTTP server request bodies.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet(attrs...), + }, + }, + }, + }, + { + Name: "http.server.response.body.size", + Description: "Size of HTTP server response bodies.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet(attrs...), + }, + }, + }, + }, + { + Name: "http.server.active_requests", + Description: "Number of active HTTP server requests.", + Unit: "{request}", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet(attrs...), + }, + }, + }, + }, + }, + } + metricdatatest.AssertEqual(t, want, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) +} diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.mod b/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.mod index 19f81273e99..6c0f5c8aa86 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.mod +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.mod @@ -8,6 +8,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.55.0 go.opentelemetry.io/otel v1.31.0 go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/sdk/metric v1.31.0 go.opentelemetry.io/otel/trace v1.31.0 ) diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.sum b/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.sum index af428acc453..7abf05af93e 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.sum +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/test/go.sum @@ -75,6 +75,8 @@ go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozR go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=