3

I'm trying to figure out the proper way to propagate a context.Context for the purposes of tracing with OpenTelemetry when using Gin.

I currently have a gin handler that calls a function and passes a *gin.Context, like so:

func (m Handler) doSomething(ginCtx *gin.Context, name string) {
  ctx, span := otel.Tracer("mytracer").Start(ginCtx.Request.Context(), "doSomething")
  defer span.End()
  // ...
}

This actually results in incorrect spans, since before doSomething() is called, I create a new context.Context in the calling function with the parent span information (similar to the snippet above).

In most of my code, I'm passing around a context.Context, but figured I could make use of gin's Context. Otherwise I have to pass both types of contexts:

func (m Handler) doSomething(ctx context.Context, ginCtx *gin.Context, name string) {
  ctx, span := otel.Tracer("mytracer").Start(ctx, "doSomething")
  defer span.End()
  // ...
}

This feels wrong since the Request stored in the *gin.Context is out of sync with what I'm passing around as a parameter via context.Context. However, I'm afraid to set the Request on the *gin.Context with a new Request that has the updated Context because

  1. It's only for this function and I'd have to un-set it (maybe through a defer()?)
  2. It doesn't seem like it'd be thread-safe (though I assume I'd need to Copy() the gin Context in this scenario anyway)

Is the proper way to handle this just Copying the *gin.Context and modifying the Request with the new context.Context, then passing the *gin.Context around instead of context.Context?

I don't know what the implications are of Copying a gin.Context from the text:

Copy returns a copy of the current context that can be safely used outside the request's scope. This has to be used when the context has to be passed to a goroutine.

Can I still Abort() through a copied *gin.Context, and is the old one still usable after copying? I just need something that behaves like a stack in the same way that passing context.Contexts around does, where simply returning from the function "pops" the context off and I'm left with the old one.

blackgreen
  • 34,072
  • 23
  • 111
  • 129
Andrew DiNunzio
  • 143
  • 4
  • 17

1 Answers1

1

I'm not overly familiar with the gin package, but I had some similar requirements for labstack/echo and I solved it by injecting what I needed into the function this way:

type Router struct {
    Inner *gin.Gin
}

func (router *Router) Handle(ctx context.Context, method string, route string, handler(context.Context, *gin.Context) gin.IRoutes {
    return router.Inner.Handle(method, route, func(gCtx *gin.Context) error {
        return handler(ctx, gCtx)
    })
}

This works by allowing you to inject the context.Context in when you declare the route. So, instead of doing this:

g := gin.Default()
g.POST("/some/route", myHandler) // myHandler accepts *gin.Context

you can now do:

r := Router{Inner: gin.Default()}
r.Handle("POST", "/some/route", myHandlerV2) // myHandlerV2 accepts *gin.Context and context.Context

This pattern would also allow you to inject other dependencies such as database connections, loggers, etc.

Woody1193
  • 7,252
  • 5
  • 40
  • 90