# How to Set Up OpenTelemetry Tracing Without Jaeger

> Set up OpenTelemetry tracing in Node.js, Python and Go: SDK instrumentation, an authenticated OTLP exporter and one self-hosted backend, not five services.

David Viejo · Published 2026-03-12 · Updated 2026-10-20 · 14 min read

Canonical: https://temps.sh/blog/how-to-set-up-opentelemetry-tracing

To set up OpenTelemetry tracing, you instrument your app with the OTel SDK, configure an OTLP exporter with an endpoint and credentials, and point it at a backend that stores and visualizes traces. With Temps, that backend is already running as part of your deployment platform: apps it deploys get the OTLP endpoint and auth headers injected, and apps hosted elsewhere set two variables, the endpoint and an auth header.

OpenTelemetry is backed by every major cloud vendor, supported in every language you'd actually use in production, and maintained as a [CNCF project](https://www.cncf.io/projects/opentelemetry/). Yet most teams still don't have tracing set up — not because it's hard to understand, but because the standard setup requires running a collector, a storage backend, and a visualization layer before you see your first span.

This guide walks you through what OpenTelemetry tracing is, how to instrument a Node.js app, and how to skip the painful infrastructure part entirely.

> **TL;DR:** OpenTelemetry tracing gives you end-to-end visibility into every request across your services. The standard setup requires 4-5 services (SDK, collector, Jaeger, storage, Grafana). You can skip all of that by pointing your OTLP exporter at a Temps instance, which includes a built-in OTLP ingest endpoint and trace viewer backed by TimescaleDB. OpenTelemetry has [official SDKs](https://opentelemetry.io/docs/) for 11 languages.

---

## What Is OpenTelemetry and Why Should You Care?

OpenTelemetry is a vendor-neutral observability framework maintained under the [CNCF](https://www.cncf.io/projects/opentelemetry/). It gives you three signals — traces, metrics, and logs — through a single set of APIs and SDKs. Traces are the one most teams need first.

A **trace** represents a single operation as it flows through your system. Think of a user clicking "Place Order" — that request hits your API gateway, calls the inventory service, writes to the database, triggers a payment processor, and sends a confirmation email. Without tracing, when that request takes 4 seconds instead of 200ms, you're grepping logs across five services.

Each step in that journey is a **span**. Spans have a start time, duration, attributes, and a parent-child relationship. Stitch them together and you get a trace — a complete picture of what happened and how long each part took.

Here's what a trace looks like conceptually:

```
[Trace: order-checkout]
|
|-- [Span: API Gateway]           12ms
|   |-- [Span: Auth Middleware]    2ms
|   |-- [Span: POST /orders]      10ms
|       |-- [Span: Inventory Check]   45ms
|       |-- [Span: Payment Service]  380ms
|       |   |-- [Span: Stripe API]   350ms
|       |-- [Span: Send Email]        22ms
|       |-- [Span: DB Write]          8ms
```

**Context propagation** is the mechanism that ties these spans together. When Service A calls Service B, it passes a trace ID in the HTTP headers (usually `traceparent`). Service B picks it up, creates a child span, and passes it along. Every span shares the same trace ID, so your visualization tool can reconstruct the full request path.

Why does this matter? Because the alternative is correlating timestamps across log files — and that doesn't work at 2am during an outage.

---

## Why Is the Traditional OpenTelemetry Setup So Painful?

Setting up OpenTelemetry from scratch means assembling multiple independent tools. Here's the minimum architecture for a self-hosted tracing pipeline:

### 1. The OTel SDK in your application

You add the OpenTelemetry SDK to your app. For Node.js, that's 4-6 packages. For Java, it's an agent JAR. For Go, it's a handful of modules plus manual span creation. This is the easy part.

### 2. The OpenTelemetry Collector

The SDK doesn't send traces directly to your storage backend. Best practice says you run an OTel Collector — a standalone service that receives, processes, and exports telemetry data. It handles batching, retry logic, and format translation. That's another Docker container to manage, configure, and keep alive.

### 3. A trace storage backend

Jaeger needs a storage backend. Your options are Elasticsearch, Cassandra, or Kafka. Each comes with its own operational complexity. Elasticsearch alone wants at least 2GB of RAM for a small cluster, and you'll need to manage indices, retention policies, and disk usage.

Alternatively, you can use Grafana Tempo, which stores traces in object storage (S3, GCS). Cheaper, but now you need to configure object storage access, retention, and compaction.

### 4. A visualization layer

Jaeger has its own UI. Grafana has another. You'll probably end up running Grafana anyway for dashboards, which means configuring data source connections, provisioning dashboards, and managing yet another service.

### 5. The total resource cost

Before you see a single trace, you're running:

| Service           | Purpose                  | Memory      |
| ----------------- | ------------------------ | ----------- |
| OTel Collector    | Receive and export spans | \~200MB     |
| Jaeger (or Tempo) | Trace backend            | \~512MB-1GB |
| Elasticsearch     | Storage                  | \~2GB+      |
| Grafana           | Visualization            | \~256MB     |

That's roughly 3-4GB of RAM dedicated to observability infrastructure — on top of whatever your actual application uses. And none of this counts the hours you spend configuring YAML files.

---

## What Are the Three Things You Actually Need for Tracing?

Strip away the complexity and distributed tracing requires exactly three components: instrumentation, transport, and visualization. According to the [OpenTelemetry documentation](https://opentelemetry.io/docs/concepts/signals/traces/), the SDK handles the first two and any OTLP-compatible backend handles the third.

**Instrument your app.** Add the OTel SDK. Enable auto-instrumentation for your HTTP framework, database driver, and external API calls. This takes 10-20 lines of code.

**Send traces somewhere.** Configure an OTLP exporter with an endpoint URL. The SDK batches spans and ships them over HTTP or gRPC. One environment variable is all you really need.

**See them.** A UI that renders trace waterfalls, lets you search by service name or trace ID, and shows latency breakdowns. This is where most of the infrastructure bloat lives — and where the biggest shortcut is possible.

The insight most guides miss: you don't need a separate collector, storage backend, and visualization tool. If your deployment platform already has a built-in OTLP endpoint and trace viewer, components two and three collapse into a single service you're already running.

---

## How Do You Add OpenTelemetry Tracing to a Node.js App?

The [OpenTelemetry JavaScript SDK](https://github.com/open-telemetry/opentelemetry-js-contrib) supports auto-instrumentation for 30+ libraries including Express, Fastify, PostgreSQL, MongoDB, Redis, and gRPC. You can get useful traces without writing a single manual span.

### Install the packages

```bash
npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions
```

Five packages. The `auto-instrumentations-node` meta-package bundles instrumentation for HTTP, Express, Fastify, pg, mysql, redis, mongodb, grpc, and more.

### Create the tracing file

Create a `tracing.ts` (or `tracing.js`) file at the root of your project:

```typescript
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'my-app',
  }),
  traceExporter: new OTLPTraceExporter({
    url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      // Disable noisy fs instrumentation
      '@opentelemetry/instrumentation-fs': { enabled: false },
      // Capture SQL query text (be careful with PII)
      '@opentelemetry/instrumentation-pg': {
        enhancedDatabaseReporting: true,
      },
    }),
  ],
});

sdk.start();

// Graceful shutdown
process.on('SIGTERM', () => {
  sdk.shutdown().then(() => process.exit(0));
});
```

### Load it before your app

The tracing file must load before any other imports. If Express or pg is imported before the SDK initializes, auto-instrumentation won't patch them.

```bash
# Node.js 18+
node --import ./tracing.js app.js

# Or with ts-node
node --import ./tracing.ts app.ts
```

For older Node.js versions, use the `--require` flag instead:

```bash
node -r ./tracing.js app.js
```

### Set the environment variables

```bash
OTEL_SERVICE_NAME=checkout-api
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
```

That's the entire instrumentation. Every HTTP request your Express or Fastify server handles, every database query, every outbound HTTP call — they all become spans automatically.

### Adding custom spans

Auto-instrumentation covers framework-level operations. For business logic, you'll want custom spans:

```typescript
import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('checkout-service');

async function processOrder(orderId: string) {
  return tracer.startActiveSpan('process-order', async (span) => {
    span.setAttribute('order.id', orderId);

    try {
      const inventory = await checkInventory(orderId);
      span.setAttribute('order.items_count', inventory.length);

      const payment = await chargeCustomer(orderId);
      span.setAttribute('order.amount', payment.amount);

      return { success: true };
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: 2, message: (error as Error).message });
      throw error;
    } finally {
      span.end();
    }
  });
}
```

Custom spans nest inside auto-instrumented spans automatically through context propagation. Your `process-order` span appears as a child of the HTTP handler span.

---

## Where Do Your Traces Actually Go?

This is the question that trips up most teams. The SDK is easy. Choosing a backend isn't. [Datadog](https://www.datadoghq.com/pricing/) APM starts at $31 per APM host per month billed annually ($36 on-demand) when the host also has Infrastructure Monitoring, or $36 bought standalone (prices checked October 2026), and includes 150GB of ingested spans per APM host, then $0.10/GB for extra spans, and [New Relic](https://newrelic.com/pricing) charges $0.40 per GB beyond the free 100 GB. At scale, tracing becomes your biggest observability bill.

Here are the realistic options:

### Self-hosted Jaeger

[Jaeger](https://www.jaegertracing.io/) is the most popular open-source tracing backend. Originally built by Uber, now a CNCF graduated project.

**Pros:** Free, well-documented, good UI for trace exploration.
**Cons:** Needs Elasticsearch or Cassandra for production. Scaling requires significant ops knowledge. You're now maintaining a distributed database for your distributed traces.

Best for teams that already run Elasticsearch for logging and have ops capacity to spare.

### Grafana Tempo

[Tempo](https://grafana.com/oss/tempo/) stores traces in object storage (S3, GCS, Azure Blob). No indexing — it searches by trace ID only, which keeps costs low.

**Pros:** Cheap storage, integrates with Grafana dashboards.
**Cons:** No full-text search across spans without Grafana Cloud. Complex configuration. Requires a separate Grafana instance for visualization.

Best for teams already invested in the Grafana ecosystem.

### SaaS platforms (Datadog, New Relic, Honeycomb)

Ship traces to a managed service. Zero infrastructure to manage.

**Pros:** Instant setup, powerful query engines, alerting built in.
**Cons:** Expensive at scale. Datadog APM is billed per host (from $31/host/month billed annually), so the cost grows with every host and microservice you instrument, plus $0.10/GB once you exceed the 150GB of ingested spans included per APM host. And you're locked into their query language and retention policies.

### Temps (built-in OTLP ingest)

If you're already running Temps for deployments, it includes a built-in OTLP ingest endpoint and trace storage backed by TimescaleDB. No additional services to deploy or configure. The [/observability](https://temps.sh/observability) page summarises what it covers.

**Pros:** Zero extra infrastructure. Traces live alongside your deployment logs, error tracking, and analytics. One dashboard for everything.
**Cons:** Designed for teams already using Temps as their deployment platform.

The observability landscape has a gap: open-source tools require significant ops investment, and SaaS tools charge per-GB prices that punish you for instrumenting more of your code. The ideal solution is a platform that bundles tracing with something you already need — like deployments.

| Option                 | Cost                                                            | Ops Overhead           | Storage        |
| ---------------------- | --------------------------------------------------------------- | ---------------------- | -------------- |
| Jaeger + Elasticsearch | Free (+ infra)                                                  | High                   | Self-managed   |
| Grafana Tempo          | Free (+ infra)                                                  | Medium                 | Object storage |
| Datadog                | From $31/APM host/mo (150GB spans/host included, then $0.10/GB) | None                   | Managed        |
| New Relic              | $0.40/GB (over free 100 GB)                                     | None                   | Managed        |
| Temps                  | Free to self-host; Temps Cloud from $29/mo (coming soon)        | None (already running) | TimescaleDB    |

---

## How Do You Set Up OpenTelemetry Tracing Without Extra Infrastructure?

If you're already running Temps, adding tracing to your app needs no additional services. Temps exposes OTLP/HTTP ingest endpoints at `/api/otel/v1/traces`, `/api/otel/v1/metrics`, and `/api/otel/v1/logs`, accepts protobuf payloads (gzip or zstd), and stores spans in TimescaleDB hypertables with a default 7-day retention. The [OpenTelemetry docs](https://temps.sh/docs/opentelemetry) are the reference for every endpoint and setting.

### What Temps' OTel ingest does

1. **Authenticated ingest.** Every request carries a token in `Authorization: Bearer <token>` or `X-Temps-Api-Key`. A deployment token (`dt_…`) already knows its project, environment and deployment; an API key (`tk_…`) also needs an `X-Temps-Project-Id` header ([authentication](https://temps.sh/docs/opentelemetry#authentication)).
2. **Rate limits and optional quotas.** Ingest is rate-limited per project (1,000 requests per 60 seconds by default, HTTP 429 beyond that). Per-project storage quotas are opt-in and off by default; set `TEMPS_OTEL_QUOTA_GB` on the Temps server to enforce one.
3. **GenAI views from ordinary spans.** Spans that carry `gen_ai.*` attributes, such as those from AI SDK instrumentation, show up in the GenAI trace views with no separate setup.

### For apps deployed on Temps

You don't set the endpoint yourself. Temps injects the standard variables into every deployment ([injected variables](https://temps.sh/docs/manage-environment-variables)):

```bash
OTEL_EXPORTER_OTLP_ENDPOINT=<TEMPS_API_URL>/otel   # e.g. https://temps.yourdomain.com/api/otel
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS=<authentication and project headers>
OTEL_SERVICE_NAME=<project name>
```

The `tracing.ts` file from the previous section works unchanged: it appends `/v1/traces` to the injected endpoint, and the OTel JavaScript exporter reads `OTEL_EXPORTER_OTLP_HEADERS` from the environment. Don't override `OTEL_EXPORTER_OTLP_ENDPOINT` in your project settings, or you'll lose the matching auth headers.

### For external apps

Temps accepts traces from any app that speaks OTLP, not just apps deployed on the platform. Running a Python service on a separate VPS, or a Go microservice on Kubernetes? Create an API key or deployment token in Temps, then set:

```bash
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-temps-instance.example.com/api/otel
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <token>"
# with a tk_ API key, also add the project:
# OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer tk_...,X-Temps-Project-Id=<project-id>"
OTEL_SERVICE_NAME=my-app
```

Or configure the exporter in code:

```python
# Python example with opentelemetry-sdk
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

exporter = OTLPSpanExporter(
    endpoint="https://your-temps-instance.example.com/api/otel/v1/traces",
    headers={"Authorization": "Bearer <token>"},
)
```

### What happens behind the scenes

When your app sends spans to Temps:

1. The ingest handler authenticates the request, decompresses the body, and decodes the protobuf at `POST /api/otel/v1/traces`
2. Spans are associated with the project (and environment and deployment, when the token carries them)
3. Spans are written to a TimescaleDB hypertable optimized for time-series queries
4. The traces UI reconstructs the full waterfall from the stored spans

No collector to configure. No Jaeger to deploy. No Elasticsearch to tune. No Grafana dashboards to build.

---

## What Do You Get From the Traces UI?

Once traces start flowing, the Temps dashboard gives you a unified view of every request across all instrumented services.

### Trace waterfall view

Click any trace and you see the full span tree rendered as a waterfall. Each bar represents a span — its width shows duration, its color indicates the service, and expanding it reveals attributes like HTTP status codes, database query text, and custom metadata.

The waterfall makes latency problems immediately visible. If your checkout endpoint takes 2 seconds, you can see that 1.8 seconds is spent waiting for the payment provider and 150ms is a slow database query. No guessing.

### Search and filtering

Find traces by:

- **Service name** — filter to a specific microservice
- **Operation name** — find all `POST /orders` traces
- **Duration** — show only requests slower than 500ms
- **Status** — filter to error traces
- **Time range** — look at traces from the last hour, day, or custom window
- **Trace ID** — paste a trace ID from your logs and jump directly to the visualization

### Deployment correlation

Traces live alongside your deployment events in the same system, and the [unified observability timeline](https://temps.sh/blog/unified-observability-timeline-requests-traces-errors-revenue) merges them with requests, [errors](https://temps.sh/docs/error-tracking) and revenue events in time order, with errors linked to their traces by trace ID. You can answer "did the last deploy make things slower?" without stitching tools together — something standalone tracing tools can't do easily because they have no concept of your deploy history.

---

## How Does OpenTelemetry Work With Python, Go, and Rust?

The OTel SDK is available in 11 languages with official support, and the auto-instrumentation experience varies significantly by ecosystem. According to the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/), Python has 40+ auto-instrumentation packages, while Rust requires manual span creation for most operations.

### Python

Python has the most mature auto-instrumentation story after Java. One command installs everything:

```bash
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
```

Then run your app with the auto-instrumentation wrapper:

```bash
opentelemetry-instrument \
  --service_name my-python-app \
  --traces_exporter otlp \
  --exporter_otlp_endpoint https://your-temps-instance.example.com/api/otel \
  --exporter_otlp_headers "Authorization=Bearer <token>" \
  python app.py
```

Django, Flask, FastAPI, SQLAlchemy, psycopg2, redis, requests, httpx — all instrumented automatically. No code changes to your application.

### Go

Go requires slightly more setup because there's no runtime patching. You wrap your HTTP handlers and database connections manually:

```go
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

// Wrap your HTTP handler
handler := otelhttp.NewHandler(mux, "server")

// Use the instrumented HTTP client
client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
```

More code than Node.js or Python, but Go's explicit style means you always know exactly what's being traced.

### Rust

Rust uses the `tracing` crate ecosystem with an OpenTelemetry bridge:

```rust
use tracing_subscriber::prelude::*;
use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::SpanExporter;

let exporter = SpanExporter::builder()
    .with_tonic()
    .build()?;

let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
    .with_batch_exporter(exporter)
    .build();

tracing_subscriber::registry()
    .with(tracing_opentelemetry::layer()
        .with_tracer(provider.tracer("my-service")))
    .init();
```

Rust has fewer auto-instrumentation libraries, but the `tracing` macros (`#[instrument]`) make manual instrumentation clean. Temps itself uses this exact pattern internally — the `temps-otel` crate bridges `tracing` spans to the OpenTelemetry pipeline.

---

## Frequently Asked Questions

### How do you set up OpenTelemetry tracing without running a collector?

Point your OTLP exporter directly at a backend that accepts OTLP natively. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to your backend URL, `OTEL_EXPORTER_OTLP_HEADERS` to its auth header, and `OTEL_SERVICE_NAME` to identify your service. With Temps, the endpoint is `https://your-temps-instance.example.com/api/otel` (the SDK appends `/v1/traces`) and the header is `Authorization=Bearer <token>`; no collector is needed. Apps deployed on Temps get all three variables injected.

### What is the performance overhead of OpenTelemetry?

The overhead is usually small. The [OTel specification](https://opentelemetry.io/docs/specs/otel/performance/) requires SDKs to keep the instrumented code path cheap, and the SDK batches spans in memory and exports them asynchronously, so the hot path of your request handler doesn't block on network calls. Measure it on your own workload; if you need to reduce it, use head-based sampling to trace a percentage of requests rather than all of them.

### Can I use OpenTelemetry with any programming language?

Yes. [OpenTelemetry](https://opentelemetry.io/docs/) provides official SDKs for 11 languages: Java, Python, JavaScript/Node.js, Go, .NET, Ruby, PHP, Swift, Erlang/Elixir, Rust, and C++. Auto-instrumentation maturity varies — Java and Python have the most libraries instrumented automatically, while Go and Rust require more manual setup. Any language that can make HTTP requests can also send traces via the OTLP HTTP protocol directly, even without an official SDK.

### How much does OpenTelemetry tracing storage cost?

The OTel SDK itself is free and open-source. Storage costs depend entirely on your backend choice. Self-hosted Jaeger with Elasticsearch costs whatever your server costs — typically $20-50/month for a small setup on a VPS. [Datadog](https://www.datadoghq.com/pricing/) APM is priced per host (from $31/host/month billed annually, 150GB of ingested spans per host included, then $0.10/GB). [New Relic](https://newrelic.com/pricing) offers 100GB free per month, then $0.40/GB. With Temps, trace storage is included — no additional cost beyond the server you already run it on. Self-hosting is free under a dual MIT / Apache 2.0 license. Temps Cloud, a hosted control plane for your own servers, is coming soon from $29/month.

### What's the difference between traces, metrics, and logs?

**Traces** show the journey of a single request across services — they answer "what happened to this specific request and how long did each step take?" **Metrics** are aggregated numerical measurements over time — request count, error rate, latency. **Logs** are discrete text events emitted by your application. OpenTelemetry supports all three signals, but traces provide the most immediate debugging value for distributed systems because they connect cause and effect across service boundaries.

### Do I need an OpenTelemetry Collector?

Not necessarily. The OTel SDK can export traces directly to any OTLP-compatible backend. The Collector is recommended for production because it handles batching, retry, and multi-destination routing — but if your backend already handles OTLP ingestion well (like Temps does), you can skip the Collector entirely and export directly from your app. If you prefer the standard collector architecture, run your own Collector and point its OTLP/HTTP exporter at Temps with the same endpoint and auth header.

---

## Start Tracing in 5 Minutes

OpenTelemetry is the industry standard for distributed tracing, and it's not going anywhere. The SDK is mature, auto-instrumentation covers most frameworks, and the OTLP protocol means you're never locked into a specific backend.

The hard part was always the infrastructure: collectors, storage, visualization. That problem disappears when your deployment platform includes a built-in OTLP ingest endpoint backed by TimescaleDB.

If you deploy on Temps, the OTLP endpoint and auth headers are already injected — add the SDK and you have production tracing. For apps hosted elsewhere, set `OTEL_EXPORTER_OTLP_ENDPOINT=https://your-temps-instance.example.com/api/otel` and an `Authorization=Bearer <token>` header. If you're not on Temps yet, you can set up an instance and get deployments, analytics, error tracking, and tracing from one install — free to self-host (dual MIT / Apache 2.0):

```bash
curl -fsSL https://temps.sh/deploy.sh | bash
```

---

Source: https://temps.sh/blog/how-to-set-up-opentelemetry-tracing · Index of all pages: https://temps.sh/llms.txt
