An executable GNU M4 lab showing how a successful batch consumer can lose most of its asynchronous trace graph when OpenTelemetry's span-link budget is set below the batch size.
The default scenario runs:
- 100 consumer batches.
- 40 messages per batch.
- 4,000 successfully processed messages.
- A custom
LinkCountLimitof 8.
Every message arrives with a valid producer span context. Processing succeeds. But each consumer span can record only eight of the forty attempted links:
| Signal | Result |
|---|---|
| Message-processing success | 100% |
| Producer links recorded | 800 |
| Producer links dropped | 3,200 |
| Causal coverage | 20% |
This is not a context-propagation failure. The contexts cross the queue and reach the consumer instrumentation. They are discarded later, when the SDK enforces a deliberately low link-count limit.
A batch receive or process operation has many upstream message contexts but only one consumer span. OpenTelemetry messaging conventions represent those many-to-one relationships with span links.
The tracing SDK specification defines LinkCountLimit, with a default of 128.
SDKs may discard links beyond the configured limit and must make the dropped
count available to exporters. OTLP carries that evidence on the span as
droppedLinksCount in JSON or dropped_links_count in protobuf.
This lab intentionally configures a limit of eight. It models an SDK that keeps the first eight links offered and drops subsequent links. Retention order is a lab assumption; the portable signal is the non-zero dropped count.
The generated payload compares:
- Low link budget: eight recorded links and
droppedLinksCount: 32on each representative 40-message batch. - Batch-sized budget: forty recorded links and
droppedLinksCount: 0.
Both consumer spans have status OK. Both process the same messages. Only the
second preserves every producer trace as a queryable causal predecessor.
The OTLP-shaped payload includes:
- Forty representative producer spans with unique trace contexts.
- Capped and preserved consumer spans with their actual link arrays.
lab.span_link.messages.processed.lab.span_link.links.attempted,links.recorded, andlinks.dropped.lab.span_link.spans_with_drops.lab.span_link.causal_coverage.- A warning log carrying
otel.span.dropped_links_count.
That lets telemetry.sh compare application success with trace completeness, find consumer spans that admit missing links, quantify orphaned producers, and pivot across the causal edges that survived.
Requirements:
- Node.js 20+
- GNU M4
npm ci
npm startOpen http://localhost:8080.
Or run the clean Alpine Linux container:
docker compose up --buildThe model has no infrastructure side effects. Inputs are standard M4 macro definitions:
m4 \
-DBATCHES=100 \
-DMESSAGES_PER_BATCH=40 \
-DLINK_COUNT_LIMIT=8 \
model/main.m4Convenience commands:
npm run simulate
npm run simulate -- --batches 12 --messages-per-batch 25 --link-count-limit 6
npm run telemetryHTTP endpoints expose the same model:
curl 'http://localhost:8080/api/simulate?messagesPerBatch=40&linkCountLimit=8'
curl 'http://localhost:8080/api/telemetry?messagesPerBatch=40&linkCountLimit=8'Every model and telemetry response includes the GNU M4 runtime version,
runtime platform, SHA-256 digest of model/main.m4, and model path. The Node
server does not duplicate the link-budget calculation.
npm ci
npm run checkTests cover the default visibility gap, adequate and zero link budgets, bounded inputs, OTLP link arrays and dropped counts, metrics, logs, runtime proof, and every HTTP endpoint. CI also builds the Alpine container.
- OpenTelemetry tracing SDK span limits
- OpenTelemetry messaging span conventions
- OTLP trace protobuf definition
MIT