propagator/propagator.go (119 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package propagator
import (
"context"
"encoding/binary"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
// TraceContextHeaderName is the HTTP header field for Google Cloud Trace
// https://cloud.google.com/trace/docs/setup#force-trace
const TraceContextHeaderName = "x-cloud-trace-context"
// traceContextHeaderFormat is the regular expression pattern for valid Cloud Trace header value.
const traceContextHeaderFormat = "^(?P<trace_id>[0-9a-f]{32})/(?P<span_id>[0-9]{1,20})(;o=(?P<trace_flags>[0-9]))?$"
// traceContextHeaderRe is a regular expression object of TraceContextHeaderFormat.
var traceContextHeaderRe = regexp.MustCompile(traceContextHeaderFormat)
// cloudTraceContextHeaders is the list of headers that are propagated. Cloud Trace only requires
// one element in the list.
var cloudTraceContextHeaders = []string{TraceContextHeaderName}
var oneWayContextHeaders = []string{}
type errInvalidHeader struct {
header string
}
func (e errInvalidHeader) Error() string {
return fmt.Sprintf("invalid header %s", e.header)
}
// CloudTraceOneWayPropagator will propagate trace context from the w3c standard
// headers (traceparent and tracestate). If traceparent is not present, it will
// extract trace context from x-cloud-trace-context, and propagate that trace
// context forward using the w3c standard headers.
//
// This is the preferred mechanism of propagation as X-Cloud-Trace-Context sampling flag
// behaves subtly different from expectations in both w3c traceparent *and* opentelemetry
// propagation.
type CloudTraceOneWayPropagator struct {
CloudTraceFormatPropagator
}
// Inject does not inject anything for the oneway propagator.
func (p CloudTraceOneWayPropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {}
// Fields returns an empty list of fields, since the one way propagator does
// not inject any fields.
func (p CloudTraceOneWayPropagator) Fields() []string {
return oneWayContextHeaders
}
var _ propagation.TextMapPropagator = CloudTraceOneWayPropagator{}
// CloudTraceFormatPropagator is a TextMapPropagator that injects/extracts a context to/from the carrier
// following Google Cloud Trace format.
type CloudTraceFormatPropagator struct{}
func (p CloudTraceFormatPropagator) getHeaderValue(carrier propagation.TextMapCarrier) string {
return carrier.Get(TraceContextHeaderName)
}
// Inject injects a context to the carrier following Google Cloud Trace format.
// In this method, SpanID is expected to be stored in big endian.
func (p CloudTraceFormatPropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
// https://cloud.google.com/trace/docs/setup#force-trace
// Trace ID: 32-char hexadecimal value representing a 128-bit number.
// Span ID: decimal representation of the unsigned interger.
ary := sc.SpanID()
sid := binary.BigEndian.Uint64(ary[:])
flag, err := strconv.Atoi(sc.TraceFlags().String())
if err != nil {
return
}
header := fmt.Sprintf("%s/%d;o=%d",
sc.TraceID().String(),
sid,
flag,
)
carrier.Set(TraceContextHeaderName, header)
}
// spanContextFromXCTCHeader creates trace.SpanContext from XCTC header value.
func spanContextFromXCTCHeader(header string) (trace.SpanContext, error) {
match := traceContextHeaderRe.FindStringSubmatch(header)
if match == nil {
return trace.SpanContext{}, errInvalidHeader{header}
}
names := traceContextHeaderRe.SubexpNames()
var traceID, spanID, traceFlags string
for i, n := range names {
switch n {
case "trace_id":
traceID = match[i]
case "span_id":
spanID = match[i]
case "trace_flags":
traceFlags = match[i]
}
}
// non-recording Span
if traceID == strings.Repeat("0", 32) || spanID == "0" {
return trace.SpanContext{}, errInvalidHeader{header}
}
// https://cloud.google.com/trace/docs/setup#force-trace
// Trace ID: 32-char hexadecimal value representing a 128-bit number.
// Span ID: decimal representation of the unsigned interger.
tid, err := trace.TraceIDFromHex(traceID)
if err != nil {
log.Printf("CloudTraceFormatPropagator: invalid trace id %#v: %v", traceID, err)
return trace.SpanContext{}, errInvalidHeader{header}
}
sidUint, err := strconv.ParseUint(spanID, 10, 64)
if err != nil {
log.Printf("CloudTraceFormatPropagator: on span ID conversion: %v", err)
return trace.SpanContext{}, errInvalidHeader{header}
}
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, sidUint)
ary := [8]byte{}
copy(ary[:], buf)
sid := trace.SpanID(ary)
// XCTC's TRACE_TRUE option
// https://cloud.google.com/trace/docs/setup#force-trace
tf := trace.TraceFlags(0x01)
if traceFlags == "0" || traceFlags == "" {
tf = trace.TraceFlags(0x00)
}
scConfig := trace.SpanContextConfig{
TraceID: tid,
SpanID: sid,
TraceFlags: tf,
Remote: true,
}
return trace.NewSpanContext(scConfig), nil
}
// SpanContextFromRequest extracts a trace.SpanContext from the HTTP request req.
// In this method, SpanID is expected to be stored in big endian.
func SpanContextFromRequest(req *http.Request) (trace.SpanContext, error) {
h := req.Header.Get(TraceContextHeaderName)
return spanContextFromXCTCHeader(h)
}
// Extract extacts a context from the carrier if the header contains Google Cloud Trace header format.
// In this method, SpanID is expected to be stored in big endian.
func (p CloudTraceFormatPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
header := p.getHeaderValue(carrier)
if header == "" {
return ctx
}
sc, err := spanContextFromXCTCHeader(header)
if err != nil {
log.Printf("CloudTraceFormatPropagator: %v", err)
return ctx
}
return trace.ContextWithRemoteSpanContext(ctx, sc)
}
// Fields just returns the header name.
func (p CloudTraceFormatPropagator) Fields() []string {
return cloudTraceContextHeaders
}
// Confirming if CloudTraceFormatPropagator satisifies the TextMapPropagator interface.
var _ propagation.TextMapPropagator = CloudTraceFormatPropagator{}