Custom Spans
While auto-instrumentation handles most tracing needs, you can create custom spans for fine-grained control.
Using the Tracer
Inject the TracerToken to create custom spans:
import { inject, Injectable } from '@navios/di'
import { TracerToken } from '@navios/otel'
@Injectable()
export class OrderService {
private readonly tracer = inject(TracerToken)
async processOrder(orderId: string) {
// Create a custom span
const span = this.tracer.startSpan('process-order')
try {
// Add attributes
span.setAttribute('order.id', orderId)
// Do work
const order = await this.fetchOrder(orderId)
span.setAttribute('order.total', order.total)
await this.validateOrder(order)
await this.chargePayment(order)
await this.sendConfirmation(order)
return order
} catch (error) {
// Record the error
span.recordException(error as Error)
throw error
} finally {
// Always end the span
span.end()
}
}
}
Using SpanFactoryService
The SpanFactoryService provides helper methods with consistent attributes:
import { inject, Injectable } from '@navios/di'
import { SpanFactoryService } from '@navios/otel'
@Injectable()
export class PaymentService {
private readonly spanFactory = inject(SpanFactoryService)
async processPayment(paymentId: string) {
// Create a child span (automatically links to parent)
const span = this.spanFactory.createChildSpan({
name: 'process-payment',
attributes: {
'payment.id': paymentId,
},
})
try {
const result = await this.chargeCard(paymentId)
return result
} catch (error) {
this.spanFactory.recordError(span, error)
throw error
} finally {
span.end()
}
}
}
Nested Spans
Spans automatically create parent-child relationships:
@Injectable()
export class OrderService {
private readonly tracer = inject(TracerToken)
private readonly paymentService = inject(PaymentService)
async createOrder(data: OrderData) {
const span = this.tracer.startSpan('create-order')
try {
// This span is a child of 'create-order'
const payment = await this.paymentService.processPayment(data.paymentId)
// This span is also a child of 'create-order'
await this.notifyCustomer(data.customerId)
return { orderId: '123', payment }
} finally {
span.end()
}
}
}
Result trace:
create-order (100ms)
├── process-payment (45ms)
│ └── charge-card (30ms)
└── notify-customer (20ms)
Getting the Current Span
Access the currently active span:
import { getCurrentSpan } from '@navios/otel'
function logWithTraceContext(message: string) {
const span = getCurrentSpan()
if (span) {
const context = span.spanContext()
console.log(`[trace_id=${context.traceId}] ${message}`)
}
}
Using TraceContextService
For context propagation across async boundaries:
import { inject, Injectable } from '@navios/di'
import { TraceContextService, TracerToken } from '@navios/otel'
@Injectable()
export class HttpClient {
private readonly tracer = inject(TracerToken)
private readonly traceContext = inject(TraceContextService)
async request(url: string, options: RequestInit = {}) {
const span = this.tracer.startSpan(`HTTP ${options.method || 'GET'} ${url}`)
try {
// Inject trace context into outgoing headers
const headers = { ...options.headers } as Record<string, string>
this.traceContext.injectIntoHeaders(headers)
const response = await fetch(url, { ...options, headers })
span.setAttribute('http.status_code', response.status)
return response
} catch (error) {
span.recordException(error as Error)
throw error
} finally {
span.end()
}
}
}
Best Practices
- Always end spans: Use
try/finallyto ensure spans are ended - Record exceptions: Call
span.recordException(error)before rethrowing - Use meaningful names: Span names should describe the operation
- Add relevant attributes: Include IDs, types, and other context
- Keep spans focused: One span per logical operation