module/apmhttp/traceheaders.go (104 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 apmhttp // import "go.elastic.co/apm/module/apmhttp/v2"
import (
"encoding/hex"
"strconv"
"strings"
"github.com/pkg/errors"
"go.elastic.co/apm/v2"
)
const (
// TraceparentHeader is the HTTP header for trace propagation.
//
// For backwards compatibility, this is currently an alias for
// for ElasticTraceparentHeader, but the more specific constants
// below should be preferred. In a future version this will be
// replaced by the standard W3C header.
TraceparentHeader = ElasticTraceparentHeader
// ElasticTraceparentHeader is the legacy HTTP header for trace propagation,
// maintained for backwards compatibility with older agents.
ElasticTraceparentHeader = "Elastic-Apm-Traceparent"
// W3CTraceparentHeader is the standard W3C Trace-Context HTTP
// header for trace propagation.
W3CTraceparentHeader = "Traceparent"
// TracestateHeader is the standard W3C Trace-Context HTTP header
// for vendor-specific trace propagation.
TracestateHeader = "Tracestate"
)
// FormatTraceparentHeader formats the given trace context as a
// traceparent header.
func FormatTraceparentHeader(c apm.TraceContext) string {
var traceId [32]byte
hex.Encode(traceId[:], c.Trace[:])
var spanId [16]byte
hex.Encode(spanId[:], c.Span[:])
opts := strconv.FormatUint(uint64(c.Options), 16)
if len(opts) == 1 {
opts = "0" + opts
}
return "00-" + string(traceId[:]) + "-" + string(spanId[:]) + "-" + opts
}
// ParseTraceparentHeader parses the given header, which is expected to be in
// the W3C Trace-Context traceparent format according to W3C Editor's Draft 23 May 2018:
//
// https://w3c.github.io/trace-context/#traceparent-field
//
// Note that the returned TraceContext's Trace and Span fields are not necessarily
// valid. The caller must decide whether or not it wishes to disregard invalid
// trace/span IDs, and validate them as required using their provided Validate
// methods.
//
// The returned TraceContext's TraceState field will be the empty value. Use
// ParseTracestateHeader to parse that separately.
func ParseTraceparentHeader(h string) (apm.TraceContext, error) {
var out apm.TraceContext
if len(h) < 3 || h[2] != '-' {
return out, errors.Errorf("invalid traceparent header %q", h)
}
var version byte
if !strings.HasPrefix(h, "00") {
decoded, err := hex.DecodeString(h[:2])
if err != nil {
return out, errors.Wrap(err, "error decoding traceparent header version")
}
version = decoded[0]
}
h = h[3:]
switch version {
case 255:
// "Version 255 is invalid."
return out, errors.Errorf("traceparent header version 255 is forbidden")
default:
// "If higher version is detected - implementation SHOULD try to parse it."
fallthrough
case 0:
// Version 00:
//
// version-format = trace-id "-" span-id "-" trace-options
// trace-id = 32HEXDIG
// span-id = 16HEXDIG
// trace-options = 2HEXDIG
const (
traceIDEnd = 32
spanIDStart = traceIDEnd + 1
spanIDEnd = spanIDStart + 16
traceOptionsStart = spanIDEnd + 1
traceOptionsEnd = traceOptionsStart + 2
)
switch {
case len(h) < traceOptionsEnd,
h[traceIDEnd] != '-',
h[spanIDEnd] != '-',
version == 0 && len(h) != traceOptionsEnd,
version > 0 && len(h) > traceOptionsEnd && h[traceOptionsEnd] != '-':
return out, errors.Errorf("invalid version %d traceparent header %q", version, h)
}
if _, err := hex.Decode(out.Trace[:], []byte(h[:traceIDEnd])); err != nil {
return out, errors.Wrapf(err, "error decoding trace-id for version %d", version)
}
if err := out.Trace.Validate(); err != nil {
return out, errors.Wrap(err, "invalid trace-id")
}
if _, err := hex.Decode(out.Span[:], []byte(h[spanIDStart:spanIDEnd])); err != nil {
return out, errors.Wrapf(err, "error decoding span-id for version %d", version)
}
if err := out.Span.Validate(); err != nil {
return out, errors.Wrap(err, "invalid span-id")
}
var traceOptions [1]byte
if _, err := hex.Decode(traceOptions[:], []byte(h[traceOptionsStart:traceOptionsEnd])); err != nil {
return out, errors.Wrapf(err, "error decoding trace-options for version %d", version)
}
out.Options = apm.TraceOptions(traceOptions[0])
return out, nil
}
}
// ParseTracestateHeader parses the given header, which is expected to be in the
// W3C Trace-Context tracestate format according to W3C Editor's Draft 18 Nov 2019:
//
// https://w3c.github.io/trace-context/#tracestate-header
//
// Note that the returned TraceState is not necessarily valid. The caller must
// decide whether or not it wishes to disregard invalid tracestate entries, and
// validate them as required using their provided Validate methods.
//
// Multiple header values may be presented, in which case they will be treated as
// if they are concatenated together with commas.
func ParseTracestateHeader(h ...string) (apm.TraceState, error) {
var entries []apm.TraceStateEntry
for _, h := range h {
for {
h = strings.TrimSpace(h)
if h == "" {
break
}
kv := h
if comma := strings.IndexRune(h, ','); comma != -1 {
kv = strings.TrimSpace(h[:comma])
h = h[comma+1:]
} else {
h = ""
}
equal := strings.IndexRune(kv, '=')
if equal == -1 {
return apm.TraceState{}, errors.New("missing '=' in tracestate entry")
}
entries = append(entries, apm.TraceStateEntry{Key: kv[:equal], Value: kv[equal+1:]})
}
}
return apm.NewTraceState(entries...), nil
}