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:
<?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:
<?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/finallyto ensure spans end even when exceptions occur.
Step 3. Create nested spans
Track hierarchical operations by activating parent spans:
<?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. Add attributes
Attributes show up as key-value pairs in SigNoz for filtering and aggregation:
<?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 5. Add events
Events capture notable moments within a span:
<?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 6. Record errors
Flag failures so they're easy to query in SigNoz:
<?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_ERRORsurfaces the span in SigNoz error views and alerts.
Step 7. Add span links
Link spans representing causally-related but not parent-child operations:
<?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
- Trigger the code paths that emit manual spans.
- In SigNoz Traces, filter by
service.nameor your span name. - Open a trace and verify attributes, events, and error status.
- 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 withoutend()won't export. - Confirm 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 don't attributes or events appear on the span?
- Call
setAttribute()oraddEvent()beforespan->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
- Set up alerts for your PHP application
- Create dashboards to visualize your custom spans
- Explore supported PHP instrumentation libraries for automatic instrumentation of frameworks and libraries