Instrumenting a Service & Sending Traces to Zipkin
From a running Zipkin to real spans flowing in from your app.
Theory is done — let’s produce real traces. You’ll start Zipkin locally, then instrument an application so it records spans and reports them. We’ll use OpenTelemetry, today’s vendor-neutral standard for instrumentation, exporting to Zipkin.
🎯 Learning Objectives
- Run Zipkin locally with Docker
- Understand automatic vs. manual instrumentation
- Instrument an app with OpenTelemetry and export to Zipkin
- Add a custom span with tags
Step 1 — Run Zipkin
The fastest way to get Zipkin is its official Docker image (uses in-memory storage — perfect for learning):
docker run -d -p 9411:9411 openzipkin/zipkinThe UI is now at http://localhost:9411. It’s empty because nothing has sent traces yet — that’s our next job.
Automatic vs. Manual Instrumentation
There are two ways to get spans out of an app:
| Approach | What it means |
|---|---|
| Automatic | An agent/library auto-instruments common frameworks (HTTP servers, clients, DB drivers) — little to no code change |
| Manual | You create spans in your own code for business operations you care about |
Real systems use both: automatic for the plumbing, manual for the important custom operations.
Step 2 — Auto-Instrument with OpenTelemetry
For a Node.js service, OpenTelemetry can auto-instrument with no app code changes and export to Zipkin. Point it at your Zipkin collector via environment variables:
npm install @opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-zipkin// tracing.js — loaded before your app
const { NodeSDK } = require("@opentelemetry/sdk-node");
const { ZipkinExporter } = require("@opentelemetry/exporter-zipkin");
const { getNodeAutoInstrumentations } =
require("@opentelemetry/auto-instrumentations-node");
const sdk = new NodeSDK({
serviceName: "orders-service",
traceExporter: new ZipkinExporter({
url: "http://localhost:9411/api/v2/spans",
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();Run your app with this loaded first, and every incoming HTTP request and outgoing call automatically becomes a span in Zipkin:
node -r ./tracing.js server.jsStep 3 — Add a Custom Span
For business logic you care about, create a manual span and tag it with useful metadata:
const { trace } = require("@opentelemetry/api");
const tracer = trace.getTracer("orders-service");
async function chargeCustomer(orderId, amount) {
const span = tracer.startSpan("charge-customer");
span.setAttribute("order.id", orderId);
span.setAttribute("payment.amount", amount);
try {
await paymentGateway.charge(amount);
} catch (err) {
span.recordException(err);
span.setStatus({ code: 2 }); // ERROR
throw err;
} finally {
span.end(); // always end the span
}
}Now Zipkin shows a charge-customer span with your order.id and payment.amount tags — searchable and visible in the waterfall.
⚠ Always end your spans
A span that never calls end() never gets reported (and leaks memory). Wrap the work in try/finally and end the span in finally so it’s recorded even on error.
💡 Instrument once, export anywhere
Because we used OpenTelemetry, switching from Zipkin to Jaeger (or a hosted backend) later is just changing the exporter — your instrumentation code stays the same. That vendor-neutrality is why OpenTelemetry has become the standard.
🧪 Hands-on Lab
Trace a Slow Dependency
You suspect an external inventory-check call is slow. Describe how you’d wrap it in a custom span (with a tag for the item ID) so its duration shows up in Zipkin.
🧠 Knowledge Check
What is the advantage of automatic instrumentation?
Why use OpenTelemetry to export to Zipkin rather than a Zipkin-specific library?
💼 Interview Preparation
How would you add tracing to an existing microservice with minimal risk?
Summary
You ran Zipkin, instrumented an app with OpenTelemetry, exported spans, and added a custom span with tags. Traces are now flowing. In the final lesson we use them: analyzing latency and debugging real problems in the Zipkin UI.