Documentation
¶
Overview ¶
Package trace — propagation across a process boundary.
Package trace — range 0.2.20.* (ADR 0051 core/trace block).
Package trace — carrying a span context on a context.Context.
Package trace — declares the sentinel *errs.Error values. Each var's name equals its errs.Define Reason in SCREAMING_SNAKE form.
Package trace — Event: a timestamped point inside a span.
Package trace — the SpanExporter contract + process-wide exporter registry.
Package trace — the two identifiers a trace is built out of, and the flag byte that travels with them.
Package trace — Link: a reference to a span in another trace.
Package trace — the sampling port.
Package trace — what a Sampler is given to decide with.
Package trace — the instrumentation scope this domain publishes.
Package trace — the four facts that identify a span on the wire.
Package trace — SpanKind: the span's relationship to its neighbours.
Package trace — SpanValue: one finished span, the unit an exporter ships.
Package trace — SpansValue: the exportable payload.
Package trace — tracestate: the W3C vendor list that travels beside a traceparent.
Package trace — Status: whether the operation the span describes succeeded.
Package trace declares the SDK's distributed-tracing PORT: the span context that travels between processes, the OpenTelemetry trace data model, the W3C Trace Context propagation format, and the Tracer/Span pair an application instruments against.
It is the third pillar of observability beside `logger` and `metrics`, and — like `metrics` since ADR 0044 — it speaks the OpenTelemetry data model while importing none of OpenTelemetry's code. OTel is a published specification; this SDK implements it from the document, as it implements the Prometheus exposition format, RFC 7517 and a five-field POSIX cron. What OTel buys is INTEROPERABILITY, and interoperability is a property of the wire, not of the import graph.
Concrete implementations — the Tracer, the samplers, the recorder and the OTLP/JSON exporter — live in internal/service/trace. See ADR 0051.
Package trace — one list member of a tracestate header.
Package trace — the traceparent header: parsing it, and writing it back.
Index ¶
- Constants
- Variables
- func ContextWithSpanContext(parent context.Context, context SpanContextValue) context.Context
- func Export(name ExporterName, spans SpansValue) error
- func FormatTraceParent(context SpanContextValue) (header string, ok bool)
- func Inject(context SpanContextValue, carrier Carrier)
- func NormalizeScope(scope coremetrics.ScopeValue) coremetrics.ScopeValue
- type Carrier
- type EventValue
- type ExporterName
- type LinkValue
- type Sampler
- type SamplingParams
- type Span
- type SpanContextValue
- type SpanExporter
- type SpanID
- type SpanKind
- type SpanParams
- type SpanSink
- type SpanValue
- type SpansValue
- type StateValue
- type StatusCode
- type StatusValue
- type TraceFlags
- type TraceID
- type Tracer
Constants ¶
const ( // ExceptionEventName is the reserved event name for a recorded error. ExceptionEventName string = "exception" // ExceptionTypeKey names the error's type. ExceptionTypeKey string = "exception.type" // ExceptionMessageKey names the error's message. ExceptionMessageKey string = "exception.message" // ExceptionStacktraceKey names the error's stack trace. This SDK never // fills it: a Go error carries no stack, and inventing one from the // recording site would name the wrong goroutine. ExceptionStacktraceKey string = "exception.stacktrace" )
ExceptionEventName and the three attribute keys around it are the OpenTelemetry semantic convention for recording an error on a span.
They are constants here rather than strings at a call site because the whole value of a convention is that two producers spell it identically: a backend renders an event named exactly "exception" as an error, and renders "Exception" as an event nobody will ever look at.
const ( // TraceIDLen is the trace identifier's length in bytes. TraceIDLen int = 16 // SpanIDLen is the span identifier's length in bytes. SpanIDLen int = 8 // TraceIDHexLen is the trace identifier's length in lowercase hex. TraceIDHexLen int = TraceIDLen * 2 // SpanIDHexLen is the span identifier's length in lowercase hex. SpanIDHexLen int = SpanIDLen * 2 )
TraceIDLen and SpanIDLen are the byte lengths W3C Trace Context and opentelemetry/proto/trace/v1 both fix: 16 bytes of trace-id, 8 of span-id. They are not tunable — a 16-byte trace-id is the format, not a default.
const ( // MaxTraceStateMembers is the list cap: `list = list-member 0*31( OWS "," // OWS list-member )` is 32 members, no more. MaxTraceStateMembers int = 32 // MaxTraceStateKeyLen is the longest legal key: `simple-key = lcalpha // 0*255(...)` is 1 + 255 characters. MaxTraceStateKeyLen int = 256 // MaxTraceStateValueLen is the longest legal value: `value = 0*255(chr) // nblk-chr` is 255 + 1 characters. MaxTraceStateValueLen int = 256 )
The three limits the W3C tracestate grammar fixes. They are the GRAMMAR's own bounds, not ceilings this SDK invented, which is why there is no knob for any of them (ADR 0031 §refuse: an SDK-chosen ceiling here would be arbitrary, and the specification already supplies a non-arbitrary one).
const ( // TraceParentHeader carries the span context. TraceParentHeader string = "traceparent" // TraceStateHeader carries the vendor list. TraceStateHeader string = "tracestate" )
TraceParentHeader and TraceStateHeader are the two header names W3C Trace Context defines. Both are lowercase: §3.2.1 asks a vendor to SEND the name in lowercase while accepting any case on receipt, which is what http.Header's canonicalisation already does for a Go receiver.
const CodeDuplicateRegistration errs.Code = 0x00_02_14_05 // 0.2.20.5
CodeDuplicateRegistration identifies a boot-time SpanExporter registry collision: a nil exporter, or a distinct exporter claiming a taken Name.
const CodeExportFailed errs.Code = 0x00_02_14_04 // 0.2.20.4
CodeExportFailed identifies a span exporter that returned an error while shipping a batch (the exporter's cause rides the wrap trail).
const CodeInvalidSpanName errs.Code = 0x00_02_14_06 // 0.2.20.6
CodeInvalidSpanName identifies a Start call with an empty span name. A span's name is the low-cardinality operation label every backend groups on, so an empty one produces a trace nobody can search for.
const CodeInvalidTraceParent errs.Code = 0x00_02_14_01 // 0.2.20.1
CodeInvalidTraceParent identifies a traceparent header that cannot be read as a span context: a malformed field, the invalid version "ff", or an all-zero trace-id or parent-id — the two values W3C Trace Context declares invalid.
const CodeInvalidTraceState errs.Code = 0x00_02_14_02 // 0.2.20.2
CodeInvalidTraceState identifies a tracestate header that does not match the W3C list grammar: a key or value outside its character set, more than 32 list members, or the same key twice.
const CodeUnknownExporter errs.Code = 0x00_02_14_03 // 0.2.20.3
CodeUnknownExporter identifies an Export/Lookup naming a span exporter that no imported package has registered.
const DefaultScopeName string = "github.com/kitsunium/sdk/pkg/v1/trace"
DefaultScopeName names this SDK's TRACE package as the instrumenting library when a caller declares no scope of their own.
It is a separate constant from metrics' DefaultScopeName, and the difference is not cosmetic: an InstrumentationScope names the library that produced THIS signal, so stamping a span batch with `…/pkg/v1/metrics` would tell a backend that the metrics package emitted spans. Reusing the value would have been the one place where sharing the metrics types stopped being reuse and started being a wrong answer — which is exactly the line ADR 0051 §Decision 2 draws.
const ( // TraceParentLen is the length of a version-00 traceparent, exactly. TraceParentLen int = 55 )
The traceparent field offsets, derived from the grammar rather than counted by hand:
value = version "-" version-format version = 2HEXDIGLC version-format = trace-id "-" parent-id "-" trace-flags trace-id = 32HEXDIGLC parent-id = 16HEXDIGLC trace-flags = 2HEXDIGLC
which is 2 + 1 + 32 + 1 + 16 + 1 + 2 = 55 characters.
const ( // VersionSupported is the version this SDK emits: "00", the only version // the current Recommendation defines. VersionSupported string = "00" )
The two versions the specification names explicitly.
Variables ¶
var ( // InvalidTraceParent is returned when a traceparent header cannot be read. // Its Public never echoes the header: it is attacker-controlled text. InvalidTraceParent = errs.Define(CodeInvalidTraceParent, "INVALID_TRACE_PARENT", "The traceparent header is not a valid W3C trace context", "core/trace.ParseTraceParent: the header failed the W3C Trace Context grammar, or carried an all-zero trace-id or parent-id") // InvalidTraceState is returned when a tracestate header cannot be read. InvalidTraceState = errs.Define(CodeInvalidTraceState, "INVALID_TRACE_STATE", "The tracestate header is not a valid W3C trace state list", "core/trace.ParseTraceState: the header failed the W3C list grammar, exceeded 32 members, or repeated a key") // UnknownExporter is returned when no SpanExporter is registered under a Name. UnknownExporter = errs.Define(CodeUnknownExporter, "UNKNOWN_EXPORTER", "No trace exporter is registered under that name", "core/trace.Export: exporter absent from registry; blank-import the exporter's package") // ExportFailed wraps an exporter's failure while shipping spans. ExportFailed = errs.Define(CodeExportFailed, "EXPORT_FAILED", "The trace exporter failed to ship the spans", "core/trace.Export: the registered exporter returned an error") // DuplicateRegistration is the boot-time SpanExporter-registry panic sentinel. DuplicateRegistration = errs.Define(CodeDuplicateRegistration, "DUPLICATE_REGISTRATION", "A trace exporter is already registered under that name", "core/trace.RegisterExporter: a distinct exporter already claims this name, or a nil exporter was supplied") // InvalidSpanName is the panic sentinel for a Start call with no name. InvalidSpanName = errs.Define(CodeInvalidSpanName, "INVALID_SPAN_NAME", "A span name must not be empty", "core/trace: Tracer.Start requires a low-cardinality operation name; an empty one produces a trace no backend can group") )
Functions ¶
func ContextWithSpanContext ¶
func ContextWithSpanContext(parent context.Context, context SpanContextValue) context.Context
ContextWithSpanContext returns a copy of parent carrying context.
An INVALID context is still stored rather than skipped. Storing it is what makes "this scope deliberately has no trace" expressible: a handler that ran under an unsampled or absent traceparent shadows any outer context instead of silently re-parenting its children onto it.
func Export ¶
func Export(name ExporterName, spans SpansValue) error
Export ships spans through the SpanExporter registered as name. A missing exporter returns UnknownExporter; an exporter failure wraps as ExportFailed.
func FormatTraceParent ¶
func FormatTraceParent(context SpanContextValue) (header string, ok bool)
FormatTraceParent renders a span context as a version-00 traceparent header value, reporting ok=false when the context names no joinable span.
The pair is deliberate rather than an empty string: "there is no header for this context" is a real outcome, not a degenerate rendering. §3.2.2.3 and §3.2.2.4 make an all-zero identifier a value every conforming receiver MUST ignore, so emitting one would spend a header to say nothing and would make "we lost the context here" indistinguishable from "we never had one". A caller writes the header only when ok.
The flag byte is SANITIZED here and nowhere else: §3.2.2.5.2 requires a vendor to set every undefined bit to zero, and this is the one moment this SDK is acting as a producer. A bit received from an upstream that defines it is preserved in the SpanContextValue and dropped on the way out, which is the only combination that satisfies both halves of the specification.
func Inject ¶
func Inject(context SpanContextValue, carrier Carrier)
Inject writes context into carrier as a traceparent, plus a tracestate when the list is non-empty.
An INVALID context writes nothing at all — not an empty header, not a zero-filled one. §3.2.2.3/§3.2.2.4 require a receiver to ignore an all-zero identifier, so emitting one would spend a header on a value the next hop is obliged to discard, and would make "we lost the context here" indistinguishable from "we never had one".
An empty tracestate is likewise omitted rather than set blank, because §4.2 says a tracestate without a traceparent "is invalid and MUST be discarded" — the two headers are a pair, and a blank one is not half of it.
func NormalizeScope ¶
func NormalizeScope(scope coremetrics.ScopeValue) coremetrics.ScopeValue
NormalizeScope returns the ScopeValue a Tracer publishes, filling an empty Name with DefaultScopeName. Version is left exactly as supplied: it is optional in the specification, and inventing one would be a claim about code this package cannot see.
Types ¶
type Carrier ¶
Carrier is the two-method surface a header set has to present for a span context to travel through it.
It is exactly http.Header's Get/Set pair, on purpose and to the letter, so `trace.Inject(ctx, req.Header)` compiles with no adapter — and so this package still imports no net/http, which would drag an HTTP opinion into a contract that also has to serve a message queue and a gRPC metadata map.
IFACE-PLUGIN: it is FROZEN at two methods (ADR 0039). Adding a third — Del, Values, a multi-value read — would break every downstream two-method double at compile time with no deprecation window, and every one of those additions is reachable as a sibling interface instead. Two is also what makes http.Header satisfy it structurally, which is the whole ergonomics.
type EventValue ¶
type EventValue struct {
// Time is when it happened. An unset instant encodes as 0, which is what
// the schema means by an unknown timestamp.
Time time.Time
// Name is the event's low-cardinality label.
Name string
// Attrs are the event's typed dimensions, sorted by Key.
Attrs []coremetrics.AttrValue
}
EventValue is one thing that happened at an instant during a span, rather than over an interval — `opentelemetry/proto/trace/v1.Span.Event`.
An event is not a cheap child span, and the distinction is worth stating because the wrong choice is invisible until a trace is unreadable: an event has no identity, no duration and no children, so nothing can be a child of it and nothing can link to it. Use one for a moment worth marking on a span's timeline (a retry, a cache miss, an exception); use a child span for work that took time.
func (EventValue) Normalized ¶
func (e EventValue) Normalized() EventValue
Normalized returns the event a span actually records: attributes sorted, validated and owned, so the caller's slice cannot be mutated afterwards through the span.
type ExporterName ¶
type ExporterName string
ExporterName is the typed key under which a SpanExporter registers (e.g. "otlpjson"). The zero value ExporterName("") is reserved invalid.
The registry is deliberately the same shape as core/metrics' — named, reached by blank-importing the exporter's package, config-addressable. It EARNS its keep here for the reason it earns it there and for one more: ADR 0051 §Decision 5 requires the OTLP/HTTP emitter to be absent from it, and "absent from the registry" is only a statement anyone can check if there is a registry to be absent from. A test pins that absence.
func AvailableExporters ¶
func AvailableExporters() []ExporterName
AvailableExporters returns the sorted list of registered ExporterNames.
type LinkValue ¶
type LinkValue struct {
// Context identifies the linked span. A link whose context is invalid is
// dropped rather than emitted — it names nothing.
Context SpanContextValue
// Attrs describe the relationship, sorted by Key.
Attrs []coremetrics.AttrValue
}
LinkValue points at a span that is causally related to this one but is not its parent — `opentelemetry/proto/trace/v1.Span.Link`.
The canonical case is a batch: one consumer span processes fifty messages produced by fifty different traces. Making any one of them the parent would be a lie about forty-nine, and making the batch a child of all fifty is not a tree. A link says "related, elsewhere" without claiming ancestry.
It carries a whole SpanContextValue rather than a loose trace-id/span-id pair because the linked context's tracestate and sampled bit are part of what a backend needs: a link into a trace that was never sampled points at spans nobody stored.
func (LinkValue) Normalized ¶
Normalized returns the link a span actually records: attributes sorted, validated and owned.
type Sampler ¶
type Sampler func(params SamplingParams) bool
Sampler decides whether a span is recorded and exported. It reports true to keep the span, false to drop it.
It is a FUNC port, not an interface, applying ADR 0041 structurally: a published func type cannot grow a method at all, so there is no widening to forbid and no sibling to invent later.
The decision is taken ONCE, at the root ¶
This is the load-bearing rule of the whole domain and it is not a convention — it is what the `sampled` bit of traceparent is FOR. A Tracer consults a Sampler only when it is starting a root span; every child, in this process or in any process downstream, inherits the answer through SpanContextValue.IsSampled.
Deciding per span produces a trace with holes in the middle, and a trace with holes is worse than no trace: a span whose parent was dropped becomes an orphan a backend renders as its own root, so one request appears as several unrelated ones and the latency of the whole is unrecoverable. The failure is also invisible at the point that causes it — the process that dropped the parent shows nothing wrong at all.
A Sampler is therefore consulted on ROOT spans only. That is a property of the Tracer, stated here because this is where a reader will look for it.
Concurrency ¶
A Sampler MUST be safe for concurrent use and MUST NOT block: it runs inline on the goroutine starting the span, before any work the span measures.
type SamplingParams ¶
type SamplingParams struct {
// Parent is the context this span descends from, or the invalid zero
// value when this is a root.
Parent SpanContextValue
// TraceID is the trace the new span belongs to.
TraceID TraceID
// Name is the span's operation name.
Name string
// Kind is the span's relationship to its neighbours.
Kind SpanKind
// Attrs are the attributes supplied at Start, sorted by Key. Attributes
// added AFTER Start are invisible here — the decision has already been
// taken by then, which is the price of taking it once.
Attrs []coremetrics.AttrValue
// Links are the links supplied at Start.
Links []LinkValue
}
SamplingParams is everything a Sampler sees. It describes a span that does not exist yet, which is the point: the decision is taken before the span is created, so an unsampled span costs no recording at all.
TraceID is present and Parent may not be. On a ROOT span the trace id has just been minted and there is no parent; on a CHILD it is the parent's, inherited unchanged. A ratio sampler reads it either way, which is what makes the same sampler usable at both ends without a special case.
type Span ¶
type Span interface {
// SpanContext returns the span's identity — what Inject propagates.
SpanContext() SpanContextValue
// SetAttrs adds or replaces typed dimensions on the span.
SetAttrs(attrs ...coremetrics.AttrValue)
// AddEvent records a timestamped point inside the span.
AddEvent(name string, attrs ...coremetrics.AttrValue)
// SetStatus records the operation's outcome.
SetStatus(code StatusCode, message string)
// End closes the span and hands it to the Tracer's SpanSink.
End()
}
Span is one live, recording span.
IFACE-PLUGIN: FROZEN at five methods (ADR 0039). Two things a reader will expect and not find, each deliberate:
- There is no RecordError. It would have to decide what an error's TYPE is, which is a judgement about the caller's error model, and it is expressible as one AddEvent call. pkg/v1/trace ships it as a HELPER over this port instead, which is exactly the shape that does not freeze a decision into a port.
- End takes no timestamp. A span that ends at a time other than "now" is either replaying history or working around a clock, and both want a sibling interface rather than a parameter every caller has to pass a zero value to.
Every method is safe for concurrent use and every one is a NO-OP on an unsampled span, so instrumentation costs nothing when sampling is off. End is idempotent: the second call is ignored rather than emitting the span twice.
type SpanContextValue ¶
type SpanContextValue struct {
// TraceID identifies the whole trace. Shared by every span in it.
TraceID TraceID
// SpanID identifies this span within the trace.
SpanID SpanID
// Flags is the traceparent flag byte, kept EXACTLY as received. Undefined
// bits are masked at format time (TraceFlags.Sanitized), never on receipt.
Flags TraceFlags
// State is the tracestate list travelling beside the traceparent. It is
// propagated verbatim; this SDK adds no entry of its own.
State StateValue
// Remote reports whether this context was EXTRACTED from a carrier rather
// than created in this process. OTLP carries the fact as a span flag, and
// a backend needs it to tell "my caller" from "my own parent span".
Remote bool
}
SpanContextValue is the immutable identity of a span: what traceparent and tracestate carry between two processes, and what a child span inherits.
It is a VALUE, not a handle. A span context can be copied, compared field by field and stored on a context.Context without any of it referring back to a live span — which is what makes propagation across a process boundary the same operation as propagation across a function call.
The zero value is the INVALID context, and that is deliberate rather than incidental: an all-zero TraceID is exactly what W3C Trace Context §3.2.2.3 declares invalid, so "no trace here" and "a trace nobody may join" are one value with one spelling. Every consumer therefore has exactly one check to make — IsValid — instead of a nil test and a validity test that can disagree.
func Extract ¶
func Extract(carrier Carrier) SpanContextValue
Extract reads a span context out of carrier, returning the INVALID zero value when there is nothing usable to read.
It returns no error, and that is a decision rather than an omission. §4.3 prescribes exactly one behaviour for a malformed traceparent — "the vendor creates a new traceparent header and deletes tracestate" — so there is nothing for a caller to decide and nothing for them to handle. Handing them an error would invite the one response the specification forbids: failing a request because a stranger wrote a bad header. ParseTraceParent is the typed-error form, for a caller who is diagnosing rather than serving.
Three consequences of that rule, each visible here:
- A malformed traceparent DROPS the tracestate with it. The vendor list describes a trace that this process is about to replace, so keeping it would attach somebody else's state to a brand-new trace id.
- A malformed TRACESTATE does not drop the traceparent. §4.3 makes validating tracestate a MAY and permits discarding just that header, and the parent is independently well-formed — losing a valid parent over an unreadable vendor list would break the trace to protect an annotation.
- Two traceparent headers merge, under the RFC's rule on repeated header fields, into "value1,value2", which fails the grammar and restarts the trace. That is the correct outcome: two upstreams claiming different parents is not a case where either may be believed.
func ParseTraceParent ¶
func ParseTraceParent(header string) (context SpanContextValue, err error)
ParseTraceParent reads a traceparent header value into a span context.
It implements §3.2.4 "Versioning of traceparent" rather than only the version-00 grammar, and the difference is the whole point of the section: a future version is FORWARD-COMPATIBLE by construction, because every version is required to keep the first 55 characters exactly where version 00 puts them. So:
- "ff" is refused by name — the specification declares it invalid.
- "00" must be EXACTLY 55 characters. The version-00 grammar has no extension point, so trailing content is not "an unknown field", it is a header this version cannot describe.
- A HIGHER version is parsed from its first 55 characters, and anything beyond them must begin with a dash. Without that check a 56-character header whose 56th byte is a hex digit would be read as a valid context with a truncated flag field.
- Nothing past the flags is looked at: "vendors MUST NOT parse or assume anything about unknown fields for this version."
The returned context is marked Remote, because a traceparent by definition came from somewhere else.
A refusal is never the caller's cue to fail the request. §4.3 is explicit that a vendor confronted with a malformed traceparent "creates a new traceparent header and deletes tracestate" — the header is written by a stranger, and a request rejected over it is a denial of service with extra steps. See Extract, which is the shape that behaviour belongs in.
func SpanContextFromContext ¶
func SpanContextFromContext(ctx context.Context) SpanContextValue
SpanContextFromContext returns the span context carried by ctx, or the invalid zero value when there is none.
It never returns an error and never panics on a nil-ish context value: an absent trace is the normal state of most code, not a fault.
func (SpanContextValue) IsSampled ¶
func (c SpanContextValue) IsSampled() bool
IsSampled reports whether the sampled bit is set on this context.
It is the ONLY sampling question anything downstream asks. The decision is taken once, at the root of the trace, and travels in this bit; re-deciding per span produces a trace with holes in the middle, which is worse than no trace at all because it looks complete.
func (SpanContextValue) IsValid ¶
func (c SpanContextValue) IsValid() bool
IsValid reports whether the context names a joinable span: both identifiers present and non-zero.
func (SpanContextValue) WithState ¶
func (c SpanContextValue) WithState(state StateValue) SpanContextValue
WithState returns a copy of c carrying state. It exists so a caller who mutates the vendor list does not have to rebuild the identity around it.
type SpanExporter ¶
type SpanExporter interface {
Name() ExporterName
Export(spans SpansValue) error
}
SpanExporter ships a batch of finished spans to a backend. Implementations MUST be safe for concurrent use.
IFACE-PLUGIN: the registry stores plug-in exporters behind this interface; concrete exporter types stay unexported per package.
func LookupExporter ¶
func LookupExporter(name ExporterName) (e SpanExporter, ok bool)
LookupExporter returns the SpanExporter registered under name.
IFACE-PLUGIN: the registry stores plug-in exporters behind the SpanExporter interface — concrete backend types stay unexported.
func RegisterExporter ¶
func RegisterExporter(e SpanExporter) SpanExporter
RegisterExporter inserts e under e.Name() and returns it for singleton binding. Panics on a nil exporter or a distinct exporter on a taken Name.
IFACE-PLUGIN: the registry hands plug-in SpanExporter instances back to callers so each backend keeps its concrete type unexported.
"Nil" here means UNUSABLE, not only an untyped nil: a typed nil pointer and a plug-in whose type is not comparable both satisfy the port and neither can serve one call (see internal/kernel/plugin).
type SpanID ¶
SpanID identifies one span within a trace: 8 bytes, 16 lowercase hex digits. The all-zero value is invalid for the same normative reason TraceID's is (§3.2.2.4), and is additionally how "this span has no parent" is spelled — a root span's ParentSpanID is the zero value, which OTLP encodes as an omitted parentSpanId.
func ParseSpanID ¶
ParseSpanID reads 16 lowercase hex digits into a SpanID, refusing an all-zero result for the reason ParseTraceID does.
type SpanKind ¶
type SpanKind int32
SpanKind says how a span relates to its parent and its children, which is what lets a backend assemble a latency waterfall instead of a flat list.
The five values and their integers are opentelemetry/proto/trace/v1.Span.SpanKind, copied from the schema so the OTLP encoder emits the integer directly (OTLP/JSON encodes an enum as its NUMBER, never as its name).
const ( // SpanKindUnspecified is SPAN_KIND_UNSPECIFIED (0). It is the zero value, // and the schema's own comment says an implementation should set INTERNAL // instead — so it CLAMPS rather than travels (ADR 0031 §clamp): a span // created with no stated kind is doing work inside this process, which is // what INTERNAL means. Nothing this SDK emits carries it. SpanKindUnspecified SpanKind = iota // SpanKindInternal is SPAN_KIND_INTERNAL (1): work inside one process, // with no remote counterpart. SpanKindInternal // SpanKindServer is SPAN_KIND_SERVER (2): handling an inbound synchronous // request. Its parent is normally a remote CLIENT span. SpanKindServer // SpanKindClient is SPAN_KIND_CLIENT (3): making an outbound synchronous // request and waiting for it. SpanKindClient // SpanKindProducer is SPAN_KIND_PRODUCER (4): enqueuing a message whose // handling happens later, and elsewhere. SpanKindProducer // SpanKindConsumer is SPAN_KIND_CONSUMER (5): handling a message that was // enqueued earlier. Its parent finished long before it started, which is // why it is not simply a SERVER span. SpanKindConsumer )
The five kinds, in the schema's own declaration order: iota reproduces SPAN_KIND_UNSPECIFIED = 0 through SPAN_KIND_CONSUMER = 5 exactly, which is what the OTLP encoder emits as the enum's integer.
func (SpanKind) Resolved ¶
Resolved returns the kind a span actually carries: SpanKindUnspecified clamps to SpanKindInternal, every declared value is honoured, and anything else — a cast, since there is no other way to produce one — clamps too.
It clamps where Temporality refuses (ADR 0044 §Decision 3), and the asymmetry applies the clamp-or-refuse question honestly: an unstated temporality changes what a NUMBER means, so no default can be chosen for the caller; an unstated kind changes only how a span is drawn, and the specification itself names the default. A clamp is right exactly when the SDK is not substituting judgement.
type SpanParams ¶
type SpanParams struct {
// Kind is the span's relationship to its neighbours. The zero value
// resolves to SpanKindInternal (SpanKind.Resolved).
Kind SpanKind
// Attrs are the dimensions known at Start. They are the ONLY ones a
// Sampler sees, so anything the sampling decision depends on belongs here
// rather than in a later SetAttrs.
Attrs []coremetrics.AttrValue
// Links point at causally related spans in other traces.
Links []LinkValue
// StartTime overrides the span's start instant. The zero value means
// "now", which is what every caller but a replaying one wants — and it is
// a CLAMP rather than a refusal because "now" is a description of what the
// tracer does, not a value chosen on the caller's behalf (ADR 0031).
StartTime time.Time
}
SpanParams are the facts a span is born with.
It is a struct rather than a variadic option list because an option type is a published func the SDK would then have to freeze (ADR 0039), and because the zero value has to be usable: SpanParams{} is an INTERNAL span starting now with no attributes, which is the common case and reads as one at the call site.
type SpanSink ¶
type SpanSink func(span SpanValue)
SpanSink receives every span that ends and was sampled. It is where a Tracer hands its output, and it is a func port for the reason Sampler is.
It is the seam a batching processor would occupy (ADR 0051 §Deferred): a batch is a SpanSink that buffers and forwards, which is why the port is one span at a time rather than a slice — a sink that took slices would force every non-batching consumer to allocate one.
A SpanSink MUST be safe for concurrent use: spans end on whichever goroutine was doing the work. It runs on that goroutine, so a slow sink slows the request it is observing.
type SpanValue ¶
type SpanValue struct {
// Context identifies this span: trace-id, span-id, flags, tracestate.
Context SpanContextValue
// Parent identifies the span this one descends from. The zero value means
// this is a root span.
Parent SpanContextValue
// Name is the low-cardinality operation label a backend groups on.
Name string
// Kind is the span's relationship to its neighbours, already resolved —
// never SpanKindUnspecified on a span a Tracer produced.
Kind SpanKind
// StartTime opens the interval the span covers.
StartTime time.Time
// EndTime closes it. A span whose EndTime is unset never ended, and no
// exporter in this SDK will ship one.
EndTime time.Time
// Attrs are the span's typed dimensions, sorted by Key.
Attrs []coremetrics.AttrValue
// Events are timestamped points inside the interval, in the order they
// were recorded.
Events []EventValue
// Links point at causally related spans in other traces.
Links []LinkValue
// Status is the recorded outcome, already resolved.
Status StatusValue
}
SpanValue is a completed span: the OpenTelemetry trace data model's `Span` message, in Go.
It is IMMUTABLE and it is what a live Span becomes at End. Splitting the two is what keeps an exporter from ever seeing a span mid-flight: the recording type has mutators and no exporter can reach it, this one has no mutators and is all an exporter is given.
Two fields are whole SpanContextValues rather than loose identifiers, and both pay for themselves:
- Context carries the trace-id, the span-id, the sampled bit and the tracestate as ONE value — the same value that travelled in the traceparent, so nothing has to be reassembled to know what was propagated.
- Parent carries its own Remote flag, which is the only place the fact is recorded that this span's parent ran in another process. OTLP has a field for exactly that (`flags`, SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK), and a backend uses it to tell a service boundary from an internal call.
A root span's Parent is the invalid zero value, which OTLP encodes as an omitted parentSpanId — the schema's own spelling for "no parent".
type SpansValue ¶
type SpansValue struct {
// Resource identifies the producer, carried once for the whole payload.
Resource coremetrics.ResourceValue
// Scope identifies the instrumentation, carried once for the whole
// payload.
Scope coremetrics.ScopeValue
// Spans are the finished spans, in the order they ended. That order is
// stable and is what makes an exporter's output diffable; it is NOT
// causal — a parent ends after its children.
Spans []SpanValue
}
SpansValue is a batch of finished spans plus the two facts that describe all of them at once — the OTLP payload hierarchy flattened into one Go value.
It is the trace counterpart of metrics' SnapshotValue and it is shaped by the same rule (ADR 0044 §Decision 9): an OTLP encoder must be able to walk it WITHOUT regrouping or reconstruction.
SpansValue -> resourceSpans[0]
.Resource -> .resource.attributes
.Scope -> .scopeSpans[0].scope {name, version}
.Spans[i] -> .scopeSpans[0].spans[]
The two single-element levels are single because one Tracer has exactly one Resource and one Scope, exactly as one Meter does.
It carries NO StartTime/Time pair, and that absence is the model rather than an omission: a metrics snapshot is one window shared by every point, while every span carries its own interval. Copying a payload-level window down onto a span would overwrite the only timestamps a trace has.
func (SpansValue) IsEmpty ¶
func (s SpansValue) IsEmpty() bool
IsEmpty reports whether the payload carries no span. An exporter uses it to skip a POST that would carry nothing.
type StateValue ¶
type StateValue struct {
// contains filtered or unexported fields
}
StateValue is the parsed `tracestate` header: an ORDERED list of vendor entries, leftmost first.
The order is load-bearing and is the reason this is not a map. §3.5 states it twice over: "the order of unmodified key/value pairs MUST be preserved" and "modified keys SHOULD be moved to the beginning (left) of the list", because the leftmost entry is the system that touched the trace most recently. A map would lose exactly that, and lose it silently.
The value is IMMUTABLE: Insert and Delete return a new StateValue rather than mutating the receiver, so a span context handed to a handler cannot be edited underneath the caller that produced it. The zero value is a valid empty list.
This SDK writes NO entry of its own. It is not a tracing vendor with state to carry, and inventing a key would put a name nobody registered on every outbound request; Insert exists for a consumer who IS one.
func ParseTraceState ¶
func ParseTraceState(header string) (state StateValue, err error)
ParseTraceState reads a tracestate header value.
It refuses the whole header rather than salvaging the members it understood. §4.3 permits exactly that — "if the tracestate header cannot be parsed the vendor MAY discard the entire header" — and salvaging is worse than it looks: a half-parsed list forwarded to the next hop is a list this process INVENTED, carrying somebody else's vendor key with entries silently missing.
Empty and whitespace-only list members are SKIPPED rather than refused, which §3.3.1.1 requires: `list-member = (key "=" value) / OWS`.
One deliberate leniency, and it is the only one: OWS is stripped around EVERY member, including before the first and after the last, where the `list` rule grants no OWS slot. A strictly-positioned parser would refuse "a=1 " — the value's last character must be nblk-chr — but RFC 7230 §3.2.4 already requires a recipient to strip leading and trailing whitespace from a field value before it is a field-value, so that check could only ever fire on input the HTTP layer is specified to have normalised, and it would pay for that by discarding another vendor's entire list over whitespace nobody can see. The nblk-chr rule is enforced in Insert instead, where it still prevents something.
func (StateValue) Delete ¶
func (s StateValue) Delete(key string) StateValue
Delete returns a copy of s without the entry under key. §3.5 asks a vendor not to delete keys it did not generate; this method is the mechanism, not the permission.
func (StateValue) Get ¶
func (s StateValue) Get(key string) (value string, ok bool)
Get returns the value recorded under key, and whether it was present.
func (StateValue) Insert ¶
func (s StateValue) Insert(key, value string) (state StateValue, err error)
Insert returns a copy of s carrying key=value at the FRONT of the list, replacing any existing entry for key.
Front, not in place, because §3.5 says a modified key "SHOULD be moved to the beginning (left) of the list": leftmost means most recently touched, and that is the only information the order carries.
When the result would exceed 32 members the OLDEST — rightmost — entry is dropped, which is §3.3.1.5's "the vendor MUST truncate whole entries". Its companion rule, "entries larger than 128 characters SHOULD be removed first", is deliberately NOT implemented: it is scoped to truncation "due to size limitations", and this list is bounded by member COUNT, never by byte length. Implementing it would drop a short-lived vendor's entry for being verbose rather than for being old, which is a different policy wearing the same name.
func (StateValue) Len ¶
func (s StateValue) Len() int
Len reports how many list members the state carries.
func (StateValue) String ¶
func (s StateValue) String() string
String renders the list as a tracestate header value: `key=value` members joined by a comma, leftmost first, with no optional whitespace.
The OWS the grammar allows is not reproduced. It is optional, it carries no meaning, and omitting it makes the header a deterministic function of the list — which is what lets a test compare bytes rather than re-parse.
type StatusCode ¶
type StatusCode int32
StatusCode is opentelemetry/proto/trace/v1.Status.StatusCode. Three values, and the third is not the negation of the second.
const ( // StatusUnset is STATUS_CODE_UNSET (0), the default and the overwhelmingly // common value. It means "no explicit judgement was recorded" — NOT "it // worked". A backend applies its own heuristics (an HTTP 5xx, a timeout); // an SDK claiming OK on the caller's behalf would suppress exactly those. StatusUnset StatusCode = iota // StatusOK is STATUS_CODE_OK (1): the developer explicitly asserted // success, which the specification says should override any backend // heuristic. It is therefore a claim, never set by this SDK on its own. StatusOK // StatusError is STATUS_CODE_ERROR (2): the operation failed. StatusError )
The three codes, in the schema's own declaration order: iota reproduces STATUS_CODE_UNSET = 0 through STATUS_CODE_ERROR = 2 exactly, which is what the OTLP encoder emits as the enum's integer.
func (StatusCode) String ¶
func (c StatusCode) String() string
String renders the code as its OpenTelemetry name, for text output and readable test failures. The wire carries the integer.
type StatusValue ¶
type StatusValue struct {
// Code is the verdict.
Code StatusCode
// Message describes the failure. It is dropped unless Code is
// StatusError, and it is the ONE free-text field a span carries — so it
// must not be given a value the operator cannot see safely. It is not an
// errs Public: a span message is read by whoever reads the trace.
Message string
}
StatusValue is a span's recorded outcome.
The Message field is only meaningful on StatusError, and the schema says so: "message … SHOULD be used only if the code is ERROR". This type enforces it in Resolved rather than trusting the caller, because a message hanging off an OK status is the sort of thing that renders as an error in one backend's UI and vanishes in another's.
func (StatusValue) IsUnset ¶
func (s StatusValue) IsUnset() bool
IsUnset reports whether the status carries no judgement at all. It is what the OTLP encoder asks before deciding whether the `status` field is worth a line.
func (StatusValue) Resolved ¶
func (s StatusValue) Resolved() StatusValue
Resolved returns the status a span actually carries: an undeclared code clamps to StatusUnset, and a Message survives only on StatusError.
type TraceFlags ¶
type TraceFlags byte
TraceFlags is the 8-bit field traceparent carries as its last component.
Version 00 of the specification defines exactly one bit, and §3.2.2.5.2 is explicit about the rest: "The behavior of other flags … is not defined and is reserved for future use. Vendors MUST set those to zero." This SDK therefore MASKS on output rather than trusting what it was handed — see Sanitized.
const FlagSampled TraceFlags = 0x01
FlagSampled is the sampled bit: the least significant one. When set, the caller "may have recorded trace data" (§3.2.2.5.1), which is what makes the sampling decision a property of the TRACE rather than of each span.
func (TraceFlags) IsSampled ¶
func (f TraceFlags) IsSampled() bool
IsSampled reports whether the sampled bit is set.
func (TraceFlags) Sanitized ¶
func (f TraceFlags) Sanitized() TraceFlags
Sanitized returns f with every bit this specification version does not define cleared.
It is applied on the way OUT, never on the way in. §3.2.2.5.2 says a vendor MUST set undefined bits to zero, so emitting a bit we did not set would make this SDK a non-conforming producer; but §3.2.4 says a receiver must not "assume anything about unknown fields", so clearing them on the way in would destroy information a future version defines. Keeping the received byte and masking at format time satisfies both halves at once.
func (TraceFlags) WithSampled ¶
func (f TraceFlags) WithSampled(sampled bool) TraceFlags
WithSampled returns f with the sampled bit set to sampled, leaving every other bit as it was.
type TraceID ¶
type TraceID [TraceIDLen]byte
TraceID identifies one trace: 16 bytes, rendered as 32 lowercase hex digits on the wire.
The ALL-ZERO value is invalid, and that is normative rather than stylistic — W3C Trace Context §3.2.2.3 says a receiver "MUST ignore the traceparent" when the trace-id is all zeroes, and the OTLP schema documents the same. It is therefore the zero value of this type AND its "unset" spelling, which is why IsValid exists and why nothing in this package accepts an id without asking.
func ParseTraceID ¶
ParseTraceID reads 32 lowercase hex digits into a TraceID, refusing an all-zero result.
Uppercase is refused rather than folded: the traceparent grammar is 32HEXDIGLC and a producer emitting uppercase is producing a header other vendors will reject, so accepting it here would hide the defect exactly one hop from where it was introduced.
func (TraceID) IsValid ¶
IsValid reports whether t is a usable trace identifier, i.e. not all zeroes.
func (TraceID) String ¶
String renders the identifier as 32 lowercase hex digits — the spelling both traceparent and OTLP/JSON use. OTLP/JSON is the one place the protobuf-JSON mapping is overridden: "the traceId and spanId byte arrays are represented as case-insensitive hex-encoded strings; they are not base64-encoded".
type Tracer ¶
type Tracer interface {
Start(ctx context.Context, name string, params SpanParams) (child context.Context, span Span)
}
Tracer starts spans. It is the only way a span comes into existence.
IFACE-PLUGIN: it is FROZEN at one method (ADR 0039). Everything a caller might want to add — a second Start taking an explicit clock, a Flush, a Shutdown — is reachable as a sibling interface or as a method on the concrete type, and none of them is worth breaking every downstream double for.
Start returns a context carrying the new span's context, so a callee that starts its own span becomes a child without either function naming the other. The Span is returned separately rather than fished back out of the context, because a caller who has to look up the span they just created will eventually look up the wrong one.
An EMPTY name panics with InvalidSpanName: a span name is the low-cardinality operation label every backend groups on, it is a literal at the call site, and it is therefore wrong on the first call or never — the same argument that makes an unusable attribute key a panic in `metrics`.