Botanu SDK follows a "thin SDK, smart collector" architecture. The SDK does minimal work in your application's hot path, delegating heavy processing to the OpenTelemetry Collector.
The SDK only performs lightweight operations during request processing:
- Generate UUIDv7
run_id - Read/write W3C Baggage
- Record token counts as span attributes
Target overhead: < 0.5ms per request
Built on OpenTelemetry primitives, not alongside them:
- Uses standard
TracerProvider - Standard
SpanProcessorfor enrichment - Standard OTLP export
- W3C Baggage for propagation
Heavy operations happen in the OTel Collector:
- PII redaction
- Cost calculation from token counts
- Vendor normalization
- Cardinality management
- Aggregation and sampling
Central configuration for the SDK:
@dataclass
class BotanuConfig:
service_name: str
deployment_environment: str
otlp_endpoint: str
propagation_mode: str # "lean" or "full"
auto_instrument_packages: List[str]Holds run metadata and provides serialization:
@dataclass
class RunContext:
run_id: str
root_run_id: str
workflow: str
event_id: str
customer_id: str
attempt: int
# ...The only span processor in the SDK. Reads baggage, writes to spans:
class RunContextEnricher(SpanProcessor):
def on_start(self, span, parent_context):
for key in self._baggage_keys:
value = baggage.get_baggage(key, parent_context)
if value:
span.set_attribute(key, value)Context managers for manual instrumentation:
track_llm_call()- LLM/model operationstrack_db_operation()- Database operationstrack_storage_operation()- Object storage operationstrack_messaging_operation()- Message queue operations
@botanu_workflow("process", event_id="evt-001", customer_id="cust-42")
def do_work():
pass- Generate UUIDv7
run_id - Create
RunContext - Set baggage in current context
- Start root span with run attributes
# Within the run
response = requests.get("https://api.example.com")- HTTP instrumentation reads current context
- Baggage is injected into request headers
- Downstream service extracts baggage
- Context continues propagating
Every span (including auto-instrumented):
RunContextEnricher.on_start()is called- Reads
botanu.run_idfrom baggage - Writes to span attributes
- Span is exported with run context
BatchSpanProcessorbatches spansOTLPSpanExportersends to collector- Collector processes (cost calc, PII redaction)
- Spans written to backend
| Operation | Location | Reason |
|---|---|---|
| run_id generation | SDK | Must be synchronous |
| Baggage propagation | SDK | Process-local |
| Token counting | SDK | Available at call site |
| Cost calculation | Collector | Pricing tables change |
| PII redaction | Collector | Consistent policy |
| Aggregation | Collector | Reduces data volume |
- Standard OTel export format
- Any OTel-compatible backend works
- Collector processors are configurable
Core SDK only requires opentelemetry-api:
dependencies = [
"opentelemetry-api >= 1.20.0",
]Full SDK adds export capabilities:
[project.optional-dependencies]
sdk = [
"opentelemetry-sdk >= 1.20.0",
"opentelemetry-exporter-otlp-proto-http >= 1.20.0",
]If you already have OTel configured:
from opentelemetry import trace
from botanu.processors.enricher import RunContextEnricher
# Add our processor to your existing provider
provider = trace.get_tracer_provider()
provider.add_span_processor(RunContextEnricher())Botanu works alongside existing instrumentation:
# Your existing setup
from opentelemetry.instrumentation.requests import RequestsInstrumentor
RequestsInstrumentor().instrument()
# Add Botanu
from botanu import enable
enable(service_name="my-service")
# Both work together - requests are instrumented AND get run_id| Operation | Typical Latency |
|---|---|
generate_run_id() |
< 0.01ms |
RunContextEnricher.on_start() |
< 0.05ms |
track_llm_call() overhead |
< 0.1ms |
| Baggage injection | < 0.01ms |
Total SDK overhead per request: < 0.5ms
- Run Context - RunContext model details
- Context Propagation - How context flows
- Collector Configuration - Collector setup