SigNoz
Docs
PricingCustomers
Get Started - Free
Docs
IntroductionContributingMigrate from DatadogSigNoz API
OpenTelemetry
What is OpenTelemetryOpenTelemetry Collector GuideOpenTelemetry Demo
Community
Support
Slack
X
Launch Week
Changelog
Dashboard Templates
DevOps Wordle
Newsletter
KubeCon, Atlanta 2025
More
SigNoz vs DatadogSigNoz vs New RelicSigNoz vs GrafanaSigNoz vs Dynatrace
Careers
AboutTermsPrivacySecurity & Compliance
SigNoz Logo
SigNoz
All systems operational
HIPAASOC-2
SigNoz Cloud - This page applies to SigNoz Cloud editions.
Self-Host - This page applies to self-hosted SigNoz editions.

How to add manual instrumentation in Node.js

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.

These steps work the same way regardless of deployment. Only your OTLP endpoint and ingestion key differ.

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/api v1.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 a finally block.

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()
    }
  })
}
  • recordException attaches the stack trace.
  • SpanStatusCode.ERROR flags 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

  1. Trigger code paths that create manual spans.
  2. In SigNoz Traces, filter by service.name or span name.
  3. Open a trace and check attributes, events, and error status.
  4. Use hasError = true to 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 startActiveSpan which 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?

  • startActiveSpan handles 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
  • Exclude HTTP endpoints such as health checks, etc.

If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack.

If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.

Last updated: May 18, 2026

Edit on GitHub

Was this page helpful?

Your response helps us improve this page.

Prev
GraphQL
Next
Selective Instrumentation
On this page
Prerequisites
Step 1. Create manual spans
Step 2. Add attributes and events
Step 3. Record errors
Step 4. Add span links (optional)
Validate
Troubleshooting
Why don't I see my spans in SigNoz?
Why are child spans missing?
Why don't attributes appear?
Spans disconnected across async calls?
Next steps

Is this page helpful?

Your response helps us improve this page.