lyzr-otel-mq-exporter
Export OpenTelemetry traces to AWS SQS and Azure Service Bus in OTLP format (Protobuf or JSON).
Ships two span processors — PersistSpanProcessor (thread-based; writes each span to the WAL inside on_end() for crash-safe delivery, works in any runtime) and AsyncSpanProcessor (asyncio bridge for FastAPI/Starlette) — plus an mmap-backed Write-Ahead Log for guaranteed delivery and an S3 Extended Client for payloads exceeding SQS's 256KB limit.
Installation
# Base
pip install lyzr-otel-mq-exporter
# With AWS support
pip install lyzr-otel-mq-exporter[aws]
# With Azure support
pip install lyzr-otel-mq-exporter[azure]
# All exporters
pip install lyzr-otel-mq-exporter[all]
Quick Start — FastAPI + SQS
from contextlib import asynccontextmanager
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from lyzr_otel_mq_exporter import SQSSpanExporter, AsyncSpanProcessor
resource = Resource.create({SERVICE_NAME: "my-fastapi-service"})
provider = TracerProvider(resource=resource)
trace.set_tracer_provider(provider)
exporter = SQSSpanExporter(
queue_url="https://sqs.us-east-1.amazonaws.com/123456789/traces",
region_name="us-east-1",
encoding="otlp_proto",
wal_enabled=True,
flush_interval_ms=5000,
max_batch_size=512,
)
processor = AsyncSpanProcessor(exporter=exporter, max_queue_size=2000)
provider.add_span_processor(processor)
@asynccontextmanager
async def lifespan(app):
await processor.start()
yield
await processor.shutdown()
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def root():
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("handle_request"):
return {"status": "ok"}
Quick Start — Azure Service Bus
from lyzr_otel_mq_exporter import AzureServiceBusSpanExporter, PersistSpanProcessor
exporter = AzureServiceBusSpanExporter(
connection_string="Endpoint=sb://namespace.servicebus.windows.net/;SharedAccessKeyName=...",
queue_name="traces",
encoding="otlp_proto",
wal_enabled=True,
)
# Crash-safe, works in any runtime. Use AsyncSpanProcessor instead if you
# specifically want the asyncio bridge on an event loop.
processor = PersistSpanProcessor(exporter, schedule_delay_millis=5000)
provider.add_span_processor(processor)
Azure AD / Managed Identity (keyless)
Recommended for production — no SAS keys in config. Provide a
fully_qualified_namespace instead of a connection string; when credential
is omitted, DefaultAzureCredential resolves managed identity, workload
identity, environment service principals, or az login automatically.
Requires the azure-identity package (included in the [azure] extra).
from lyzr_otel_mq_exporter import AzureServiceBusSpanExporter
# Keyless — DefaultAzureCredential (managed identity in Azure, az login locally)
exporter = AzureServiceBusSpanExporter(
fully_qualified_namespace="namespace.servicebus.windows.net",
queue_name="traces",
wal_enabled=True,
)
# Or pass an explicit credential (not closed by the exporter — you own it):
from azure.identity import ManagedIdentityCredential
exporter = AzureServiceBusSpanExporter(
fully_qualified_namespace="namespace.servicebus.windows.net",
queue_name="traces",
credential=ManagedIdentityCredential(client_id="<user-assigned-mi-client-id>"),
)
Provide either connection_string or fully_qualified_namespace, not
both. credential is only valid with fully_qualified_namespace.
Choosing a span processor
| Processor | Runtime | What on_end() does |
Crash-safe? |
|---|---|---|---|
PersistSpanProcessor |
any (sync or async) | persists to the exporter (WAL write) synchronously, then a background thread flushes | ✅ with wal_enabled=True |
AsyncSpanProcessor |
asyncio only (FastAPI/Starlette) | enqueues to an asyncio queue; a background task persists + flushes | ⚠️ in-memory window before the WAL write |
OTel BatchSpanProcessor |
any | enqueues to an in-memory queue; a background thread persists + flushes | ⚠️ in-memory window |
Reach for PersistSpanProcessor when you want the strongest guarantee: the span is written to the WAL before on_end() returns, so it survives a process crash. It runs a background thread (like OTel's BatchSpanProcessor), so it works in any runtime — Gunicorn/Uvicorn workers, Celery, plain scripts — not just async.
Quick Start — PersistSpanProcessor (crash-safe)
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from lyzr_otel_mq_exporter import SQSSpanExporter, PersistSpanProcessor
provider = TracerProvider()
trace.set_tracer_provider(provider)
exporter = SQSSpanExporter(
queue_url="https://sqs.us-east-1.amazonaws.com/123456789/traces",
wal_enabled=True, # required for the crash-safe guarantee
)
processor = PersistSpanProcessor(
exporter,
max_export_batch_size=512, # flush once this many spans are pending...
schedule_delay_millis=5000, # ...or after this long, whichever first
block_on_full=True, # backpressure (don't drop) when the WAL is full
)
provider.add_span_processor(processor)
# No start() needed — the worker thread starts on construction.
with trace.get_tracer(__name__).start_as_current_span("do-work"):
...
provider.shutdown() # drains WAL, flushes, closes
Under FastAPI it needs no event loop — just drain on shutdown:
@asynccontextmanager
async def lifespan(app):
yield
provider.shutdown() # PersistSpanProcessor.shutdown(): drain + flush + close WAL
Quick Start — Sync (BatchSpanProcessor)
For non-async applications (Django, Flask, scripts), use the standard BatchSpanProcessor:
from opentelemetry.sdk.trace.export import BatchSpanProcessor
exporter = SQSSpanExporter(
queue_url="https://sqs.us-east-1.amazonaws.com/123456789/traces",
wal_enabled=True,
)
processor = BatchSpanProcessor(exporter)
provider.add_span_processor(processor)
Architecture
on_end(span) -> asyncio.Queue -> micro-batch (up to 64 spans)
|
run_in_executor
|
exporter.export(batch)
|
serialize each span -> WAL write_batch()
(single lock, single mmap flush)
|
check flush conditions:
- time since last flush > interval?
- WAL pending count >= max_batch_size?
| (if yes)
merge all pending WAL entries -> single OTLP message
|
send to SQS/Azure Service Bus -> mark delivered
Key design: Each span is durable on disk the moment it arrives (WAL write). Flushing to the queue happens separately — on interval or when the pending count hits the threshold. This gives maximum crash safety with batched network I/O.
How It Works
AsyncSpanProcessor
A thin async bridge between OpenTelemetry's sync on_end() callback and the async world. All batching and flush logic lives in the exporter.
- Micro-batching: After getting the first span from the queue, drains up to 63 more that are already waiting. This reduces thread pool submissions by up to 64x under load.
- Idle flush: When no spans arrive for 1 second, calls
export([])to give the exporter a chance to flush pending WAL entries.
PersistSpanProcessor
A thread-based processor (modeled on OpenTelemetry's BatchSpanProcessor) that moves the persist step into on_end():
- Synchronous persist:
on_end()callsexporter.persist([span])before returning. Withwal_enabled=Truethe span is on disk (WAL) at that moment, so it survives a process crash. A daemon thread flushes pending WAL entries to the backend onschedule_delay_millis, or early oncemax_export_batch_sizespans are pending. - Backpressure: when the WAL is full (e.g. a sustained backend outage),
on_end()blocks until the worker frees space (block_on_full=True, default) instead of dropping spans. Setblock_on_full=Falseto drop and count, orfull_block_timeout_millisto cap the block as a safety valve. - Any runtime: uses an OS thread, so it works in sync apps (Flask, Celery, Gunicorn) as well as async — no event loop required.
- Without
wal_enabled, it degrades to a best-effort in-memory buffer, likeBatchSpanProcessor. Durability is a property of the exporter (wal_enabled), not the processor.
Exporters (SQS / Azure Service Bus)
Two modes of operation:
WAL mode (wal_enabled=True):
- Each span is serialized and written to WAL immediately via
write_batch()(single file lock + single mmap flush for the whole micro-batch) - Pending WAL entries are merged into a single OTLP message and sent as one SQS/Azure API call
- On success, all entries are marked delivered. On transient failure, entries stay in WAL for retry.
In-memory mode (wal_enabled=False, default):
- Spans are buffered in a list
- When the buffer reaches
max_batch_sizeorflush_interval_mselapses, the batch is serialized and sent - On crash, in-memory spans are lost
Write-Ahead Log (WAL)
mmap-backed durable storage with per-operation file locking for multi-process safety (Gunicorn, Uvicorn, Celery workers can share a single WAL file).
- Level 2 durability: Process crash safe. Each
write_batch()does a singlemmap.flush()after all entries are written. - CRC32 per entry: Detects corruption without invalidating the entire file
- Auto-compaction: Triggered when >50% of entries are delivered, or when space runs out
- Crash recovery: On startup, scans for orphan entries past the write offset
S3 Extended Client (SQS only)
When a merged payload exceeds the threshold (default 250KB), it's uploaded to S3 and a reference is sent via SQS:
exporter = SQSSpanExporter(
queue_url="...",
s3_bucket="my-traces-bucket",
s3_prefix="otel-traces/",
large_payload_threshold_kb=250,
)
The SQS message includes payload_location=s3 and s3_bucket in message attributes. Your consumer reads the attribute to decide whether to fetch from S3 or read inline.
Configuration Reference
SQSSpanExporter
| Parameter | Type | Default | Description |
|---|---|---|---|
queue_url |
str |
required | AWS SQS queue URL |
region_name |
str |
"us-east-1" |
AWS region |
encoding |
str |
"otlp_proto" |
"otlp_proto" or "otlp_json" |
aws_access_key_id |
str |
None |
AWS credentials (for dev; use IAM roles in prod) |
aws_secret_access_key |
str |
None |
AWS credentials (for dev) |
wal_enabled |
bool |
False |
Enable Write-Ahead Log |
wal_file_path |
str |
None |
WAL file path (default: .otel_wal/sqs_exporter.wal) |
wal_max_size |
int |
67108864 |
WAL file size in bytes (default: 64MB) |
s3_bucket |
str |
None |
S3 bucket for large payloads |
s3_prefix |
str |
"otel-traces/" |
S3 key prefix |
large_payload_threshold_kb |
int |
250 |
Size threshold (KB) to trigger S3 upload |
flush_interval_ms |
int |
5000 |
Flush interval in milliseconds |
max_batch_size |
int |
512 |
Flush when this many spans are pending |
AzureServiceBusSpanExporter
| Parameter | Type | Default | Description |
|---|---|---|---|
connection_string |
str |
None |
SAS connection string (mutually exclusive with fully_qualified_namespace) |
queue_name |
str |
required | Queue name |
encoding |
str |
"otlp_proto" |
"otlp_proto" or "otlp_json" |
servicebus_namespace |
str |
None |
Namespace (for logging) |
fully_qualified_namespace |
str |
None |
Service Bus FQDN for Azure AD auth (mutually exclusive with connection_string) |
credential |
TokenCredential |
None |
azure-identity credential for Azure AD auth; only valid with fully_qualified_namespace. Defaults to DefaultAzureCredential() when a namespace is given without one |
wal_enabled |
bool |
False |
Enable Write-Ahead Log |
wal_file_path |
str |
None |
WAL file path (default: .otel_wal/azure_exporter.wal) |
wal_max_size |
int |
67108864 |
WAL file size in bytes (default: 64MB) |
flush_interval_ms |
int |
5000 |
Flush interval in milliseconds |
max_batch_size |
int |
512 |
Flush when this many spans are pending |
Provide either
connection_stringorfully_qualified_namespace. Azure AD auth requires theazure-identitypackage (bundled in the[azure]extra).
PersistSpanProcessor
| Parameter | Type | Default | Description |
|---|---|---|---|
exporter |
SpanExporter |
required | Exporter exposing persist() / flush_to_queue() (SQS or Azure) |
max_export_batch_size |
int |
512 |
Wake the worker to flush once this many spans are pending |
schedule_delay_millis |
int |
5000 |
Maximum time between flushes |
block_on_full |
bool |
True |
Block on_end() when the WAL is full (backpressure) instead of dropping |
full_block_timeout_millis |
int |
None |
Cap how long on_end() may block before dropping (safety valve); None = block indefinitely |
AsyncSpanProcessor
| Parameter | Type | Default | Description |
|---|---|---|---|
exporter |
SpanExporter |
required | The span exporter to use |
max_queue_size |
int |
1000 |
Max spans in the asyncio.Queue before dropping |
Encoding Formats
| Format | Size | Speed | Use case |
|---|---|---|---|
otlp_proto |
~121 KB / 500 spans | Faster | Production (default) |
otlp_json |
~243 KB / 500 spans | Slightly slower | Debugging, human readability |
Both formats are compatible with the OpenTelemetry Collector's SQS and Azure Service Bus receivers.
Benchmarks
Run the benchmarks:
# WAL write() vs write_batch() comparison
uv run python benchmarks/bench_wal.py
# Full end-to-end pipeline benchmarks
uv run python benchmarks/bench_pipeline.py
Results on Apple Silicon (M-series):
| Benchmark | Result |
|---|---|
WAL write_batch() speedup |
10x faster than write() loop at 1024 spans |
| Micro-batch effectiveness | 62.5x fewer export() calls (32 vs 2000) |
| Full pipeline (WAL mode) | ~7,400 spans/sec |
| Full pipeline (in-memory) | ~136,000 spans/sec |
| Sustained throughput (3s) | ~4,200 spans/sec, 0 drops, ~529 spans/SQS call |
Graceful Shutdown
# AsyncSpanProcessor (asyncio) — needs start()/shutdown() on the event loop
@asynccontextmanager
async def lifespan(app):
await processor.start()
yield
await processor.shutdown() # drains queue, flushes WAL, closes connections
# PersistSpanProcessor (threaded) — no start(); drain via the provider
@asynccontextmanager
async def lifespan(app):
yield
provider.shutdown() # cascades to processor.shutdown(): drain + flush + close WAL
# Sync apps / scripts (either processor)
import atexit
atexit.register(provider.shutdown)
Development
git clone https://github.com/NeuralgoLyzr/lyzr-otel-mq-exporter.git
cd lyzr-otel-mq-exporter
# Install with dev dependencies
uv sync --all-extras
# Run tests
uv run pytest
# Run benchmarks
uv run python benchmarks/bench_pipeline.py
License
MIT
Metadata
Release files for lyzr-otel-mq-exporter 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| lyzr_otel_mq_exporter-0.1.0.tar.gz | 37.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lyzr_otel_mq_exporter-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.8 kB
Release files / lyzr_otel_mq_exporter-0.1.0.tar.gz
| Download URL | lyzr_otel_mq_exporter-0.1.0.tar.gz |
|---|---|
| Size | 37.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
752261ff425abdc0e60876866aa0a5161706e18c0941f153ac11bbcfe8abfe9d
|
|
BLAKE2b-256 checksum How to use checksums |
846d46d7f70596b55b4806e8c6575e28e742aa75c4d0941c6138f238c4512474
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.
Transparency logRelease files / lyzr_otel_mq_exporter-0.1.0-py3-none-any.whl
| Download URL | lyzr_otel_mq_exporter-0.1.0-py3-none-any.whl |
|---|---|
| Size | 30.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5387cadc9c207b4f739a7ab06daf635dc12d4619d35b8a28e5e69d0fbc89300f
|
|
BLAKE2b-256 checksum How to use checksums |
a3e3c190085515cdb9ac2382795198be460b1eb389e5202bd1720450a73c2363
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.
Transparency logSecurity for lyzr-otel-mq-exporter 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for lyzr_otel_mq_exporter-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.