For the complete documentation index, see llms.txt. Markdown versions are available by appending .md to documentation URLs.

How to add manual instrumentation in PHP

SigNoz Cloud - This page applies to SigNoz Cloud editions.
Self-Host - This page applies to self-hosted SigNoz editions.

Manual instrumentation gives you fine-grained control when automatic instrumentation alone cannot express important business operations. Use it to capture steps that matter for debugging, attach business-specific attributes, or ensure failures surface with the right context in SigNoz.

Prerequisites

  • PHP 8.1+.
  • OpenTelemetry SDK installed via Composer:
    composer require open-telemetry/sdk open-telemetry/exporter-otlp
  • Environment variables configured to export traces to SigNoz. See the PHP instrumentation guide for setup instructions.

Step 1. Get a tracer

Acquire a tracer from the global tracer provider to create spans:

tracer.php
<?php
 
use OpenTelemetry\API\Globals;
 
$tracerProvider = Globals::tracerProvider();
$tracer = $tracerProvider->getTracer(
    'my-app',      // instrumentation scope name
    '1.0.0'        // version (optional)
);

Reuse the tracer instance throughout your application instead of creating new ones for each request.

Step 2. Create manual spans

Wrap important operations inside custom spans:

create-span.php
<?php
 
use OpenTelemetry\API\Globals;
 
function processOrder(string $orderId): void
{
    $tracer = Globals::tracerProvider()->getTracer('order-service');
 
    $span = $tracer->spanBuilder('process-order')->startSpan();
 
    try {
        // Your business logic here
        validateOrder($orderId);
        chargePayment($orderId);
        fulfillOrder($orderId);
    } finally {
        $span->end();
    }
}

Tips:

  • Use descriptive span names that match business operations (checkout, validate-user, send-email).
  • Always call end() on spans—they won't export otherwise.
  • Use try/finally to ensure spans end even when exceptions occur.

Step 3. Create nested spans

Track hierarchical operations by activating parent spans:

nested-spans.php
<?php
 
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\Span;
 
function processCheckout(string $cartId): void
{
    $tracer = Globals::tracerProvider()->getTracer('checkout-service');
 
    // Parent span
    $parentSpan = $tracer->spanBuilder('checkout')->startSpan();
    $scope = $parentSpan->activate();
 
    try {
        // Child span - automatically linked to parent
        $childSpan = $tracer->spanBuilder('validate-cart')->startSpan();
        try {
            validateCart($cartId);
        } finally {
            $childSpan->end();
        }
 
        // Another child span
        $paymentSpan = $tracer->spanBuilder('process-payment')->startSpan();
        try {
            processPayment($cartId);
        } finally {
            $paymentSpan->end();
        }
    } finally {
        $scope->detach();
        $parentSpan->end();
    }
}

Always detach scopes to prevent context leaks.

Step 4. Propagate context

PHP keeps the active span in a context store the runtime manages. $span->activate() puts your span there and hands back a Scope you have to detach. Two things break that chain: a scope you forget to detach, and another service. See Context Propagation for the concepts behind this step.

Inside your process

A span becomes the parent of later spans only after you activate it. Detaching the scope restores whatever was current before:

in-process.php
<?php
 
use OpenTelemetry\API\Trace\Span;
 
$parent = $tracer->spanBuilder('process-order')->startSpan();
$scope = $parent->activate();
 
try {
    // Child of process-order, because process-order is active
    $child = $tracer->spanBuilder('validate-cart')->startSpan();
    $child->end();
 
    // Read the active span anywhere below this point
    Span::getCurrent()->setAttribute('order.id', $orderId);
} finally {
    $scope->detach();
    $parent->end();
}
 
// Back to the previous context. A span started here is NOT a child of process-order.
$sibling = $tracer->spanBuilder('audit-order')->startSpan();
$sibling->end();

Skip the detach() and every later span in the request attaches to a span that already ended.

Across a service boundary

Auto-instrumentation handles the frameworks and clients it covers. Write the calls yourself for any hop it does not.

propagate.php
<?php
 
use OpenTelemetry\API\Globals;
 
// Outgoing: inject writes traceparent into the carrier
function chargeCard(array $payload): void
{
    $tracer = Globals::tracerProvider()->getTracer('checkout-service');
 
    $span = $tracer->spanBuilder('charge-card')->startSpan();
    $scope = $span->activate();
 
    try {
        $carrier = [];
        Globals::propagator()->inject($carrier);
 
        // $carrier is now ['traceparent' => '00-<trace-id>-<span-id>-01']
        $client->post('https://payments.internal/charge', [
            'headers' => $carrier,
            'json' => $payload,
        ]);
    } finally {
        $scope->detach();
        $span->end();
    }
}

On the receiving side, extract and pass the result to setParent:

extract.php
<?php
 
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
 
function handleCharge(ServerRequestInterface $request): void
{
    $tracer = Globals::tracerProvider()->getTracer('payment-service');
 
    $context = Globals::propagator()->extract($request->getHeaders());
 
    $span = $tracer->spanBuilder('handle-charge')
        ->setParent($context)
        ->setSpanKind(SpanKind::KIND_SERVER)
        ->startSpan();
 
    $scope = $span->activate();
 
    try {
        // handle-charge is now a child of charge-card in the caller's trace
        capturePayment($request);
    } finally {
        $scope->detach();
        $span->end();
    }
}

Step 5. Add attributes

Attributes show up as key-value pairs in SigNoz for filtering and aggregation:

attributes.php
<?php
 
use OpenTelemetry\API\Globals;
 
function handlePayment(float $amount, string $currency): void
{
    $tracer = Globals::tracerProvider()->getTracer('payment-service');
 
    $span = $tracer->spanBuilder('payment')->startSpan();
 
    try {
        $span->setAttribute('payment.amount', $amount);
        $span->setAttribute('payment.currency', $currency);
        $span->setAttribute('payment.method', 'credit_card');
        $span->setAttribute('user.id', getCurrentUserId());
 
        // Process payment...
    } finally {
        $span->end();
    }
}

Use semantic conventions for common attributes when possible.

Step 6. Add events

Events capture notable moments within a span:

events.php
<?php
 
use OpenTelemetry\API\Globals;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
 
function processJob(string $jobId): void
{
    $tracer = Globals::tracerProvider()->getTracer('job-service');
 
    $span = $tracer->spanBuilder('process-job')->startSpan();
 
    try {
        $span->addEvent('job.started');
 
        // Do work...
 
        $span->addEvent('job.completed', Attributes::create([
            'job.id' => $jobId,
            'job.result' => 'success',
            'job.duration_ms' => 1234,
        ]));
    } finally {
        $span->end();
    }
}

Use events to mark retries, cache hits/misses, queue waits, and similar milestones.

Step 7. Record errors

Flag failures so they're easy to query in SigNoz:

error-handling.php
<?php
 
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\StatusCode;
 
function riskyOperation(): void
{
    $tracer = Globals::tracerProvider()->getTracer('my-service');
 
    $span = $tracer->spanBuilder('risky-operation')->startSpan();
 
    try {
        doSomethingRisky();
        $span->setStatus(StatusCode::STATUS_OK);
    } catch (Throwable $e) {
        $span->recordException($e, [
            'exception.escaped' => true,
        ]);
        $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
        throw $e;
    } finally {
        $span->end();
    }
}
  • recordException() attaches exception details including stack trace.
  • Setting status to STATUS_ERROR surfaces the span in SigNoz error views and alerts.

Link spans representing causally-related but not parent-child operations:

span-links.php
<?php
 
use OpenTelemetry\API\Globals;
 
function processBatch(array $itemSpanContexts): void
{
    $tracer = Globals::tracerProvider()->getTracer('batch-service');
 
    $spanBuilder = $tracer->spanBuilder('batch-process');
 
    // Link to all items being processed in this batch
    foreach ($itemSpanContexts as $context) {
        $spanBuilder->addLink($context);
    }
 
    $span = $spanBuilder->startSpan();
 
    try {
        // Process batch...
    } finally {
        $span->end();
    }
}

Validate

  1. Trigger the code paths that emit manual spans.
  2. In SigNoz Traces, filter by service.name or your span name.
  3. Open a trace and verify attributes, events, and error status.
  4. Check the Errors view to confirm failures show up with recorded exceptions.

Troubleshooting

Still not seeing data in SigNoz? Work through Debug missing traces, logs, and metrics, which covers SDK diagnostics, Collector connectivity, and the common ingestion errors for all three signals.

Why don't I see my custom spans in SigNoz?

  • Ensure the OpenTelemetry extension is loaded: php --ri opentelemetry
  • Verify environment variables are set correctly (OTEL_PHP_AUTOLOAD_ENABLED=true, OTEL_EXPORTER_OTLP_ENDPOINT, etc.).
  • Check that span->end() is being called—spans without end() won't export.
  • Make sure that the SDK is properly initialized before your application code runs.

Why are child spans not linked to parent spans?

  • Make sure you call activate() on the parent span before creating children.
  • Always detach() the scope when done to prevent context leaks.
  • Verify you're using the same tracer provider instance throughout.

Why does the downstream service start its own trace?

  • Make sure that the caller runs inject and that the outgoing request carries a traceparent header.
  • Make sure that the receiver calls setParent($context) on the span builder. Without it the entry span takes whatever is current and starts a new trace.
  • Make sure that both services register the same propagator format. See Context Propagation.

Why don't attributes or events appear on the span?

  • Call setAttribute() or addEvent() before span->end(). Post-end mutations are ignored.
  • Attribute values must be scalar types (string, int, float, bool) or arrays of scalars.
  • Avoid reusing finished spans—always create a new span for each invocation.

Next steps

Is this page helpful

Last updatedSeptember 03, 2026

Edit on GitHub