Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
132 lines (113 loc) · 4.54 KB

File metadata and controls

132 lines (113 loc) · 4.54 KB
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<?php
declare(strict_types=1);
/*
* Observability: getStats() -> Prometheus -> Grafana, plus a JSON access log.
*
* The server has no embedded exporter. getStats() returns a plain PHP array and
* $render below turns it into the Prometheus text format — swap that for
* OpenMetrics or StatsD without touching the server.
*
* php examples/observability-server.php
* curl -s localhost:8080/metrics
*
* docker compose -f examples/docker/observability/docker-compose.yml up -d
* open http://localhost:3001 # Grafana, no login, dashboard loaded
*/
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
use TrueAsync\LogSeverity;
$port = (int) (getenv('PORT') ?: 8080);
$workers = (int) (getenv('WORKERS') ?: 4);
$logPath = getenv('ACCESS_LOG') ?: (sys_get_temp_dir() . '/tas-access.log');
/*
* A closure, not a `function render(...)`. Under setWorkers(N) the handler is
* copied into a worker thread, which has its own function table — a named
* function declared here does not exist there and calling it aborts the worker.
* Closures are copied, so reach them through use() (or declare them in a
* bootloader).
*
* A *_total is monotonic and becomes a Prometheus `counter`; the rest are
* point-in-time readings and become `gauge`s. That split is not cosmetic: the
* server inherits a retiring worker's totals but not its gauges, so across a
* reload() tas_requests_total keeps climbing while tas_conns_active_h1 falls
* back to what is really open.
*/
$render = function (array $stats): string {
/* Prometheus wants the _total as a SUFFIX; the server spells this one the
* other way round. */
$rename = ['total_requests' => 'requests_total'];
$out = [];
foreach ($stats['totals'] as $name => $value) {
$name = $rename[$name] ?? $name;
$metric = 'tas_' . $name;
$out[] = '# TYPE ' . $metric . ' '
. (str_ends_with($name, '_total') ? 'counter' : 'gauge');
$out[] = $metric . ' ' . $value;
/* One series per worker: sum() them back up, or spot the single worker
* that has gone quiet. */
foreach ($stats['workers'] as $id => $counters) {
$raw = array_search($name, $rename, true) ?: $name;
if (isset($counters[$raw])) {
$out[] = $metric . '{worker="' . $id . '"} ' . $counters[$raw];
}
}
}
$out[] = '# TYPE tas_workers gauge';
$out[] = 'tas_workers ' . count($stats['workers']);
return implode("\n", $out) . "\n";
};
$config = (new HttpServerConfig())
->addListener('0.0.0.0', $port)
->setWorkers($workers)
->setStatsEnabled(true) /* opt-in: no counters, no cost, without it */
->setLogSinks([
/* 'file', not 'stream': each worker reopens the path itself, whereas a
* parent-opened stream resource cannot cross into a worker thread.
* 'access' = one record per request, in OTel semantic conventions. */
[
'type' => 'file',
'path' => $logPath,
'format' => 'json',
'category' => 'access',
'level' => LogSeverity::INFO,
],
[
'type' => 'stderr',
'format' => 'pretty',
'category' => 'app',
'level' => LogSeverity::INFO,
],
]);
$server = new HttpServer($config);
$server->addHttpHandler(function ($req, $res) use ($server, $render) {
switch ($req->getPath()) {
case '/metrics':
/* Prometheus scrapes this. Content type must be text/plain. */
$res->setStatusCode(200)
->setHeader('Content-Type', 'text/plain; version=0.0.4')
->setBody($render($server->getStats()))
->end();
return;
case '/slow':
/* Something to make the latency panels move. */
Async\delay(random_int(20, 300));
$res->setStatusCode(200)->setBody("slow\n")->end();
return;
case '/boom':
/* Feeds responses_5xx_total. */
$res->setStatusCode(500)->setBody("boom\n")->end();
return;
case '/missing':
$res->setStatusCode(404)->setBody("not found\n")->end();
return;
default:
$res->setStatusCode(200)
->setBody("hello from " . $req->getRemoteAddress() . "\n")
->end();
}
});
fwrite(STDERR, sprintf(
"listening on :%d with %d workers\n metrics http://localhost:%d/metrics\n access log %s\n",
$port, $workers, $port, $logPath
));
$server->start();
Morty Proxy This is a proxified and sanitized view of the page, visit original site.