DEV Community

Cover image for Illuminating the Black Box: OpenTelemetry in Laravel 🔍
Prajapati Paresh
Prajapati Paresh

Posted on Originally published at smarttechdevs.in

Illuminating the Black Box: OpenTelemetry in Laravel 🔍

The Microservice Visibility Crisis

Transitioning from a monolithic application to a microservices architecture solves organizational scaling issues, but it introduces a terrifying operational blind spot. In a monolith, if a user clicks "Checkout" and the request takes 8 seconds, you open your Laravel Telescope or standard log files, look at the single stack trace, and instantly identify the slow database query.

In a distributed enterprise system, that same "Checkout" click initiates a cascade of network calls. The Laravel API Gateway calls the Node.js Inventory Service, which calls the Python Pricing Service, which ultimately drops an event onto a Kafka queue. If the request takes 8 seconds, whose fault is it? Standard logging is completely useless here because the logs are scattered across four different servers in completely different formats. You are left guessing, manually comparing timestamps across Kibana dashboards, and wasting hours of engineering time.

At Smart Tech Devs, we eliminate architectural blind spots by implementing Distributed Tracing powered by the OpenTelemetry (OTel) standard. Distributed tracing stitches these isolated jumps together, providing a single, mathematically precise waterfall graph of exactly where your request spent every millisecond of its lifecycle.

Understanding the Trace Context

The magic of distributed tracing relies on two core concepts: Traces and Spans.

  • A Trace: The overarching story of a single user request from the moment it hits your load balancer to the moment the final response is delivered. It is identified by a globally unique trace_id.
  • A Span: A single unit of work within that trace (e.g., "Query MySQL", "Call Stripe API", "Redis Lookup"). Each span has a start time, end time, and a span_id.

To connect microservices together, the upstream service must inject the trace_id into the HTTP headers (known as Context Propagation) before making a network call. The downstream service extracts this header and continues writing spans under the same global trace.

Phase 1: Architecting OpenTelemetry in Laravel

To implement this in Laravel, we utilize the official OpenTelemetry PHP SDK. Instead of manually starting and stopping timers for every line of code, we architect an auto-instrumentation layer that hooks into Laravel's native event system.


namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\API\Trace\SpanKind;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Event;
use Illuminate\Database\Events\QueryExecuted;

class OpenTelemetryServiceProvider extends ServiceProvider
{
    public function boot(TracerInterface $tracer)
    {
        // 1. Auto-Instrument Database Queries
        Event::listen(QueryExecuted::class, function (QueryExecuted $event) use ($tracer) {
            $span = $tracer->spanBuilder('sql.query')
                ->setSpanKind(SpanKind::KIND_CLIENT)
                ->setAttribute('db.system', 'mysql')
                ->setAttribute('db.statement', $event->sql)
                ->setAttribute('db.user', $event->connection->getConfig('username'))
                ->startSpan();

            // We simulate the execution time backward since the event fires AFTER the query
            $span->end((microtime(true) - ($event->time / 1000)) * 1e9); 
        });

        // 2. Context Propagation for Outbound HTTP Calls
        // When Laravel calls another microservice, inject the Trace ID into the headers
        Http::globalRequestMiddleware(function ($request) {
            $context = \OpenTelemetry\Context\Context::getCurrent();
            $headers = [];
            
            // The W3C Trace Context propagator injects the 'traceparent' header
            \OpenTelemetry\API\Globals::propagator()->inject($context, \OpenTelemetry\Context\Propagation\ArrayAccessGetterSetter::getInstance(), $headers);
            
            return $request->withHeaders($headers);
        });
    }
}

Phase 2: Extracting Context via Middleware

When an incoming request hits your Laravel application from an upstream service (like an API Gateway), you must check if a trace is already in progress and attach your new operations to it.


namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\Context\Context;

class TraceIncomingRequest
{
    public function handle(Request $request, Closure $next)
    {
        // 1. Extract W3C 'traceparent' headers from the incoming request
        $parentContext = Globals::propagator()->extract($request->headers->all());
        
        // 2. Set the extracted context as the active context
        $scope = $parentContext->activate();

        // 3. Create a Root Span for this specific service's work
        $tracer = Globals::tracerProvider()->getTracer('laravel-backend');
        $span = $tracer->spanBuilder($request->method() . ' ' . $request->path())
            ->setSpanKind(SpanKind::KIND_SERVER)
            ->setAttribute('http.method', $request->method())
            ->setAttribute('http.url', $request->fullUrl())
            ->startSpan();

        // 4. Make this span active for all subsequent code (DB queries, etc.)
        $spanScope = $span->activate();

        try {
            $response = $next($request);
            $span->setAttribute('http.status_code', $response->getStatusCode());
            return $response;
        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setAttribute('error', true);
            throw $e;
        } finally {
            // 5. Always close spans to prevent memory leaks
            $span->end();
            $spanScope->detach();
            $scope->detach();
        }
    }
}

Phase 3: Exporting the Telemetry Data

Spans stored in RAM are useless. We must export them to an observability backend like Jaeger, Zipkin, or Datadog. We configure the OpenTelemetry PHP exporter via environment variables to utilize the standard OTLP (OpenTelemetry Protocol) over gRPC or HTTP.


# .env Configuration
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-collector:4317
OTEL_SERVICE_NAME=billing-microservice

The Engineering ROI and MTTR Reduction

Implementing OpenTelemetry across your enterprise architecture radically reduces your Mean Time To Resolution (MTTR). When a critical API begins degrading, you no longer sift through scattered log files. You open your Jaeger dashboard and instantly visualize the entire distributed transaction as a pristine waterfall graph. You can pinpoint with absolute mathematical certainty that the latency is caused by a specific Redis lock in the pricing service, or an unindexed query in the inventory service. By standardizing on vendor-agnostic OpenTelemetry, you future-proof your observability stack, allowing you to swap out analysis tools (like moving from Jaeger to New Relic) without rewriting a single line of application code.

Top comments (0)