What happens
getStats() aggregates across pool workers; the request timings do not reach it, and getTelemetry() reads them from the thread it runs on.
The counters live in the shared slab (src/core/stats_registry.c), so any worker that answers a scrape reports for the whole pool. The timing fields are plain members of http_server_object — sojourn_sum_ns, service_sum_ns, sojourn_max_ns, sojourn_samples (src/http_server_class.c:389-392), written by http_server_aggregate_sample() (:996-1001) and read by getTelemetry() (:5264-5273). Under setWorkers(4) each worker keeps its own four numbers and no API sums them.
Why it matters
A metrics endpoint written in PHP is served by whichever worker took the request, so consecutive scrapes read different workers: 7 ms, then 30 ms, then 7 ms again, with nothing in the response saying which worker answered. One Prometheus series drawn from four unrelated averages carries no trend and cannot carry an alert.
The exporter added to laravel-spawn (YanGusik/laravel-spawn#56) therefore emits counters and gauges only and leaves latency() out of the exposition — the single number an operator asks for first is the one that cannot be exported.
Naming the worker instead of summing does not close it either: a worker cannot report its own slot id, so a PHP caller cannot even label the series it gets.
Reproduction
$config = (new HttpServerConfig())
->addListener('127.0.0.1', 8080)
->setWorkers(4)
->setStatsEnabled(true)
->setTelemetryEnabled(true); /* without this the timings stay zero */
$server = new HttpServer($config);
$server->addHttpHandler(function ($req, $res) use ($server) {
if ($req->getPath() === '/t') {
$t = $server->getTelemetry();
$res->setBody("{$t['sojourn_samples']} samples, {$t['service_avg_ms']} ms\n")->end();
return;
}
Async\delay(random_int(1, 50));
$res->setBody("ok\n")->end();
});
$server->start();
Drive some load, then scrape /t repeatedly: the sample count jumps between four small independent tallies rather than growing with the traffic, and the average follows whichever worker answered.
Suggested fix
Move the four fields into http_server_counters_t and add them to HTTP_SERVER_COUNTER_TABLE (include/php_http_server.h:898):
X(sojourn_sum_ns, SUM)
X(service_sum_ns, SUM)
X(sojourn_samples, SUM)
X(sojourn_max_ns, MAX)
The combine semantics the registry needs already exist — http_stats_registry_totals() sums SUM fields and takes the maximum of MAX ones (src/core/stats_registry.c:214-232) — and the retired-totals block keeps a reload from resetting them. getStats() would then carry pool-wide sums, from which an average is one division, while workers[] keeps the per-worker breakdown a caller may still want. Two constraints go with the move: the static assert requires the table and the struct to stay in step, and resetTelemetry() currently zeroes these fields directly (:5370-5373), which would become a write into the worker's slot.
Percentiles are a separate matter and stay out of scope here (stage A6 of #5): this is about the sum, the maximum and the sample count that already exist being unreadable for a pool.
What happens
getStats()aggregates across pool workers; the request timings do not reach it, andgetTelemetry()reads them from the thread it runs on.The counters live in the shared slab (
src/core/stats_registry.c), so any worker that answers a scrape reports for the whole pool. The timing fields are plain members ofhttp_server_object—sojourn_sum_ns,service_sum_ns,sojourn_max_ns,sojourn_samples(src/http_server_class.c:389-392), written byhttp_server_aggregate_sample()(:996-1001) and read bygetTelemetry()(:5264-5273). UndersetWorkers(4)each worker keeps its own four numbers and no API sums them.Why it matters
A metrics endpoint written in PHP is served by whichever worker took the request, so consecutive scrapes read different workers: 7 ms, then 30 ms, then 7 ms again, with nothing in the response saying which worker answered. One Prometheus series drawn from four unrelated averages carries no trend and cannot carry an alert.
The exporter added to laravel-spawn (YanGusik/laravel-spawn#56) therefore emits counters and gauges only and leaves
latency()out of the exposition — the single number an operator asks for first is the one that cannot be exported.Naming the worker instead of summing does not close it either: a worker cannot report its own slot id, so a PHP caller cannot even label the series it gets.
Reproduction
Drive some load, then scrape
/trepeatedly: the sample count jumps between four small independent tallies rather than growing with the traffic, and the average follows whichever worker answered.Suggested fix
Move the four fields into
http_server_counters_tand add them toHTTP_SERVER_COUNTER_TABLE(include/php_http_server.h:898):The combine semantics the registry needs already exist —
http_stats_registry_totals()sumsSUMfields and takes the maximum ofMAXones (src/core/stats_registry.c:214-232) — and the retired-totals block keeps a reload from resetting them.getStats()would then carry pool-wide sums, from which an average is one division, whileworkers[]keeps the per-worker breakdown a caller may still want. Two constraints go with the move: the static assert requires the table and the struct to stay in step, andresetTelemetry()currently zeroes these fields directly (:5370-5373), which would become a write into the worker's slot.Percentiles are a separate matter and stay out of scope here (stage A6 of #5): this is about the sum, the maximum and the sample count that already exist being unreadable for a pool.