Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(otelgin): Support metrics for otelgin #6245

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
111 changes: 109 additions & 2 deletions instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
package otelgin // import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"

import (
"bytes"
"fmt"
"io"
"time"

"github.com/gin-gonic/gin"

"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil"
"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"
Expand Down Expand Up @@ -43,6 +48,56 @@
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("Measures the duration of inbound RPC."),
otelmetric.WithUnit("ms"))
if err != nil {
otel.Handle(err)
if cfg.reqDuration == nil {
cfg.reqDuration = noop.Float64Histogram{}

Check warning on line 67 in instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go#L65-L67

Added lines #L65 - L67 were not covered by tests
}
}

cfg.reqSize, err = meter.Int64UpDownCounter("http.server.request.body.size",
otelmetric.WithDescription("Measures size of RPC request messages (uncompressed)."),
otelmetric.WithUnit("By"))
if err != nil {
otel.Handle(err)
if cfg.reqSize == nil {
cfg.reqSize = noop.Int64UpDownCounter{}

Check warning on line 77 in instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go#L75-L77

Added lines #L75 - L77 were not covered by tests
}
}

cfg.respSize, err = meter.Int64UpDownCounter("http.server.response.body.size",
otelmetric.WithDescription("Measures size of RPC response messages (uncompressed)."),
otelmetric.WithUnit("By"))
if err != nil {
otel.Handle(err)
if cfg.respSize == nil {
cfg.respSize = noop.Int64UpDownCounter{}

Check warning on line 87 in instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go#L85-L87

Added lines #L85 - L87 were not covered by tests
}
}

cfg.activeReqs, err = meter.Int64UpDownCounter("http.server.active_requests",
otelmetric.WithDescription("Measures the number of messages received per RPC. Should be 1 for all non-streaming RPCs."),
otelmetric.WithUnit("{count}"))
if err != nil {
otel.Handle(err)
if cfg.activeReqs == nil {
cfg.activeReqs = noop.Int64UpDownCounter{}

Check warning on line 97 in instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go#L95-L97

Added lines #L95 - L97 were not covered by tests
}
}

return func(c *gin.Context) {
for _, f := range cfg.Filters {
if !f(c.Request) {
Expand Down Expand Up @@ -71,6 +126,7 @@
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()
Expand All @@ -79,24 +135,48 @@
}
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))

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))

extraAttrs := make([]attribute.KeyValue, 0, 2) // pre-alloc cap of 2
if status > 0 {
span.SetAttributes(semconv.HTTPStatusCode(status))
extraAttrs = append(extraAttrs, semconv.HTTPStatusCode(status))
}
if len(c.Errors) > 0 {
span.SetAttributes(attribute.String("gin.errors", c.Errors.String()))
extraAttrs = append(extraAttrs, attribute.String("gin.errors", c.Errors.String()))
}
span.SetAttributes(extraAttrs...) // call set just once

extraMetricAttrSet := attribute.NewSet(extraAttrs...) // create another set to minimize alloc
cfg.reqDuration.Record(ctx, elapsedTime, otelmetric.WithAttributeSet(metricAttrSet), otelmetric.WithAttributeSet(extraMetricAttrSet))
cfg.activeReqs.Add(ctx, 1, otelmetric.WithAttributeSet(metricAttrSet), otelmetric.WithAttributeSet(extraMetricAttrSet))
}
}

Expand Down Expand Up @@ -134,3 +214,30 @@
}()
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

Check warning on line 235 in instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go#L235

Added line #L235 was not covered by tests
}

// 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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
package otelgin

import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -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)
}
}
28 changes: 14 additions & 14 deletions instrumentation/github.com/gin-gonic/gin/otelgin/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading
Loading