Ballerina's Built-In Observability: Automatic Tracing Without the Boilerplate
Ballerina's ballerina/observability module auto-instruments HTTP, database, and messaging calls with OpenTelemetry, cutting tracing boilerplate—but you still need to manage overhead and know when to add manual spans.
14 Jun 2026, 04:28 UTC

The Boilerplate Tax on Distributed Tracing
If you have wired up tracing by hand in a JVM or Node.js service, you know the pattern: import a tracing library, create a span at every boundary, pass context headers through HTTP calls, and configure an exporter. In a microservices mesh, that boilerplate multiplies across every service and every downstream call. Ballerina’s ballerina/observability module attacks this problem from the language runtime level. Instead of asking you to instrument each function, it hooks into the network and concurrency primitives you already use.
What the Runtime Instruments for You
The module provides automatic instrumentation for HTTP services, database calls, and message producers and consumers using OpenTelemetry APIs. In practice, that means a plain HTTP listener or a SQL client call can emit trace spans without a single line of tracing code. The runtime also handles context propagation across asynchronous workers and strands—Ballerina’s lightweight concurrency units—so a trace that begins in one worker can continue through a downstream call made in another.
This matters because Ballerina services often fan out work across workers. Without runtime-level propagation, you would lose trace continuity exactly where the interesting latency lives. The observability module keeps the OpenTelemetry context attached to the strand, so the parent-child relationship survives the async boundary.
A Minimal Observable Service
Consider a basic HTTP service. The only change needed to turn on automatic telemetry is the import:
import ballerina/http;
import ballerina/observability;
service / on new http:Listener(9090) {
resource function get greeting() returns string {
return "hello";
}
}
Next, add the ballerinax/opentelemetry package to your project and configure an exporter. The module is stable from Ballerina 2201.0 (Swan Lake) onward and requires the ballerinax/opentelemetry package for exporters. In your Config.toml (or via environment variables), enable observability and point the exporter at your collector. Exact configuration keys vary by Ballerina version, so verify them against the module documentation for your Swan Lake release.
Once the service is running, send a request:
curl http://localhost:9090/greeting
If tracing is enabled, the Jaeger UI should show a span for the greeting operation. For metrics, scrape the /metrics endpoint exposed by the observability module—typically on a separate port from your business listener—and confirm that Prometheus can ingest the output. If nothing appears, the first diagnostic step is to confirm the exporter endpoint is reachable and that the observability module is actually enabled in configuration.
The Cost of Always-On Telemetry
Automatic instrumentation is convenient, but it is not free. Every span and metric carries runtime overhead. In high-throughput services, the default behavior can add measurable latency and memory pressure. The practical response is to adjust sampling rates so that only a fraction of traces are exported, or to disable specific exporters when a backend is not needed. These are configuration decisions, not code rewrites, but they require you to know your traffic profile.
The second limitation is depth. Automatic hooks cover the standard boundaries—HTTP, database, messaging—but they cannot know the semantics of your business logic. When you need a span inside a complex calculation or a custom retry loop, you must drop to the OpenTelemetry API manually. That reintroduces the boilerplate the module was meant to avoid, and it increases code complexity. Treat manual instrumentation as a targeted tool, not a default.
Start Small, Measure, Then Tune
The fastest way to evaluate this feature is to take one existing Ballerina service, add the ballerina/observability import, and point an OTLP exporter at a local Jaeger instance. Send a handful of requests and confirm that spans appear. Then check the /metrics endpoint. If the service handles production-scale traffic, benchmark before and after, and tune sampling before you enable it broadly. Observability should reduce guesswork, not add a new performance mystery.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.