How to Use OpenTelemetry: The Beginner's Guide (Auto-Instrumentation, the Collector, First Trace, and the Sampling Lesson)

CodexFuse Team · 2026-10-31T21:32:38.000Z · 20 min read min read

Learn how to use OpenTelemetry from zero: auto-instrumentation, the Collector, your first trace, manual spans, metrics, logs, sampling, and the cost lessons.

If you have ever stared at a dashboard at 2 AM wondering which service in your stack actually broke, learning how to use OpenTelemetry is the single highest-leverage observability skill you can pick up. This opentelemetry tutorial for beginners takes you from zero to a working setup: what OpenTelemetry is (and the #1 thing it is not), the three signals it produces, zero-code auto-instrumentation of a real Python app, the Collector, your first trace in a live backend, manual spans, your first metric, logs, the same arc in Node.js, and the sampling and cardinality lessons that keep observability bills from eating your budget. The only assumption is a machine with Python and Docker — if Docker is new to you, our Docker beginner guide gets you there first. Here is the beginner arc this guide follows:

  1. Learn what OpenTelemetry is — an open standard and toolset for producing telemetry, not a place to view it.
  2. Understand the three signals — traces, metrics, and logs — and the questions each one answers.
  3. Auto-instrument a Python app with zero code changes via opentelemetry-instrument.
  4. Run the OpenTelemetry Collector — the receiving pipeline every serious setup puts between apps and backends.
  5. See your first trace in a real backend (Jaeger) and read what a span waterfall is telling you.
  6. Add a manual span and your first metric for the parts auto-instrumentation cannot see.
  7. Wire in logs, then repeat the core arc in Node.js.
  8. Learn sampling and cardinality — the two cost lessons that separate beginner setups from production ones.

What is OpenTelemetry, exactly?

OpenTelemetry — usually shortened to OTel — is an open standard plus a set of APIs, SDKs, and tools for producing telemetry data: the traces, metrics, and logs your software emits about itself. It is a Cloud Native Computing Foundation project that grew out of two older projects, OpenTracing and OpenCensus, which merged into a single effort in 2019. The key word is producing. This is the confusion that derails most beginners: OpenTelemetry does not store your data, and it does not draw dashboards. It standardizes how your code emits telemetry so you can send it anywhere — Jaeger, Prometheus, Grafana, a commercial vendor — and switch destinations later without rewriting your instrumentation. The win, in one sentence: instrument once, and your application code never again cares which backend receives its data.

As of this writing the ecosystem is moving fast and current: the Python SDK sits at v1.45.2 (released October 29, 2026, roughly three days ago), the JavaScript SDK at v2.12.0 (October 6, 2026), and the Collector at v0.164.0 (October 26, 2026) — so any tutorial older than a few months may reference packaging or defaults that have already shifted. The opentelemetry python packages below are the current install path. The Python repository carries roughly 2,700 GitHub stars and is Apache-2.0 licensed. This guide pins its commands to current conventions and flags the spots where older guides go stale.

What are traces, metrics, and logs?

OpenTelemetry's three signals answer three different questions, and beginners go wrong by trying to make one of them do the job of another. The table below is the decision map — when something is slow, you reach for traces; when something is unhealthy, you reach for metrics; when something is broken, you reach for logs:

SignalQuestion it answersWhat it looks likeTypical beginner use
TracesWhere did this request spend its time?A tree of spans: one root span per request, child spans for each operation (HTTP handler, DB query, external call)Finding which service or database call makes an endpoint slow
MetricsIs the system healthy, and is it getting worse?Aggregated numbers over time: request rate, error rate, latency percentiles, CPU/memoryDashboards and alerts: error rate above 1% pages someone
LogsWhat exactly happened at this moment?Timestamped text records with context attached (the trace ID)Seeing the exact exception and the values around it when a request failed

The magic is the correlation: OpenTelemetry injects the trace ID into your metrics and logs, so a spike on a metric graph is one click away from the slow trace, which is one click away from the log lines that explain it. One caveat worth knowing early: in the Python SDK, traces and metrics are marked Stable, but logs are still marked Development — logging works, but expect that side to evolve more than the other two.

How does OpenTelemetry actually work?

OTel has three building blocks, and naming them correctly saves you from half the beginner confusion. First, the API: the interface your code calls to create spans and meters — it is intentionally a no-op when no SDK is configured, so library authors can instrument code without forcing a backend on anyone. Second, the SDK: the real implementation per language that records spans, batches them, samples them, and exports them. Third, the Collector: an optional but near-universal middle layer that receives telemetry from all your apps (in OTLP format, usually gRPC on port 4317 or HTTP on 4318), processes it, and forwards it to one or more backends.

Why the middle layer matters: without the Collector, every app needs the backend's credentials and every backend change means redeploying every app. With it, apps ship to one local OTLP endpoint and the Collector's config decides where data lands. This is also why OpenTelemetry is vendor-neutral by design — you can point the same instrumented app at Prometheus today, a commercial vendor tomorrow, and Jaeger in your homelab, changing only Collector config, never application code.

What do you need before you start?

Three things: Python 3 (3.9 or newer), pip, and Docker for running the backend pieces in containers. Everything in this guide runs on your local machine, but the Collector and Jaeger both live happily on a small virtual server too — read what is VPS hosting if you do not have a server yet and want one. Create a scratch directory, and start with a tiny app worth observing: a FastAPI service with one endpoint that simulates an order lookup, so the trace has something to show. Install the app dependencies plus the OTel packages (current package names, verified at write time):

mkdir orders-api && cd orders-api
python3 -m venv venv
source venv/bin/activate   # Windows: .\venv\Scripts\Activate.ps1
pip install fastapi "uvicorn[standard]" opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-instrumentation-logging
opentelemetry-bootstrap -a install

What each package does: opentelemetry-distro brings the API, the SDK, and two commands you will use constantly — opentelemetry-bootstrap and opentelemetry-instrument. opentelemetry-exporter-otlp lets the app send data over OTLP to the Collector. opentelemetry-instrumentation-logging connects Python's logging module to OpenTelemetry. And opentelemetry-bootstrap -a install inspects what is installed and adds the matching instrumentation libraries — here, FastAPI and requests-style HTTP clients — so run it after installing your frameworks, because it decides based on what it finds.

How do you auto-instrument a Python app with zero code changes?

Auto-instrumentation is OpenTelemetry's best party trick: you launch your app through the opentelemetry-instrument wrapper and it generates spans for every request, every outgoing HTTP call, and every database query — without you writing a single line of instrumentation code. Write a minimal FastAPI app first (the official Python getting-started guide uses the same shape — a tiny app you can hit in the browser):

# app.py
import logging
from fastapi import FastAPI

logger = logging.getLogger(__name__)
app = FastAPI()

@app.get("/orders/{order_id}")
def get_order(order_id: int):
    logger.info("Processing order %s", order_id)
    return {"order_id": order_id, "status": "shipped"}

Then run it through the instrumenter with a few environment variables — this is the opentelemetry auto instrumentation pattern you will see in every current tutorial:

export OTEL_SERVICE_NAME=orders-api
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
opentelemetry-instrument uvicorn app:app --host 0.0.0.0 --port 8000

OTEL_SERVICE_NAME labels every span with your service's name (otherwise it shows up as "unknown_service", which is useless once you have three services). OTEL_EXPORTER_OTLP_ENDPOINT points the SDK at the Collector's HTTP endpoint. Two gotchas the official docs will not warn you about loudly enough: first, Python only lets WARNING-and-above log records through by default — set your logger level to INFO or your "Processing order" lines silently vanish. Second, run uvicorn as a single process here: --reload and --workers spawn subprocesses that break Python auto-instrumentation's span propagation in confusing ways. Use one worker while learning, and graduate to a manual-instrumentation setup if you need worker processes later.

What is the OpenTelemetry Collector?

The OpenTelemetry Collector is the receiving station for your telemetry: a small binary (or the otel/opentelemetry-collector-contrib Docker image, which carries the widest set of receivers and exporters) that you configure with a single YAML file. A Collector config has three concepts in a pipeline: receivers (how data arrives — here, OTLP on gRPC 4317 and HTTP 4318), processors (what happens to data in flight — batching, sampling, attribute cleanup), and exporters (where data goes — here, a debug exporter so you can watch spans land in the console). This is the otel collector setup beginners should memorize, because every production Collector is this same three-part shape scaled up:

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
      http:

processors:
  batch: {}

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Run it:

docker run -p 4317:4317 -p 4318:4318 \
  -v $(pwd)/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml \
  otel/opentelemetry-collector-contrib:0.164.0

Now curl your endpoint (curl localhost:8000/orders/42) and watch the Collector console print the span: a root span for the HTTP request with attributes like the route, status code, and duration. Congratulations — your app is emitting OpenTelemetry. This numbered arc is the whole first-trace flow worth committing to memory:

  1. Install the distro packages and run opentelemetry-bootstrap -a install after your frameworks.
  2. Set OTEL_SERVICE_NAME so spans carry your service's real name.
  3. Point OTEL_EXPORTER_OTLP_ENDPOINT at the Collector (localhost:4318 for HTTP).
  4. Launch the app through opentelemetry-instrument — one process, no --workers.
  5. Run the Collector with an OTLP receiver and a debug exporter first.
  6. Hit an endpoint and confirm spans appear in the Collector console.
  7. Add a backend exporter (next section) and confirm the same spans land there.
  8. Then add one manual span and one metric — the Collector config does not change.

How do you see your first trace in Jaeger?

The debug exporter proves data flows; Jaeger shows you what it means. Jaeger is the classic open-source trace backend, and its all-in-one Docker image accepts OTLP natively, so you do not need protocol translation. Start it and point the Collector's exporter at it — change only the exporters block in the config you already have:

docker run -d --name jaeger \
  -p 16686:16686 -p 4317:4317 -p 4318:4318 \
  jaegertracing/all-in-one:latest

# in otel-collector-config.yaml, add:
exporters:
  otlp/jaeger:
    endpoint: localhost:4317
    tls:
      insecure: true
# and in service.pipelines.traces, add otlp/jaeger to exporters

Restart the Collector, hit /orders/42 a few times, then open http://localhost:16686, pick orders-api as the service, and click Find Traces. You get the waterfall every distributed-tracing tutorial shows: the root span for the incoming request, child spans beneath it for framework internals, and durations laid out left to right. The beginner reading skill is the span name plus duration: the longest bar is where the request's time went. Right now the tree is shallow — one service, no downstream calls — which is exactly what the manual-span section below fixes. Jaeger persists nothing across restarts in this config, and that is fine: this is a learning backend, not your monitoring strategy.

How do you add a manual span to your own code?

Auto-instrumentation sees frameworks, not your business logic. When you want to know how long the "reserve inventory" step takes inside your handler — as opposed to the whole HTTP request — you create a manual span with the tracer. This is the first SDK code most developers write, and it is deliberately small:

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

@app.get("/orders/{order_id}")
def get_order(order_id: int):
    with tracer.start_as_current_span("reserve-inventory") as span:
        span.set_attribute("order.id", order_id)
        # ... your inventory logic here ...
        span.set_attribute("inventory.reserved", True)
    return {"order_id": order_id, "status": "shipped"}

Two rules make manual spans behave. First, start_as_current_span nests the span under whatever span is currently active — your "reserve-inventory" span appears as a child of the auto-generated HTTP span, which is why context propagation across your own function calls "just works" when you stay in the same process. Second, attributes are cheap and searchable; log-style string building inside span names is not — put the order ID in an attribute, not in the span's name. Restart the app through opentelemetry-instrument exactly as before: the SDK picks up your manual spans alongside the automatic ones, and the next trace in Jaeger shows the nested child span with its attributes.

How do you record your first metric?

Metrics in OpenTelemetry come from instruments you create with a meter — counters, histograms, gauges — and the canonical first metric is a counter of processed orders. Add it next to your tracer:

from opentelemetry import metrics

meter = metrics.get_meter(__name__)
orders_processed = meter.create_counter(
    "orders.processed",
    description="Number of orders processed",
    unit="1",
)

@app.get("/orders/{order_id}")
def get_order(order_id: int):
    orders_processed.add(1, {"status": "shipped"})
    ...

The second argument is a set of attributes — labels — and this is where the metrics lesson hides (see the cardinality section below): every unique label combination creates a new time series. status=shipped is safe. A label like order_id would create one series per order and bankrupt your metrics backend within the week — never put unbounded values in metric labels. Route metrics through the Collector too (add a metrics pipeline mirroring the traces one), because the same receiver-processor-exporter shape applies to all three signals.

What about logs in OpenTelemetry?

OTel logs are the newest and least-settled signal, and the honest framing matters: in the Python SDK, traces and metrics are marked Stable while logs are marked Development. The practical path is the one verified in current tutorials: install opentelemetry-instrumentation-logging, and your existing logging.info(...) calls get shipped to the Collector with the trace ID attached — no logging call of yours changes. One version wrinkle to know: before Python SDK 1.40.0 you had to set OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true to turn log instrumentation on; in current versions (we are at 1.45.2) you do not, as long as the instrumentation package is installed. If your logging backend of choice is a dedicated log store rather than your trace backend, that is a normal architecture — our Grafana Loki beginner guide covers a purpose-built open-source log backend that pairs well with OTel-instrumented apps.

How do you use OpenTelemetry in Node.js?

The Node.js story mirrors Python's, with one packaging difference: the JavaScript SDK (v2.12.0 as of this writing) exposes a batteries-included @opentelemetry/sdk-node package plus an auto-instrumentation bundle, so the manual setup is shorter. The minimal version — this opentelemetry node.js flow is the fastest path to spans from a JavaScript app:

npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http

// tracing.js — run with: node --import ./tracing.js app.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');

const sdk = new NodeSDK({
  serviceName: 'orders-api-node',
  traceExporter: new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' }),
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

Launch with node --import ./tracing.js app.js and Express (or Fastify, or NestJS) requests generate spans exactly like the Python app did, landing in the same Collector and the same Jaeger UI. This is the payoff of the standard: two services in two languages, one instrumentation model, one backend. Manual spans in Node use the same tracer pattern (trace.getTracer(...).startActiveSpan(...)), and the attribute discipline from the Python section applies unchanged.

What is sampling in OpenTelemetry, and why does it matter?

Opentelemetry sampling is the answer to the question every team asks when their first observability bill arrives: "do we really need to keep every single trace?" At any real traffic level, storing 100% of traces is unaffordable, and OpenTelemetry's default — parentbased_always_on, which keeps everything — is a getting-started default, not a production setting. Sampling decides which traces are recorded and exported; the rest are dropped early, before they cost you storage and egress. The beginner-safe way to try it is one environment variable:

export OTEL_TRACES_SAMPLER=traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1   # keep ~10% of traces

That keeps roughly one trace in ten, chosen deterministically by trace ID so sampling decisions stay consistent across services. The cost lesson: start with 100% while learning (you want every trace when you are debugging your setup), then move to ratio-based sampling the moment traffic is real, and keep error traces at 100% via your Collector config or backend's retention rules — the traces you delete are never the ones you wish you had deleted. Sampling applies to traces; metrics are aggregated, not sampled, which is why the cardinality discipline in the next section is the metrics-side equivalent of this lesson.

Head-based vs tail-based sampling: which should a beginner use?

There are two places a sampling decision can be made, and the difference is what information is available at decision time:

Head-based samplingTail-based sampling
Decision pointWhen the trace starts (at the SDK)After the trace completes (at the Collector)
Knows about errors/latency?No — decides blindYes — can keep all errors, drop all fast successes
Beginner setupOTEL_TRACES_SAMPLER=traceidratio — one env varCollector tail-sampling processor config — a real config to maintain
Cost controlPredictable percentage of trafficSmarter retention, harder to predict volume
The right callStart here — every beginner setupGraduate here when you know which traces you actually read

The rule of thumb: head-based sampling answers "how do I keep 10% of traffic," tail-based answers "how do I keep 100% of the interesting 1%." Beginners should ship head-based and revisit the question in six months with real data about what they query.

What is cardinality, and why do metrics get expensive?

Cardinality is the number of unique time series your metrics produce, and it is the silent budget-killer of observability: every unique combination of metric name plus label values is a separate series your backend must store and index. A counter named http.requests with labels for method (4 values), route (50 values), and status (6 values) produces at most 1,200 series — manageable. Add a user_id label with 2 million users and you have created 2 million series, and most backends will fall over or bill you into the ground. The discipline is three rules: label with bounded, low-cardinality dimensions (method, route template like /orders/{id} — never the raw path /orders/42); put high-cardinality identifiers in span attributes and logs, never in metric labels; and treat any new label as a multiplication, not an addition. This is the warning the OTel metrics guides keep repeating for a reason — cardinality mistakes are the most common reason beginner setups get expensive.

OpenTelemetry vs Prometheus: what's the difference?

This is the most Googled comparison in the space, and the answer is that they are not competitors — they are different layers. OpenTelemetry is the instrumentation standard: it produces traces, metrics, and logs from your code. Prometheus is a metrics backend: it scrapes, stores, and queries metrics, with PromQL and Alertmanager. The standard architecture composes them: your app emits OTel metrics, the Collector receives them via OTLP and re-exports them in Prometheus format (the Collector's Prometheus exporter, or Prometheus scraping the Collector), and Prometheus does what it has always done — store, query, alert. Our Prometheus beginner guide covers the backend half in depth. The mental model to keep: OTel answers "how does data get out of my code," Prometheus answers "where do my metrics live." Choosing Prometheus as a backend is not choosing against OpenTelemetry; most serious Prometheus setups today ingest OTel-produced metrics.

What are the 7 mistakes beginners make with OpenTelemetry?

Read this table once and skip the debugging sessions most beginners pay for in their first month:

MistakeWhy it hurtsThe fix
Treating OTel as a backendYou instrument the app, then stare at an empty screen because nothing stores or displays the dataPlan the backend first — Collector plus Jaeger, Prometheus, or a vendor
Never setting OTEL_SERVICE_NAMEEvery span shows up as "unknown_service" and multi-service traces are unreadableExport OTEL_SERVICE_NAME per service before launching
Running uvicorn with --workers or --reloadSubprocesses break auto-instrumentation's span propagation in confusing waysSingle process while learning; manual instrumentation if you need workers
Following 2024-era package names blindlySDK packaging moves fast — old tutorials reference renamed packages or defaultsCheck the current release (Python 1.45.2, JS 2.12.0, Collector 0.164.0 as of this writing) and prefer current docs
High-cardinality metric labelsOne user_id or order_id label multiplies your time series into the millionsBounded labels only on metrics; identifiers go in span attributes and logs
100% trace retention in productionStorage and egress costs scale with traffic, not with valueLearn at 100%, ship with traceidratio sampling, keep errors at 100%
Skipping the CollectorEvery app carries backend credentials and every backend change means redeploying everythingApps ship to one local OTLP endpoint; the Collector config owns destinations

Sources

Facts in this guide were verified at write time (November 1, 2026) from: the GitHub open-telemetry/opentelemetry-python releases API (latest tag v1.45.2, published 2026-10-29; repository API: ~2,700 stars, Apache-2.0, created 2019-05-07); the GitHub open-telemetry/opentelemetry-collector releases API (latest tag v0.164.0, published 2026-10-26); the GitHub open-telemetry/opentelemetry-js releases API (latest tag v2.12.0, published 2026-10-06); the official opentelemetry.io Python getting-started guide (Flask rolldice example; zero-code instrumentation via opentelemetry-instrument; opentelemetry-distro + opentelemetry-bootstrap -a install flow); the devstories.blog "OpenTelemetry for Beginners" guide (updated 6 days ago: pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-instrumentation-logging; bootstrap after installing frameworks; opentelemetry-instrument with OTEL_SERVICE_NAME; Python logs Development stability; OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED unneeded since 1.40.0; WARNING-and-above logging default gotcha); the bugfree.ai distributed-tracing guide (updated 21 hours ago: manual span pattern, exporter setup, context propagation across services); the sawmills.ai OpenTelemetry metrics guide (the cardinality warning); the freeCodeCamp "How OpenTelemetry Works" guide (56 days ago: auto vs manual instrumentation, Node SDK arc); the funkysi1701.com .NET getting-started guide (544 days ago: the API/SDK/exporter component model); and this run's live SERP for "how to use opentelemetry" (beginner how-to guides + official docs only — zero tool pages, no reroute).

Frequently Asked Questions

Is OpenTelemetry free?

Yes. OpenTelemetry is open source under the Apache-2.0 license — no account, no license key, no paid tier for the standard, SDKs, or Collector. Your costs come from wherever you store the data: your own Jaeger/Prometheus servers or a commercial observability vendor's pricing.

Do I need the OpenTelemetry Collector, or can my app send data straight to the backend?

You can send straight to a backend — the SDKs export OTLP directly — but the Collector is the recommended middle layer for anything beyond a toy: it centralizes credentials, lets you change backends without redeploying apps, and hosts processors for batching, sampling, and attribute cleanup. Start with the Collector; it is one YAML file and one Docker command.

What is the difference between OpenTelemetry and Prometheus?

OpenTelemetry is the instrumentation standard that produces traces, metrics, and logs from your code. Prometheus is a metrics backend that stores, queries, and alerts on metrics. They compose: OTel-instrumented apps export metrics that Prometheus (often via the Collector) stores and queries. See the comparison section above for the full breakdown.

Which language SDK should I start with?

Start with whichever language your app is already written in — the concepts are identical everywhere. Python has the friendliest zero-code path via opentelemetry-instrument; Node.js has the batteries-included @opentelemetry/sdk-node. Both are covered step by step in this guide.

How much of my traffic should I sample?

Keep 100% while learning so every trace is available when you debug your setup. In production, a common starting point is 10% of traces via OTEL_TRACES_SAMPLER=traceidratio with OTEL_TRACES_SAMPLER_ARG=0.1, keeping error traces at a higher rate through your Collector or backend retention rules. Tune from real query data, not from guesses.

Featured on Twelve Tools Launch Llama Tools Launch your MCP