This guide explains the distributed tracing and correlation ID implementation in the Amana project, which provides end-to-end request tracing for faster production incident triage.
The distributed tracing system provides:
- Correlation IDs: Unique identifiers that span multiple services and requests
- Request IDs: Unique identifiers for individual HTTP request/response pairs
- OpenTelemetry Integration: Industry-standard distributed tracing
- Automatic Propagation: Headers automatically propagated across services
- Comprehensive Observability: Detailed tracing for frontend-backend interactions
-
Correlation ID Middleware (
src/middleware/correlationId.middleware.ts)- Generates/validates correlation IDs from HTTP headers
- Creates unique request IDs for each request
- Attaches IDs to request objects for downstream use
-
Tracing Middleware (
src/middleware/tracing.middleware.ts)- OpenTelemetry span creation for HTTP requests
- Integration with correlation ID system
- Request/response attribute tracking
-
Tracing Configuration (
src/config/tracing.ts)- OpenTelemetry SDK initialization
- Exporter configuration (Jaeger, Zipkin, Prometheus)
- Tracing utilities for custom spans
-
Traced HTTP Client (
src/lib/traced-http-client.ts)- Automatic correlation ID propagation for external service calls
- OpenTelemetry span creation for HTTP clients
- Error handling and retry logic
-
Service Integration
- IPFS service tracing (
src/services/ipfs.service.ts) - Stellar service tracing (
src/services/stellar.service.ts) - Extensible to other services
- IPFS service tracing (
-
Traced HTTP Client (
src/lib/traced-fetch.ts)- Browser-based HTTP client with correlation ID propagation
- Automatic request/response timing
- Error handling and retry logic
-
React Hooks (
src/hooks/useTracedFetch.ts)useTracedFetch: Generic traced HTTP requests with state managementuseTracedGet: Simplified GET requestsuseTracedMutation: POST/PUT/PATCH/DELETE requests
The system uses these HTTP headers for tracing:
| Header | Purpose | Source |
|---|---|---|
x-correlation-id |
Logical trace ID spanning multiple services | Client-generated or server-generated |
x-request-id |
Unique ID for specific HTTP request/response | Always server-generated |
# OpenTelemetry Configuration
JAEGER_ENDPOINT=http://localhost:14268/api/traces
ZIPKIN_ENDPOINT=http://localhost:9411/api/v2/spans
PROMETHEUS_PORT=9464
# Service Configuration
OTEL_SERVICE_NAME=amana-backend
OTEL_EXPORTER_JAEGER_AGENT_HOST=localhost
OTEL_EXPORTER_JAEGER_AGENT_PORT=6831- Dependencies (already added to package.json):
{
"@opentelemetry/api": "^1.8.0",
"@opentelemetry/auto-instrumentations-node": "^0.46.1",
"@opentelemetry/exporter-jaeger": "^1.22.0",
"@opentelemetry/exporter-prometheus": "^0.48.0",
"@opentelemetry/exporter-zipkin": "^1.22.0",
"@opentelemetry/instrumentation": "^0.48.0",
"@opentelemetry/instrumentation-express": "^0.40.1",
"@opentelemetry/instrumentation-http": "^0.48.0",
"@opentelemetry/resources": "^1.22.0",
"@opentelemetry/sdk-metrics": "^1.22.0",
"@opentelemetry/sdk-node": "^0.48.0",
"@opentelemetry/sdk-trace-base": "^1.22.0",
"@opentelemetry/semantic-conventions": "^1.22.0"
}- Initialization (already in
src/index.ts):
import { initializeTracing } from "./config/tracing";
// Initialize distributed tracing before any other imports
initializeTracing();- Middleware Registration (already in
src/app.ts):
app.use(correlationIdMiddleware);
app.use(tracingMiddleware);
app.use(loggerMiddleware);- Initialize HTTP Client:
import { initializeHttpClient } from './lib/traced-fetch';
// Initialize with backend URL
initializeHttpClient('http://localhost:4000');- Use in Components:
import { useTracedFetch } from './hooks/useTracedFetch';
function MyComponent() {
const { data, loading, error, correlationId } = useTracedFetch('/api/trades');
// Component logic...
}import { TracingHelper } from '../config/tracing';
// Wrap async operations with tracing
const result = await TracingHelper.withSpan(
'database.query',
async (span) => {
span.setAttributes({
'db.operation': 'SELECT',
'db.table': 'trades',
});
const data = await database.query('SELECT * FROM trades');
return data;
}
);
// Add attributes to current span
TracingHelper.setAttributes({
'user.id': userId,
'operation.type': 'trade_creation',
});
// Add events to current span
TracingHelper.addEvent('validation_start', { field: 'amount' });
// Record exceptions
try {
await riskyOperation();
} catch (error) {
TracingHelper.recordException(error);
}import { tracedHttpClient } from '../lib/traced-http-client';
// Automatic tracing and correlation ID propagation
const response = await tracedHttpClient.get('/external/api/data');
// POST with data
const result = await tracedHttpClient.post('/external/api/create', {
name: 'test',
value: 123,
});
// Custom client for specific service
const stellarClient = createTracedClient('https://horizon.stellar.org', 'stellar-service');
const balance = await stellarClient.get(`/accounts/${publicKey}`);import { tracedHttpClient } from './lib/traced-fetch';
// Basic GET request
const response = await tracedHttpClient.get('/api/trades');
console.log('Correlation ID:', response.correlationId);
// POST with data
const result = await tracedHttpClient.post('/api/trades', {
commodity: 'gold',
quantity: 100,
});
// With custom correlation ID
const response = await tracedHttpClient.get('/api/trades', {
correlationId: 'user-flow-123',
});import { useTracedFetch, useTracedMutation } from './hooks/useTracedFetch';
// GET request with state management
function TradeList() {
const { data: trades, loading, error, correlationId } = useTracedFetch('/api/trades');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<div>Correlation ID: {correlationId}</div>
{trades?.map(trade => <TradeItem key={trade.id} trade={trade} />)}
</div>
);
}
// Mutation with loading/error states
function CreateTradeForm() {
const { mutate: createTrade, loading, error } = useTracedMutation();
const handleSubmit = async (tradeData) => {
try {
await createTrade('POST', '/api/trades', tradeData);
// Success handling
} catch (err) {
// Error handling
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Trade'}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}Access Jaeger UI at http://localhost:16686 to:
- Search traces by correlation ID, service name, or operation
- View detailed trace timelines
- Analyze performance bottlenecks
- Debug distributed request flows
Access metrics at http://localhost:9464/metrics:
- HTTP request metrics
- Custom application metrics
- OpenTelemetry instrumentation metrics
All logs include correlation and request IDs:
{
"level": "info",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"requestId": "123e4567-e89b-12d3-a456-426614174000",
"msg": "Trade created successfully",
"tradeId": "trade_123"
}# Run all tests
npm test
# Run specific tracing tests
npm test -- --testNamePattern="tracing"
# Run with coverage
npm test -- --coverage# Run tests (when implemented)
npm test- Use UUIDs for correlation IDs
- Propagate correlation IDs across all service calls
- Include correlation IDs in all logs and errors
- Use descriptive, consistent span names
- Include operation type and target resource
- Follow naming conventions:
service.operation
- Add relevant attributes to spans for context
- Include business context (user IDs, operation types)
- Avoid sensitive data in attributes
- Always record exceptions in spans
- Include error context and correlation IDs
- Use appropriate span status codes
- Keep spans focused on specific operations
- Avoid overly long-running spans
- Use events for significant milestones
-
Missing Correlation IDs
- Ensure middleware is registered in correct order
- Check that correlation ID middleware runs before logger
- Verify headers are not being stripped by proxies
-
Spans Not Appearing in Jaeger
- Check Jaeger endpoint configuration
- Verify network connectivity to Jaeger
- Ensure service name is correctly configured
-
High Memory Usage
- Check for span leaks (unclosed spans)
- Verify proper error handling in spans
- Monitor span duration and count
Enable debug logging:
OTEL_LOG_LEVEL=debug npm run devVerify tracing is working:
curl -H "x-correlation-id: test-123" http://localhost:4000/healthShould return correlation ID headers:
x-correlation-id: test-123
x-request-id: generated-uuid- Import TracingHelper:
import { TracingHelper } from '../config/tracing';- Wrap Operations:
// Before
async function processTrade(tradeId: string) {
const trade = await getTrade(tradeId);
return validateTrade(trade);
}
// After
async function processTrade(tradeId: string) {
return TracingHelper.withSpan(
'trade.process',
async (span) => {
span.setAttributes({ 'trade.id': tradeId });
const trade = await getTrade(tradeId);
const result = validateTrade(trade);
return result;
}
);
}- Update External Calls:
// Before
const response = await axios.get('/external/api');
// After
import { tracedHttpClient } from '../lib/traced-http-client';
const response = await tracedHttpClient.get('/external/api');- Header Validation: Correlation IDs are validated to prevent header injection
- Data Privacy: Avoid sensitive data in span attributes
- Access Control: Ensure tracing endpoints are properly secured
- Data Retention: Configure appropriate retention policies for trace data
The tracing system is designed for minimal performance impact:
- Overhead: < 5ms per request
- Memory: ~1MB per 1000 concurrent spans
- Network: Minimal additional header size (~100 bytes)
- Sampling: Add configurable sampling strategies
- Metrics: Expand custom metrics collection
- Alerting: Integration with monitoring systems
- Dashboard: Custom tracing dashboards
- Service Mesh: Integration with Istio/Linkerd
For questions or issues with the tracing implementation:
- Check this documentation
- Review test files for examples
- Check Jaeger UI for trace visualization
- Review logs for correlation ID propagation