
If you've added distributed tracing to a TypeScript backend, you've written code that looks like this: a span that says "this function queries a database," next to a db.query() call. A span that says "this function calls another service," next to a paymentService.charge() call. A span that says "this function publishes a message," next to a topic.publish() call.
Unlike a comment that restates the obvious, this duplication is load-bearing. Forget a span and you lose visibility into that operation in production. Forget to propagate context across a service call and the trace breaks in half. The failure mode is silent: you don't know what you can't see.
The OpenTelemetry project standardized tracing as a library-level concern. It defines both a wire protocol (OTLP) for transmitting telemetry and language SDKs for producing it. In practice, the SDK is what you interact with as a developer: you import it, create spans, set attributes, propagate context. The standard works across every language and framework because it doesn't assume anything about your application. It gives you an API and you describe your operations manually.
Auto-instrumentation libraries help for common frameworks. There's a package that wraps Express HTTP handlers, one that wraps pg database calls, one for Redis. But these capture surface-level information: a query was executed, an HTTP call was made. They don't capture the semantic structure, which service is calling which, what the API contract looks like, how pub/sub topics connect producers and consumers. The OTel docs distinguish between "automatic" and "manual" instrumentation and recommend manual instrumentation for anything application-specific. In practice, that's everything interesting.
The result is predictable. Teams instrument the operations they think will matter and skip the rest. Coverage depends on which developer remembered to add spans. Pub/sub messages are sometimes traced. Cache operations are rarely traced. Outbound HTTP calls to third-party APIs depend on the developer who wrote the integration. The CNCF 2024 survey found tracing is the least adopted observability signal, behind both metrics and logging. The setup cost is real, but the coverage problem is worse.
In a traditional Express or Fastify codebase, a compiler can't actually distinguish a service-to-service RPC from a random HTTP call, or figure out which database calls belong to which logical database. The structure is there in the sense that a human can read it and work it out, but it's not something a tool can extract.
That changes when your framework uses typed primitives for infrastructure. When an API endpoint is declared with api(), a database with new SQLDatabase(), a pub/sub topic with new Topic(), the framework knows the full structure: which functions are endpoints, which calls cross service boundaries, which operations are database queries, pub/sub publishes, or cache lookups.
Encore uses a Rust-based static analyzer that parses your TypeScript (or Go) at compile time and builds a complete graph of services, APIs, and infrastructure from these declarations. The runtime uses that graph to trace every operation automatically: API calls with full request/response bodies, database queries with bound parameters (the actual values, not just $1), pub/sub messages with payloads, cache operations with keys and results, outbound HTTP calls with timing. No SDK, no manual spans, no context propagation code.
Coverage is 100% by default because the compiler sees all operations, not just the ones someone decided to instrument. When you add a new database call or a new service-to-service RPC, it appears in traces immediately. There's no second step.
This runs the same way in local development (localhost:9400 during encore run) and in production through Encore Cloud. Most debugging happens during development, and most development environments have no tracing at all. Having the same trace viewer locally changes how you build, because you're looking at actual request flows instead of reading code and guessing.
The common objection is that compiler-level tracing can't capture business-logic-specific information. If you want a span around a fraud check or an attribute for high-value orders, you need manual instrumentation.
That's a fair point, and it's something we want to support in Encore in the future. But the operations that matter for debugging and performance, the ones you need 100% of the time, are structural: API calls, database queries, service communication, pub/sub, cache, outbound HTTP. These account for the vast majority of what you look at in a trace. The current model treats structural tracing and business-logic tracing as the same problem, requiring the same manual effort. The result is that most teams don't do either well.
When every operation is guaranteed to be in the trace, you can build features that aren't possible with partial coverage.
We recently shipped a new Trace Explorer in Encore Cloud. You can compare request volumes across time ranges, correlate error spikes with deploys, filter by latency percentiles, group by service or endpoint. Every one of these features assumes complete data. With partial instrumentation, every query result comes with an implicit caveat: these numbers only reflect what someone instrumented. With compiler-level tracing, the data is the system.
We're also working on OpenTelemetry export so traces generated by the Encore runtime can be sent to Datadog, Grafana Cloud, Honeycomb, Jaeger, or any OTLP endpoint. The runtime generates the traces, the OTel ecosystem handles the rest. Instrumentation moves to the compiler, but the wire format and the backends stay the same.
There's a broader pattern here. Infrastructure provisioning used to be a manual, application-external concern: you wrote Terraform configs in a separate language, maintained in a separate repo. Now it's moving into typed application code, where a framework can read the declarations and provision the infrastructure automatically.
Tracing is going the same way. When the framework uses typed primitives for APIs, databases, pub/sub, and caches, the application's structure becomes something a compiler can read. Asking developers to re-describe that structure through instrumentation APIs was the only option when the toolchain couldn't understand the application. Once it can, tracing becomes a compiler output instead of a developer task.
Encore is an open-source backend framework where infrastructure is declared in TypeScript or Go and provisioned from the code. Tracing is built into the runtime. GitHub.


