feat(gin): add gin-gonic/gin server instrumentation#501
Conversation
Single-hook approach: hook (*Context).Next to enrich the span already created by the net/http server hook. Sets span name to "METHOD /route" and records the http.route attribute, aligned with otelgin conventions. Includes unit tests, test app, and e2e tests.
d73eb0b to
ebb7144
Compare
Move gin_test.go from test/e2e/ to test/integration/gin_server_test.go and update the build tag from e2e to integration, since the test exercises a single instrumented process (not multiple services). Follows the convention in docs/testing.md and matches the existing integration tests for http, grpc, redis, and db_client.
NameHaibinZhang
left a comment
There was a problem hiding this comment.
Review Summary
Nice approach — enriching the existing net/http span instead of creating a duplicate is the right call. The idempotency guard via c.Set/c.Get handles the multi-middleware c.Next() problem cleanly.
Issues to address
-
Integration test hardcodes port 8080 — Unlike
http_server_test.gowhich parameterizes the port, the gin test uses a fixed port. This can cause flaky failures if the port is occupied. Suggest passing-port=<unused>like other tests:f.BuildAndStart("ginserver", "-port=8090")
-
No integration test for the 404/no-route case — The unit test covers empty route, but an end-to-end test verifying that a request to an unregistered path still produces a span named just
"GET"(without route pattern) would strengthen regression protection. -
Heavy transitive dependency tree — The
go.modpulls in prometheus, grpc-gateway, autoexport, and many OTLP exporters indirectly. For a hook that only needsotel/trace+otel/semconv, this seems excessive. This may be inherited fromshared/pkgand not specific to this PR, but worth investigating whether the dependency graph can be trimmed.
Nits
- PR scope in title:
feat(instrumentation)is broad —feat(gin)would be more specific. - Consider documenting
"otel.gin.route.set"as a reserved key in gin context, to avoid potential collision with user middleware.
Overall
Solid contribution. The design is sound, tests cover the key scenarios, and it aligns well with otelgin conventions. Addressing the hardcoded port should be straightforward. 👍
There was a problem hiding this comment.
Pull request overview
Adds compile-time instrumentation support for github.com/gin-gonic/gin servers by hooking (*gin.Context).Next to enrich the existing net/http server span with a route-based span name and http.route attribute, aligning output with common otelgin conventions.
Changes:
- Introduces a Gin server instrumentation module with a
Context.Nexthook that updates span name and setshttp.route. - Adds unit tests for the hook behavior (route enrichment, empty route no-op, idempotency, enable/disable).
- Adds an integration test app (
ginserver) plus integration tests validating span naming/attributes for route patterns and 5xx errors.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/integration/gin_server_test.go | New integration tests asserting route-template span naming and http.route/status/error attributes. |
| test/apps/ginserver/main.go | Minimal Gin server app used by integration tests. |
| test/apps/ginserver/go.mod | Module definition for the Gin test app. |
| test/apps/ginserver/go.sum | Dependency lockfile for the Gin test app. |
| pkg/instrumentation/gin/server/server.yaml | Adds the compile-time hook rule for (*gin.Context).Next. |
| pkg/instrumentation/gin/server/go.mod | Module definition for the Gin instrumentation package. |
| pkg/instrumentation/gin/server/go.sum | Dependency lockfile for the Gin instrumentation package. |
| pkg/instrumentation/gin/server/gin.go | Defines the Gin instrumentation enablement key and logger. |
| pkg/instrumentation/gin/server/context_hook.go | Implements BeforeNext hook to set span name and http.route. |
| pkg/instrumentation/gin/server/context_hook_test.go | Unit tests covering span enrichment and idempotency behavior. |
Comments suppressed due to low confidence (2)
pkg/instrumentation/gin/server/context_hook_test.go:90
- Repository docs/code expect OTEL_GO_ENABLED_INSTRUMENTATIONS values to be lowercase (the list is lowercased during parsing). For consistency with other tests and documentation, prefer using "gin" instead of "GIN" here.
sr, tr := setupContextTracer(t)
t.Setenv("OTEL_GO_ENABLED_INSTRUMENTATIONS", "GIN")
pkg/instrumentation/gin/server/context_hook_test.go:111
- Repository docs/code expect OTEL_GO_ENABLED_INSTRUMENTATIONS values to be lowercase (the list is lowercased during parsing). For consistency with other tests and documentation, prefer using "gin" instead of "GIN" here.
sr, tr := setupContextTracer(t)
t.Setenv("OTEL_GO_ENABLED_INSTRUMENTATIONS", "GIN")
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // c.Next() is called by each middleware in the chain, so this hook fires | ||
| // multiple times per request. Only the first call needs to update the span. | ||
| if _, already := c.Get(routeSetKey); already { | ||
| return | ||
| } | ||
| c.Set(routeSetKey, struct{}{}) | ||
|
|
||
| span := trace.SpanFromContext(c.Request.Context()) | ||
| if !span.IsRecording() { | ||
| return | ||
| } |
|
|
||
| func TestBeforeNext_UpdatesSpanNameAndRoute(t *testing.T) { | ||
| sr, tr := setupContextTracer(t) | ||
| t.Setenv("OTEL_GO_ENABLED_INSTRUMENTATIONS", "GIN") |
|
|
||
| func TestBeforeNext_DisabledIsNoop(t *testing.T) { | ||
| sr, tr := setupContextTracer(t) | ||
| t.Setenv("OTEL_GO_DISABLED_INSTRUMENTATIONS", "GIN") |
…ions
Set routeSetKey only after the IsRecording check passes. Previously a
non-recording first call would burn the gate and prevent a later recording
span on the same request from being enriched.
Switch test env values for OTEL_GO_{ENABLED,DISABLED}_INSTRUMENTATIONS to
lowercase 'gin', matching the convention documented on shared.Instrumented
and used by the nethttp, grpc, and redis tests.
Pass -port=8090 to ginserver in the integration tests so the port is
explicit at the call site, matching http_server_test.go.
Add a regression test that exercises the non-recording-then-recording
path to lock in the gate ordering.
|
Thanks @NameHaibinZhang for the review. Here are the fixes that I have made.
let me know if anything else needs change. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #501 +/- ##
==========================================
+ Coverage 63.30% 63.52% +0.22%
==========================================
Files 62 64 +2
Lines 4796 4842 +46
==========================================
+ Hits 3036 3076 +40
- Misses 1509 1512 +3
- Partials 251 254 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| target: github.com/gin-gonic/gin | ||
| func: Next | ||
| recv: "*Context" | ||
| before: BeforeNext |
There was a problem hiding this comment.
Could be interesting to have an AfterNext to catch all the c.Errors(), similar to go-contrib approach
There was a problem hiding this comment.
Hi @txabman42
Added AfterNext hook that captures c.Errors() after (*gin.Context).Next returns — sets span status to Error and records each error as an exception event via span.RecordError(), matching the otelgin approach. Uses the same flag-based idempotency as BeforeNext to handle multiple middleware calling c.Next(). Also added a /error route to the test app and TestGinServer_GinError integration test that exercises c.Error() on a 200 OK response to verify gin-level errors are captured independently of HTTP status.
There was a problem hiding this comment.
As you correctly did in the BeforeNext, we need to handle in AfterNext the case where multiple middlewares call c.Next().
We can count in the BeforeNext the number of middlewares and use it to iterate over all errors in the outer middleware.
There was a problem hiding this comment.
Switched from the flag based approach to a depth counter as you suggested. BeforeNext increments the counter for each c.Next() call and AfterNext decrements it, only recording errors when the count reaches zero at the outermost middleware return. This way errors added by middleware after c.Next() returns are also captured.
c97a127 to
f1140d1
Compare
2b2329c to
510768d
Compare
00c61ad to
a94c40d
Compare
|
@txabman42 I have improved the gin integration tests so they share a single build of the test app instead of building it 4 times. This saves about 90 seconds on the Windows job and keeps the suite well inside the ten minute timeout. |
txabman42
left a comment
There was a problem hiding this comment.
LGTM! ❤️
Just a minor nitpick
| ) | ||
|
|
||
| func TestGinServer(t *testing.T) { | ||
| build := testutil.NewTestFixture(t, testutil.WithoutCollector()) |
There was a problem hiding this comment.
No needed, f := testutil.NewTestFixture(t) is done inside each run
| "github.com/open-telemetry/opentelemetry-go-compile-instrumentation/test/testutil" | ||
| ) | ||
|
|
||
| func TestGinServer(t *testing.T) { |
There was a problem hiding this comment.
Regarding integration test fail:
Couple of ideas to evaluate:
- We are not seeing why the app crashed, it seems
cmd.Stdoutis nil (defined in Start). - Why can verify if the port is already binded during start. If so, we can improve the cleanup phase so it doesn't happen or use different ports.
There was a problem hiding this comment.
Thanks for helping. I need a bit of time to figure out the right approach. Will push an update soon.
Description
Adds compile-time instrumentation for
gin-gonic/ginHTTP servers using a single-hook enrichment approach.Instead of creating a separate span, we hook
(*gin.Context).Nextand enrich the span already created by thenet/httpserver hook. By the timeNextis called, gin's router has matched the request and populatedc.FullPath(), so we update the span name from"METHOD"to"METHOD /route/pattern"and record thehttp.routeattribute.This keeps the telemetry output aligned with
otelginconventions: same span name format, same route attribute, one span per request.Key design decisions:
net/http'sserverHandler.ServeHTTPalready fires for gin (sincegin.Engineimplementshttp.Handler), so no need for a second span-creation hookc.Next()is called by each middleware in the chain, so the hook usesc.Set/c.Getto ensure the span is only updated once per requestResolves #479
Checklist
make formatmake lintmake test