Add OpenTelemetry tracing to an existing service without rewrites
For developers who need traces from an already-running service without refactoring business logic. This shows the fastest path: run the OpenTelemetry Collector, enable language auto-instrumentation or middleware hooks, export OTLP traces, and verify them end to end with concrete commands and failure symptoms.
TL;DR — The fastest way to add tracing to an existing service is: run an OpenTelemetry Collector locally or in-cluster, point your service at it with
OTEL_EXPORTER_OTLP_ENDPOINT, and use auto-instrumentation where your runtime supports it (NODE_OPTIONS=--require ...,java -javaagent:...,ddtrace-style equivalents do not apply here). If traces do not appear, the most likely fix is that your app is exporting to the wrong OTLP protocol/port pair: use HTTP/protobuf on:4318or gRPC on:4317, and verify with Collector logs before touching app code. Reading time: ~5 min
Goal
When you finish, your existing service will emit OpenTelemetry traces for inbound requests and common outbound calls to an OpenTelemetry Collector, and you will be able to prove it by generating a request and seeing a trace export succeed in Collector logs or your tracing backend.
Prerequisites
- Shell access to the host, container, or deployment running the service
- Permission to restart the service or roll out a deployment
- Docker >= 24 if you want the quickest local Collector setup — check with:
docker --version
- One of these runtimes in the service:
- Node.js >= 18 — check with
node --version - Java >= 11 — check with
java -version - Python >= 3.10 — check with
python3 --version
- Node.js >= 18 — check with
curlinstalled — check with:
curl --version
- The service listen address or a health endpoint you can hit, for example
http://localhost:8080/health - A place to send traces:
- local Collector logs only for smoke testing, or
- your tracing backend OTLP endpoint, for example
http://otel-collector:4318
Steps
Step 1: Run an OpenTelemetry Collector that accepts OTLP and logs traces
Create a Collector config file:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug]
Start the Collector:
cat > otel-collector.yaml <<'YAML'
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug]
YAML
docker run --rm --name otelcol \
-p 4317:4317 -p 4318:4318 \
-v "$PWD/otel-collector.yaml:/etc/otelcol/config.yaml" \
otel/opentelemetry-collector:latest
You should see a startup line similar to:
Everything is ready. Begin running and processing data.
Step 2: Point your service at the Collector with explicit OTLP settings
Set these environment variables in the service process environment:
export OTEL_SERVICE_NAME=my-existing-service
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=dev,service.version=1.0.0
If your service runs in Docker, add these flags to docker run:
docker run --rm \
-e OTEL_SERVICE_NAME=my-existing-service \
-e OTEL_TRACES_EXPORTER=otlp \
-e OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
-e OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 \
-e OTEL_RESOURCE_ATTRIBUTES=deployment.environment=dev,service.version=1.0.0 \
your-image:tag
You should see the service start normally; no trace output appears yet until instrumentation is loaded.
Step 3: Enable auto-instrumentation for your runtime
Pick the subsection that matches your service.
Node.js
Install the packages in the service directory:
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
Create otel.mjs:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Start the existing app without changing app code:
NODE_OPTIONS='--import ./otel.mjs' node server.js
You should see the app start as usual; incoming HTTP requests will now produce spans.
Java
Download the Java agent:
curl -L -o opentelemetry-javaagent.jar https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
Start the existing JVM with the agent:
java -javaagent:./opentelemetry-javaagent.jar -jar app.jar
If you use a service manager, add the same -javaagent flag to the JVM startup options. You should see normal app startup plus a line indicating the agent initialized.
Python
Install the distro and instrumentation packages:
python3 -m pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests
python3 -m opentelemetry.bootstrap -a install
Start the existing app through the wrapper:
opentelemetry-instrument python3 app.py
You should see the app start normally; Flask/FastAPI/Django and requests calls are commonly instrumented once the relevant packages are present.
Step 4: Generate a request that should produce a trace
Hit a real endpoint on the service:
curl -i http://127.0.0.1:8080/health
Example success output shape:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15
{"status":"ok"}
You should get a normal application response. Within a few seconds, the Collector terminal should print span data.
Step 5: If you have a backend, switch the Collector exporter from debug to OTLP
⚠️ This changes where traces are sent. If you remove the
debugexporter before verifying backend connectivity, you lose the easiest local proof that spans are being received. Replace the exporter section with your backend OTLP endpoint. Example for OTLP/HTTP without TLS:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
otlphttp:
endpoint: http://your-backend:4318
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp]
Restart the Collector:
docker rm -f otelcol
docker run --rm --name otelcol \
-p 4317:4317 -p 4318:4318 \
-v "$PWD/otel-collector.yaml:/etc/otelcol/config.yaml" \
otel/opentelemetry-collector:latest
You should see the Collector start cleanly with no config parse errors.
Verify it works
First, verify the Collector is listening:
curl -i http://127.0.0.1:4318/
Expected output shape:
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
404 page not found
A 404 here is fine; it proves something is listening on :4318.
Next, generate traffic:
curl -i http://127.0.0.1:8080/health
Then inspect Collector logs. With the debug exporter, you should see output shaped like:
ResourceSpans #0
Resource SchemaURL:
Resource attributes:
-> service.name: Str(my-existing-service)
ScopeSpans #0
Span #0
Name : GET /health
Kind : Server
Trace ID : 5b7d7c0b8c4d2f0a9b3d2c1e4f6a7b8c
Span ID : 1a2b3c4d5e6f7890
Status code: Unset
If you are exporting to a backend instead of debug, confirm a new trace appears there with service name my-existing-service within 10-30 seconds after the curl request.
Common pitfalls
OTLP protocol/port mismatch
Mistake: setting OTEL_EXPORTER_OTLP_PROTOCOL=grpc while sending to http://host:4318, or http/protobuf while sending to :4317.
Symptom: app logs export errors like:
connection refused
or
404 Not Found
Fix: use http/protobuf with http://HOST:4318, or use gRPC with HOST:4317.
Container cannot reach 127.0.0.1 on the host
Mistake: using OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 inside a containerized app.
Symptom: no traces, and exporter errors show connect failures to localhost.
Fix: in Docker Desktop use http://host.docker.internal:4318; in Compose or Kubernetes use the Collector service DNS name.
Service starts without instrumentation because the preload flag is wrong
Mistake: starting Node normally after creating otel.mjs, or forgetting -javaagent for Java, or running Python directly instead of opentelemetry-instrument.
Symptom: app works, but Collector logs stay empty after requests.
Fix: start exactly with NODE_OPTIONS='--import ./otel.mjs' node server.js, java -javaagent:./opentelemetry-javaagent.jar -jar app.jar, or opentelemetry-instrument python3 app.py.
Missing instrumentation package for the framework or HTTP client
Mistake: assuming auto-instrumentation covers a library that is not installed or not supported by default.
Symptom: you see inbound server spans but no outbound DB/HTTP spans, or no spans for your framework.
Fix: install the matching package explicitly, for example Python opentelemetry-instrumentation-requests or the relevant framework package, then restart.
Sampling set to zero upstream or in environment
Mistake: inherited environment contains a sampler setting that drops all traces. Symptom: app starts cleanly, requests succeed, Collector receives nothing. Fix: set:
export OTEL_TRACES_SAMPLER=parentbased_always_on
and restart the service.
Reverse proxy terminates requests before the instrumented app sees them
Mistake: testing the wrong port or only hitting a proxy health endpoint.
Symptom: curl returns 200, but the app emits no spans because the request never reached it.
Fix: hit an application route on the instrumented service directly once, for example curl -i http://127.0.0.1:8080/health, then test through the proxy after local proof.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI