pkg/xcontext/context.go (337 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. // Package xcontext implements a generic context with integrated logger, // metrics, tracer and recoverer. package xcontext import ( "context" "os" "os/user" "runtime" "sync" "time" "github.com/facebookincubator/contest/pkg/xcontext/buildinfo" "github.com/facebookincubator/contest/pkg/xcontext/fields" "github.com/facebookincubator/contest/pkg/xcontext/logger" "github.com/facebookincubator/contest/pkg/xcontext/metrics" "github.com/google/uuid" ) var ( // DefaultLogTraceID defines if traceID should be logged by default. // // If it is disabled, then logging of traceID for a specific // context could be enforced this way: // ctx = ctx.WithField("traceID", ctx.TraceID()) DefaultLogTraceID = false // DefaultLogHostname defines if hostname should be logged by default. DefaultLogHostname = false // DefaultLogUsername defines if hostname should be logged by default. DefaultLogUsername = false ) // Fields is a multiple of fields which are attached to logger/tracer messages. type Fields = fields.Fields // TraceID is a passthrough ID used to track a sequence of events across // multiple processes/services. It is supposed to pass it with Thrift-requests // through a HTTP-header "X-Trace-Id". type TraceID string // String implements fmt.Stringer. func (traceID TraceID) String() string { return string(traceID) } // NewTraceID returns a new random unique TraceID. func NewTraceID() TraceID { return TraceID(uuid.New().String()) } // Logger is an abstract logger used by a Context. type Logger = logger.Logger // Context is a generic extension over context.Context with provides also: // * Logger which allows to send messages to a log. // * Metrics which allows to update metrics. // * Tracer which allows to log time spans (to profile delays of the application). // * TraceID to track across multiple processes. type Context interface { context.Context logger.MinimalLogger // Clone just returns a copy of the Context safe to be modified. Clone() Context // TraceID returns the TraceID attached to the Context TraceID() TraceID // WithTraceID returns a clone of the Context, but with passed TraceID. WithTraceID(TraceID) Context // Logger returns the Logger handler attached to the Context Logger() Logger // WithLogger returns a clone of the Context, but with passed Logger handler. WithLogger(logger Logger) Context // Metrics returns the Metrics handler attached to the Context. Metrics() Metrics // WithMetrics returns a clone of the Context, but with passed Metrics handler. WithMetrics(Metrics) Context // Tracer returns the Tracer handler attached to the Context. Tracer() Tracer // WithTracer returns a clone of the Context, but with passed Tracer handler. WithTracer(Tracer) Context // WithTag returns a clone of the context, but with added tag with key // "key" and value "value". // // Note about Tag vs Field: Tag is supposed to be used for limited amount // of values, while Field is supposed to be used for arbitrary values. // Basically currentTags are used for everything (Logger, Metrics and Tracer), // while Fields are used only for Logger and Tracer. // We cannot use arbitrary values for Metrics because it will create // "infinite" amount of metrics. WithTag(key string, value interface{}) Context // WithTags returns a clone of the context, but with added tags with // key and values according to map "Fields". // // See also WithTag. WithTags(Fields) Context // WithField returns a clone of the context, but with added field with key // "key" and value "value". // // See also WithTag. WithField(key string, value interface{}) Context // WithFields returns a clone of the context, but with added fields with // key and values according to map "Fields". // // See also WithTag. WithFields(Fields) Context // Until works similar to Done(), but it is possible to specify specific // signal to wait for. // // If err is nil, then waits for any event. Until(err error) <-chan struct{} // StdCtxUntil is the same as Until, but returns a standard context // instead of a channel. StdCtxUntil(err error) context.Context // IsSignaledWith returns true if the context received a cancel // or a notification signal equals to any of passed ones. // // If errs is empty, then returns true if the context received any // cancel or notification signal. IsSignaledWith(errs ...error) bool // Notifications returns all the received notifications (including events // received by parents). // // This is a read-only value, do not modify it. Notifications() []error // Recover is use instead of standard "recover()" to also log the panic. Recover() interface{} // private: resetEventHandler() addEventHandler() *eventHandler addValue(key, value interface{}) cloneWithStdContext(context.Context) Context } // TimeSpan is the object represents the time span to be reported by a Tracer. type TimeSpan interface { // Finish sets the end time of the span to time.Now() and sends // the time span to the log of the Tracer. Finish() time.Duration } // Tracer is a handler responsible to track time spans. // // Is supposed to be used this way: // // defer ctx.Tracer().StartSpan("some label here").Finish() type Tracer interface { // StartSpan creates a time span to be reported (if Finish will be called) // which starts counting time since the moment StartSpan was called. StartSpan(label string) TimeSpan // WithField returns a Tracer with an added field to be reported with the time span (when Finish will be called). WithField(key string, value interface{}) Tracer // WithField returns a Tracer with added fields to be reported with the time span (when Finish will be called). WithFields(Fields) Tracer } // Metrics is a handler of metrics (like Prometheus, ODS) type Metrics = metrics.Metrics var _ context.Context = &ctxValue{} type debugTools struct { loggerInstance Logger metricsInstance Metrics tracerInstance Tracer pendingFields fields.PendingFields pendingTags fields.PendingFields } func (tools *debugTools) Clone() *debugTools { return &debugTools{ loggerInstance: tools.loggerInstance, metricsInstance: tools.metricsInstance, tracerInstance: tools.tracerInstance, pendingFields: tools.pendingFields.Clone(), pendingTags: tools.pendingTags.Clone(), } } type ctxValue struct { mutationSyncer sync.Once traceIDValue TraceID debugTools *debugTools valuesHandler *eventHandler // parent is used only to warn the GC to do not run finalizers for eventHandlers-s // of parents. parent *ctxValue } var ( background = NewContext(context.Background(), "", nil, nil, nil, nil, nil) ) // Background is analog of standard context.Context which returns just a simple dummy context which does nothing. func Background() Context { return background } var ( hostname string curUser *user.User ) func init() { hostname, _ = os.Hostname() curUser, _ = user.Current() } // Extend converts a standard context to an extended one func Extend(parent context.Context) Context { return NewContext(parent, "", nil, nil, nil, nil, nil) } // NewContext is a customizable constructor of a context. // // It is not intended to be called by an user not familiar with this package, // there are special helpers for that, see for example bundles.NewContextWithLogrus. func NewContext( stdCtx context.Context, traceID TraceID, loggerInstance Logger, metrics Metrics, tracer Tracer, tags Fields, fields Fields, ) Context { if traceID == "" { traceID = NewTraceID() } if loggerInstance == nil { loggerInstance = logger.Dummy() } ctx := &ctxValue{ traceIDValue: traceID, debugTools: &debugTools{ loggerInstance: loggerInstance, metricsInstance: metrics, tracerInstance: tracer, }, } if stdCtx != nil && stdCtx != context.Background() { ctx = ctx.cloneWithStdContext(stdCtx).(*ctxValue) } if tags == nil { tags = Fields{} if buildinfo.BuildMode != "" { tags["buildMode"] = buildinfo.BuildMode } if buildinfo.BuildDate != "" { tags["buildDate"] = buildinfo.BuildDate } if buildinfo.Revision != "" { tags["revision"] = buildinfo.Revision } if DefaultLogHostname && hostname != "" { tags["hostname"] = hostname } if DefaultLogUsername && curUser != nil { tags["username"] = curUser.Name } } if len(tags) > 0 { ctx.debugTools.pendingTags.AddMultiple(tags) } if fields == nil { fields = Fields{} } if DefaultLogTraceID { fields["traceID"] = traceID } ctx.debugTools.pendingFields.AddMultiple(fields) return ctx } type CancelFunc = context.CancelFunc func (ctx *ctxValue) clone() *ctxValue { return &ctxValue{ traceIDValue: ctx.traceIDValue, debugTools: ctx.loadDebugTools().Clone(), valuesHandler: ctx.valuesHandler, eventHandler: ctx.eventHandler, parent: ctx, } } // Clone returns a derivative context in a new scope. Modifications of // this context will not affect the original one. func (ctx *ctxValue) Clone() Context { return ctx.clone() } // TraceID returns the tracing ID of the context. The tracing ID is // the passing-through ID which is used to identify a flow across multiple // services. func (ctx *ctxValue) TraceID() TraceID { return ctx.traceIDValue } // WithTraceID returns a derivative context with passed tracing ID. func (ctx *ctxValue) WithTraceID(traceID TraceID) Context { ctxClone := ctx.clone() ctxClone.traceIDValue = traceID return ctxClone.WithTag("traceID", traceID) } func (ctx *ctxValue) considerPendingTags() { oldDebugTools := ctx.loadDebugTools() if oldDebugTools.pendingTags.Slice == nil { return } // considerPendingTags could be called simultaneously with // pendingTags.Clone. // // Therefore storePendingTags is in the bottom. pendingTags := oldDebugTools.pendingTags.Compile() newDebugTools := *oldDebugTools if oldDebugTools.loggerInstance != nil { newDebugTools.loggerInstance = oldDebugTools.loggerInstance.WithFields(pendingTags) } if oldDebugTools.tracerInstance != nil { newDebugTools.tracerInstance = oldDebugTools.tracerInstance.WithFields(pendingTags) } if oldDebugTools.metricsInstance != nil { newDebugTools.metricsInstance = oldDebugTools.metricsInstance.WithTags(pendingTags) } // reset pending tags newDebugTools.pendingTags = fields.PendingFields{} // store ctx.storeDebugTools(&newDebugTools) } func (ctx *ctxValue) considerPendingFields() { oldDebugTools := ctx.loadDebugTools() if oldDebugTools.pendingFields.Slice == nil { return } // considerPendingFields could be called simultaneously with // pendingFields.Clone. // // Therefore storePendingFields is in the bottom. pendingFields := oldDebugTools.pendingFields.Compile() newDebugTools := *oldDebugTools if oldDebugTools.loggerInstance != nil { newDebugTools.loggerInstance = oldDebugTools.loggerInstance.WithFields(pendingFields) } if oldDebugTools.tracerInstance != nil { newDebugTools.tracerInstance = oldDebugTools.tracerInstance.WithFields(pendingFields) } // reset pending fields newDebugTools.pendingFields = fields.PendingFields{} // store ctx.storeDebugTools(&newDebugTools) } func (ctx *ctxValue) considerPending() { ctx.considerPendingTags() ctx.considerPendingFields() } // Logger returns a Logger. func (ctx *ctxValue) Logger() Logger { loggerInstance := ctx.loadDebugTools().loggerInstance if loggerInstance == nil { return nil } ctx.mutationSyncer.Do(func() { ctx.considerPending() loggerInstance = ctx.loadDebugTools().loggerInstance }) return loggerInstance } // WithLogger returns a derivative context with the Logger replaced with // the passed one. func (ctx *ctxValue) WithLogger(logger Logger) Context { ctxClone := ctx.clone() ctxClone.debugTools.loggerInstance = logger return ctxClone } // Metrics returns a Metrics handler. func (ctx *ctxValue) Metrics() Metrics { metricsInstance := ctx.loadDebugTools().metricsInstance if metricsInstance == nil { return nil } ctx.mutationSyncer.Do(func() { ctx.considerPending() metricsInstance = ctx.loadDebugTools().metricsInstance }) return metricsInstance } // WithMetrics returns a derivative context with the Metrics handler replaced with // the passed one. func (ctx *ctxValue) WithMetrics(metrics Metrics) Context { ctxClone := ctx.clone() ctxClone.debugTools.metricsInstance = metrics return ctxClone } // Tracer returns a Tracer handler. func (ctx *ctxValue) Tracer() Tracer { tracerInstance := ctx.loadDebugTools().tracerInstance if tracerInstance == nil { return dummyTracerInstance } ctx.mutationSyncer.Do(func() { ctx.considerPending() tracerInstance = ctx.loadDebugTools().tracerInstance }) return tracerInstance } // WithTracer returns a derivative context with the Tracer handler replaced with // the passed one. func (ctx *ctxValue) WithTracer(tracer Tracer) Context { ctxClone := ctx.clone() ctxClone.debugTools.tracerInstance = tracer return ctxClone } // WithTracer returns a derivative context with an added tag. func (ctx *ctxValue) WithTag(key string, value interface{}) Context { ctxClone := ctx.clone() ctxClone.debugTools.pendingTags.AddOne(key, value) return ctxClone } // WithTracer returns a derivative context with added tags. func (ctx *ctxValue) WithTags(fields Fields) Context { ctxClone := ctx.clone() ctxClone.debugTools.pendingTags.AddMultiple(fields) return ctxClone } // WithTracer returns a derivative context with an added field. func (ctx *ctxValue) WithField(key string, value interface{}) Context { ctxClone := ctx.clone() ctxClone.debugTools.pendingFields.AddOne(key, value) return ctxClone } // WithTracer returns a derivative context with added fields. func (ctx *ctxValue) WithFields(fields Fields) Context { ctxClone := ctx.clone() ctxClone.debugTools.pendingFields.AddMultiple(fields) return ctxClone } // Recover if supposed to be used in defer-s to log and stop panics. func (ctx *ctxValue) Recover() interface{} { // TODO: this is a very naive implementation and there's no way to // inject another handler, yet. // It makes sense to design and introduce a Sentry-like handling // of panics. r := recover() if r != nil { b := make([]byte, 65536) n := runtime.Stack(b, false) b = b[:n] ctx.Errorf("received panic: %v\n%s\n", r, b) } return r } func (ctx *ctxValue) Value(key interface{}) interface{} { if ctx.valuesHandler == nil { return nil } return ctx.valuesHandler.Value(key) } // LoggerFrom returns a logger from a context.Context if can find any. And // returns a dummy logger (which does nothing) if wasn't able to find any. func LoggerFrom(stdCtx context.Context) Logger { if stdCtx == nil { return logger.Dummy() } ctx, ok := stdCtx.(Context) if !ok { return logger.Dummy() } return ctx.Logger() } // StdCtxUntil is the same as Until, but returns a standard context // instead of a channel. func (ctx *ctxValue) StdCtxUntil(err error) context.Context { child := ctx.clone() if child.eventHandler == nil { return child } // Chain of thoughts: // * Implement resulting context.Context through an xcontext instance. // * It will require to wait until ctx.Until(err), but it will require // an additional routine. // * We do not want to leak routines, therefore we cancel the routine // if resulting ctx.Done() is not reachable anymore (GC-ed). Thus // we exploit SetFinalizer. // * To make SetFinalizer work we need to have different cancelSignal-s // in the waiting routine and in the finalizer, otherwise the SetFinalizer // will wait for a cancelSignal which will be always reachable from the // goroutine. // * To make different cancelSignals we create two eventHandlers: one // will be monitored by SetFinalizer (childHandler), the other will be // cancelled by the goroutine (parentHandler). And we will return // the childHandler. child.eventHandler = nil parentHandler := child.addEventHandler() childHandler := child.addEventHandler() garbageCollected := make(chan struct{}) runtime.SetFinalizer(&childHandler.cancelSignal, func(*chan struct{}) { close(garbageCollected) }) go func() { select { case <-garbageCollected: return case <-ctx.Until(err): parentHandler.cancel(ErrCanceled) } }() return child }