PHP SDK

Full observability for PHP services — HTTP, databases, errors, and exceptions captured automatically via OpenTelemetry

Installation

composer require obtrace/sdk-php

Requires PHP 8.1+.

Minimum Setup

bootstrap/obtrace.php
<?php
 
use Obtrace\Sdk\ObtraceClient;
use Obtrace\Sdk\ObtraceConfig;
 
$client = new ObtraceClient(new ObtraceConfig(
    apiKey: getenv("OBTRACE_API_KEY") ?: "",
    serviceName: "order-api",
));

Two fields are required: apiKey and serviceName. Everything else has defaults.

Built on OpenTelemetry

The PHP SDK uses open-telemetry/sdk under the hood to export telemetry to the Obtrace ingest endpoint. The OTel tracer is used for span creation and trace context propagation. Laravel middleware and Symfony event subscribers use the OTel tracer for request instrumentation.

What's Captured Automatically

After creating an ObtraceClient, the SDK configures OpenTelemetry and registers error handlers, HTTP instrumentation, and a shutdown function. You don't need to write any extra code for this:

WhatHowNeeds code?
PHP errorsset_error_handler intercepts errors with level mapping: E_NOTICE -> info, E_WARNING -> warn, E_ERROR -> errorNo
Uncaught exceptionsset_exception_handler catches unhandled exceptions and logs them at fatal levelNo
Outbound HTTP (cURL)curl_exec calls are instrumented via OTel with spans containing method, URL, status, and duration, plus traceparent header injectionNo
Outbound HTTP (Guzzle)Guzzle middleware is auto-registered via OTel when Guzzle is detected, creating spans and injecting trace headersNo
PDO database queriesPDO operations are instrumented with spans when the OTel PDO instrumentation is installedNo
Laravel auto-detectionLaravel requests, database queries, and queue jobs are instrumented when using the ServiceProviderNo
Shutdown flushregister_shutdown_function flushes buffered telemetry when the PHP process exitsNo

Opting Out of Auto HTTP Instrumentation

$client = new ObtraceClient(new ObtraceConfig(
    apiKey: getenv("OBTRACE_API_KEY") ?: "",
    serviceName: "order-api",
    autoInstrumentHttp: false,
));

Opting Out of Auto Error/Exception Capture

$client = new ObtraceClient(new ObtraceConfig(
    apiKey: getenv("OBTRACE_API_KEY") ?: "",
    serviceName: "order-api",
    autoCaptureErrors: false,
));

Optional: Custom Telemetry

The automatic error, exception, and HTTP capture covers basic observability. For business-specific events, use the SDK methods directly.

Logging

Use $client->log() for structured events. Logs are best for state transitions, errors, and audit trails.

$client->log("info", "order.created", [
    "order.id" => $orderId,
    "order.total" => 149.99,
    "customer.tier" => "premium",
]);
 
$client->log("error", "payment.declined", [
    "order.id" => $orderId,
    "payment.provider" => "stripe",
    "decline.code" => "insufficient_funds",
]);
 
$client->log("warn", "inventory.low", [
    "sku" => "WIDGET-42",
    "remaining" => 3,
    "reorder.threshold" => 10,
]);

Levels: trace, debug, info, warn, error, fatal.

Metrics

Use $client->metric() for numerical measurements you want to track over time.

$client->metric("http.server.request.duration", 42.5, "ms", [
    "http.method" => "POST",
    "http.route" => "/api/orders",
]);
 
$client->metric("queue.depth", 847, "1", [
    "queue.name" => "order-processing",
]);
 
$client->metric("cache.hit_ratio", 0.92, "1", [
    "cache.name" => "product-catalog",
]);
 
$client->metric("db.pool.active_connections", 14, "1");

The unit parameter follows OTLP conventions: "ms" for milliseconds, "By" for bytes, "1" for dimensionless values.

Tracing / Spans

Use $client->span() for operations with duration. Spans appear in the trace waterfall and are best for database queries, HTTP calls, and queue processing.

$client->span("db.query SELECT orders", attrs: [
    "db.system" => "mysql",
    "db.statement" => "SELECT * FROM orders WHERE customer_id = ?",
    "db.operation" => "SELECT",
    "db.sql.table" => "orders",
]);
 
$client->span("http.client POST /payments", attrs: [
    "http.method" => "POST",
    "http.url" => "https://api.stripe.com/v1/charges",
    "http.status_code" => 201,
    "payment.amount" => 149.99,
]);
 
$client->span("queue.publish order.created", attrs: [
    "messaging.system" => "rabbitmq",
    "messaging.destination" => "order.created",
]);

Framework Integration

Laravel (ServiceProvider)

The SDK includes a Laravel ServiceProvider for zero-config setup. Add your credentials to config/services.php:

config/services.php
'obtrace' => [
    'api_key' => env('OBTRACE_API_KEY'),
],

Register the ServiceProvider in bootstrap/providers.php (Laravel 11+):

bootstrap/providers.php
<?php
 
return [
    Obtrace\Sdk\Laravel\ObtraceServiceProvider::class,
];

The ServiceProvider registers ObtraceClient as a singleton, adds request instrumentation middleware to the global middleware stack, and wires up shutdown flushing. The middleware emits a span and metric for every HTTP request.

Use the client anywhere via DI:

app/Http/Controllers/OrderController.php
<?php
 
namespace App\Http\Controllers;
 
use Obtrace\Sdk\ObtraceClient;
 
class OrderController extends Controller
{
    public function __construct(private ObtraceClient $client) {}
 
    public function show(string $id)
    {
        $this->client->log("info", "order.fetched", ["order.id" => $id]);
        return response()->json(["id" => $id, "status" => "shipped"]);
    }
}

Laravel (Manual)

If you prefer manual setup over the ServiceProvider:

app/Providers/ObtraceServiceProvider.php
<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use Obtrace\Sdk\ObtraceClient;
use Obtrace\Sdk\ObtraceConfig;
 
class ObtraceServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(ObtraceClient::class, function () {
            return new ObtraceClient(new ObtraceConfig(
                apiKey: config("services.obtrace.api_key"),
                serviceName: "order-api",
                env: app()->environment(),
                serviceVersion: config("app.version", "1.0.0"),
            ));
        });
    }
}
app/Http/Middleware/ObtraceMiddleware.php
<?php
 
namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Http\Request;
use Obtrace\Sdk\ObtraceClient;
 
class ObtraceMiddleware
{
    public function __construct(private ObtraceClient $client) {}
 
    public function handle(Request $request, Closure $next)
    {
        $start = microtime(true);
        $response = $next($request);
        $durationMs = (microtime(true) - $start) * 1000;
 
        $route = $request->route()?->uri() ?? $request->getPathInfo();
        $method = $request->getMethod();
 
        $this->client->span("http.server {$method} /{$route}", attrs: [
            "http.method" => $method,
            "http.route" => "/{$route}",
            "http.status_code" => $response->getStatusCode(),
        ]);
 
        $this->client->metric("http.server.request.duration", $durationMs, "ms", [
            "http.method" => $method,
            "http.route" => "/{$route}",
            "http.status_code" => $response->getStatusCode(),
        ]);
 
        return $response;
    }
}

Register in bootstrap/app.php or app/Http/Kernel.php:

->withMiddleware(function ($middleware) {
    $middleware->append(\App\Http\Middleware\ObtraceMiddleware::class);
})

Symfony

Register as a service and create an event subscriber:

src/EventSubscriber/ObtraceSubscriber.php
<?php
 
namespace App\EventSubscriber;
 
use Obtrace\Sdk\ObtraceClient;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
 
class ObtraceSubscriber implements EventSubscriberInterface
{
    public function __construct(private ObtraceClient $client) {}
 
    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::TERMINATE => "onTerminate"];
    }
 
    public function onTerminate(TerminateEvent $event): void
    {
        $request = $event->getRequest();
        $response = $event->getResponse();
        $method = $request->getMethod();
        $route = $request->attributes->get("_route", $request->getPathInfo());
 
        $this->client->span("http.server {$method} {$route}", attrs: [
            "http.method" => $method,
            "http.route" => $route,
            "http.status_code" => $response->getStatusCode(),
        ]);
 
        $this->client->flush();
    }
}

CLI / Queue Workers

For long-running processes, flush periodically:

app/Console/Commands/ProcessOrders.php
$client = app(ObtraceClient::class);
 
while ($job = $queue->pop()) {
    try {
        processOrder($job);
        $client->metric("orders.processed", 1, "1");
    } catch (\Throwable $e) {
        $client->log("error", "order.processing.failed", [
            "order.id" => $job->orderId,
            "error.type" => get_class($e),
            "error.message" => $e->getMessage(),
        ]);
    }
 
    if ($iterationCount % 100 === 0) {
        $client->flush();
    }
}

Configuration Reference

ParameterTypeRequiredDefaultDescription
apiKeystringYesObtrace API key
serviceNamestringYesStable name for this service
tenantId?stringNofrom keyTenant identifier
projectId?stringNofrom keyProject identifier
appId?stringNoApplication identifier within the project
envstringNo"production"Deployment environment
serviceVersion?stringNoVersion string for this deployment
autoCaptureErrorsboolNotrueRegister error/exception handlers automatically
autoInstrumentHttpboolNotrueInstrument cURL and Guzzle outbound HTTP calls
validateSemanticMetricsboolNofalseWarn on non-standard metric names
debugboolNofalseEnable verbose logging to stderr
maxQueueSizeintNo2048Maximum buffered items before dropping
flushIntervalMsintNo5000Auto-flush interval in milliseconds

Validation Checklist

After deploying, verify:

  • serviceName, env, and serviceVersion are stable across deployments and match what you see in the Obtrace UI
  • At least one real request path emits both a log and a span
  • No 401 or 403 errors appear in stderr when debug is true
  • The shutdown function is firing (check that telemetry arrives even without explicit flush() calls in short-lived scripts)
  • Outbound cURL/Guzzle calls show auto-generated spans
  • Long-running workers call flush() periodically rather than relying solely on the shutdown hook
  • Metrics use OTLP-standard units (ms, By, 1) not custom strings