Skip to content

Commit a40a341

Browse files
committed
feat: working metrics
1 parent 410ee14 commit a40a341

22 files changed

+826
-48
lines changed

.cspell.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"eqalpha",
1010
"esbuild",
1111
"formbody",
12+
"fsreqcallback",
1213
"gjuchault",
1314
"healthcheck",
1415
"isready",
@@ -23,8 +24,10 @@
2324
"ryansonshine",
2425
"slonik",
2526
"socio",
27+
"tcpsocketwrap",
2628
"timestamptz",
2729
"tini",
30+
"tlswrap",
2831
"traceparent",
2932
"tsdoc",
3033
"typescript-service-starter",

docker-compose.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ services:
2929
HTTP_ADDRESS: 0.0.0.0
3030
HTTP_PORT: 3000
3131
OTLP_TRACE_ENDPOINT: http://jaeger:4318
32+
OTLP_METRICS_ENDPOINT: http://prometheus:9090
3233

3334
postgres:
3435
container_name: postgres
@@ -62,6 +63,10 @@ services:
6263

6364
prometheus:
6465
image: prom/prometheus
66+
container_name: prometheus
67+
command:
68+
- --config.file=/etc/prometheus/prometheus.yml
69+
- --web.enable-otlp-receiver
6570
volumes:
6671
- ./prometheus.yml:/etc/prometheus/prometheus.yml
6772
ports:
@@ -76,6 +81,7 @@ services:
7681

7782
jaeger:
7883
image: jaegertracing/jaeger
84+
container_name: jaeger
7985
ports:
8086
- 16686:16686 # web ui
8187
- 4317:4317 # accept OpenTelemetry Protocol (OTLP) over gRPC

otel-collector-config.yml

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/infrastructure/http-server/http-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import rateLimit from "@fastify/rate-limit";
1111
import session from "@fastify/session";
1212
import swagger from "@fastify/swagger";
1313
import underPressure from "@fastify/under-pressure";
14-
import { context, propagation, SpanKind, trace } from "@opentelemetry/api";
14+
import { SpanKind, context, propagation } from "@opentelemetry/api";
1515
import { RedisStore } from "connect-redis";
1616
import {
1717
type FastifyBaseLogger,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { monitorEventLoopDelay } from "node:perf_hooks";
2+
import type { Meter } from "@opentelemetry/api";
3+
import type { MonitorOptions } from "./index.ts";
4+
5+
export function monitorEventLoopLag(
6+
nodeMetrics: Meter,
7+
{ prefix, labels, eventLoopMonitoringPrecision }: MonitorOptions,
8+
): void {
9+
const nodejsEventLoopLag = "nodejs_eventloop_lag_seconds";
10+
const nodejsEventLoopLagMin = "nodejs_eventloop_lag_min_seconds";
11+
const nodejsEventLoopLagMax = "nodejs_eventloop_lag_max_seconds";
12+
const nodejsEventLoopLagMean = "nodejs_eventloop_lag_mean_seconds";
13+
const nodejsEventLoopLagStddev = "nodejs_eventloop_lag_stddev_seconds";
14+
const nodejsEventLoopLagP50 = "nodejs_eventloop_lag_p50_seconds";
15+
const nodejsEventLoopLagP90 = "nodejs_eventloop_lag_p90_seconds";
16+
const nodejsEventLoopLagP99 = "nodejs_eventloop_lag_p99_seconds";
17+
18+
const eventLoopDelayMonitor = monitorEventLoopDelay({
19+
resolution: eventLoopMonitoringPrecision,
20+
});
21+
eventLoopDelayMonitor.enable();
22+
23+
nodeMetrics
24+
.createObservableGauge(prefix + nodejsEventLoopLag, {
25+
description: "Lag of event loop in seconds.",
26+
})
27+
.addCallback(async (observable) => {
28+
const startTime = process.hrtime();
29+
await new Promise((resolve) => setImmediate(() => resolve(undefined)));
30+
const delta = process.hrtime(startTime);
31+
const seconds = delta[0] + delta[1] / 1e9;
32+
observable.observe(seconds, labels);
33+
});
34+
35+
nodeMetrics
36+
.createObservableGauge(prefix + nodejsEventLoopLagMin, {
37+
description: "The minimum recorded event loop delay.",
38+
})
39+
.addCallback((observable) => {
40+
observable.observe(eventLoopDelayMonitor.min / 1e9, labels);
41+
});
42+
43+
nodeMetrics
44+
.createObservableGauge(prefix + nodejsEventLoopLagMax, {
45+
description: "The maximum recorded event loop delay.",
46+
})
47+
.addCallback((observable) => {
48+
observable.observe(eventLoopDelayMonitor.max / 1e9, labels);
49+
});
50+
51+
nodeMetrics
52+
.createObservableGauge(prefix + nodejsEventLoopLagMean, {
53+
description: "The mean of the recorded event loop delays.",
54+
})
55+
.addCallback((observable) => {
56+
observable.observe(eventLoopDelayMonitor.mean / 1e9, labels);
57+
});
58+
59+
nodeMetrics
60+
.createObservableGauge(prefix + nodejsEventLoopLagStddev, {
61+
description: "The standard deviation of the recorded event loop delays.",
62+
})
63+
.addCallback((observable) => {
64+
observable.observe(eventLoopDelayMonitor.stddev / 1e9, labels);
65+
});
66+
67+
nodeMetrics
68+
.createObservableGauge(prefix + nodejsEventLoopLagP50, {
69+
description: "The 50th percentile of the recorded event loop delays.",
70+
})
71+
.addCallback((observable) => {
72+
observable.observe(eventLoopDelayMonitor.percentile(50) / 1e9, labels);
73+
});
74+
75+
nodeMetrics
76+
.createObservableGauge(prefix + nodejsEventLoopLagP90, {
77+
description: "The 90th percentile of the recorded event loop delays.",
78+
})
79+
.addCallback((observable) => {
80+
observable.observe(eventLoopDelayMonitor.percentile(90) / 1e9, labels);
81+
});
82+
83+
nodeMetrics
84+
.createObservableGauge(prefix + nodejsEventLoopLagP99, {
85+
description: "The 99th percentile of the recorded event loop delays.",
86+
})
87+
.addCallback((observable) => {
88+
observable.observe(eventLoopDelayMonitor.percentile(99) / 1e9, labels);
89+
});
90+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { constants, PerformanceObserver } from "node:perf_hooks";
2+
import type { Meter } from "@opentelemetry/api";
3+
import type { MonitorOptions } from "./index.ts";
4+
5+
export function monitorGc(
6+
meter: Meter,
7+
{ prefix, labels }: MonitorOptions,
8+
): void {
9+
const nodejsGcDurationSeconds = "nodejs_gc_duration_seconds";
10+
11+
const histogram = meter.createHistogram(prefix + nodejsGcDurationSeconds, {
12+
description:
13+
"Garbage collection duration by kind, one of major, minor, incremental or weakcb.",
14+
});
15+
16+
const kinds: Record<number, Record<string, string>> = {};
17+
kinds[constants.NODE_PERFORMANCE_GC_MAJOR] = { ...labels, kind: "major" };
18+
kinds[constants.NODE_PERFORMANCE_GC_MINOR] = { ...labels, kind: "minor" };
19+
kinds[constants.NODE_PERFORMANCE_GC_INCREMENTAL] = {
20+
...labels,
21+
kind: "incremental",
22+
};
23+
kinds[constants.NODE_PERFORMANCE_GC_WEAKCB] = { ...labels, kind: "weakcb" };
24+
25+
const obs = new PerformanceObserver((list) => {
26+
const entry = list.getEntries().at(0);
27+
28+
if (
29+
entry === undefined ||
30+
!(
31+
typeof entry.detail === "object" &&
32+
entry.detail !== null &&
33+
"kind" in entry.detail &&
34+
typeof entry.detail.kind === "number"
35+
)
36+
) {
37+
return;
38+
}
39+
40+
// Convert duration from milliseconds to seconds
41+
histogram.record(entry.duration / 1000, kinds[entry.detail.kind]);
42+
});
43+
44+
// We do not expect too many gc events per second, so we do not use buffering
45+
obs.observe({ entryTypes: ["gc"], buffered: false });
46+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Meter } from "@opentelemetry/api";
2+
import { safeMemoryUsage } from "./helpers/safe-memory-usage.ts";
3+
import type { MonitorOptions } from "./index.ts";
4+
5+
export function monitorHeapSizeAndUsed(
6+
meter: Meter,
7+
{ prefix, labels }: MonitorOptions,
8+
): void {
9+
const nodejsHeapSizeTotal = "nodejs_heap_size_total_bytes";
10+
const nodejsHeapSizeUsed = "nodejs_heap_size_used_bytes";
11+
const nodejsExternalMemory = "nodejs_external_memory_bytes";
12+
13+
let stats: NodeJS.MemoryUsage | undefined;
14+
function getStats() {
15+
if (stats !== undefined) {
16+
return stats;
17+
}
18+
19+
stats = safeMemoryUsage() ?? undefined;
20+
21+
setTimeout(() => {
22+
stats = undefined;
23+
}, 1000).unref();
24+
25+
return stats;
26+
}
27+
28+
meter
29+
.createObservableGauge(prefix + nodejsHeapSizeTotal, {
30+
description: "Process heap size from Node.js in bytes.",
31+
})
32+
.addCallback((observable) => {
33+
getStats();
34+
35+
if (stats?.heapTotal !== undefined) {
36+
observable.observe(stats.heapTotal, labels);
37+
}
38+
});
39+
40+
meter
41+
.createObservableGauge(prefix + nodejsHeapSizeUsed, {
42+
description: "Process heap size used from Node.js in bytes.",
43+
})
44+
.addCallback((observable) => {
45+
getStats();
46+
47+
if (stats?.heapTotal !== undefined) {
48+
observable.observe(stats.heapUsed, labels);
49+
}
50+
});
51+
52+
meter
53+
.createObservableGauge(prefix + nodejsExternalMemory, {
54+
description: "Node.js external memory size in bytes.",
55+
})
56+
.addCallback((observable) => {
57+
getStats();
58+
59+
if (stats?.external !== undefined) {
60+
observable.observe(stats.external, labels);
61+
}
62+
});
63+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import v8 from "node:v8";
2+
import type { Meter } from "@opentelemetry/api";
3+
import type { MonitorOptions } from "./index.ts";
4+
5+
const nodeJsHeapSize = {
6+
total: "nodejs_heap_space_size_total_bytes",
7+
used: "nodejs_heap_space_size_used_bytes",
8+
available: "nodejs_heap_space_size_available_bytes",
9+
};
10+
11+
const spaceRegex = /_space$/;
12+
13+
export function monitorHeapSpacesSizeAndUsed(
14+
meter: Meter,
15+
{ prefix, labels }: MonitorOptions,
16+
): void {
17+
const labelsBySpace: Record<
18+
string,
19+
Record<"total" | "used" | "available", Record<string, string>>
20+
> = {};
21+
22+
let stats:
23+
| {
24+
total: { value: number; labels: Record<string, string> };
25+
used: { value: number; labels: Record<string, string> };
26+
available: { value: number; labels: Record<string, string> };
27+
}[]
28+
| undefined;
29+
function getStats() {
30+
if (stats !== undefined) {
31+
return stats;
32+
}
33+
34+
stats = v8.getHeapSpaceStatistics().map((space) => {
35+
const spaceLabels =
36+
labelsBySpace[space.space_name] ?? defaultLabelsBySpace(space, labels);
37+
labelsBySpace[space.space_name] = spaceLabels;
38+
39+
return {
40+
total: { value: space.space_size, labels: spaceLabels.total },
41+
used: { value: space.space_used_size, labels: spaceLabels.used },
42+
available: {
43+
value: space.space_available_size,
44+
labels: spaceLabels.available,
45+
},
46+
};
47+
});
48+
49+
setTimeout(() => {
50+
stats = undefined;
51+
}, 1000).unref();
52+
return stats;
53+
}
54+
55+
meter
56+
.createObservableGauge(prefix + nodeJsHeapSize.total, {
57+
description: "Process heap space size total from Node.js in bytes.",
58+
})
59+
.addCallback((observable) => {
60+
getStats();
61+
62+
for (const stat of stats ?? []) {
63+
observable.observe(stat.total.value, stat.total.labels);
64+
}
65+
});
66+
67+
meter
68+
.createObservableGauge(prefix + nodeJsHeapSize.used, {
69+
description: "Process heap space size used from Node.js in bytes.",
70+
})
71+
.addCallback((observable) => {
72+
getStats();
73+
74+
for (const stat of stats ?? []) {
75+
observable.observe(stat.used.value, stat.used.labels);
76+
}
77+
});
78+
79+
meter
80+
.createObservableGauge(prefix + nodeJsHeapSize.available, {
81+
description: "Process heap space size available from Node.js in bytes.",
82+
})
83+
.addCallback((observable) => {
84+
getStats();
85+
86+
for (const stat of stats ?? []) {
87+
observable.observe(stat.available.value, stat.available.labels);
88+
}
89+
});
90+
}
91+
92+
function defaultLabelsBySpace(
93+
space: v8.HeapSpaceInfo,
94+
labels: Record<string, string>,
95+
): Record<"total" | "used" | "available", Record<string, string>> {
96+
const spaceName = space.space_name.replace(spaceRegex, "");
97+
98+
return {
99+
total: { ...labels, space: spaceName },
100+
used: { ...labels, space: spaceName },
101+
available: { ...labels, space: spaceName },
102+
};
103+
}

0 commit comments

Comments
 (0)