Mortar

Context

Jump to Section

If you are not familiar with context.Context, please read this and that first.

From the context documentation package:

Since we are building a gRPC web service, it’s part of the design. Everything gRPC related already has a context.Context as the first argument.

gRPC

  • Client:

    func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error
    
  • Interceptors:

    type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
    
    type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
    
  • Also, all the generated server endpoints have context.Context as the first argument.

Storage

One of the “drawbacks” in Go is a lack of storage per go routine. You can “solve” this drawback by:

  • Having a sync map[goroutine-id]storage, but it’s probably not a practical idea.
  • Propagate context.Context as the first parameter for every public function.

Actually, the second option is largely encouraged and has become a kind of a standard.

  • Tracing uses context to store a traceId, spanId and other information.
  • grpc-gateway reverse proxy injects selected http.Request headers into the context.

In Mortar, most interfaces have a way to leverage on context.Context, and depending on the package there are different ContextExtractor definitions.

Let’s look at Logging as an example.

Logging Example

All Logger methods have a context.Context as their first argument:

Debug(ctx context.Context, format string, args ...interface{})
Info(ctx context.Context, format string, args ...interface{})

Meaning that every log entry has a context…

Internally, just before we send the log to the implementing library, we iterate on every provided ContextExtractor and add all the extracted key->value pairs to the log entry.

func (c *contextAwareLogEntry) enrich(ctx context.Context) (logger log.Fields) {
	defer func() {
		if r := recover(); r != nil {
			c.innerLogger.WithField("__panic__", r).Error(ctx, "one of the context extractors panicked")
			logger = c.innerLogger
		}
	}()
	logger = c.innerLogger
	for _, extractor := range c.contextExtractors {
		for k, v := range extractor(ctx) {
			logger = logger.WithField(k, v)
		}
	}
	return
}

One of such ContextExtractor is provided by the bjaeger wrapper.

func extractFromSpanContext(ctx jaeger.SpanContext) map[string]interface{} {
	var output = make(map[string]interface{}, 4)
	output[SpanIDKey] = ctx.SpanID().String()
	output[ParentSpanIDKey] = ctx.ParentID().String()
	output[SampledKey] = ctx.IsSampled()
	if traceID := ctx.TraceID(); traceID.IsValid() {
		output[TraceIDKey] = traceID.String()
	}
	return output
}

If there is a Trace Span within the Context, we will include its information in the log entry.

For additional information about Logging, Context and Telemetry read here.