SigNoz Cloud - This page is relevant for SigNoz Cloud editions.
Self-Host - This page is relevant for self-hosted SigNoz editions.

Manual Node.js Instrumentation

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

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.

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

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: March 26, 2026

Edit on GitHub