When I first integrated OpenTelemetry into my production services, I was surprised by how quickly I could get valuable telemetry. Within 10 minutes of adding the SDK, I had automatic traces for every HTTP request, database query, and Redis operation - without changing a single line of application code.
This article walks you through that same experience. You'll build a TypeScript Express API, add OpenTelemetry instrumentation, and see distributed traces flowing to Jaeger - all from scratch.
Prerequisites
Before we start, ensure you have:
node--version# v18.0.0 or higher (v20+ recommended)npm--version# 9.0.0 or higher
Optional but recommended:
Docker for running Jaeger locally
Basic familiarity with Express.js
Understanding of async/await in TypeScript
Project Setup
Let's create a new TypeScript project from scratch:
# Create project directorymkdirotel-express-democdotel-express-demo# Initialize npm projectnpminit-y# Install TypeScript and development toolsnpminstall-Dtypescript@types/nodetsx# Initialize TypeScript configurationnpxtsc--init
Configure TypeScript (tsconfig.json):
Update package.json scripts:
Building the Base Application
First, let's create a simple Express API without any OpenTelemetry instrumentation.
Install Express dependencies:
Create src/app.ts:
Test the application:
Great! Our API works, but we have zero visibility into what's happening inside.
Adding OpenTelemetry
Now comes the magic - we'll add comprehensive observability with minimal code changes.
Install OpenTelemetry packages:
Create src/instrumentation.ts:
Update package.json to load instrumentation before app:
That's it! Now run the application:
You'll see:
Make some requests:
Check your console output - you'll see detailed trace information:
Without changing a single line of application code, we now have:
β Automatic tracing for every HTTP request
β Request duration measurements
β HTTP status codes
β URL paths and methods
β User agent information
Setting Up Jaeger for Visualization
Console output is great for learning, but let's visualize traces properly with Jaeger.
Start Jaeger using Docker:
Install OTLP exporter:
Update src/instrumentation.ts:
Restart your application and make some requests:
Open Jaeger UI: http://localhost:16686
Select service: order-service
Click "Find Traces"
Click on any trace to see the detailed timeline
You'll see beautiful visualizations showing:
Total request duration
Individual operation timing
Request metadata (URL, method, status code)
Hierarchical span relationships
Adding Custom Instrumentation
Auto-instrumentation is great, but let's add custom spans to track our business logic.
Update src/app.ts to import the tracer:
Restart and test:
Now in Jaeger, you'll see your custom spans (findOrderById, processPayment) nested under the HTTP request span, with all the custom attributes you added!
What We've Accomplished
In less than 10 minutes, you've:
β Built a TypeScript Express API from scratch
β Added OpenTelemetry SDK with auto-instrumentation
β Exported traces to Jaeger
β Created custom spans for business logic
β Added meaningful attributes to spans
β Visualized distributed traces in Jaeger UI
Real-World Insights
This simple setup has already given you superpowers:
Performance Analysis:
You can now see that processPayment takes 200ms while findOrderById takes only 50ms. If users complain about slow checkout, you know exactly where to optimize.
Error Debugging:
When a payment fails, you can find the exact trace, see all the attributes (order ID, amount, user ID), and understand the complete context.
Usage Patterns:
By analyzing traces in Jaeger, you can see which endpoints are called most frequently and optimize accordingly.
Next Steps
You've successfully instrumented your first TypeScript application! Continue to Automatic Instrumentation where you'll:
Explore all available auto-instrumentation libraries
Add PostgreSQL and Redis to your application
See automatic traces for database queries
Learn how to configure instrumentation libraries
Understand what gets traced automatically vs what needs custom spans
npm run dev
# In another terminal:
curl http://localhost:3000/health
# Create an order
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d '{"userId": "user123", "amount": 99.99}'
# Get the order (use the ID from previous response)
curl http://localhost:3000/api/orders/ORD-1704705825000
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'order-service',
[ATTR_SERVICE_VERSION]: '1.0.0',
}),
// Export to Jaeger via OTLP
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('SDK shut down'))
.catch((error) => console.error('Shutdown error', error))
.finally(() => process.exit(0));
});
console.log('OpenTelemetry β Jaeger configured');
npm run dev
# Create several orders
for i in {1..5}; do
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d "{\"userId\": \"user$i\", \"amount\": $(($i * 25))}"
sleep 1
done
# Fetch some orders
curl http://localhost:3000/api/orders/ORD-1704705825000
npm run dev
# Make requests and check Jaeger
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d '{"userId": "user999", "amount": 299.99}'