How Obtrace Zero Works
Architecture deep dive — from Pod creation to telemetry delivery, through language detection, webhook mutation, and multi-strategy injection.
How Obtrace Zero Works
Obtrace Zero intercepts Pod creation in Kubernetes, detects what language each container runs, and injects instrumentation using the best strategy for that language — all before the Pod is scheduled.
End-to-end flow
Phase 1: Installation
When you run obtrace-zero install, the following resources are created in your cluster:
| Resource | Purpose |
|---|---|
Namespace obtrace-system | Isolates the operator from application workloads |
Deployment obtrace-zero-operator | The operator itself (distroless image, non-root) |
| MutatingWebhookConfiguration | Intercepts Pod creation events |
| ClusterRole + ClusterRoleBinding | Permission to read workloads and mutate Pods |
| Certificate (via cert-manager) | TLS for the webhook endpoint |
| ObtraceInstrumentation CRD | Declarative configuration resource |
The operator runs as a single replica with health checks on port 8081 and metrics on port 8080.
Phase 2: Continuous discovery
The operator scans the cluster every 60 seconds (configurable), cataloging all Deployments, StatefulSets, and DaemonSets. For each workload, it runs the language detector to classify:
- Language — Node.js, Python, Java, .NET, PHP, Ruby, Go, Rust, or unknown
- Framework — Express, FastAPI, Spring, Rails, Laravel, etc.
- Strategy — SDK injection or eBPF sidecar
- Confidence — 0.0 to 1.0
Discovery results are stored in the ObtraceInstrumentation status and logged by the operator.
System namespaces are always excluded: kube-system, kube-public, kube-node-lease, cert-manager, linkerd, argocd, obtrace-system, obtrace-infra.
Phase 3: Language detection
When a Pod is created, the detector analyzes three sources of information in order:
1. Explicit hints (confidence: 1.0)
Or via Pod label:
2. Container image and command (confidence: 0.9)
The detector pattern-matches the image name and command/args:
| Pattern match | Detection |
|---|---|
Image contains node, bun, deno | Node.js |
Image contains python, fastapi, flask, django | Python |
Image contains openjdk, temurin, corretto | Java |
Image contains dotnet, aspnet | .NET |
Image contains php, laravel, symfony | PHP |
Image contains ruby, rails, puma | Ruby |
Image contains golang | Go (→ eBPF) |
Image contains rust | Rust (→ eBPF) |
Framework detection goes deeper — if the command contains uvicorn or fastapi, the framework is set to FastAPI. If rails appears, framework is Rails.
3. Fallback (confidence: 0.3)
If nothing matches, the workload is classified as unknown and gets eBPF instrumentation.
Phase 4: Webhook mutation
The mutating webhook intercepts every Pod CREATE event and runs this decision flow:
- Already injected? — If
obtrace.io/injected=trueannotation exists → skip - Excluded? — If
obtrace.io/exclude=trueon Pod or namespace → skip - Config exists? — Find an ObtraceInstrumentation CRD matching this namespace
- Detect — Run language/framework detection on the container spec
- Override — Apply strategy override and language hints from the CRD
- Resolve metadata:
- Service name: label
app.kubernetes.io/name→ labelapp→pod.generateName→pod.name - Environment: label
obtrace.io/environment→ namespace inference (prod*→ production)
- Service name: label
- Inject — Apply the selected strategy (SDK, eBPF, or hybrid)
- Return — JSON Patch response to the API server
The webhook has a 10-second timeout and uses failurePolicy: Ignore — if the operator is down, Pods are created normally without instrumentation.
Phase 5: SDK injection
For interpreted languages, the operator mutates the Pod spec to add:
An init container that copies language-specific loader files into a shared volume:
Environment variables on every application container, including a language-specific hook:
| Language | Hook mechanism |
|---|---|
| Node.js | NODE_OPTIONS=--require /obtrace/obtrace-loader.js |
| Python | PYTHONSTARTUP=/obtrace/obtrace_loader.py |
| Java | JAVA_TOOL_OPTIONS=-javaagent:/obtrace/obtrace-agent.jar |
| .NET | DOTNET_STARTUP_HOOKS=/obtrace/Obtrace.AutoInstrument.dll |
| PHP | PHP_INI_SCAN_DIR=/obtrace/php.d/:${PHP_INI_SCAN_DIR} |
| Ruby | RUBYOPT=-r /obtrace/obtrace_loader |
These are native mechanisms of each runtime — not hacks. Every runtime has a documented way to load code at startup, and Obtrace Zero uses exactly that.
A shared volume mounted read-only on application containers:
Base environment variables on all containers:
Phase 6: eBPF injection
For compiled languages or unknown workloads, the operator adds an eBPF sidecar container:
The Pod gets shareProcessNamespace: true so the sidecar can observe the main container's network activity.
The eBPF sidecar attaches kernel probes to capture HTTP traffic, DNS queries, and TLS-encrypted data without touching the application binary. See eBPF deep dive for details.
Phase 7: Telemetry delivery
Every agent (SDK or eBPF) sends telemetry directly to ingest-edge in OTLP JSON format:
Headers:
No intermediate collector is required. The agents batch telemetry in memory (max 500 items) and flush every 2 seconds. On process shutdown, a final flush is executed.
From ingest-edge, the data flows through the standard Obtrace pipeline: Kafka → workers → ClickHouse/Postgres → query-gateway → frontend.
Annotations and labels set on instrumented Pods
After mutation, the Pod carries:
| Annotation/Label | Value | Purpose |
|---|---|---|
obtrace.io/injected (annotation) | true | Prevents double injection |
obtrace.io/detected-language (annotation) | nodejs, python, etc. | Debugging and status |
obtrace.io/strategy (annotation) | sdk, ebpf, hybrid | Debugging and status |
obtrace.io/detected-framework (annotation) | express, fastapi, etc. | Debugging and status |
obtrace.io/instrumented (label) | true | Queryable via kubectl get pods -l obtrace.io/instrumented=true |