Span API
🚧 This document is work in progress.
This document uses key words such as "MUST", "SHOULD", and "MAY" as defined in RFC 2119 to indicate requirement levels.
The APIs specified in this document MUST be implemented by all SDKs that don't use OpenTelemetry as their underlying tracing implementation. SDKs using OTel SHOULD follow their own already established span APIs but MAY orient themselves on this document if applicable.
Spans are measuring the duration of certain operations in an application.
The topmost member of a (distributed) span tree is called the "Root Span". This span has no parent span and groups together its children with a representative name for the entire operation, such as GET / in case of a request to a backend application.
The topmost span within a service boundary is called the "Segment Span". Segment spans have a parent_span_id pointing to a "remote" span from the parent service.
For example, a distributed trace from backend to frontend, would have a segment span for the backend, and a segment span for the frontend. The frontend segment span is also the root span of the entire span tree.
SDKs MUST NOT expose names like "segment span" (e.g. in APIs) to users and SHOULD NOT (read "avoid") exposing "root span" if possible.
SDKs' span implementations MUST at minimum implement the following span interface.
interface Span {
private _spanId: string;
end(endTimestamp?: SpanTimeInput): void;
setAttribute(key: string, value: SpanAttributeValue | undefined): this;
setAttributes(attributes: SpanAttributes): this;
removeAttribute(key: string): this;
setStatus(status: 'ok' | 'error'): this;
setName(name: string): this;
addLink(link: SpanLink): this;
addLinks(links: SpanLink[]): this;
getName(): string;
getAttributes(): Record<string, SpanAttributeValue>
}
interface Span {
private _spanId: string;
end(endTimestamp?: SpanTimeInput): void;
setAttribute(key: string, value: SpanAttributeValue | undefined): this;
setAttributes(attributes: SpanAttributes): this;
removeAttribute(key: string): this;
setStatus(status: 'ok' | 'error'): this;
setName(name: string): this;
addLink(link: SpanLink): this;
addLinks(links: SpanLink[]): this;
getName(): string;
getAttributes(): Record<string, SpanAttributeValue>
}
class Span:
@property
def span_id(self) -> str: ...
def set_attribute(self, key: str, value: SpanAttributeValue) -> None: ...
def set_attributes(self, attributes: SpanAttributes) -> None: ...
def remove_attribute(self, key: str) -> None: ...
def get_attributes(self) -> SpanAttributes: ...
@property
def status(self) -> str: ...
@status.setter
def status(self, status: Literal["ok", "error"]) -> None: ...
@property
def name(self) -> str: ...
@name.setter
def name(self, name: str) -> None: ...
When implementing the span interface, consider the following guidelines:
- SDKs MAY implement additional APIs, such as getters/setters for properties (e.g.
span.getStatus()), or additional methods for convenience (e.g.Span::spanContext()). - SDK implementers SHOULD disallow direct mutation (without setters) of span properties such as the span name, depending on the platform and the challenges involved.
- SDK implementers MAY disallow direct read access to span properties, depending on the platform and the challenges involved.
SDKs MUST expose at least one API to start a span. SDKs MAY expose additional APIs, depending on the platform, language conventions and requirements.
SDKs MUST expose a default startSpan API that takes options and returns a span:
function startSpan(options: StartSpanOptions): Span;
interface StartSpanOptions {
name: string;
attributes?: Record<string, SpanAttributeValue>;
parentSpan?: Span | null;
active?: boolean;
}
function startSpan(options: StartSpanOptions): Span;
interface StartSpanOptions {
name: string;
attributes?: Record<string, SpanAttributeValue>;
parentSpan?: Span | null;
active?: boolean;
}
# The context-manager way of starting a span. The span will be finished
# automatically when exiting the context manager.
with start_span(
name, # type: str
attributes, # type: SpanAttributes
parent_span, # type: Span
active, # type: bool
) as span:
...
# Alternative API without the use of a context manager, to allow for more
# flexibility when to end the span:
span = start_span(name, attributes, parent_span, active)
span.end()
SDKs MUST allow specifying the following options to be passed to startSpan:
| Option | Required | Description |
|---|---|---|
name | Yes | The name of the span. MUST be set by users |
attributes | No | Attributes to attach to the span. |
parentSpan | No | The parent span. See description below for implications of allowed values |
active | No | Whether the started span should be active (i.e. if spans started while this span is active should become children of the started span). |
Behaviour:
- Spans MUST be started as active by default. This means that any span started, while the initial span is active, MUST be attached as a child span of the active span.
- Only if users set
active: false, the span will be started as inactive, meaning spans started while this span is not yet ended, will not become children, but siblings of the started span. - If a
Spanis passed viaparentSpan, the span will be started as the child of the passed parent span. This has precedence over the currently active span. - If
nullis passed viaparentSpan, the new span will be started as a root/segment span. - SDKs MUST NOT end the span automatically. This is the responsibility of the user.
startSpanMUST always return a span instance, even if the started span's trace is negatively sampled.
SDKs MAY expose additional span starting APIs or variants of startSpan that make sense for the platform. These could be decorators, annotations, or closure- or callback-based APIs. Additional APIs MAY e.g. end spans automatically (for example, when a callback terminates, the span is ended automatically). Likewise, additional APIs MAY also adjust the span status based on errors thrown.
At this time, SDKs MUST NOT expose APIs like Span::startChild or similar functionality that explicitly creates a child span. This is still TBD but the parentSpan option should suffice to serve this use case.
A Noop span is a span that implements the full Span Interface but does not record any data and has no influence on the trace hierarchy.
startSpan MUST always return a valid Span instance. If a span should not be recorded (for example due to sampling or filtering decisions), the SDK MUST return a Noop span instead. This allows callers to use the span without null checks.
SDKs MUST return a Noop span in the following scenarios:
- Unsampled spans: When a span's trace is negatively sampled.
- Ignored spans: When a span matches an
ignoreSpanspattern (see Filtering).
- All mutating methods (
setAttribute,setAttributes,removeAttribute,setStatus,setName,addLink,addLinks,end) MUST be no-ops — they MUST NOT record or store any data. - Getter methods (
getName,getAttributes) SHOULD return empty/default values (e.g., empty string, empty map). - A Noop span MUST NOT be sent to Sentry.
- If a Noop span is active, its dummy properties MUST NOT be used for populating outgoing trace headers. The headers MUST be populated from the propagation context instead. Custom instrumentation API like
getTraceparentandgetBaggageMUST also use the propagation context.
For platforms where span properties are non-nullable, a Noop span MUST use the following default values:
| Property | Default Value |
|---|---|
traceId | 00000000000000000000000000000000 (32 zero hex chars) |
spanId | 0000000000000000 (16 zero hex chars) |
name | Empty string "" |
attributes | Empty map {} |
status | ok |
For platforms that support properties with nullable types, returning null MUST be used instead.
SDKs MAY expose additional utility APIs for users, or internal usage to access certain spans. For example,
Scope::getSpan()- returns the currently active span.Scope::_INTERNAL_getSegmentSpan()- returns the segment span of the currently active span (MUST NOT be documented for users)
const checkoutSpan = Sentry.startSpan({ name: 'on-checkout-click', attributes: { 'user.id': '123' } })
const validationSpan = Sentry.startSpan({ name: 'validate-shopping-cart'})
startFormValidation().then((result) => {
validationSpan.setAttribute('valid-form-data', result.success);
validationSpan.end();
})
const processSpan = Sentry.startSpan({ name: 'process-order', parentSpan: checkoutSpan});
processOrder().then((result) => {
processSpan.setAttribute('order-processed', result.success);
processSpan.end();
}).catch((error) => {
processSpan.setStatus('error');
processSpan.setAttribute('order-processed', 'error');
processSpan.end();
});
const unrelatedSpan = Sentry.startSpan({ name: 'log-order', parentSpan: null});
logOrder()
unrelatedSpan.end();
on('checkout-finished', ({ timestamp }) => {
checkoutSpan.end(timestamp);
})
const checkoutSpan = Sentry.startSpan({ name: 'on-checkout-click', attributes: { 'user.id': '123' } })
const validationSpan = Sentry.startSpan({ name: 'validate-shopping-cart'})
startFormValidation().then((result) => {
validationSpan.setAttribute('valid-form-data', result.success);
validationSpan.end();
})
const processSpan = Sentry.startSpan({ name: 'process-order', parentSpan: checkoutSpan});
processOrder().then((result) => {
processSpan.setAttribute('order-processed', result.success);
processSpan.end();
}).catch((error) => {
processSpan.setStatus('error');
processSpan.setAttribute('order-processed', 'error');
processSpan.end();
});
const unrelatedSpan = Sentry.startSpan({ name: 'log-order', parentSpan: null});
logOrder()
unrelatedSpan.end();
on('checkout-finished', ({ timestamp }) => {
checkoutSpan.end(timestamp);
})
with sentry_sdk.start_span(name="checkout", attributes={"user.id": "123"}) as checkout_span:
with sentry_sdk.start_span(name="process-order") as process_order_span:
result = process()
process_order_span.set_attribute("order-status", result.message)
with sentry_sdk.start_span(name="process-payment") as process_payment_span:
try:
result = process_payment()
process_payment_span.set_attribute("order-status", result.message)
except:
process_payment_span.status = "error"
process_payment_span.set_attribute("order-status", "error")
span = sentry_sdk.start_span(name="log-order", parent_span=None)
log_order()
span.end()
final checkoutSpan = Sentry.startSpan(
'on-checkout-click',
attributes: {
'user.id': SentryAttribute.string('123'),
},
);
final validationSpan = Sentry.startSpan('validate-shopping-cart');
startFormValidation().then((result) {
validationSpan.setAttribute('valid-form-data', SentryAttribute.bool(result.success));
validationSpan.end();
});
final processSpan = Sentry.startSpan(
'process-order',
parentSpan: checkoutSpan,
);
processOrder().then((result) {
processSpan.setAttribute('order-processed', SentryAttribute.bool(result.success));
processSpan.end();
}).catchError((error) {
processSpan.status = SpanV2Status.error;
processSpan.setAttribute('order-processed', SentryAttribute.string('error'));
processSpan.end();
});
final unrelatedSpan = Sentry.startSpan(
'log-order',
parentSpan: null,
);
logOrder();
unrelatedSpan.end();
on('checkout-finished', (timestamp) {
checkoutSpan.end(endTimestamp: timestamp);
});
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").