2022-04-06 23:20:29 +00:00
|
|
|
package tracing
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
2022-04-11 23:02:13 +00:00
|
|
|
"fmt"
|
2022-05-18 11:11:38 +00:00
|
|
|
"math"
|
2022-04-06 23:20:29 +00:00
|
|
|
"net/http"
|
2022-04-11 23:02:13 +00:00
|
|
|
"os"
|
|
|
|
"runtime"
|
2022-07-26 21:00:53 +00:00
|
|
|
"strings"
|
2022-04-06 23:20:29 +00:00
|
|
|
|
2022-04-11 19:57:50 +00:00
|
|
|
"github.com/rs/zerolog"
|
2022-04-21 21:37:16 +00:00
|
|
|
otelContrib "go.opentelemetry.io/contrib/propagators/jaeger"
|
2022-04-06 23:20:29 +00:00
|
|
|
"go.opentelemetry.io/otel"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
"go.opentelemetry.io/otel/codes"
|
|
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
|
|
|
"go.opentelemetry.io/otel/propagation"
|
|
|
|
"go.opentelemetry.io/otel/sdk/resource"
|
|
|
|
tracesdk "go.opentelemetry.io/otel/sdk/trace"
|
|
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
service = "cloudflared"
|
|
|
|
tracerInstrumentName = "origin"
|
|
|
|
|
2022-04-11 23:02:13 +00:00
|
|
|
TracerContextName = "cf-trace-id"
|
|
|
|
TracerContextNameOverride = "uber-trace-id"
|
2022-04-11 19:57:50 +00:00
|
|
|
|
|
|
|
IntCloudflaredTracingHeader = "cf-int-cloudflared-tracing"
|
2022-05-18 11:11:38 +00:00
|
|
|
|
|
|
|
MaxErrorDescriptionLen = 100
|
|
|
|
traceHttpStatusCodeKey = "upstreamStatusCode"
|
2022-07-26 21:00:53 +00:00
|
|
|
|
|
|
|
traceID128bitsWidth = 128 / 4
|
|
|
|
separator = ":"
|
2022-04-06 23:20:29 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2022-04-11 19:57:50 +00:00
|
|
|
CanonicalCloudflaredTracingHeader = http.CanonicalHeaderKey(IntCloudflaredTracingHeader)
|
2022-04-11 23:02:13 +00:00
|
|
|
Http2TransportAttribute = trace.WithAttributes(transportAttributeKey.String("http2"))
|
|
|
|
QuicTransportAttribute = trace.WithAttributes(transportAttributeKey.String("quic"))
|
|
|
|
HostOSAttribute = semconv.HostTypeKey.String(runtime.GOOS)
|
|
|
|
HostArchAttribute = semconv.HostArchKey.String(runtime.GOARCH)
|
2022-04-06 23:20:29 +00:00
|
|
|
|
2022-04-11 23:02:13 +00:00
|
|
|
otelVersionAttribute attribute.KeyValue
|
|
|
|
hostnameAttribute attribute.KeyValue
|
|
|
|
cloudflaredVersionAttribute attribute.KeyValue
|
|
|
|
serviceAttribute = semconv.ServiceNameKey.String(service)
|
|
|
|
|
|
|
|
transportAttributeKey = attribute.Key("transport")
|
|
|
|
otelVersionAttributeKey = attribute.Key("jaeger.version")
|
2022-04-06 23:20:29 +00:00
|
|
|
|
|
|
|
errNoopTracerProvider = errors.New("noop tracer provider records no spans")
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
// Register the jaeger propagator globally.
|
|
|
|
otel.SetTextMapPropagator(otelContrib.Jaeger{})
|
2022-04-11 23:02:13 +00:00
|
|
|
otelVersionAttribute = otelVersionAttributeKey.String(fmt.Sprintf("go-otel-%s", otel.Version()))
|
|
|
|
if hostname, err := os.Hostname(); err == nil {
|
|
|
|
hostnameAttribute = attribute.String("hostname", hostname)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func Init(version string) {
|
|
|
|
cloudflaredVersionAttribute = semconv.ProcessRuntimeVersionKey.String(version)
|
2022-04-06 23:20:29 +00:00
|
|
|
}
|
|
|
|
|
2022-07-26 21:00:53 +00:00
|
|
|
type TracedHTTPRequest struct {
|
2022-04-06 23:20:29 +00:00
|
|
|
*http.Request
|
2022-07-26 21:00:53 +00:00
|
|
|
*cfdTracer
|
2022-04-06 23:20:29 +00:00
|
|
|
}
|
|
|
|
|
2022-07-26 21:00:53 +00:00
|
|
|
// NewTracedHTTPRequest creates a new tracer for the current HTTP request context.
|
|
|
|
func NewTracedHTTPRequest(req *http.Request, log *zerolog.Logger) *TracedHTTPRequest {
|
2022-04-06 23:20:29 +00:00
|
|
|
ctx, exists := extractTrace(req)
|
|
|
|
if !exists {
|
2022-07-26 21:00:53 +00:00
|
|
|
return &TracedHTTPRequest{req, &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}}
|
|
|
|
}
|
|
|
|
return &TracedHTTPRequest{req.WithContext(ctx), newCfdTracer(ctx, log)}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tr *TracedHTTPRequest) ToTracedContext() *TracedContext {
|
|
|
|
return &TracedContext{tr.Context(), tr.cfdTracer}
|
|
|
|
}
|
|
|
|
|
|
|
|
type TracedContext struct {
|
|
|
|
context.Context
|
|
|
|
*cfdTracer
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewTracedHTTPRequest creates a new tracer for the current HTTP request context.
|
|
|
|
func NewTracedContext(ctx context.Context, traceContext string, log *zerolog.Logger) *TracedContext {
|
|
|
|
ctx, exists := extractTraceFromString(ctx, traceContext)
|
|
|
|
if !exists {
|
|
|
|
return &TracedContext{ctx, &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}}
|
2022-04-06 23:20:29 +00:00
|
|
|
}
|
2022-07-26 21:00:53 +00:00
|
|
|
return &TracedContext{ctx, newCfdTracer(ctx, log)}
|
|
|
|
}
|
|
|
|
|
|
|
|
type cfdTracer struct {
|
|
|
|
trace.TracerProvider
|
|
|
|
exporter InMemoryClient
|
|
|
|
log *zerolog.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCfdTracer creates a new tracer for the current request context.
|
|
|
|
func newCfdTracer(ctx context.Context, log *zerolog.Logger) *cfdTracer {
|
2022-04-06 23:20:29 +00:00
|
|
|
mc := new(InMemoryOtlpClient)
|
2022-07-26 21:00:53 +00:00
|
|
|
exp, err := otlptrace.New(ctx, mc)
|
2022-04-06 23:20:29 +00:00
|
|
|
if err != nil {
|
2022-07-26 21:00:53 +00:00
|
|
|
return &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}
|
2022-04-06 23:20:29 +00:00
|
|
|
}
|
|
|
|
tp := tracesdk.NewTracerProvider(
|
|
|
|
// We want to dump to in-memory exporter immediately
|
|
|
|
tracesdk.WithSyncer(exp),
|
|
|
|
// Record information about this application in a Resource.
|
|
|
|
tracesdk.WithResource(resource.NewWithAttributes(
|
|
|
|
semconv.SchemaURL,
|
2022-04-11 23:02:13 +00:00
|
|
|
serviceAttribute,
|
|
|
|
otelVersionAttribute,
|
|
|
|
hostnameAttribute,
|
|
|
|
cloudflaredVersionAttribute,
|
|
|
|
HostOSAttribute,
|
|
|
|
HostArchAttribute,
|
2022-04-06 23:20:29 +00:00
|
|
|
)),
|
|
|
|
)
|
|
|
|
|
2022-07-26 21:00:53 +00:00
|
|
|
return &cfdTracer{tp, mc, log}
|
2022-04-06 23:20:29 +00:00
|
|
|
}
|
|
|
|
|
2022-07-26 21:00:53 +00:00
|
|
|
func (cft *cfdTracer) Tracer() trace.Tracer {
|
2022-04-06 23:20:29 +00:00
|
|
|
return cft.TracerProvider.Tracer(tracerInstrumentName)
|
|
|
|
}
|
|
|
|
|
2022-07-26 21:00:53 +00:00
|
|
|
// GetSpans returns the spans as base64 encoded string of protobuf otlp traces.
|
|
|
|
func (cft *cfdTracer) GetSpans() (enc string) {
|
2022-04-11 19:57:50 +00:00
|
|
|
enc, err := cft.exporter.Spans()
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
break
|
|
|
|
case errNoTraces:
|
2022-07-26 21:00:53 +00:00
|
|
|
cft.log.Error().Err(err).Msgf("expected traces to be available")
|
2022-04-11 19:57:50 +00:00
|
|
|
return
|
|
|
|
case errNoopTracer:
|
|
|
|
return // noop tracer has no traces
|
|
|
|
default:
|
2022-07-26 21:00:53 +00:00
|
|
|
cft.log.Error().Err(err)
|
2022-04-11 19:57:50 +00:00
|
|
|
return
|
|
|
|
}
|
2022-07-26 21:00:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddSpans assigns spans as base64 encoded protobuf otlp traces to provided
|
|
|
|
// HTTP headers.
|
|
|
|
func (cft *cfdTracer) AddSpans(headers http.Header) {
|
|
|
|
if headers == nil {
|
|
|
|
cft.log.Error().Msgf("provided headers map is nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
enc := cft.GetSpans()
|
2022-04-11 19:57:50 +00:00
|
|
|
// No need to add header if no traces
|
|
|
|
if enc == "" {
|
2022-07-26 21:00:53 +00:00
|
|
|
cft.log.Error().Msgf("no traces provided and no error from exporter")
|
2022-04-11 19:57:50 +00:00
|
|
|
return
|
|
|
|
}
|
2022-05-18 10:11:48 +00:00
|
|
|
|
2022-04-11 19:57:50 +00:00
|
|
|
headers[CanonicalCloudflaredTracingHeader] = []string{enc}
|
2022-04-06 23:20:29 +00:00
|
|
|
}
|
|
|
|
|
2022-05-18 11:11:38 +00:00
|
|
|
// EndWithErrorStatus will set a status for the span and then end it.
|
|
|
|
func EndWithErrorStatus(span trace.Span, err error) {
|
|
|
|
endSpan(span, -1, codes.Error, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// EndWithStatusCode will set a status for the span and then end it.
|
|
|
|
func EndWithStatusCode(span trace.Span, statusCode int) {
|
|
|
|
endSpan(span, statusCode, codes.Ok, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// EndWithErrorStatus will set a status for the span and then end it.
|
|
|
|
func endSpan(span trace.Span, upstreamStatusCode int, spanStatusCode codes.Code, err error) {
|
2022-04-06 23:20:29 +00:00
|
|
|
if span == nil {
|
|
|
|
return
|
|
|
|
}
|
2022-05-18 11:11:38 +00:00
|
|
|
|
|
|
|
if upstreamStatusCode > 0 {
|
|
|
|
span.SetAttributes(attribute.Int(traceHttpStatusCodeKey, upstreamStatusCode))
|
|
|
|
}
|
|
|
|
|
|
|
|
// add error to status buf cap description
|
|
|
|
errDescription := ""
|
|
|
|
if err != nil {
|
|
|
|
errDescription = err.Error()
|
|
|
|
l := int(math.Min(float64(len(errDescription)), MaxErrorDescriptionLen))
|
|
|
|
errDescription = errDescription[:l]
|
|
|
|
}
|
|
|
|
|
|
|
|
span.SetStatus(spanStatusCode, errDescription)
|
2022-04-06 23:20:29 +00:00
|
|
|
span.End()
|
|
|
|
}
|
|
|
|
|
2022-07-26 21:00:53 +00:00
|
|
|
// extractTraceFromString will extract the trace information from the provided
|
|
|
|
// propagated trace string context.
|
|
|
|
func extractTraceFromString(ctx context.Context, trace string) (context.Context, bool) {
|
|
|
|
if trace == "" {
|
|
|
|
return ctx, false
|
|
|
|
}
|
|
|
|
// Jaeger specific separator
|
|
|
|
parts := strings.Split(trace, separator)
|
|
|
|
if len(parts) != 4 {
|
|
|
|
return ctx, false
|
|
|
|
}
|
|
|
|
if parts[0] == "" {
|
|
|
|
return ctx, false
|
|
|
|
}
|
|
|
|
// Correctly left pad the trace to a length of 32
|
|
|
|
if len(parts[0]) < traceID128bitsWidth {
|
|
|
|
left := traceID128bitsWidth - len(parts[0])
|
|
|
|
parts[0] = strings.Repeat("0", left) + parts[0]
|
|
|
|
trace = strings.Join(parts, separator)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Override the 'cf-trace-id' as 'uber-trace-id' so the jaeger propagator can extract it.
|
|
|
|
traceHeader := map[string]string{TracerContextNameOverride: trace}
|
|
|
|
remoteCtx := otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(traceHeader))
|
|
|
|
return remoteCtx, true
|
|
|
|
}
|
|
|
|
|
2022-04-11 23:02:13 +00:00
|
|
|
// extractTrace attempts to check for a cf-trace-id from a request and return the
|
|
|
|
// trace context with the provided http.Request.
|
2022-04-06 23:20:29 +00:00
|
|
|
func extractTrace(req *http.Request) (context.Context, bool) {
|
|
|
|
// Only add tracing for requests with appropriately tagged headers
|
2022-04-11 23:02:13 +00:00
|
|
|
remoteTraces := req.Header.Values(TracerContextName)
|
2022-04-06 23:20:29 +00:00
|
|
|
if len(remoteTraces) <= 0 {
|
|
|
|
// Strip the cf-trace-id header
|
2022-04-11 23:02:13 +00:00
|
|
|
req.Header.Del(TracerContextName)
|
2022-04-06 23:20:29 +00:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
2022-04-11 23:02:13 +00:00
|
|
|
traceHeader := map[string]string{}
|
2022-04-06 23:20:29 +00:00
|
|
|
for _, t := range remoteTraces {
|
|
|
|
// Override the 'cf-trace-id' as 'uber-trace-id' so the jaeger propagator can extract it.
|
|
|
|
// Last entry wins if multiple provided
|
2022-04-11 23:02:13 +00:00
|
|
|
traceHeader[TracerContextNameOverride] = t
|
2022-04-06 23:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Strip the cf-trace-id header
|
2022-04-11 23:02:13 +00:00
|
|
|
req.Header.Del(TracerContextName)
|
2022-04-06 23:20:29 +00:00
|
|
|
|
2022-04-11 23:02:13 +00:00
|
|
|
if traceHeader[TracerContextNameOverride] == "" {
|
2022-04-06 23:20:29 +00:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
remoteCtx := otel.GetTextMapPropagator().Extract(req.Context(), propagation.MapCarrier(traceHeader))
|
|
|
|
return remoteCtx, true
|
|
|
|
}
|