OpenTelemetry
BoxOffice is written in Java (gateway, order, inventory, payment, catalog), Python (the AI assistant), runs Nginx at the edge, Kafka in the middle and PostgreSQL at the bottom. Ten years ago, tracing a checkout across all of that meant a vendor agent per language, a proprietary header per vendor, and a full re-instrumentation the day the company changed vendors. Worse, a trace broke at the first component that spoke a different dialect. OpenTelemetry (OTel) fixed this by standardising the boring parts β the API, the wire protocol, the context header and the names of things β so that every library and every backend can speak the same language1. (The ideas themselves β trace IDs, spans, parent links, sampling β come from Google's Dapper paper4.)
What OpenTelemetry is (and is not)
OpenTelemetry is a CNCF project that defines how telemetry is produced and transported. It is not a backend: it does not store or display anything. You still choose where the data goes β Prometheus, Tempo, Jaeger, Loki, a cloud service or a vendor β and you can change that choice without touching application code.
| Piece | What it is | BoxOffice example |
|---|---|---|
| API | interfaces to create spans, metrics and logs; safe no-ops if no SDK is installed | Tracer, Meter used inside payment |
| SDK | the implementation: sampling, batching, resource detection, exporters | configured by the Java agent from environment variables |
| Instrumentation libraries | ready-made spans/metrics for frameworks: Spring MVC, JDBC, Kafka clients, FastAPI, HTTP clients | the SELECT seat_holds span, created without any code |
| Semantic conventions | standard names for attributes and metrics: http.route, db.system, messaging.destination.name | every service names the route the same way, so one dashboard works for all |
| OTLP | the OpenTelemetry Protocol, over gRPC (port 4317) or HTTP/protobuf (port 4318) | all services export to the Collector over OTLP |
| Collector | a standalone process that receives, processes and exports telemetry | redacts card numbers, applies tail sampling, fans out to backends |
The signals have matured at different speeds: traces and metrics are stable, logs became stable later (OTel mostly bridges existing logging libraries, adding the trace context), and profiles were added as the fourth signal and are still maturing.
flowchart LR
subgraph services ["BoxOffice services"]
J["Java services<br/>OTel Java agent"]
PY["assistant (Python)<br/>OTel SDK"]
NG["Nginx<br/>OTel module"]
end
subgraph col ["OTel Collector"]
R["receivers<br/>otlp"] --> PR["processors<br/>memory_limiter, k8s attrs,<br/>redaction, tail sampling, batch"] --> E["exporters"]
end
J -- OTLP --> R
PY -- OTLP --> R
NG -- OTLP --> R
E --> TEMPO[("Tempo / Jaeger<br/>traces")]
E --> PROM[("Prometheus / Mimir<br/>metrics")]
E --> LOKI[("Loki<br/>logs")] Context propagation
A trace exists only if every hop knows which trace it belongs to. Context propagation is the mechanism: the caller injects the current trace context into the outgoing request, and the callee extracts it and creates its own span as a child.
The traceparent header
OpenTelemetry uses the W3C Trace Context standard2 by default. One header carries the context:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
β β β ββ flags: 01 = sampled
β β ββ parent-id: the caller's current span (64 bits)
β ββ trace-id: same for the whole request (128 bits)
ββ version
On every outgoing call, a service keeps the trace-id and the flags and replaces the parent-id with the ID of its own current span. No service needs to understand the whole trace; the tree is rebuilt by the backend from the parent links. A second header, tracestate, carries vendor-specific data, and baggage (baggage: tenant=festival-x,channel=mobile) carries business key-value pairs that every downstream service can read β useful, but every byte travels on every request, so keep it small and never put secrets in it.
Following a checkout
Play the diagram and watch the parent-id change at each hop while the trace-id stays the same β including across Kafka, where the context travels in a message header:
sequenceDiagram
autonumber
participant GW as gateway
participant O as order
participant I as inventory
participant P as payment
participant X as payment provider
participant K as Kafka
participant N as notification
GW->>O: POST /orders<br/>traceparent: 00-4bf9β¦-a1a1β¦-01
O->>I: PUT /holds/h7/confirm<br/>traceparent: 00-4bf9β¦-b2b2β¦-01
I-->>O: 200 OK (90 ms)
O->>P: POST /charges<br/>traceparent: 00-4bf9β¦-c3c3β¦-01
P->>X: POST /v1/charges (CLIENT span recorded by payment)
X-->>P: 200 OK (1.0 s)
P-->>O: 201 Created
O->>K: produce OrderConfirmed<br/>header traceparent: 00-4bf9β¦-d4d4β¦-01
O-->>GW: 201 Created
K->>N: consume OrderConfirmed (CONSUMER span, parent d4d4β¦)
N->>N: send e-mail and push Three details matter in practice:
- The external provider is a black box. It will not send its spans to you. The
CLIENTspan recorded bypaymentaround the call is all you will ever see of it β which is why that span's duration and status are worth an alert of their own. Many teams also striptraceparentandbaggageat egress, so internal IDs do not leak to third parties. - Asynchronous hops propagate through message headers. A consumer that handles one message continues the producer's trace (parent-child). A consumer that processes a batch of 500 messages cannot have 500 parents; it starts its own trace and adds span links to each producer's span.
- The trace ends where propagation breaks. One component that drops the header splits every trace in two.
Where propagation usually breaks
| Place | Symptom | Fix |
|---|---|---|
| A proxy or gateway that rebuilds requests and drops unknown headers | traces start at the service behind it | forward traceparent, tracestate, baggage; instrument the proxy |
Work handed to another thread (@Async, executors, reactive code) | child spans appear as separate traces | use instrumented executors (the Java agent wraps common ones) or pass Context explicitly |
| Messaging without headers (old clients, custom envelopes) | consumers start new traces | use headers; or carry the context in the message envelope |
| A homemade HTTP client | the call has no CLIENT span and the callee starts a new trace | use an instrumented client or inject the context manually |
In the BoxOffice code: the trace through the outbox (lab 8)
The agent propagates the context over HTTP and in Kafka headers by itself, but BoxOffice's events do not go straight to Kafka: since lab 7, order writes them to an outbox table in the same transaction as the order, and a relay publishes them later, on a scheduler thread β a hop the agent cannot see, which split every order into two unrelated traces. Hands-on 8 β Instrument everything carries the traceparent with the data: one new column (V2__outbox_traceparent.sql), inject when the row is written, then extract and makeCurrent() before the relay sends, so the agent's producer span becomes a child of the original request. In the lab's test run the result is one trace: the request took 9 ms, the relay published 22 ms after it started, and notification spent 67 ms.
@Transactional(propagation = Propagation.MANDATORY)
public void append(UUID eventId, String aggregateType, UUID aggregateId, String eventType, Object payload) {
String json = mapper.writeValueAsString(payload); // Jackson 3: exceptions are unchecked
// capture the trace context of the current request (null without the agent)
Map<String, String> carrier = new HashMap<>();
W3CTraceContextPropagator.getInstance().inject(Context.current(), carrier, (c, k, v) -> c.put(k, v));
jdbc.sql("""
INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload, traceparent)
VALUES (?, ?, ?, ?, ?::jsonb, ?)
""")
.params(eventId, aggregateType, aggregateId, eventType, json, carrier.get("traceparent"))
.update();
}
for (Pending p : batch) {
ProducerRecord<String, String> record =
new ProducerRecord<>(topic, p.aggregateId().toString(), p.payload());
record.headers().add("eventId", p.id().toString().getBytes(StandardCharsets.UTF_8));
record.headers().add("eventType", p.eventType().getBytes(StandardCharsets.UTF_8));
// continue the trace of the request that created the event
Context origin = W3CTraceContextPropagator.getInstance().extract(Context.root(), p, ROW);
try (Scope ignored = origin.makeCurrent()) {
acks.add(kafka.send(record)); // the agent's producer span is a child of the request
}
}
Instrumenting BoxOffice
Zero-code: the Java agent
The OpenTelemetry Java agent attaches to the JVM and instruments hundreds of libraries β Spring MVC/WebFlux, JDBC, HikariCP, Kafka clients, HTTP clients, Logback β by bytecode injection, with no code changes. It also puts trace_id and span_id into the logging context (MDC), so structured logs carry them automatically.
# Dockerfile (excerpt) β pin a version; check the latest release before upgrading
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.32.0/opentelemetry-javaagent.jar /otel/opentelemetry-javaagent.jar
ENTRYPOINT ["java", "-javaagent:/otel/opentelemetry-javaagent.jar", "-jar", "/app/app.jar"]
# docker-compose.yaml (excerpt) β the order service
environment:
OTEL_SERVICE_NAME: order
OTEL_RESOURCE_ATTRIBUTES: service.namespace=boxoffice,service.version=1.4.2,deployment.environment.name=dev
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318 # agent 2.x default protocol: http/protobuf
OTEL_TRACES_SAMPLER: parentbased_always_on # keep all; the Collector decides (tail sampling)
OTEL_METRIC_EXPORT_INTERVAL: "15000"
service.name is the most important attribute in OpenTelemetry: without it, every service appears as unknown_service:java and no dashboard can tell them apart. service.version and deployment.environment.name let you compare a canary with the stable version, or staging with production.
In the BoxOffice code: one agent for every Java service (lab 8)
Hands-on 8 β Instrument everything does not bake the agent into the images, as the Dockerfile above does: scripts/get-otel-agent.sh downloads a pinned version (2.32.0) once into infra/otel/, and every Java service mounts it read-only and loads it through JAVA_TOOL_OPTIONS. The settings live in one YAML anchor merged into each service, so the services cannot drift apart; only OTEL_SERVICE_NAME changes β three catalog replicas, one service. OTEL_ON=false runs the same image without the agent, which is how the lab measures what the agent costs; metrics are exported every 10 s instead of 60 s, so the dashboards react within a short experiment.
# lab 8: OpenTelemetry settings shared by every Java service (merged into each "environment")
x-otel: &otel
JAVA_TOOL_OPTIONS: -javaagent:/otel/opentelemetry-javaagent.jar
OTEL_JAVAAGENT_ENABLED: ${OTEL_ON:-true} # false = same image, agent off
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-lgtm:4318
OTEL_METRIC_EXPORT_INTERVAL: "10000" # 10 s instead of 60 s
OTEL_RESOURCE_ATTRIBUTES: service.namespace=boxoffice,deployment.environment.name=local
OTEL_TRACES_SAMPLER: ${OTEL_TRACES_SAMPLER:-parentbased_always_on}
OTEL_TRACES_SAMPLER_ARG: ${OTEL_TRACES_SAMPLER_ARG:-1.0}
LOGGING_PATTERN_CORRELATION: "[%X{trace_id:-},%X{span_id:-}] " # trace id in console logs
x-catalog: &catalog
build: ./services/catalog
image: boxoffice-catalog
volumes:
- ./infra/otel:/otel:ro # lab 8: the Java agent
environment:
<<: *otel
OTEL_SERVICE_NAME: catalog # three replicas, ONE service
Zero-code: the Python assistant
uv add opentelemetry-distro opentelemetry-exporter-otlp
uv run opentelemetry-bootstrap -a requirements > otel-instrumentations.txt # instrumentations for the libraries found (FastAPI, httpx, psycopgβ¦)
uv add -r otel-instrumentations.txt
OTEL_SERVICE_NAME=assistant \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \
uv run opentelemetry-instrument uvicorn app.main:app --host 0.0.0.0 --port 8000
Manual spans where the agent cannot see
Automatic instrumentation sees I/O. It cannot see business steps or waiting inside your code β the connection pool of the payment adapter in the Signals waterfall. A few manual spans in the right places are worth more than hundreds of automatic ones:
private static final Tracer tracer = GlobalOpenTelemetry.getTracer("boxoffice.payment");
ProviderConnection acquire() throws TimeoutException {
Span span = tracer.spanBuilder("pool.acquire").startSpan();
try (Scope ignored = span.makeCurrent()) {
long start = System.nanoTime();
ProviderConnection c = pool.borrow(Duration.ofMillis(500)); // bounded wait, never forever
span.setAttribute("pool.wait_ms", (System.nanoTime() - start) / 1_000_000);
return c;
} catch (TimeoutException e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, "pool exhausted");
throw e;
} finally {
span.end();
}
}
With the agent attached, the simplest alternative is an annotation on a method: @WithSpan("seatmap.render"), with @SpanAttribute on the parameters you want recorded.
Attributes: rich, but not dangerous
Span attributes can have high cardinality β order.id, event.id, customer.tier are what make a trace searchable. They must not contain secrets or personal data: no card numbers, tokens or full e-mails. Redact at the source and add a redaction processor in the Collector as a second barrier.
The Collector
Services could export directly to each backend, but then every service must know every backend, its credentials and its retry policy. The Collector decouples them: services send OTLP to one place, and the Collector's pipelines decide what happens next. A pipeline has three stages per signal: receivers (how data gets in), processors (what is done to it, in order), exporters (where it goes).
# otel-collector.yaml β gateway Collector for BoxOffice (sketch)
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
memory_limiter: # first: refuse data instead of being OOM-killed
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 20
k8sattributes: {} # add k8s.pod.name, k8s.namespace.name, k8s.deployment.name
attributes/redact:
actions:
- key: payment.card_number
action: delete
- key: enduser.email
action: hash
tail_sampling:
decision_wait: 10s # wait for the trace's spans before deciding
policies:
- name: keep-errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: keep-slow
type: latency
latency: { threshold_ms: 2000 }
- name: some-of-the-rest
type: probabilistic
probabilistic: { sampling_percentage: 5 }
batch: {} # last: send in batches
exporters:
otlp/tempo:
endpoint: tempo:4317
tls: { insecure: true }
otlphttp/prometheus:
endpoint: http://prometheus:9090/api/v1/otlp
otlphttp/loki:
endpoint: http://loki:3100/otlp
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, attributes/redact, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlphttp/prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, attributes/redact, batch]
exporters: [otlphttp/loki]
The order of processors matters: memory_limiter first, so an overloaded Collector sheds data instead of crashing; batch last, so it batches what survived.
Deployment patterns
| Pattern | Where it runs | Good for | Watch out |
|---|---|---|---|
| No Collector (SDK β backend) | β | demos, one backend | every service holds backend config and credentials |
| Agent | DaemonSet (one per node) or sidecar | local batching, host metrics, adding node/pod attributes | per-node resources; cannot see whole traces |
| Gateway | a Deployment behind a Service, scaled horizontally | central policy: redaction, tail sampling, routing, credentials | a new critical component: size it and monitor it |
| Agent + gateway (two tiers) | both | large clusters | tail sampling needs all spans of a trace on the same gateway instance β route by trace ID (loadbalancing exporter) |
The Collector is production infrastructure
During the flash sale, telemetry volume rises with traffic. A Collector without memory_limiter, without enough replicas, or exporting synchronously to a slow backend becomes a bottleneck β and if SDKs block on export, it can even slow the services. SDKs export asynchronously and drop data when their queue is full: losing telemetry is acceptable; slowing checkout to report on it is not. Monitor the Collector's own metrics (accepted vs refused vs dropped spans, queue size).
Sampling
Keeping every span of every request is rarely affordable (see what telemetry costs). Sampling keeps a subset of traces β complete traces, not random spans, otherwise the trees break.
| Strategy | Where the decision is made | Pros | Cons |
|---|---|---|---|
Head sampling (e.g. parentbased_traceidratio 10%) | at the first span, from the trace ID; travels in the sampled flag so every service agrees | cheap; no buffering; consistent across services | blind: it drops 90% of the rare error and slow traces too |
| Tail sampling3 | in the Collector, after the whole trace has arrived (decision_wait) | keeps interesting traces: every error, every slow request, plus a sample of the rest | every span must reach the Collector; memory to buffer; all spans of a trace on one instance |
| Rate limiting | SDK or Collector: at most N traces per second | predictable cost | during a spike, almost everything is dropped β exactly when you need it |
Head and tail sampling are applied in sequence: a trace dropped at the head can never be rescued by the tail. The comparison below uses illustrative numbers for a busy BoxOffice day β 2,000 requests/s, 8 spans per request of 500 bytes, 1% of requests failing and 2% slower than the 2 s threshold:
| Strategy | Traces kept | Storage per day | Error traces kept per day |
|---|---|---|---|
| keep everything | 100.0% | 691.2 GB | 1,728,000 (100%) |
| head sampling 10% | 10.0% | 69.1 GB | 172,800 (10%) |
| head sampling 1% | 1.0% | 6.9 GB | 17,280 (1%) |
| tail: all errors + all slow + 5% of the rest | 7.8% | 54.3 GB | 1,728,000 (100%) |
Tail sampling stores less than 10% head sampling and still keeps every error trace β at the price of shipping 100% of the spans over the network to the Collector and buffering them for the decision window. A common production recipe: head sampling at 100% (or a high ratio) in the services, tail sampling in a gateway Collector, and metrics computed from all requests before sampling, so dashboards and SLOs are exact even though most traces are discarded.
Exemplars: from a metric to a trace
An exemplar is a trace_id attached to a sample of a metric β for instance, to the latency histogram bucket where a 9 s checkout landed. In Grafana, the latency panel shows exemplars as dots; clicking one opens that exact trace in Tempo. It is the bridge between "p99 is high" and "here is one request that was slow" β make sure the exemplar's trace was kept by your sampler (tail sampling of slow requests guarantees it).
Backends and a local stack
OpenTelemetry leaves the backend to you. The same OTLP data can go to any of them:
Grafana LGTM: Loki (logs), Grafana (UI), Tempo (traces), Mimir or Prometheus (metrics), plus Pyroscope (profiles). Alternatives: Jaeger for traces, OpenSearch for logs, ClickHouse-based stacks (SigNoz) for everything. You operate the storage.
CloudWatch (metrics, logs, alarms), X-Ray / CloudWatch Application Signals (traces), Amazon Managed Service for Prometheus and Managed Grafana. AWS Distro for OpenTelemetry (ADOT) packages the Collector.
Cloud Monitoring, Cloud Logging and Cloud Trace (Google Cloud Observability); Managed Service for Prometheus. Accepts OTLP through the Collector.
Azure Monitor: Application Insights (traces, requests), Log Analytics (logs), Managed Prometheus and Managed Grafana. The Azure Monitor OpenTelemetry distro instruments applications.
Datadog, Honeycomb, New Relic, Grafana Cloud, Dynatrace, Elastic Cloud and others accept OTLP. You pay per volume (spans, series, GB of logs) β the sampling and cardinality decisions of this class are also cost decisions.
For development β and in Hands-on 8 β a single container bundles a Collector with the whole LGTM stack:
# docker-compose.yaml (excerpt)
services:
otel-lgtm:
image: grafana/otel-lgtm
ports:
- "3000:3000" # Grafana
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
Trade-offs
| Decision | Option A | Option B | Guidance |
|---|---|---|---|
| Instrumentation | zero-code agent | manual SDK calls | start with the agent; add manual spans for business steps and internal waits |
| Export path | SDK β backend | SDK β Collector β backend | a Collector as soon as there are two services or one policy (redaction, sampling) |
| Collector topology | agent per node | central gateway | agents for local enrichment; a gateway for tail sampling and credentials |
| Sampling | head | tail | head for cost simplicity; tail to keep every error and slow trace |
| Protocol | OTLP gRPC (4317) | OTLP HTTP (4318) | either; HTTP is friendlier to proxies and browsers |
| Backend | self-hosted | managed / SaaS | managed until the bill or data-residency rules say otherwise |
In the interview
"How do you trace a request across microservices?" β "Every service is instrumented with OpenTelemetry; the W3C traceparent header (and Kafka message headers for async hops) propagates the trace ID; spans go over OTLP to a Collector that applies tail sampling β keep all errors and slow traces, a few percent of the rest β and exports to a tracing backend. Logs carry the trace ID, and the latency histograms carry exemplars, so I can jump from a dashboard to a trace to its logs." Then add the cost angle: metrics are computed from 100% of the traffic; traces are sampled.
Best practices
- Pin the agent and SDK versions, and upgrade on purpose β a new instrumentation release can rename metrics and attributes (semantic conventions evolve), silently breaking dashboards and alerts. In BoxOffice:
scripts/get-otel-agent.shpins 2.32.0. - Configure OpenTelemetry once, through environment variables shared by every service β exporter, sampler, resource attributes; only
OTEL_SERVICE_NAMEdiffers per service. In BoxOffice: thex-otelanchor (box above). - Use a parent-based sampler at the head β the root decides once and every hop follows it, so a trace is kept or dropped whole, never in fragments. In BoxOffice:
OTEL_TRACES_SAMPLER=parentbased_always_on, andparentbased_traceidratioat 10% in experiment B of Hands-on 8 β Measure it. - Carry
traceparentacross every asynchronous hop you own β outbox rows, job tables, custom envelopes; each hop without it splits every trace in two. In BoxOffice: thetraceparentcolumn of the outbox (box above). - Strip
traceparentandbaggageat the egress to third parties β internal ids and business context should not leak to a payment provider that will never send you its spans. - Monitor the Collector itself β its own metrics (refused and failed exports, queue size, memory) are the only sign that telemetry is being dropped; a silent Collector looks exactly like a quiet system.
Common pitfalls
- Protocol and port mismatch: the Java agent 2.x defaults to OTLP over HTTP (4318); pointing it at 4317 without
OTEL_EXPORTER_OTLP_PROTOCOL=grpcsilently produces no data. - Missing
service.name: everything shows up asunknown_service. - Head sampling at 1% and no tail sampling: the one trace you need during the incident was dropped.
- Tail sampling behind a round-robin load balancer: spans of the same trace land on different Collectors, and every decision is made on fragments.
- No
memory_limiter: the Collector is OOM-killed exactly during the traffic peak. - Secrets and personal data in attributes or baggage: telemetry is copied to many systems and kept for weeks.
How it connects
flowchart LR
KH["Kafka message headers,<br/>span links β Messaging"] --> CTX["context propagation:<br/>traceparent, baggage"]
CTX --> SVC["services: Java agent,<br/>Python SDK, Nginx"]
SVC -->|"OTLP 4317 / 4318"| COL["Collector:<br/>receivers β processors β exporters"]
COL --> RD["redaction:<br/>no cards, no e-mails"]
COL --> TS["tail sampling:<br/>errors + slow + 5%"]
TS --> TR["traces backend<br/>Tempo / Jaeger"]
COL --> MET["metrics from all traffic<br/>β SLOs, Alerting"]
COL --> LG["logs backend<br/>Loki"]
MET -->|"exemplars"| TR
COL --> OPS["memory_limiter, gateway replicas<br/>β Cloud-native"] Palette components: tracing Β· metrics Β· logs Β· app (instrumented services) Β· apigw (must forward traceparent) Β· kafka (context in message headers) Β· payment (a black box: only the CLIENT span). Scenarios: observability-stack, event-fanout (traces that cross Kafka). Continues in: Signals, Alerting, Messaging β Kafka, Cloud-native β Kubernetes (Collector as DaemonSet or Deployment), Hands-on 8 β instrument everything.
Key takeaways
- OpenTelemetry standardises the API, SDK, semantic conventions, protocol (OTLP) and Collector β not the backend; change backends without re-instrumenting.
- Context propagation with W3C
traceparent(and message headers for Kafka) is what makes a trace; one hop that drops it splits every trace. - Start with zero-code instrumentation, then add manual spans for business steps and for time spent waiting inside services.
- The Collector pipelines (receivers β processors β exporters) centralise redaction, enrichment, sampling and routing; treat it as production infrastructure.
- Tail sampling keeps every error and slow trace at a fraction of the storage; compute metrics from all traffic before sampling, and use exemplars to jump from metrics to traces.
-
OPENTELEMETRY. What is OpenTelemetry? and Collector documentation.Β β©
-
W3C. Trace Context. W3C Recommendation. See also W3C Baggage.Β β©
-
OPENTELEMETRY COLLECTOR CONTRIB. Tail Sampling Processor.Β β©
-
SIGELMAN, B. H. et al. Dapper, a Large-Scale Distributed Systems Tracing Infrastructure. Google Technical Report, 2010.Β β©