How to Set Up Zipkin Distributed Tracing in PHP Webman Projects
This guide explains Zipkin's architecture, data collection methods, and step‑by‑step installation and configuration for PHP applications, including creating tracers, recording spans, and integrating a middleware for full‑stack monitoring in Webman microservice environments.
Overview
Zipkin is an open‑source distributed tracing system that reconstructs call chains, aggregates request counts, visualizes topology, and analyzes service dependencies, helping developers locate performance bottlenecks in microservice architectures.
Official repository: https://github.com/openzipkin/zipkin
Architecture
Primary tracing workflow
Client applications embed language‑specific SDKs (supporting OpenTracing) to report service call data.
The reported data is aggregated and persisted by Zipkin, producing detailed traces, performance overviews, and real‑time topologies for troubleshooting.
Trace data can be forwarded to downstream analysis services for offline processing and alerting.
Data reporting methods
Direct reporting (traditional frameworks)
Direct data reporting without an agent (e.g., PHP‑FPM + Nginx).
ThinkPHP 6.0
Laravel
Yii 2.0
Agent‑based reporting (modern frameworks)
Data reporting via an agent (command‑line mode).
webman
Swoole
Installation
Install via Composer:
composer require openzipkin/zipkinUsage
Create a Tracer
A Tracer creates Span objects to record distributed operation timings. It also configures the reporting endpoint, local IP, and sampling rate.
function create_tracing($endpointName, $ipv4) {
$endpoint = Endpoint::create($endpointName, $ipv4, null, 2555);
$logger = new \Monolog\Logger('log');
$logger->pushHandler(new \Monolog\Handler\ErrorLogHandler());
$reporter = new Zipkin\Reporters\Http(Zipkin\Reporters\Http\CurlFactory::create());
$sampler = BinarySampler::createAsAlwaysSample();
$tracing = TracingBuilder::create()
->havingLocalEndpoint($endpoint)
->havingSampler($sampler)
->havingReporter($reporter)
->build();
return $tracing;
}Record request data
$rootSpan = $tracer->newTrace();
$rootSpan->setName('encode');
$rootSpan->start();
try {
doSomethingExpensive();
} finally {
$rootSpan->finish();
}To record child or subsequent operations, pass the parent context to newChild() as shown.
$span = $tracer->newChild($parentSpan->getContext());
$span->setName('encode');
$span->start();
try {
doSomethingExpensive();
} finally {
$span->finish();
}Overall flow
Client Span Server Span
┌──────────────────┐ ┌──────────────────┐
│ │ │ │
│ TraceContext │ Http Request Headers │ TraceContext │
│ ┌──────────────┐ │ X‑B3‑TraceId │ ┌──────────────┐ │
│ │ TraceId │ │ X‑B3‑ParentSpanId │ │ TraceId │ │
│ │ ParentSpanId │ │ X‑B3‑SpanId │ │ ParentSpanId │ │
│ │ SpanId │ │ X‑B3‑Sampled │ │ SpanId │ │
│ │ Sampled │ │ │ │ Sampled │ │
│ └──────────────┘ │ │ └──────────────┘ │
└──────────────────┘ └──────────────────┘Webman integration example
Middleware implementation
<?php
declare(strict_types=1);
namespace app\middleware;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Logger;
use think\facade\Db;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
use Zipkin\Reporters\Http as ZipkinHttp;
use Zipkin\TracingBuilder;
use Zipkin\Samplers\BinarySampler;
use Zipkin\Endpoint;
use Workerman\Timer;
use const Zipkin\Tags\SQL_QUERY;
class ArmsMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $next): Response
{
static $tracing = null, $tracer = null;
if (!$tracing) {
$endpoint = Endpoint::create('OpenTechStack', $request->getRealIp(), null, 2555);
$logger = new Logger('log');
$logger->pushHandler(new ErrorLogHandler());
// Replace the URL with your Zipkin collector endpoint
$reporter = new ZipkinHttp(['endpoint_url' => config('security')['endpoint_url']]);
$sampler = BinarySampler::createAsAlwaysSample();
$tracing = TracingBuilder::create()
->havingLocalEndpoint($endpoint)
->havingSampler($sampler)
->havingReporter($reporter)
->build();
$tracer = $tracing->getTracer();
Timer::add(55, function () use ($tracer) { $tracer->flush(); });
register_shutdown_function(function () use ($tracer) { $tracer->flush(); });
}
$rootSpan = $tracer->newTrace();
$rootSpan->setName($request->controller . '::' . $request->action);
$rootSpan->start();
$request->rootSpan = $rootSpan;
$request->tracer = $tracer;
$result = $next($request);
if (class_exists(Db::class)) {
$logs = Db::getDbLog(true);
if (!empty($logs['sql'])) {
foreach ($logs['sql'] as $sql) {
$sqlSpan = $tracer->newChild($rootSpan->getContext());
$sqlSpan->setName(SQL_QUERY);
$sqlSpan->start();
$sqlSpan->tag('db.statement', $sql);
$sqlSpan->finish();
}
}
}
$rootSpan->finish();
return $result;
}
}Register the middleware
return [
'' => [
\app\middleware\ArmsMiddleware::class,
],
// other middleware ...
];Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
