go-agent icon indicating copy to clipboard operation
go-agent copied to clipboard

Context propagation with B3 Headers + Gin

Open kevinmichaelchen opened this issue 2 years ago • 1 comments

Summary

Hello 👋🏼

(Apologies if this feels less like a GH Issue and more like a GH Discussion).

I have a project that is using Hasura as a GraphQL federation layer as well as for inter-process communication.

2022-11-16-o11y-with-hasura

The flow is as follows:

First, an external GraphQL client calls Hasura. Then...

  1. Hasura forwards the request to an internal Go server, sending along B3 headers
  2. The Go service runs on Gin HTTP and is instrumented with this NR Go Agent. This is the part I haven't figured out yet — Gin middleware needs to be able to extract the B3 headers to ascertain the trace ID and parent span.
  3. The Go service makes a B3 instrumented request to Hasura using the nrb3 module.

Hasura only supports B3 headers for propagation, whereas New Relic (from what I gather) only supports W3C headers.

Desired Behaviour

The desired behavior would be to see hierarchical spans all under one trace.

Possible Solution

Unsure. I'm exploring a resolution on the Hasura side where we simply alias the b3 headers as w3c headers.

Additional context

kevinmichaelchen avatar Nov 28 '22 20:11 kevinmichaelchen

I just discovered InsertDistributedTraceHeaders and AcceptDistributedTraceHeaders.

Should be possible to write some Gin middleware that parses B3 headers and "passes them in" as W3C headers:

// NewB3Handler creates a Gin middleware that links New Relic transactions by
// accepting distributed trace headers from another transaction.
func NewB3Handler() gin.HandlerFunc {
  return func(c *gin.Context) {
    txn := nrgin.Transaction(c)

    if txn != nil {
      traceID := c.GetHeader("X-B3-TraceId")
      spanID := c.GetHeader("X-B3-SpanId")
      sampled := c.GetHeader("X-B3-Sampled")

      // w3c spec uses hex encoding: https://www.w3.org/TR/trace-context/#trace-flags
      sampledHex := "00"
      if sampled == "1" || sampled == "true" {
        sampledHex = "01"
      }

      // https://www.w3.org/TR/trace-context/#traceparent-header
      w3cTraceParent := fmt.Sprintf("00-%s-%s-%s", traceID, spanID, sampledHex)

      // https://docs.newrelic.com/docs/distributed-tracing/concepts/how-new-relic-distributed-tracing-works/#headers
      var hdrs http.Header = map[string][]string{
        newrelic.DistributedTraceW3CTraceParentHeader: []string{w3cTraceParent},
        // Leaving the tracestate header blank for now.
        // Per the official W3C spec: failure to parse tracestate MUST NOT affect the parsing of traceparent.
        // https://github.com/newrelic/go-agent/blob/v3.20.1/v3/newrelic/distributed_tracing_test.go#L249-L277
        newrelic.DistributedTraceW3CTraceStateHeader: []string{""},
        newrelic.DistributedTraceNewRelicHeader:      []string{c.GetHeader(newrelic.DistributedTraceNewRelicHeader)},
      }

      // Links transactions by accepting distributed trace headers from
      // another transaction.
      txn.AcceptDistributedTraceHeaders(newrelic.TransportHTTP, hdrs)
      txn.AcceptDistributedTraceHeaders(newrelic.TransportHTTPS, hdrs)
    }

    c.Next()
  }
}

kevinmichaelchen avatar Nov 29 '22 00:11 kevinmichaelchen