ops: Add prometheus metrics

Adds a /metrics endpoint in serve mode. Can be disabled via --disable-metrics.

In fetch, dumps the metrics if --metrics is specified (defaults to false).

None of the metrics being collected are on a hot path, so they're just always
collected using atomic operations (i.e. no `if (!enabled) return;`). Because
the operations are cheap and infrequent enough not to matter.

The one place I want to add more metrics is in HttpClient (bytes, status,
counts, ...) but after the pending HttpClient-related PR is merged.

./lightpanda fetch --log-level fatal --metrics "https://lightpanda.io/"

```
build_info{version="1.0.0-dev.7837+70493ce35"} 1
cdp_connections_total 0
cdp_connection_limit_total 0
cdp_active_connections 0
cdp_commands_total 0
cdp_unknown_commands_total 0
js_heap_limits_total 0
script_errors_total 0
arena_hit_total{size="tiny"} 908
arena_hit_total{size="small"} 35
arena_hit_total{size="medium"} 0
arena_hit_total{size="large"} 27
arena_miss_total{size="tiny"} 193
arena_miss_total{size="small"} 31
arena_miss_total{size="medium"} 5
arena_miss_total{size="large"} 23
navigate_total{type="page"} 1
navigate_total{type="iframe"} 0
navigate_total{type="popup"} 0
js_heap_size_bytes_bucket{le="4194304"} 0
js_heap_size_bytes_bucket{le="8388608"} 0
js_heap_size_bytes_bucket{le="16777216"} 1
js_heap_size_bytes_bucket{le="33554432"} 1
js_heap_size_bytes_bucket{le="67108864"} 1
js_heap_size_bytes_bucket{le="134217728"} 1
js_heap_size_bytes_bucket{le="268435456"} 1
js_heap_size_bytes_bucket{le="536870912"} 1
js_heap_size_bytes_bucket{le="+Inf"} 1
js_heap_size_bytes_sum 11223040
js_heap_size_bytes_count 1
```
This commit is contained in:
Karl Seguin committed 2026-07-14 09:17:14 +08:00
1 parent 56c5701b7b
commit 4d81e48e44
14 files changed
+377 -24

No files matched your search

+24 -4
View File
@@ -156,6 +156,7 @@ fn spawnWorker(self: *Server, socket: posix.socket_t) !void {
while (current < self.max_connections) {
current = self.active_threads.cmpxchgWeak(current, current + 1, .monotonic, .monotonic) orelse break;
} else {
lp.metrics.cdp_connection_limit.incr();
return error.MaxThreadsReached;
}
errdefer _ = self.active_threads.fetchSub(1, .monotonic);
@@ -218,6 +219,12 @@ fn handleConnection(self: *Server, socket: posix.socket_t) void {
return;
}
// only count websocket (i.e. CDP) connections, not HTTP requests like
// /json/version probes or /metrics scrapes
lp.metrics.cdp_connections.incr();
lp.metrics.cdp_active_connections.incr();
defer lp.metrics.cdp_active_connections.decr();
{
// Transition from .handshake state to .live
// Lock needed even though the main thread hasn't seen this yet because
@@ -551,6 +558,18 @@ test "server: get /json/version" {
}
}
test "server: get /metrics" {
var c = try createTestClient();
defer c.deinit();
const res = try c.httpRequest("GET /metrics HTTP/1.1\r\n\r\n");
std.debug.print("1\n", .{});
try testing.expect(std.mem.startsWith(u8, res, "HTTP/1.1 200 OK\r\n"));
try testing.expect(std.mem.indexOf(u8, res, "Content-Type: text/plain; version=0.0.4") != null);
try testing.expect(std.mem.indexOf(u8, res, "build_info{version=") != null);
try testing.expect(std.mem.indexOf(u8, res, "# TYPE cdp_connections_total counter") != null);
}
fn assertHTTPError(
comptime expected_status: u16,
comptime expected_body: []const u8,
@@ -648,7 +667,7 @@ fn createTestClient() !TestClient {
const TestClient = struct {
stream: std.net.Stream,
buf: [1024]u8 = undefined,
buf: [4096]u8 = undefined,
reader: WS.Reader(false),
const WS = @import("network/WS.zig");
@@ -664,10 +683,11 @@ const TestClient = struct {
var pos: usize = 0;
var total_length: ?usize = null;
while (true) {
pos += try self.stream.read(self.buf[pos..]);
if (pos == 0) {
return error.NoMoreData;
const n = try self.stream.read(self.buf[pos..]);
if (n == 0) {
return if (pos == self.buf.len) error.MessageTooLarge else error.NoMoreData;
}
pos += n;
const response = self.buf[0..pos];
if (total_length == null) {
const header_end = std.mem.indexOf(u8, response, "\r\n\r\n") orelse continue;