PHP
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
245: Distributed Tracing Across PHP Services
By now, you're probably comfortable logging errors within a single PHP application. But as soon as you split your logic into microservices—say, an Order service that calls a Payment service—your logs become a fragmented mess. You see an error in the Payment service, but you have no idea which specific Order request triggered it. This is where distributed tracing comes in.
We're going to build a tiny two-service system. I'll use OpenTelemetry (OTel) because it's the industry standard, and it keeps us from being locked into a specific vendor like New Relic or Datadog. We'll simulate a "Place Order" request that hits an order.php script, which then makes a CURL request to payment.php.
Setting up the Order Gateway
First, let's get the Order service to start a "root span." A span is essentially a timed segment of work. I want to wrap the entire order process in one span so I can see exactly how long the total request took.
<?php
require 'vendor/autoload.php';
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\SDK\Trace\TracerProviderFactory;
$tracerProvider = (new TracerProviderFactory())->create();
$tracer = $tracerProvider->getTracer('order-service');
// Start the root span
$rootSpan = $tracer->spanBuilder('place_order_request')->startSpan();
$rootSpan->activate();
try {
// Simulate some local work
usleep(50000);
// We'll add the HTTP call here in a moment...
echo "Order processed!";
} finally {
$rootSpan->end();
$tracerProvider->shutdown();
}
?>
Creating the Payment Receiver
Now we need the Payment service. This service needs to be able to "extract" a trace ID from the incoming request headers. If it finds one, it should attach its own work to that existing trace instead of starting a new one.
<?php
require 'vendor/autoload.php';
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\SDK\Trace\TracerProviderFactory;
$tracerProvider = (new TracerProviderFactory())->create();
$tracer = $tracerProvider->getTracer('payment-service');
// Extract the context from the HTTP headers
$propagator = TraceContextPropagator::getInstance();
$context = $propagator->extract($_SERVER);
// Start a span that is a child of the extracted context
$span = $tracer->spanBuilder('process_payment')
->setParent($context)
->startSpan();
try {
usleep(100000); // Simulate payment gateway latency
echo "Payment successful!";
} finally {
$span->end();
$tracerProvider->shutdown();
}
?>
The "Ghost Trace" Problem
Here is where I usually trip up when I'm first setting this up. I'll write the code to call the payment service using CURL, but I'll forget that the trace ID doesn't magically travel through the air—it has to be explicitly sent in the HTTP headers.
I'll add this to my order.php:
$ch = curl_init('http://payment.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
When I run this and check my tracing dashboard (like Jaeger), I see two completely separate traces: one for the order and one for the payment. They have different Trace IDs. To the system, they look like two unrelated events. This is a "ghost trace"—the connection is missing because I didn't propagate the context.
Wiring up the Propagation
To fix this, I need to use the TraceContextPropagator to inject the current span's ID into the CURL headers. This tells the Payment service, "Hey, you're part of Trace X."
Here is the corrected logic for the order.php request:
$propagator = TraceContextPropagator::getInstance();
$headers = [];
// This injects the traceparent header into our $headers array
$propagator->inject($headers);
// Now we convert that array into a format CURL understands
$curlHeaders = [];
foreach ($headers as $key => $value) {
$curlHeaders[] = "$key: $value";
}
$ch = curl_init('http://payment.php');
curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
Now, when the Payment service calls $propagator->extract($_SERVER), it finds the traceparent header I just sent. The two spans are now linked in a single timeline. You can now see that the 150ms total request time was actually 50ms in the Order service and 100ms in the Payment service. That's the power of distributed tracing.
📋 Practical Task
Exercise: Fixing the Broken Shipping Trace
You have been handed a legacy distributed system. The OrderService calls the ShippingService, but the traces are broken. The ShippingService is already correctly using TraceContextPropagator::getInstance()->extract($_SERVER), but the OrderService is sending the request using a Guzzle HTTP client without any trace headers.
Your Task: Modify the OrderService code below to inject the current OpenTelemetry trace context into the Guzzle request headers so that the ShippingService can link the two spans together.
// OrderService.php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\SDK\Trace\TracerProviderFactory;
$tracerProvider = (new TracerProviderFactory())->create();
$tracer = $tracerProvider->getTracer('order-service');
$span = $tracer->spanBuilder('ship_order')->startSpan();
$span->activate();
$client = new Client();
// TODO: Inject the trace context into the headers array here
$headers = [];
$response = $client->request('GET', 'http://shipping-service/api/ship', [
'headers' => $headers
]);
$span->end();
$tracerProvider->shutdown();There are no comments for now.