Observability in PHP: How to Stop Guessing and Start Seeing Everything Clearly
\n\nWhen your PHP application starts slowing down, throwing 500 errors, or just behaving strangely, the first reaction is to dig into the logs. But logs (even perfectly structured ones) are only part of the puzzle. Modern development requires much more: you need to know how long each request took, which external services are slowing down the system, and how much memory a specific endpoint is consuming.
\n\nAll of this is about observability. In this article, we'll break down the three pillars of observability in PHP: tracing, metrics, and logs. You'll learn how to set up real monitoring with code examples and get ready-to-use recipes that you can implement today.
\n\nWhat is Observability and Why Does a PHP Developer Need It?
\n\nObservability (O11y) is the ability to understand the internal state of a system based on its external outputs. In the PHP world, this means you can answer questions like:
\n\n- \n
- Why did a specific API request take 5 seconds? \n
- Which SQL query took the longest in the last hour? \n
- How many requests per second is your server handling right now? \n
- Where exactly is memory leaking? \n
Without observability, you're working blind. With it, you see every move your application makes. And yes, this applies not only to microservices in Go or Java. A PHP monolith can and should be observed too.
\n\nTracing in PHP: Distributed Tracing with OpenTelemetry
\n\nTracing is the story of a single request. You see how a request flows through your application, which functions are called, which external APIs are hit, and how much time is spent on each step. In PHP, the OpenTelemetry standard is used for this.
\n\nInstalling OpenTelemetry for PHP
\n\nFirst, install the necessary packages via Composer:
\n\ncomposer require open-telemetry/opentelemetry\ncomposer require open-telemetry/transport-grpc\ncomposer require open-telemetry/exporter-otlp\n\n\nNow, let's configure a basic tracer. Create a file tracing.php:
// tracing.php\nuse OpenTelemetry\\API\\Globals;\nuse OpenTelemetry\\SDK\\Trace\\TracerProvider;\nuse OpenTelemetry\\SDK\\Trace\\SpanProcessor\\BatchSpanProcessor;\nuse OpenTelemetry\\SDK\\Trace\\Exporter\\ConsoleSpanExporter;\n\nrequire 'vendor/autoload.php';\n\n// Create an exporter (to console for now)\n$exporter = new ConsoleSpanExporter();\n$processor = new BatchSpanProcessor($exporter);\n$tracerProvider = new TracerProvider($processor);\n\n// Register the global tracer\nGlobals::setTracerProvider($tracerProvider);\n\n// Get a tracer with your application name\n$tracer = Globals::tracerProvider()->getTracer('my-php-app');\n\n\nCreating Spans in Real Code
\n\nLet's say you have a controller that processes an order. Wrap the key operations in spans:
\n\n// OrderController.php\nclass OrderController {\n public function createOrder(array $data) {\n $tracer = Globals::tracerProvider()->getTracer('order-service');\n \n // Main span — the entire request\n $span = $tracer->spanBuilder('POST /orders')->startSpan();\n $span->setAttribute('order.id', $data['id'] ?? 'unknown');\n \n try {\n // Span for user validation\n $userSpan = $tracer->spanBuilder('validate.user')->startSpan();\n $user = $this->validateUser($data['user_id']);\n $userSpan->end();\n \n // Span for database save\n $dbSpan = $tracer->spanBuilder('save.order')->startSpan();\n $order = $this->saveOrder($data);\n $dbSpan->end();\n \n // Span for sending email\n $emailSpan = $tracer->spanBuilder('send.email')