Manual instrumentation lets you trace business operations that auto-instrumentation misses. Use it to capture checkout flows, payment processing, or any code path where you need visibility into what happened and why.
Prerequisites
- Complete the Node.js instrumentation guide so your tracer provider and exporters are configured.
- Install the API package:
npm install @opentelemetry/api - Tested with Node.js 18+ and
@opentelemetry/apiv1.9.0.
Step 1. Create manual spans
Get a tracer and wrap operations in spans:
manual_span.js
const { trace } = require('@opentelemetry/api')
const tracer = trace.getTracer('order-service')
async function processOrder(orderId) {
return tracer.startActiveSpan('process-order', async (span) => {
try {
span.setAttribute('order.id', orderId)
span.setAttribute('order.status', 'processing')
// Business logic here
// Child spans in called functions link automatically
await validateOrder(orderId)
return { success: true }
} finally {
span.end()
}
})
}
For nested operations, child spans link to the parent automatically:
nested_spans.js
async function processOrder(orderId) {
return tracer.startActiveSpan('process-order', async (parentSpan) => {
try {
parentSpan.setAttribute('order.id', orderId)
// Child span tracks a sub-operation
await tracer.startActiveSpan('validate-inventory', async (childSpan) => {
try {
childSpan.setAttribute('warehouse.id', 'WH-001')
await checkInventory(orderId)
} finally {
childSpan.end()
}
})
} finally {
parentSpan.end()
}
})
}
Tips:
- Reuse tracer instances instead of creating one per request.
- Use descriptive span names that match business steps (
checkout,fetch-user). - Always call
span.end()in afinallyblock.
Step 2. Add attributes and events
Attributes let you filter and aggregate spans in SigNoz. Events mark notable moments within a span.
attributes.js
const { trace } = require('@opentelemetry/api')
function handlePayment(amount, currency = 'USD') {
const span = trace.getActiveSpan()
if (!span) return
span.setAttribute('payment.amount', amount)
span.setAttribute('payment.currency', currency)
span.setAttribute('payment.method', 'credit_card')
// Events mark milestones
span.addEvent('payment.validated')
// Process payment...
span.addEvent('payment.processed', {
'status': 'success',
'transaction.id': 'txn_123456'
})
}
For standard attribute names, use semantic conventions:
npm install @opentelemetry/semantic-conventions
semantic_attributes.js
const { trace } = require('@opentelemetry/api')
const { ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } = require('@opentelemetry/semantic-conventions')
const span = trace.getActiveSpan()
if (span) {
span.setAttribute(ATTR_HTTP_REQUEST_METHOD, 'GET')
span.setAttribute(ATTR_URL_FULL, 'https://api.example.com/users')
}
- Use semantic conventions when a standard attribute exists.
- Add events for retries, cache hits/misses, queue waits.
Step 3. Record errors
Mark failures so they show up in SigNoz error views:
error_handling.js
const { trace, SpanStatusCode } = require('@opentelemetry/api')
async function riskyOperation() {
return tracer.startActiveSpan('risky-operation', async (span) => {
try {
const result = await doSomethingRisky()
span.setStatus({ code: SpanStatusCode.OK })
return result
} catch (error) {
span.recordException(error)
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
throw error
} finally {
span.end()
}
})
}
recordExceptionattaches the stack trace.SpanStatusCode.ERRORflags the span in SigNoz error views and alerts.- Re-throw so calling code can handle the failure.
Step 4. Add span links (optional)
Links connect causally related spans that aren't parent-child:
span_links.js
const { trace } = require('@opentelemetry/api')
const tracer = trace.getTracer('my-service')
// First operation - capture context
let linkContext
tracer.startActiveSpan('batch-job', (span) => {
linkContext = span.spanContext()
span.end()
})
// Later operation linked to the first
tracer.startActiveSpan('process-result', { links: [{ context: linkContext }] }, (span) => {
// Linked to batch-job but not a child
span.end()
})
Validate
- Trigger code paths that create manual spans.
- In SigNoz Traces, filter by
service.nameor span name. - Open a trace and check attributes, events, and error status.
- Use
hasError = trueto find spans with recorded exceptions.
Troubleshooting
Why don't I see my spans in SigNoz?
- Tracer provider must initialize before your app code runs.
- Check sampler settings. Ratio-based sampling may drop spans in dev.
- Confirm traffic actually hits the instrumented functions.
Why are child spans missing?
- Use
startActiveSpanwhich sets parent context automatically. - If using
startSpan, you must manage context manually. - Parent span can't end before children are created.
Why don't attributes appear?
- Values must be strings, booleans, numbers, or arrays of these.
- Set attributes before
span.end(). Post-end mutations are ignored.
Spans disconnected across async calls?
startActiveSpanhandles async context in most cases.- For complex flows, use
context.with()to propagate context explicitly.
Next steps
- Correlate traces with logs for faster debugging
- Return to the Node.js instrumentation guide