209 lines
8.1 KiB
Go
209 lines
8.1 KiB
Go
|
package otelchi
|
||
|
|
||
|
import (
|
||
|
"net/http"
|
||
|
|
||
|
"github.com/go-chi/chi/v5"
|
||
|
otelmetric "go.opentelemetry.io/otel/metric"
|
||
|
"go.opentelemetry.io/otel/propagation"
|
||
|
oteltrace "go.opentelemetry.io/otel/trace"
|
||
|
)
|
||
|
|
||
|
// These defaults are used in `TraceHeaderConfig`.
|
||
|
const (
|
||
|
DefaultTraceIDResponseHeaderKey = "X-Trace-Id"
|
||
|
DefaultTraceSampledResponseHeaderKey = "X-Trace-Sampled"
|
||
|
)
|
||
|
|
||
|
// config is used to configure the mux middleware.
|
||
|
type config struct {
|
||
|
TracerProvider oteltrace.TracerProvider
|
||
|
MeterProvider otelmetric.MeterProvider
|
||
|
Propagators propagation.TextMapPropagator
|
||
|
ChiRoutes chi.Routes
|
||
|
RequestMethodInSpanName bool
|
||
|
Filters []Filter
|
||
|
TraceIDResponseHeaderKey string
|
||
|
TraceSampledResponseHeaderKey string
|
||
|
PublicEndpointFn func(r *http.Request) bool
|
||
|
|
||
|
DisableMeasureInflight bool
|
||
|
DisableMeasureSize bool
|
||
|
}
|
||
|
|
||
|
// Option specifies instrumentation configuration options.
|
||
|
type Option interface {
|
||
|
apply(*config)
|
||
|
}
|
||
|
|
||
|
type optionFunc func(*config)
|
||
|
|
||
|
func (o optionFunc) apply(c *config) {
|
||
|
o(c)
|
||
|
}
|
||
|
|
||
|
// Filter is a predicate used to determine whether a given http.Request should
|
||
|
// be traced. A Filter must return true if the request should be traced.
|
||
|
type Filter func(*http.Request) bool
|
||
|
|
||
|
// WithPropagators specifies propagators to use for extracting
|
||
|
// information from the HTTP requests. If none are specified, global
|
||
|
// ones will be used.
|
||
|
func WithPropagators(propagators propagation.TextMapPropagator) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.Propagators = propagators
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
|
||
|
// If none is specified, the global provider is used.
|
||
|
func WithTracerProvider(provider oteltrace.TracerProvider) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.TracerProvider = provider
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithMeterProvider specifies a meter provider to use for creating a meter.
|
||
|
// If none is specified, the global provider is used.
|
||
|
func WithMeterProvider(provider otelmetric.MeterProvider) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.MeterProvider = provider
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithMeasureInflightDisabled specifies whether to measure the number of inflight requests.
|
||
|
// If this option is not set, the number of inflight requests will be measured.
|
||
|
func WithMeasureInflightDisabled(isDisabled bool) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.DisableMeasureInflight = isDisabled
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithMeasureSizeDisabled specifies whether to measure the size of the response body.
|
||
|
// If this option is not set, the size of the response body will be measured.
|
||
|
func WithMeasureSizeDisabled(isDisabled bool) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.DisableMeasureSize = isDisabled
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithChiRoutes specified the routes that being used by application. Its main
|
||
|
// purpose is to provide route pattern as span name during span creation. If this
|
||
|
// option is not set, by default the span will be given name at the end of span
|
||
|
// execution. For some people, this behavior is not desirable since they want
|
||
|
// to override the span name on underlying handler. By setting this option, it
|
||
|
// is possible for them to override the span name.
|
||
|
func WithChiRoutes(routes chi.Routes) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.ChiRoutes = routes
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithRequestMethodInSpanName is used for adding http request method to span name.
|
||
|
// While this is not necessary for vendors that properly implemented the tracing
|
||
|
// specs (e.g Jaeger, AWS X-Ray, etc...), but for other vendors such as Elastic
|
||
|
// and New Relic this might be helpful.
|
||
|
//
|
||
|
// See following threads for details:
|
||
|
//
|
||
|
// - https://github.com/riandyrn/otelchi/pull/3#issuecomment-1005883910
|
||
|
// - https://github.com/riandyrn/otelchi/issues/6#issuecomment-1034461912
|
||
|
func WithRequestMethodInSpanName(isActive bool) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.RequestMethodInSpanName = isActive
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithFilter adds a filter to the list of filters used by the handler.
|
||
|
// If any filter indicates to exclude a request then the request will not be
|
||
|
// traced. All filters must allow a request to be traced for a Span to be created.
|
||
|
// If no filters are provided then all requests are traced.
|
||
|
// Filters will be invoked for each processed request, it is advised to make them
|
||
|
// simple and fast.
|
||
|
func WithFilter(filter Filter) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.Filters = append(cfg.Filters, filter)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithTraceIDResponseHeader enables adding trace id into response header.
|
||
|
// It accepts a function that generates the header key name. If this parameter
|
||
|
// function set to `nil` the default header key which is `X-Trace-Id` will be used.
|
||
|
//
|
||
|
// Deprecated: use `WithTraceResponseHeaders` instead.
|
||
|
func WithTraceIDResponseHeader(headerKeyFunc func() string) Option {
|
||
|
cfg := TraceHeaderConfig{
|
||
|
TraceIDHeader: "",
|
||
|
TraceSampledHeader: "",
|
||
|
}
|
||
|
if headerKeyFunc != nil {
|
||
|
cfg.TraceIDHeader = headerKeyFunc()
|
||
|
}
|
||
|
return WithTraceResponseHeaders(cfg)
|
||
|
}
|
||
|
|
||
|
// TraceHeaderConfig is configuration for trace headers in the response.
|
||
|
type TraceHeaderConfig struct {
|
||
|
TraceIDHeader string // if non-empty overrides the default of X-Trace-ID
|
||
|
TraceSampledHeader string // if non-empty overrides the default of X-Trace-Sampled
|
||
|
}
|
||
|
|
||
|
// WithTraceResponseHeaders configures the response headers for trace information.
|
||
|
// It accepts a TraceHeaderConfig struct that contains the keys for the Trace ID
|
||
|
// and Trace Sampled headers. If the provided keys are empty, default values will
|
||
|
// be used for the respective headers.
|
||
|
func WithTraceResponseHeaders(cfg TraceHeaderConfig) Option {
|
||
|
return optionFunc(func(c *config) {
|
||
|
c.TraceIDResponseHeaderKey = cfg.TraceIDHeader
|
||
|
if c.TraceIDResponseHeaderKey == "" {
|
||
|
c.TraceIDResponseHeaderKey = DefaultTraceIDResponseHeaderKey
|
||
|
}
|
||
|
|
||
|
c.TraceSampledResponseHeaderKey = cfg.TraceSampledHeader
|
||
|
if c.TraceSampledResponseHeaderKey == "" {
|
||
|
c.TraceSampledResponseHeaderKey = DefaultTraceSampledResponseHeaderKey
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// WithPublicEndpoint is used for marking every endpoint as public endpoint.
|
||
|
// This means if the incoming request has span context, it won't be used as
|
||
|
// parent span by the span generated by this middleware, instead the generated
|
||
|
// span will be the root span (new trace) and then linked to the span from the
|
||
|
// incoming request.
|
||
|
//
|
||
|
// Let say we have the following scenario:
|
||
|
//
|
||
|
// 1. We have 2 systems: `SysA` & `SysB`.
|
||
|
// 2. `SysA` has the following services: `SvcA.1` & `SvcA.2`.
|
||
|
// 3. `SysB` has the following services: `SvcB.1` & `SvcB.2`.
|
||
|
// 4. `SvcA.2` is used internally only by `SvcA.1`.
|
||
|
// 5. `SvcB.2` is used internally only by `SvcB.1`.
|
||
|
// 6. All of these services already instrumented otelchi & using the same collector (e.g Jaeger).
|
||
|
// 7. In `SvcA.1` we should set `WithPublicEndpoint()` since it is the entry point (a.k.a "public endpoint") for entering `SysA`.
|
||
|
// 8. In `SvcA.2` we should not set `WithPublicEndpoint()` since it is only used internally by `SvcA.1` inside `SysA`.
|
||
|
// 9. Point 7 & 8 also applies to both services in `SysB`.
|
||
|
//
|
||
|
// Now, whenever `SvcA.1` calls `SvcA.2` there will be only a single trace generated. This trace will contain 2 spans: root span from `SvcA.1` & child span from `SvcA.2`.
|
||
|
//
|
||
|
// But if let say `SvcA.2` calls `SvcB.1`, then there will be 2 traces generated: trace from `SysA` & trace from `SysB`. But in trace generated in `SysB` there will be like a marking that this trace is actually related to trace in `SysA` (a.k.a linked with the trace from `SysA`).
|
||
|
func WithPublicEndpoint() Option {
|
||
|
return WithPublicEndpointFn(func(r *http.Request) bool { return true })
|
||
|
}
|
||
|
|
||
|
// WithPublicEndpointFn runs with every request, and allows conditionally
|
||
|
// configuring the Handler to link the generated span with an incoming span
|
||
|
// context.
|
||
|
//
|
||
|
// If the function return `true` the generated span will be linked with the
|
||
|
// incoming span context. Otherwise, the generated span will be set as the
|
||
|
// child span of the incoming span context.
|
||
|
//
|
||
|
// Essentially it has the same functionality as `WithPublicEndpoint` but with
|
||
|
// more flexibility.
|
||
|
func WithPublicEndpointFn(fn func(r *http.Request) bool) Option {
|
||
|
return optionFunc(func(cfg *config) {
|
||
|
cfg.PublicEndpointFn = fn
|
||
|
})
|
||
|
}
|