
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Implementing observability for Deno with OpenTelemetry requires a different approach than Node.js because Deno uses native Rust-based telemetry bindings rather than npm SDKs. While traditional runtimes rely on monkey-patching modules at runtime, Deno exposes low-level hooks directly through its standard library and unstable APIs. This guide walks you through configuring the OpenTelemetry (OTel) pipeline specifically for the Deno environment, ensuring your traces, metrics, and logs reach backends like Jaeger or Grafana Tempo without compatibility layers.
@opentelemetry/sdk-node equivalent via Deno's npm compatibility layer or use the native deno telemetry API available in recent releases. Configure an OTLP exporter pointing to your collector endpoint, register global trace providers, and ensure the --unstable-otel flag is set during execution to activate native instrumentation hooks.Before writing any instrumentation code, it helps to understand how signals flow from a Deno process to your backend. Unlike managed cloud services where agents are injected automatically, self-hosted Deno applications require explicit configuration of the telemetry pipeline. For a deeper comparison of signal types, refer to our breakdown of metrics, logs, and traces compared. The architecture below illustrates the direct export path most teams use before introducing a collector.
How do you configure observability for Deno with OpenTelemetry?
Configuration in Deno differs from Node.js primarily in dependency resolution and permission management. You cannot simply run require('@opentelemetry/api') without declaring permissions or mapping specifiers. In 2026, the recommended path uses the npm compatibility layer for SDK components while leveraging Deno’s native runtime hooks for automatic instrumentation.
Setting up dependencies and permissions
Deno requires explicit network permissions to export telemetry data. If you omit these flags, your application will start but silently drop all spans and metrics. Create a deno.json configuration file to manage these settings declaratively rather than passing CLI flags repeatedly.
{
"imports": {
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
"@opentelemetry/sdk-trace-base": "npm:@opentelemetry/sdk-trace-base@^1.25.0",
"@opentelemetry/exporter-trace-otlp-http": "npm:@opentelemetry/exporter-trace-otlp-http@^0.52.0",
"@opentelemetry/resources": "npm:@opentelemetry/resources@^1.25.0",
"@opentelemetry/semantic-conventions": "npm:@opentelemetry/semantic-conventions@^1.25.0"
},
"unstable": ["otel"],
"permissions": {
"net": ["otel-collector.internal:4318", "api.example.com:443"],
"env": ["OTEL_SERVICE_NAME", "OTEL_EXPORTER_OTLP_ENDPOINT"]
}
} The "unstable": ["otel"] field is critical. Without it, Deno’s native telemetry hooks remain inactive even if the JavaScript SDK initializes correctly. This flag enables the bridge between the V8 isolate and the Rust telemetry subsystem, allowing automatic capture of HTTP fetch calls, file I/O, and database queries without manual wrapping.
Initializing the tracer provider
Create a dedicated telemetry.ts module that runs before your application logic. This separation ensures tracing is active before any instrumented code executes. Import this module as the very first line in your main entry point.
import { trace } from "@opentelemetry/api";
import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
const resource = new Resource({
[ATTR_SERVICE_NAME]: Deno.env.get("OTEL_SERVICE_NAME") || "deno-service",
[ATTR_SERVICE_VERSION]: "1.0.0",
"deployment.environment": Deno.env.get("DENO_ENV") || "production",
});
const provider = new BasicTracerProvider({
resource: resource,
});
const exporter = new OTLPTraceExporter({
url: Deno.env.get("OTEL_EXPORTER_OTLP_ENDPOINT") || "http://localhost:4318/v1/traces",
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
console.log(`[Telemetry] Initialized for ${resource.attributes[ATTR_SERVICE_NAME]}`); A common mistake is initializing the provider inside an async function. The provider must be registered synchronously at module load time; otherwise, early requests will lack trace context propagation. Always verify initialization by checking console output or querying your backend for the service name immediately after deployment.
What signals does Deno support natively versus via polyfills?
Understanding the boundary between native support and polyfilled functionality prevents debugging nightmares. Deno’s telemetry story has matured significantly, but gaps remain compared to the Node.js ecosystem. Refer to the OpenTelemetry observability standard overview for protocol-level details that apply across runtimes.
| Signal Type | Native Support | Polyfill / npm SDK | Notes |
|---|---|---|---|
| Tracing (Spans) | ✅ Full | ✅ Compatible | Native hooks auto-instrument fetch, Deno.serve, fs ops |
| Metrics | ⚠️ Partial | ✅ Recommended | Runtime metrics (heap, CPU) require npm SDK; custom metrics work natively |
| Logging | ❌ None | ✅ Required | Use std/log + OTel log bridge or structured JSON to stdout |
| Context Propagation | ✅ Full | ✅ Compatible | W3C TraceContext headers injected automatically in fetch |
| Baggage | ✅ Full | ✅ Compatible | Cross-service metadata propagation works out of box |
In practice, most production Deno deployments use a hybrid approach: native tracing for zero-overhead request visibility, npm SDKs for metrics aggregation, and structured logging piped to a separate shipper. Trying to force pure-native telemetry for all three signals often leads to missing data or excessive boilerplate.
How do you instrument HTTP servers and fetch calls in Deno?
HTTP instrumentation is where Deno’s native OTel integration shines brightest. When the --unstable-otel flag is active, both Deno.serve() and global fetch() automatically create spans with correct parent-child relationships. However, you still need to enrich these spans with business context.
Enriching auto-instrumented spans
While Deno creates the server span automatically, it doesn’t know about your domain model. Use the active span API to add attributes that make traces searchable and meaningful for debugging.
import { trace, SpanStatusCode } from "@opentelemetry/api";
Deno.serve(async (req) => {
const span = trace.getActiveSpan();
const url = new URL(req.url);
// Add business context to the auto-created span
span?.setAttributes({
"http.route": url.pathname,
"user.id": req.headers.get("x-user-id") || "anonymous",
"request.method": req.method,
});
try {
// Outgoing fetch automatically inherits trace context
const upstream = await fetch("https://api.example.com/data");
const data = await upstream.json();
span?.setAttribute("response.item_count", data.items.length);
return Response.json(data);
} catch (err) {
span?.recordException(err);
span?.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
return new Response("Internal Error", { status: 500 });
}
}); Note that trace.getActiveSpan() returns undefined if called outside an instrumented context. Always use optional chaining to prevent crashes during testing or when running without the unstable flag. The outgoing fetch() call automatically receives the traceparent header, linking downstream services to this request without manual header manipulation.
How do you export Deno telemetry to Grafana or Jaeger?
Export configuration determines whether your telemetry actually reaches analysts. Most teams deploy an OpenTelemetry Collector as a sidecar or daemonset to handle batching, retry logic, and protocol translation. For detailed backend setup, see our guide on Tempo distributed tracing with Grafana.
Configuring OTLP exporters for production
Never send telemetry directly from Deno to a public backend endpoint in production. Network blips will cause unbounded memory growth in the batch processor. Always route through a local collector or use authenticated HTTPS endpoints with proper timeout configuration.
# Environment variables for production deployment
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.internal:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_COMPRESSION=gzip
OTEL_EXPORTER_OTLP_TIMEOUT=10000
OTEL_TRACES_SAMPLER=parentbased_tracealways
OTEL_TRACES_SAMPLER_ARG=0.1 The http/protobuf protocol is preferred over JSON for Deno applications because it reduces payload size by 40–60% and serializes faster in V8. Compression further cuts bandwidth costs, which matters when hosting in regions with expensive egress pricing. Sampling should always be configured at the head (in Deno) rather than tail (in the backend) to reduce serialization overhead.
Validating your pipeline before going live
Before deploying to production, validate that spans arrive correctly using the console exporter or a local Jaeger instance. A frequent issue in Deno is missing service names due to unset environment variables, causing all traces to appear under "unknown_service". Run this validation script:
- Start a local collector with debug logging enabled
- Run your Deno app with
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 - Send test requests and verify span attributes in collector logs
- Check that
service.name,service.version, anddeployment.environmentare populated - Confirm trace IDs propagate through multi-hop calls
If spans appear but lack parent-child links, verify that your middleware isn’t stripping headers before Deno’s instrumentation reads them. Frameworks like Oak or Fresh sometimes consume request objects before the native hook can extract context.
Common pitfalls when implementing observability for Deno with OpenTelemetry
Even experienced engineers hit Deno-specific gotchas. These issues rarely appear in Node.js tutorials and can waste hours of debugging time.
- Permission denial silence: Deno fails open for telemetry. If network permissions are missing, exports fail silently. Always check stderr for permission warnings during startup.
- Cold start latency: Initializing the OTel SDK adds 50–150ms to cold starts in serverless environments. Consider lazy initialization for non-critical paths or pre-warming strategies.
- Memory leaks in long-running processes: Early Deno versions had bugs in span cleanup. Ensure you’re on Deno 2.x+ where V8 garbage collection properly releases span references.
- Metric cardinality explosions: Adding user IDs or request paths as metric labels crashes Prometheus. Use attributes only on traces; aggregate metrics with bounded label sets.
- Timezone confusion: Deno timestamps are always UTC. If your backend expects local time, convert during visualization, not at emission. Mixing timezones breaks correlation.
Another subtle issue arises when mixing npm packages with Deno-native modules. Some npm instrumentation libraries attempt to patch Node.js built-ins that don’t exist in Deno, throwing errors during initialization. Wrap such imports in try/catch blocks or use Deno-specific alternatives when available.
Next steps for production-grade Deno monitoring
Getting observability for Deno with OpenTelemetry working is just the foundation. Production readiness requires defining meaningful SLIs, setting up alerting thresholds, and establishing runbooks that reference actual trace IDs. Start by identifying your critical user journeys and ensuring each has end-to-end trace coverage. Then define error budgets based on real latency percentiles rather than arbitrary uptime targets. Our guide on defining meaningful SLIs and SLOs provides frameworks adapted for modern runtimes like Deno.
If your team needs help designing a telemetry strategy that scales with your Deno infrastructure, reach out to discuss your observability architecture. Whether you're migrating from Node.js or building greenfield, getting the instrumentation right early prevents costly rework later.