@spinajs/telemetry 2.0.482
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +482 -0
- package/lib/cjs/InMemoryPerfSink.d.ts +51 -0
- package/lib/cjs/InMemoryPerfSink.d.ts.map +1 -0
- package/lib/cjs/InMemoryPerfSink.js +100 -0
- package/lib/cjs/InMemoryPerfSink.js.map +1 -0
- package/lib/cjs/PromMetricSink.d.ts +26 -0
- package/lib/cjs/PromMetricSink.d.ts.map +1 -0
- package/lib/cjs/PromMetricSink.js +68 -0
- package/lib/cjs/PromMetricSink.js.map +1 -0
- package/lib/cjs/bootstrap.d.ts +13 -0
- package/lib/cjs/bootstrap.d.ts.map +1 -0
- package/lib/cjs/bootstrap.js +57 -0
- package/lib/cjs/bootstrap.js.map +1 -0
- package/lib/cjs/config/telemetry.d.ts +63 -0
- package/lib/cjs/config/telemetry.d.ts.map +1 -0
- package/lib/cjs/config/telemetry.js +78 -0
- package/lib/cjs/config/telemetry.js.map +1 -0
- package/lib/cjs/controllers/Metrics.d.ts +26 -0
- package/lib/cjs/controllers/Metrics.d.ts.map +1 -0
- package/lib/cjs/controllers/Metrics.js +63 -0
- package/lib/cjs/controllers/Metrics.js.map +1 -0
- package/lib/cjs/controllers/Telemetry.d.ts +92 -0
- package/lib/cjs/controllers/Telemetry.d.ts.map +1 -0
- package/lib/cjs/controllers/Telemetry.js +226 -0
- package/lib/cjs/controllers/Telemetry.js.map +1 -0
- package/lib/cjs/dto/index.d.ts +21 -0
- package/lib/cjs/dto/index.d.ts.map +1 -0
- package/lib/cjs/dto/index.js +64 -0
- package/lib/cjs/dto/index.js.map +1 -0
- package/lib/cjs/endpoints.d.ts +32 -0
- package/lib/cjs/endpoints.d.ts.map +1 -0
- package/lib/cjs/endpoints.js +38 -0
- package/lib/cjs/endpoints.js.map +1 -0
- package/lib/cjs/health.d.ts +55 -0
- package/lib/cjs/health.d.ts.map +1 -0
- package/lib/cjs/health.js +111 -0
- package/lib/cjs/health.js.map +1 -0
- package/lib/cjs/index.d.ts +16 -0
- package/lib/cjs/index.d.ts.map +1 -0
- package/lib/cjs/index.js +32 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/metrics.d.ts +16 -0
- package/lib/cjs/metrics.d.ts.map +1 -0
- package/lib/cjs/metrics.js +32 -0
- package/lib/cjs/metrics.js.map +1 -0
- package/lib/cjs/middleware.d.ts +81 -0
- package/lib/cjs/middleware.d.ts.map +1 -0
- package/lib/cjs/middleware.js +210 -0
- package/lib/cjs/middleware.js.map +1 -0
- package/lib/cjs/package.json +1 -0
- package/lib/cjs/policies/PublicPolicy.d.ts +16 -0
- package/lib/cjs/policies/PublicPolicy.d.ts.map +1 -0
- package/lib/cjs/policies/PublicPolicy.js +34 -0
- package/lib/cjs/policies/PublicPolicy.js.map +1 -0
- package/lib/cjs/policies/TelemetryTokenPolicy.d.ts +19 -0
- package/lib/cjs/policies/TelemetryTokenPolicy.d.ts.map +1 -0
- package/lib/cjs/policies/TelemetryTokenPolicy.js +65 -0
- package/lib/cjs/policies/TelemetryTokenPolicy.js.map +1 -0
- package/lib/cjs/requestStats.d.ts +81 -0
- package/lib/cjs/requestStats.d.ts.map +1 -0
- package/lib/cjs/requestStats.js +134 -0
- package/lib/cjs/requestStats.js.map +1 -0
- package/lib/cjs/responses.d.ts +23 -0
- package/lib/cjs/responses.d.ts.map +1 -0
- package/lib/cjs/responses.js +35 -0
- package/lib/cjs/responses.js.map +1 -0
- package/lib/cjs/routeStats.d.ts +43 -0
- package/lib/cjs/routeStats.d.ts.map +1 -0
- package/lib/cjs/routeStats.js +59 -0
- package/lib/cjs/routeStats.js.map +1 -0
- package/lib/cjs/schemas/HealthResponse.schema.d.ts +30 -0
- package/lib/cjs/schemas/HealthResponse.schema.d.ts.map +1 -0
- package/lib/cjs/schemas/HealthResponse.schema.js +15 -0
- package/lib/cjs/schemas/HealthResponse.schema.js.map +1 -0
- package/lib/cjs/schemas/PerfResponse.schema.d.ts +56 -0
- package/lib/cjs/schemas/PerfResponse.schema.d.ts.map +1 -0
- package/lib/cjs/schemas/PerfResponse.schema.js +36 -0
- package/lib/cjs/schemas/PerfResponse.schema.js.map +1 -0
- package/lib/cjs/schemas/ReadyResponse.schema.d.ts +36 -0
- package/lib/cjs/schemas/ReadyResponse.schema.d.ts.map +1 -0
- package/lib/cjs/schemas/ReadyResponse.schema.js +23 -0
- package/lib/cjs/schemas/ReadyResponse.schema.js.map +1 -0
- package/lib/cjs/schemas/RequestStatsSnapshot.schema.d.ts +79 -0
- package/lib/cjs/schemas/RequestStatsSnapshot.schema.d.ts.map +1 -0
- package/lib/cjs/schemas/RequestStatsSnapshot.schema.js +30 -0
- package/lib/cjs/schemas/RequestStatsSnapshot.schema.js.map +1 -0
- package/lib/cjs/schemas/RoutesResponse.schema.d.ts +100 -0
- package/lib/cjs/schemas/RoutesResponse.schema.d.ts.map +1 -0
- package/lib/cjs/schemas/RoutesResponse.schema.js +25 -0
- package/lib/cjs/schemas/RoutesResponse.schema.js.map +1 -0
- package/lib/cjs/schemas/StatsResponse.schema.d.ts +158 -0
- package/lib/cjs/schemas/StatsResponse.schema.d.ts.map +1 -0
- package/lib/cjs/schemas/StatsResponse.schema.js +19 -0
- package/lib/cjs/schemas/StatsResponse.schema.js.map +1 -0
- package/lib/cjs/schemas/TimelineResponse.schema.d.ts +109 -0
- package/lib/cjs/schemas/TimelineResponse.schema.d.ts.map +1 -0
- package/lib/cjs/schemas/TimelineResponse.schema.js +28 -0
- package/lib/cjs/schemas/TimelineResponse.schema.js.map +1 -0
- package/lib/cjs/store.d.ts +58 -0
- package/lib/cjs/store.d.ts.map +1 -0
- package/lib/cjs/store.js +108 -0
- package/lib/cjs/store.js.map +1 -0
- package/lib/cjs/timeline.d.ts +48 -0
- package/lib/cjs/timeline.d.ts.map +1 -0
- package/lib/cjs/timeline.js +70 -0
- package/lib/cjs/timeline.js.map +1 -0
- package/lib/mjs/InMemoryPerfSink.d.ts +51 -0
- package/lib/mjs/InMemoryPerfSink.d.ts.map +1 -0
- package/lib/mjs/InMemoryPerfSink.js +97 -0
- package/lib/mjs/InMemoryPerfSink.js.map +1 -0
- package/lib/mjs/PromMetricSink.d.ts +26 -0
- package/lib/mjs/PromMetricSink.d.ts.map +1 -0
- package/lib/mjs/PromMetricSink.js +65 -0
- package/lib/mjs/PromMetricSink.js.map +1 -0
- package/lib/mjs/bootstrap.d.ts +13 -0
- package/lib/mjs/bootstrap.d.ts.map +1 -0
- package/lib/mjs/bootstrap.js +54 -0
- package/lib/mjs/bootstrap.js.map +1 -0
- package/lib/mjs/config/telemetry.d.ts +63 -0
- package/lib/mjs/config/telemetry.d.ts.map +1 -0
- package/lib/mjs/config/telemetry.js +76 -0
- package/lib/mjs/config/telemetry.js.map +1 -0
- package/lib/mjs/controllers/Metrics.d.ts +26 -0
- package/lib/mjs/controllers/Metrics.d.ts.map +1 -0
- package/lib/mjs/controllers/Metrics.js +60 -0
- package/lib/mjs/controllers/Metrics.js.map +1 -0
- package/lib/mjs/controllers/Telemetry.d.ts +92 -0
- package/lib/mjs/controllers/Telemetry.d.ts.map +1 -0
- package/lib/mjs/controllers/Telemetry.js +223 -0
- package/lib/mjs/controllers/Telemetry.js.map +1 -0
- package/lib/mjs/dto/index.d.ts +21 -0
- package/lib/mjs/dto/index.d.ts.map +1 -0
- package/lib/mjs/dto/index.js +58 -0
- package/lib/mjs/dto/index.js.map +1 -0
- package/lib/mjs/endpoints.d.ts +32 -0
- package/lib/mjs/endpoints.d.ts.map +1 -0
- package/lib/mjs/endpoints.js +34 -0
- package/lib/mjs/endpoints.js.map +1 -0
- package/lib/mjs/health.d.ts +55 -0
- package/lib/mjs/health.d.ts.map +1 -0
- package/lib/mjs/health.js +107 -0
- package/lib/mjs/health.js.map +1 -0
- package/lib/mjs/index.d.ts +16 -0
- package/lib/mjs/index.d.ts.map +1 -0
- package/lib/mjs/index.js +16 -0
- package/lib/mjs/index.js.map +1 -0
- package/lib/mjs/metrics.d.ts +16 -0
- package/lib/mjs/metrics.d.ts.map +1 -0
- package/lib/mjs/metrics.js +16 -0
- package/lib/mjs/metrics.js.map +1 -0
- package/lib/mjs/middleware.d.ts +81 -0
- package/lib/mjs/middleware.d.ts.map +1 -0
- package/lib/mjs/middleware.js +207 -0
- package/lib/mjs/middleware.js.map +1 -0
- package/lib/mjs/package.json +1 -0
- package/lib/mjs/policies/PublicPolicy.d.ts +16 -0
- package/lib/mjs/policies/PublicPolicy.d.ts.map +1 -0
- package/lib/mjs/policies/PublicPolicy.js +31 -0
- package/lib/mjs/policies/PublicPolicy.js.map +1 -0
- package/lib/mjs/policies/TelemetryTokenPolicy.d.ts +19 -0
- package/lib/mjs/policies/TelemetryTokenPolicy.d.ts.map +1 -0
- package/lib/mjs/policies/TelemetryTokenPolicy.js +62 -0
- package/lib/mjs/policies/TelemetryTokenPolicy.js.map +1 -0
- package/lib/mjs/requestStats.d.ts +81 -0
- package/lib/mjs/requestStats.d.ts.map +1 -0
- package/lib/mjs/requestStats.js +130 -0
- package/lib/mjs/requestStats.js.map +1 -0
- package/lib/mjs/responses.d.ts +23 -0
- package/lib/mjs/responses.d.ts.map +1 -0
- package/lib/mjs/responses.js +30 -0
- package/lib/mjs/responses.js.map +1 -0
- package/lib/mjs/routeStats.d.ts +43 -0
- package/lib/mjs/routeStats.d.ts.map +1 -0
- package/lib/mjs/routeStats.js +55 -0
- package/lib/mjs/routeStats.js.map +1 -0
- package/lib/mjs/schemas/HealthResponse.schema.d.ts +30 -0
- package/lib/mjs/schemas/HealthResponse.schema.d.ts.map +1 -0
- package/lib/mjs/schemas/HealthResponse.schema.js +13 -0
- package/lib/mjs/schemas/HealthResponse.schema.js.map +1 -0
- package/lib/mjs/schemas/PerfResponse.schema.d.ts +56 -0
- package/lib/mjs/schemas/PerfResponse.schema.d.ts.map +1 -0
- package/lib/mjs/schemas/PerfResponse.schema.js +34 -0
- package/lib/mjs/schemas/PerfResponse.schema.js.map +1 -0
- package/lib/mjs/schemas/ReadyResponse.schema.d.ts +36 -0
- package/lib/mjs/schemas/ReadyResponse.schema.d.ts.map +1 -0
- package/lib/mjs/schemas/ReadyResponse.schema.js +21 -0
- package/lib/mjs/schemas/ReadyResponse.schema.js.map +1 -0
- package/lib/mjs/schemas/RequestStatsSnapshot.schema.d.ts +79 -0
- package/lib/mjs/schemas/RequestStatsSnapshot.schema.d.ts.map +1 -0
- package/lib/mjs/schemas/RequestStatsSnapshot.schema.js +28 -0
- package/lib/mjs/schemas/RequestStatsSnapshot.schema.js.map +1 -0
- package/lib/mjs/schemas/RoutesResponse.schema.d.ts +100 -0
- package/lib/mjs/schemas/RoutesResponse.schema.d.ts.map +1 -0
- package/lib/mjs/schemas/RoutesResponse.schema.js +20 -0
- package/lib/mjs/schemas/RoutesResponse.schema.js.map +1 -0
- package/lib/mjs/schemas/StatsResponse.schema.d.ts +158 -0
- package/lib/mjs/schemas/StatsResponse.schema.d.ts.map +1 -0
- package/lib/mjs/schemas/StatsResponse.schema.js +14 -0
- package/lib/mjs/schemas/StatsResponse.schema.js.map +1 -0
- package/lib/mjs/schemas/TimelineResponse.schema.d.ts +109 -0
- package/lib/mjs/schemas/TimelineResponse.schema.d.ts.map +1 -0
- package/lib/mjs/schemas/TimelineResponse.schema.js +23 -0
- package/lib/mjs/schemas/TimelineResponse.schema.js.map +1 -0
- package/lib/mjs/store.d.ts +58 -0
- package/lib/mjs/store.d.ts.map +1 -0
- package/lib/mjs/store.js +105 -0
- package/lib/mjs/store.js.map +1 -0
- package/lib/mjs/timeline.d.ts +48 -0
- package/lib/mjs/timeline.d.ts.map +1 -0
- package/lib/mjs/timeline.js +66 -0
- package/lib/mjs/timeline.js.map +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -0
- package/lib/tsconfig.mjs.tsbuildinfo +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# `@spinajs/telemetry`
|
|
2
|
+
|
|
3
|
+
Observability for SpinaJS, derived from the swagger-stats design but adapted to
|
|
4
|
+
SpinaJS's DI + middleware conventions. It provides:
|
|
5
|
+
|
|
6
|
+
- **`Metrics`** — a `@Singleton()` wrapper over a private `prom-client` `Registry`
|
|
7
|
+
(isolated from the global default registry) with a declarative metric factory
|
|
8
|
+
(`defineMetrics`), default process metrics (`collectDefault`), and async
|
|
9
|
+
Prometheus text rendering (`render` / `contentType`).
|
|
10
|
+
- **`TelemetryMiddleware`** — an `@Injectable(ServerMiddleware)` that times every
|
|
11
|
+
HTTP request and records a duration **histogram**, a request **counter**, and
|
|
12
|
+
an in-flight **gauge** (keyed by `method` / `route` / `status`), and feeds the
|
|
13
|
+
shared `TelemetryStore`. All of it runs from a `res.on('finish')` handler, and
|
|
14
|
+
telemetry errors never break the request.
|
|
15
|
+
- **`TelemetryStore`** — the `@Singleton()` that owns the collected aggregates
|
|
16
|
+
(`RequestStats`, `Timeline`, `RouteStats`). The middleware is its only writer;
|
|
17
|
+
the JSON endpoints are readers. It exists because a `ServerMiddleware` is not a
|
|
18
|
+
singleton, so writer and reader cannot be the same object.
|
|
19
|
+
- **`PromMetricSink`** — an `@Injectable(PerfSink)` that bridges the
|
|
20
|
+
`@spinajs/log` **`Perf`** facade to Prometheus: every `Perf.measure` /
|
|
21
|
+
`@Measure` / `Perf.count` measurement (including the ORM `orm.query` spans and
|
|
22
|
+
HTTP per-request rollups) is exported as a `perf_*` metric with **no extra
|
|
23
|
+
wiring**.
|
|
24
|
+
- **`RequestStats`** — pure status-class counters (1xx..5xx), error rate, request
|
|
25
|
+
rate, response-time min/max/avg, and **Apdex**.
|
|
26
|
+
- **`Timeline`** — a rolling ring of `RequestStats` buckets (default 60 x 1 min).
|
|
27
|
+
- **`RouteStats`** — a bounded per method+route breakdown.
|
|
28
|
+
- **controllers** — `/metrics` (Prometheus exposition) and a `/telemetry/*` JSON
|
|
29
|
+
API, each guarded by a policy named in configuration. Registered automatically;
|
|
30
|
+
the response DTOs carry schemas, so they show up in the generated OpenAPI
|
|
31
|
+
document when `@spinajs/http-swagger` is installed.
|
|
32
|
+
- **`HealthCheck`** — the abstract readiness probe backing `/telemetry/ready`.
|
|
33
|
+
The package ships no concrete checks; you register your own.
|
|
34
|
+
- **endpoint handlers** — `metricsHandler(metrics)` and `statsHandler(store)`,
|
|
35
|
+
plain `(req, res)` handlers for apps that do not use the SpinaJS controller
|
|
36
|
+
stack. Not needed otherwise; the controllers above cover it.
|
|
37
|
+
|
|
38
|
+
The registry is **isolated** (not `prom-client`'s global default), so multiple
|
|
39
|
+
SpinaJS apps in one process — and repeated init in tests — stay independent.
|
|
40
|
+
|
|
41
|
+
> Replacing `@spinajs/metrics`? Read
|
|
42
|
+
> [`docs/migrations/2026-07-23-metrics-to-telemetry.md`](../../docs/migrations/2026-07-23-metrics-to-telemetry.md)
|
|
43
|
+
> first — the http metric series are renamed **and** their unit changed from
|
|
44
|
+
> seconds to milliseconds, which takes dashboards down silently.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
Install the package. The controllers, middleware, perf bridge and default
|
|
51
|
+
process metrics are wired automatically; set the auth token and you are done.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// configuration
|
|
55
|
+
{
|
|
56
|
+
telemetry: {
|
|
57
|
+
auth: { token: process.env.METRICS_TOKEN },
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Endpoint | Returns | Default access |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `GET /metrics` | Prometheus exposition text | token |
|
|
65
|
+
| `GET /telemetry/stats` | `{ all, timeline }` — lifetime stats + rolling timeline | token |
|
|
66
|
+
| `GET /telemetry/timeline?buckets=N` | The timeline alone, buckets annotated with their window | token |
|
|
67
|
+
| `GET /telemetry/routes` | Per method+route request breakdown | token |
|
|
68
|
+
| `GET /telemetry/perf` | Aggregated `Perf` spans and events | token |
|
|
69
|
+
| `GET /telemetry/health` | Liveness — uptime, pid, node version, optional app version | public |
|
|
70
|
+
| `GET /telemetry/ready` | Readiness — runs every registered `HealthCheck`, 503 when any is down | public |
|
|
71
|
+
|
|
72
|
+
Guarded endpoints expect the token on the `x-metrics-token` header:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -H "x-metrics-token: $METRICS_TOKEN" http://localhost:8080/metrics
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`TelemetryTokenPolicy` is bypassed entirely when `configuration.isDevelopment`
|
|
79
|
+
is set, so a local run needs no token at all.
|
|
80
|
+
|
|
81
|
+
Every endpoint's policy is a config key, so access can be changed without code:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
telemetry: {
|
|
85
|
+
auth: {
|
|
86
|
+
policies: {
|
|
87
|
+
metrics: 'TelemetryTokenPolicy',
|
|
88
|
+
health: 'PublicPolicy', // probes cannot carry a token
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`/telemetry/health` and `/telemetry/ready` are public by default because kubelet
|
|
95
|
+
probes and load balancers cannot send a header. They expose uptime, pid, the node
|
|
96
|
+
version and — if you set `telemetry.health.version` — the app version. Point them
|
|
97
|
+
at `TelemetryTokenPolicy` if that is more than you want to publish.
|
|
98
|
+
|
|
99
|
+
### Readiness checks
|
|
100
|
+
|
|
101
|
+
Telemetry ships no concrete checks — a database check belongs where the database
|
|
102
|
+
dependency already is. Register your own:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { Injectable } from '@spinajs/di';
|
|
106
|
+
import { HealthCheck, IHealthResult } from '@spinajs/telemetry';
|
|
107
|
+
|
|
108
|
+
@Injectable(HealthCheck)
|
|
109
|
+
export class DatabaseCheck extends HealthCheck {
|
|
110
|
+
public Name = 'database';
|
|
111
|
+
|
|
112
|
+
public async check(): Promise<IHealthResult> {
|
|
113
|
+
try {
|
|
114
|
+
await db.raw('select 1');
|
|
115
|
+
return { status: 'up' };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return { status: 'down', message: (err as Error).message };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`status` is `'up' | 'degraded' | 'down'`; a check may also return a `data` bag.
|
|
124
|
+
Each check is raced against `telemetry.health.timeoutMs` ( default 2000 ), so a
|
|
125
|
+
hung dependency cannot hang the probe — a timed-out check, or one that throws,
|
|
126
|
+
counts as `down`. The overall status is the worst of them, and `/ready` answers
|
|
127
|
+
503 when it is `down` ( or `degraded`, with `telemetry.health.failOnDegraded` ).
|
|
128
|
+
|
|
129
|
+
### Mounting on a bare express app
|
|
130
|
+
|
|
131
|
+
`metricsHandler( metrics )` and `statsHandler( store )` are exported for apps
|
|
132
|
+
that do not use the SpinaJS controller stack:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { DI } from '@spinajs/di';
|
|
136
|
+
import { Metrics, TelemetryStore, metricsHandler, statsHandler } from '@spinajs/telemetry';
|
|
137
|
+
|
|
138
|
+
router.get('/metrics', metricsHandler(await DI.resolve(Metrics)));
|
|
139
|
+
router.get('/telemetry/stats', statsHandler(await DI.resolve(TelemetryStore)));
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
These are unguarded — the policy lives on the controllers, so an app wiring the
|
|
143
|
+
handlers by hand owns its own auth. `statsHandler` also reports `req_rate` /
|
|
144
|
+
`err_rate` as whatever the last `/telemetry/stats` controller call left behind
|
|
145
|
+
( `0` if nobody has called it ), because only the controller derives them.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## HTTP request metrics
|
|
150
|
+
|
|
151
|
+
Once `TelemetryMiddleware` is active, every request is timed (with
|
|
152
|
+
`process.hrtime.bigint()`) and recorded against the shared registry:
|
|
153
|
+
|
|
154
|
+
| Series | Type | Labels |
|
|
155
|
+
| --- | --- | --- |
|
|
156
|
+
| `http_requests_total` | counter | `method`, `route`, `status` |
|
|
157
|
+
| `http_request_duration_ms` | histogram | `method`, `route`, `status` |
|
|
158
|
+
| `http_requests_in_flight` | gauge | — |
|
|
159
|
+
|
|
160
|
+
The `route` label prefers the **matched** route path (`req.route.path`) over the
|
|
161
|
+
raw URL, keeping cardinality bounded. The prefix (`http`) and duration buckets
|
|
162
|
+
are **configuration**, not code — do not subclass just to change them:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// configuration
|
|
166
|
+
{
|
|
167
|
+
telemetry: {
|
|
168
|
+
prefix: 'api', // -> api_requests_total, api_request_duration_ms, api_requests_in_flight
|
|
169
|
+
buckets: [5, 25, 100, 500, 2500],
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> **Do not subclass `TelemetryMiddleware` to re-prefix.** Decorating a subclass
|
|
175
|
+
> with `@Injectable(ServerMiddleware)` **adds** a middleware, it does not replace
|
|
176
|
+
> the base one — see [Replacing the middleware](#replacing-the-middleware). You
|
|
177
|
+
> would get `http_*` **and** `api_*` for every request, plus double counts in
|
|
178
|
+
> `/telemetry/stats`.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Custom application metrics
|
|
183
|
+
|
|
184
|
+
Use `Metrics.defineMetrics(prefix, defs)` to declare your own metrics on the same
|
|
185
|
+
isolated registry — they show up in the same `/metrics` scrape. It is
|
|
186
|
+
**idempotent** (a duplicate name is removed and recreated), so re-defining on a
|
|
187
|
+
test re-init won't throw.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { DI } from '@spinajs/di';
|
|
191
|
+
import { Metrics } from '@spinajs/telemetry';
|
|
192
|
+
import type { Counter, Histogram } from 'prom-client';
|
|
193
|
+
|
|
194
|
+
const metrics = await DI.resolve(Metrics);
|
|
195
|
+
|
|
196
|
+
const map = metrics.defineMetrics('orders', [
|
|
197
|
+
{ name: 'created_total', help: 'Orders created', type: 'counter', labelNames: ['channel'] },
|
|
198
|
+
{ name: 'value_eur', help: 'Order value in EUR', type: 'histogram', buckets: [10, 50, 100, 500] },
|
|
199
|
+
{ name: 'pending', help: 'Orders awaiting payment', type: 'gauge' },
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
(map['created_total'] as Counter<string>).inc({ channel: 'web' });
|
|
203
|
+
(map['value_eur'] as Histogram<string>).observe(129.9);
|
|
204
|
+
|
|
205
|
+
// Render on demand ( render() is async in prom-client 14 )
|
|
206
|
+
const exposition = await metrics.render();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`MetricDef.type` is `'counter' | 'gauge' | 'histogram' | 'summary'`; `labelNames`
|
|
210
|
+
is optional, as are `buckets` (histogram only) and `percentiles` (summary only).
|
|
211
|
+
Every name is prefixed with `${prefix}_`, so pass the base name. Keep label
|
|
212
|
+
**values** low-cardinality — never put ids, emails, or raw URLs in a label.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Performance metrics → Prometheus (the perf dual-sink)
|
|
217
|
+
|
|
218
|
+
`@spinajs/log` exposes a `Perf` facade for instrumenting arbitrary code
|
|
219
|
+
(`Perf.measure` / `Perf.start` / `Perf.count` / `Perf.value` and the `@Measure`
|
|
220
|
+
decorator). Each measurement is fanned out to every registered `PerfSink`. This
|
|
221
|
+
package ships **`PromMetricSink`**, so **installing `@spinajs/telemetry` alongside
|
|
222
|
+
`@spinajs/http` automatically exports all of it to Prometheus** — you don't call
|
|
223
|
+
prom-client yourself.
|
|
224
|
+
|
|
225
|
+
### What gets exported
|
|
226
|
+
|
|
227
|
+
| Series | Type | Labels | Source |
|
|
228
|
+
| --- | --- | --- | --- |
|
|
229
|
+
| `perf_span_duration_ms` | histogram | `name` | every `Perf.measure` / `@Measure` / `span.end()` |
|
|
230
|
+
| `perf_events_total` | counter | `name` | every `Perf.count` / `Perf.value` |
|
|
231
|
+
| `perf_scope_total_ms` | histogram | `name` | per-request rollup totals (e.g. total DB time per request) |
|
|
232
|
+
|
|
233
|
+
The `name` label is the measurement name (e.g. `orm.query`, `http.request`, or
|
|
234
|
+
your own). **It must be low-cardinality** — use a fixed vocabulary of names, not
|
|
235
|
+
per-request unique strings.
|
|
236
|
+
|
|
237
|
+
### Instrument your code — it lands in Prometheus for free
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { Perf, Measure } from '@spinajs/log';
|
|
241
|
+
|
|
242
|
+
// wrap a block ( async or sync )
|
|
243
|
+
await Perf.measure('report.build', () => buildReport(customerId));
|
|
244
|
+
// -> perf_span_duration_ms{name="report.build"}
|
|
245
|
+
|
|
246
|
+
// count events
|
|
247
|
+
Perf.count('cache.miss');
|
|
248
|
+
// -> perf_events_total{name="cache.miss"}
|
|
249
|
+
|
|
250
|
+
// decorate a method
|
|
251
|
+
class ReportService {
|
|
252
|
+
@Measure('report.render')
|
|
253
|
+
async render() { /* ... */ }
|
|
254
|
+
// -> perf_span_duration_ms{name="report.render"}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Out of the box you also get, with zero extra code:
|
|
259
|
+
|
|
260
|
+
- **`perf_span_duration_ms{name="orm.query"}`** — every SQL query, timed by the
|
|
261
|
+
ORM (`SqlDriver.execute`).
|
|
262
|
+
- **`perf_scope_total_ms{name="orm.query"}`** — total DB time *per HTTP request*
|
|
263
|
+
(emitted by the HTTP `PerfRollup` middleware at request end).
|
|
264
|
+
|
|
265
|
+
Example Prometheus queries:
|
|
266
|
+
|
|
267
|
+
```promql
|
|
268
|
+
# p95 query latency
|
|
269
|
+
histogram_quantile(0.95, sum by (le) (rate(perf_span_duration_ms_bucket{name="orm.query"}[5m])))
|
|
270
|
+
|
|
271
|
+
# average DB time contributed per request
|
|
272
|
+
rate(perf_scope_total_ms_sum{name="orm.query"}[5m]) / rate(perf_scope_total_ms_count{name="orm.query"}[5m])
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Log thresholds vs. prom export
|
|
276
|
+
|
|
277
|
+
The default **log** sink (`LogMetricSink` in `@spinajs/log`) only writes a line
|
|
278
|
+
when a span exceeds its `logger.perf.thresholds` (slow → `warn`, fast → `trace`).
|
|
279
|
+
`PromMetricSink` is independent — it records **every** measurement regardless of
|
|
280
|
+
threshold, so your histograms are complete. Tune log noise with `logger.perf.*`
|
|
281
|
+
without affecting the metrics.
|
|
282
|
+
|
|
283
|
+
### Registration timing
|
|
284
|
+
|
|
285
|
+
`TelemetryBootstrapper` builds both sinks and calls `Perf.refreshSinks()` at
|
|
286
|
+
startup, so the bridge is live for every app that has this package installed —
|
|
287
|
+
with or without `@spinajs/http`. Nothing to wire.
|
|
288
|
+
|
|
289
|
+
It builds them through `Array.ofType(PerfSink)` rather than
|
|
290
|
+
`DI.resolve(PromMetricSink)`, and that detail is load-bearing: the container
|
|
291
|
+
caches a directly-resolved instance under its own type name only, and a later
|
|
292
|
+
`Array.ofType(PerfSink)` returns that cached instance **without** adding it to
|
|
293
|
+
the `PerfSink` list — leaving `Perf` blind to the sink it just handed back. If
|
|
294
|
+
you add a sink of your own, resolve it the same way.
|
|
295
|
+
|
|
296
|
+
### Write your own sink
|
|
297
|
+
|
|
298
|
+
`PerfSink` is a plain registerable abstract class — add another destination
|
|
299
|
+
(StatsD, OTLP, a DB) without touching the producers. Register it with
|
|
300
|
+
`@Injectable(PerfSink)` and `Perf` fans measurements to it too:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { Injectable } from '@spinajs/di';
|
|
304
|
+
import { PerfSink, IPerfMetric, IPerfRollup } from '@spinajs/log';
|
|
305
|
+
|
|
306
|
+
@Injectable(PerfSink)
|
|
307
|
+
export class StatsdSink extends PerfSink {
|
|
308
|
+
public collect(m: IPerfMetric): void {
|
|
309
|
+
if (m.kind === 'span') statsd.timing(m.name, m.durationMs ?? 0);
|
|
310
|
+
else statsd.increment(m.name, m.value ?? 1);
|
|
311
|
+
}
|
|
312
|
+
public onScopeEnd(rollup: IPerfRollup): void {
|
|
313
|
+
for (const [name, e] of Object.entries(rollup.byName)) statsd.timing(`${name}.request`, e.totalMs);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
A sink's `collect` / `onScopeEnd` must never throw meaningfully — the `Perf`
|
|
319
|
+
facade already guards every call so one bad sink can't break measured code or the
|
|
320
|
+
other sinks.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## JSON stats endpoint
|
|
325
|
+
|
|
326
|
+
`statsHandler(store)` writes `{ all, timeline }` from the shared store's lifetime
|
|
327
|
+
`RequestStats` and rolling `Timeline`:
|
|
328
|
+
|
|
329
|
+
```jsonc
|
|
330
|
+
{
|
|
331
|
+
"all": {
|
|
332
|
+
"requests": 1284, "responses": 1284, "errors": 12,
|
|
333
|
+
"info": 0, "success": 1201, "redirect": 60, "client_error": 11, "server_error": 1,
|
|
334
|
+
"total_time": 48210, "max_time": 812, "min_time": 1, "avg_time": 37.5,
|
|
335
|
+
"apdex_satisfied": 1180, "apdex_tolerated": 40, "apdex_score": 0.94,
|
|
336
|
+
"req_rate": 0, "err_rate": 0
|
|
337
|
+
},
|
|
338
|
+
"timeline": {
|
|
339
|
+
"29014823": { "requests": 42, "responses": 42, "avg_time": 33.1, "apdex_score": 0.95 /* ... */ }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- **`RequestStats`** accumulates status-class counters, response-time
|
|
345
|
+
aggregates, and an **Apdex** score (`(satisfied + tolerated/2) / responses`;
|
|
346
|
+
default satisfied threshold 25 ms, tolerated up to 4×).
|
|
347
|
+
- **`Timeline`** keeps a rolling ring of per-bucket `RequestStats` (default
|
|
348
|
+
60 buckets × 60 s), keyed by `floor(timestamp / bucketMs)`.
|
|
349
|
+
|
|
350
|
+
Both are pure and take the timestamp in, so they're deterministic under test.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Metrics reference
|
|
355
|
+
|
|
356
|
+
| Series | Type | Labels | Emitted by |
|
|
357
|
+
| --- | --- | --- | --- |
|
|
358
|
+
| `http_requests_total` | counter | `method`, `route`, `status` | `TelemetryMiddleware` |
|
|
359
|
+
| `http_request_duration_ms` | histogram | `method`, `route`, `status` | `TelemetryMiddleware` |
|
|
360
|
+
| `http_requests_in_flight` | gauge | — | `TelemetryMiddleware` |
|
|
361
|
+
| `perf_span_duration_ms` | histogram | `name` | `PromMetricSink` (from `Perf.measure`/`@Measure`) |
|
|
362
|
+
| `perf_events_total` | counter | `name` | `PromMetricSink` (from `Perf.count`/`Perf.value`) |
|
|
363
|
+
| `perf_scope_total_ms` | histogram | `name` | `PromMetricSink` (from per-request rollups) |
|
|
364
|
+
| `process_*` / `nodejs_*` | various | — | `metrics.collectDefault()`, called at bootstrap unless `telemetry.collectDefaultMetrics` is `false` |
|
|
365
|
+
|
|
366
|
+
Duration histogram buckets (ms): `http_request_duration_ms` uses
|
|
367
|
+
`DURATION_BUCKETS_MS` = `[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]`;
|
|
368
|
+
the `perf_*` histograms use `PERF_DURATION_BUCKETS_MS` (same, plus a leading `1`).
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Configuration reference
|
|
373
|
+
|
|
374
|
+
| Key | Default | Meaning |
|
|
375
|
+
| --- | --- | --- |
|
|
376
|
+
| `telemetry.auth.token` | `''` | Expected value of the `x-metrics-token` header |
|
|
377
|
+
| `telemetry.auth.policies.<endpoint>` | see below | Policy class name per endpoint |
|
|
378
|
+
| `telemetry.collectDefaultMetrics` | `true` | Register `process_*` / `nodejs_*` metrics at bootstrap |
|
|
379
|
+
| `telemetry.prefix` | `'http'` | Metric name prefix for the http metrics |
|
|
380
|
+
| `telemetry.buckets` | `[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]` | Duration histogram buckets ( ms ) |
|
|
381
|
+
| `telemetry.apdexThresholdMs` | `25` | Apdex satisfied threshold; tolerated is 4x |
|
|
382
|
+
| `telemetry.timeline.length` | `60` | Buckets retained in the timeline ring |
|
|
383
|
+
| `telemetry.timeline.bucketMs` | `60000` | Timeline bucket width ( ms ) |
|
|
384
|
+
| `telemetry.routes.enabled` | `true` | Collect the per-route breakdown |
|
|
385
|
+
| `telemetry.routes.maxEntries` | `500` | Cap on distinct method+route keys |
|
|
386
|
+
| `telemetry.perf.enabled` | `true` | Collect the in-memory perf aggregate behind `/telemetry/perf` |
|
|
387
|
+
| `telemetry.perf.maxNames` | `200` | Cap on distinct perf measurement names |
|
|
388
|
+
| `telemetry.health.timeoutMs` | `2000` | Per-check timeout for `/telemetry/ready` |
|
|
389
|
+
| `telemetry.health.failOnDegraded` | `false` | Serve 503 for a degraded overall status |
|
|
390
|
+
| `telemetry.health.version` | unset | Version string reported by `/telemetry/health`; omitted from the response when unset |
|
|
391
|
+
|
|
392
|
+
The `<endpoint>` keys are `metrics`, `stats`, `timeline`, `routes`, `perf`,
|
|
393
|
+
`health` and `ready`. Policy defaults: `TelemetryTokenPolicy` for `metrics`,
|
|
394
|
+
`stats`, `timeline`, `routes` and `perf`; `PublicPolicy` for `health` and
|
|
395
|
+
`ready`.
|
|
396
|
+
|
|
397
|
+
`telemetry.perf.enabled` and `telemetry.perf.maxNames` bound the **JSON** view
|
|
398
|
+
only ( `InMemoryPerfSink` ). The `perf_*` Prometheus series come from a separate
|
|
399
|
+
sink and are unaffected by either.
|
|
400
|
+
|
|
401
|
+
### Cardinality
|
|
402
|
+
|
|
403
|
+
Both the `route` label and the perf `name` label are meant for bounded
|
|
404
|
+
vocabularies. The `maxEntries` / `maxNames` caps bound the JSON views, but the
|
|
405
|
+
Prometheus histograms have no such cap — the `route` label falls back to the raw
|
|
406
|
+
request path when no route matched, so a 404-scanning bot inflates the series
|
|
407
|
+
count. Put telemetry behind a router that 404s unmatched paths early, or subclass
|
|
408
|
+
`TelemetryMiddleware` and override `routeLabel()` to collapse unmatched requests
|
|
409
|
+
to a constant:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { DI, Injectable } from '@spinajs/di';
|
|
413
|
+
import { ServerMiddleware, Request as sRequest } from '@spinajs/http';
|
|
414
|
+
import { TelemetryMiddleware } from '@spinajs/telemetry';
|
|
415
|
+
|
|
416
|
+
@Injectable(ServerMiddleware)
|
|
417
|
+
export class BoundedTelemetry extends TelemetryMiddleware {
|
|
418
|
+
protected routeLabel(req: sRequest): string {
|
|
419
|
+
const matched = (req as any).route?.path ?? (req.storage as any)?.route;
|
|
420
|
+
// anything the router never matched collapses to one series
|
|
421
|
+
return typeof matched === 'string' && matched.length > 0 ? matched : '__unmatched__';
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// REQUIRED — drop the base registration, otherwise BOTH middlewares run.
|
|
426
|
+
// See "Replacing the middleware" below.
|
|
427
|
+
DI.unregister(TelemetryMiddleware);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Replacing the middleware
|
|
431
|
+
|
|
432
|
+
`@Injectable(ServerMiddleware)` **appends** to the `ServerMiddleware` registration
|
|
433
|
+
list — it does not displace the base class. `TelemetryMiddleware` registers itself
|
|
434
|
+
when `@spinajs/telemetry` is imported, so a subclass that carries its own
|
|
435
|
+
`@Injectable(ServerMiddleware)` leaves **two** telemetry middlewares registered.
|
|
436
|
+
`HttpServer` resolves all of them (`@Autoinject(ServerMiddleware)`), so both run
|
|
437
|
+
on every request:
|
|
438
|
+
|
|
439
|
+
- both write to the same singleton `TelemetryStore`, so `/telemetry/stats`,
|
|
440
|
+
`/telemetry/timeline` and `/telemetry/routes` report **double** the real
|
|
441
|
+
counts;
|
|
442
|
+
- both call `ensureMetrics()` with the same prefix, and `defineMetrics()`
|
|
443
|
+
removes-and-recreates a duplicate name, so one of the two instances ends up
|
|
444
|
+
holding **deregistered** metric objects — whose `routeLabel()` reaches the live
|
|
445
|
+
histogram then depends on middleware ordering.
|
|
446
|
+
|
|
447
|
+
Remove the base registration with `DI.unregister()`:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
import { DI } from '@spinajs/di';
|
|
451
|
+
import { TelemetryMiddleware } from '@spinajs/telemetry';
|
|
452
|
+
import { BoundedTelemetry } from './BoundedTelemetry.js'; // must be imported, so its @Injectable has run
|
|
453
|
+
|
|
454
|
+
DI.unregister(TelemetryMiddleware);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Two rules:
|
|
458
|
+
|
|
459
|
+
1. Run it **after** both modules are imported ( the decorators register at import
|
|
460
|
+
time ) and **before** `HttpServer` is resolved — the middleware list is read
|
|
461
|
+
once, when the server is resolved.
|
|
462
|
+
2. `unregister` matches by **type name**, so it removes only
|
|
463
|
+
`TelemetryMiddleware`; your subclass stays registered.
|
|
464
|
+
|
|
465
|
+
If you only need a different prefix or different buckets, do **not** subclass at
|
|
466
|
+
all — use the `telemetry.prefix` / `telemetry.buckets` config keys.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Notes
|
|
471
|
+
|
|
472
|
+
- **Isolated registry.** `Metrics` never touches prom-client's global default
|
|
473
|
+
registry, so tests and multiple apps in one process stay independent. Get the
|
|
474
|
+
raw registry with `metrics.getRegistry()` if you must.
|
|
475
|
+
- **Error-safe.** All middleware telemetry and all sink calls are guarded — a
|
|
476
|
+
telemetry failure can never break a request or the measured code.
|
|
477
|
+
- **Cardinality.** Both the `route` label and the perf `name` label are meant for
|
|
478
|
+
bounded vocabularies. Never emit per-request/per-entity unique label values —
|
|
479
|
+
see [Cardinality](#cardinality) for the unmatched-path case, which is the one
|
|
480
|
+
that bites without you doing anything wrong.
|
|
481
|
+
- **Migrating from `@spinajs/metrics`?** See
|
|
482
|
+
[`docs/migrations/2026-07-23-metrics-to-telemetry.md`](../../docs/migrations/2026-07-23-metrics-to-telemetry.md).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PerfSink, IPerfMetric } from '@spinajs/log';
|
|
2
|
+
/** One aggregated span row. */
|
|
3
|
+
export interface IPerfSpanEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
count: number;
|
|
6
|
+
totalMs: number;
|
|
7
|
+
avgMs: number;
|
|
8
|
+
maxMs: number;
|
|
9
|
+
}
|
|
10
|
+
/** One aggregated counter / value row. */
|
|
11
|
+
export interface IPerfEventEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
count: number;
|
|
14
|
+
total: number;
|
|
15
|
+
}
|
|
16
|
+
/** Serializable snapshot served by `GET /telemetry/perf`. */
|
|
17
|
+
export interface IPerfSnapshot {
|
|
18
|
+
/** True once the name cap was hit and new names stopped being tracked. */
|
|
19
|
+
truncated: boolean;
|
|
20
|
+
spans: IPerfSpanEntry[];
|
|
21
|
+
events: IPerfEventEntry[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A {@link PerfSink} that keeps a bounded, JSON-readable aggregate of every
|
|
25
|
+
* measurement, backing `GET /telemetry/perf`.
|
|
26
|
+
*
|
|
27
|
+
* This exists alongside `PromMetricSink` rather than replacing it: prom
|
|
28
|
+
* histograms are the right shape for Prometheus but their bucket counts cannot
|
|
29
|
+
* be read back as a mean or a max, which is what a JSON snapshot needs.
|
|
30
|
+
*
|
|
31
|
+
* Per-request rollups are intentionally NOT implemented here — `onScopeEnd`
|
|
32
|
+
* totals share the span names, so folding them in would double-count against
|
|
33
|
+
* the same rows. The per-request view lives in `perf_scope_total_ms` on the
|
|
34
|
+
* prometheus side.
|
|
35
|
+
*/
|
|
36
|
+
export declare class InMemoryPerfSink extends PerfSink {
|
|
37
|
+
protected Enabled: boolean;
|
|
38
|
+
protected MaxNames: number;
|
|
39
|
+
private spans;
|
|
40
|
+
private events;
|
|
41
|
+
private truncated;
|
|
42
|
+
collect(metric: IPerfMetric): void;
|
|
43
|
+
toJSON(): IPerfSnapshot;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a NEW name may be tracked. Spans and events share one budget so the
|
|
46
|
+
* total retained-name count is what `maxNames` bounds.
|
|
47
|
+
*/
|
|
48
|
+
private track;
|
|
49
|
+
private create;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=InMemoryPerfSink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InMemoryPerfSink.d.ts","sourceRoot":"","sources":["../../src/InMemoryPerfSink.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAErD,+BAA+B;AAC/B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,qBAEa,gBAAiB,SAAQ,QAAQ;IAE5C,SAAS,CAAC,OAAO,EAAG,OAAO,CAAC;IAG5B,SAAS,CAAC,QAAQ,EAAG,MAAM,CAAC;IAE5B,OAAO,CAAC,KAAK,CAAwE;IACrF,OAAO,CAAC,MAAM,CAAuD;IACrE,OAAO,CAAC,SAAS,CAAS;IAEnB,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAsBlC,MAAM,IAAI,aAAa;IAgB9B;;;OAGG;IACH,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,MAAM;CAIf"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.InMemoryPerfSink = void 0;
|
|
13
|
+
const di_1 = require("@spinajs/di");
|
|
14
|
+
const configuration_1 = require("@spinajs/configuration");
|
|
15
|
+
const log_1 = require("@spinajs/log");
|
|
16
|
+
/**
|
|
17
|
+
* A {@link PerfSink} that keeps a bounded, JSON-readable aggregate of every
|
|
18
|
+
* measurement, backing `GET /telemetry/perf`.
|
|
19
|
+
*
|
|
20
|
+
* This exists alongside `PromMetricSink` rather than replacing it: prom
|
|
21
|
+
* histograms are the right shape for Prometheus but their bucket counts cannot
|
|
22
|
+
* be read back as a mean or a max, which is what a JSON snapshot needs.
|
|
23
|
+
*
|
|
24
|
+
* Per-request rollups are intentionally NOT implemented here — `onScopeEnd`
|
|
25
|
+
* totals share the span names, so folding them in would double-count against
|
|
26
|
+
* the same rows. The per-request view lives in `perf_scope_total_ms` on the
|
|
27
|
+
* prometheus side.
|
|
28
|
+
*/
|
|
29
|
+
let InMemoryPerfSink = class InMemoryPerfSink extends log_1.PerfSink {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(...arguments);
|
|
32
|
+
this.spans = new Map();
|
|
33
|
+
this.events = new Map();
|
|
34
|
+
this.truncated = false;
|
|
35
|
+
}
|
|
36
|
+
collect(metric) {
|
|
37
|
+
if (this.Enabled === false)
|
|
38
|
+
return;
|
|
39
|
+
if (!metric || typeof metric.name !== 'string')
|
|
40
|
+
return;
|
|
41
|
+
if (metric.kind === 'span') {
|
|
42
|
+
const entry = this.spans.get(metric.name) ?? (this.track(metric.name) ? this.create(this.spans, metric.name, { count: 0, totalMs: 0, maxMs: 0 }) : undefined);
|
|
43
|
+
if (!entry)
|
|
44
|
+
return;
|
|
45
|
+
const durationMs = metric.durationMs ?? 0;
|
|
46
|
+
entry.count += 1;
|
|
47
|
+
entry.totalMs += durationMs;
|
|
48
|
+
if (durationMs > entry.maxMs)
|
|
49
|
+
entry.maxMs = durationMs;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const entry = this.events.get(metric.name) ?? (this.track(metric.name) ? this.create(this.events, metric.name, { count: 0, total: 0 }) : undefined);
|
|
53
|
+
if (!entry)
|
|
54
|
+
return;
|
|
55
|
+
entry.count += 1;
|
|
56
|
+
entry.total += metric.value ?? 1;
|
|
57
|
+
}
|
|
58
|
+
toJSON() {
|
|
59
|
+
const spans = [];
|
|
60
|
+
for (const [name, e] of this.spans) {
|
|
61
|
+
spans.push({ name, count: e.count, totalMs: e.totalMs, avgMs: e.count > 0 ? e.totalMs / e.count : 0, maxMs: e.maxMs });
|
|
62
|
+
}
|
|
63
|
+
spans.sort((a, b) => b.totalMs - a.totalMs);
|
|
64
|
+
const events = [];
|
|
65
|
+
for (const [name, e] of this.events) {
|
|
66
|
+
events.push({ name, count: e.count, total: e.total });
|
|
67
|
+
}
|
|
68
|
+
events.sort((a, b) => b.total - a.total);
|
|
69
|
+
return { truncated: this.truncated, spans, events };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether a NEW name may be tracked. Spans and events share one budget so the
|
|
73
|
+
* total retained-name count is what `maxNames` bounds.
|
|
74
|
+
*/
|
|
75
|
+
track(_name) {
|
|
76
|
+
if (this.spans.size + this.events.size >= (this.MaxNames ?? 200)) {
|
|
77
|
+
this.truncated = true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
create(map, name, initial) {
|
|
83
|
+
map.set(name, initial);
|
|
84
|
+
return initial;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
exports.InMemoryPerfSink = InMemoryPerfSink;
|
|
88
|
+
__decorate([
|
|
89
|
+
(0, configuration_1.Config)('telemetry.perf.enabled', { defaultValue: true }),
|
|
90
|
+
__metadata("design:type", Boolean)
|
|
91
|
+
], InMemoryPerfSink.prototype, "Enabled", void 0);
|
|
92
|
+
__decorate([
|
|
93
|
+
(0, configuration_1.Config)('telemetry.perf.maxNames', { defaultValue: 200 }),
|
|
94
|
+
__metadata("design:type", Number)
|
|
95
|
+
], InMemoryPerfSink.prototype, "MaxNames", void 0);
|
|
96
|
+
exports.InMemoryPerfSink = InMemoryPerfSink = __decorate([
|
|
97
|
+
(0, di_1.Singleton)(),
|
|
98
|
+
(0, di_1.Injectable)(log_1.PerfSink)
|
|
99
|
+
], InMemoryPerfSink);
|
|
100
|
+
//# sourceMappingURL=InMemoryPerfSink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InMemoryPerfSink.js","sourceRoot":"","sources":["../../src/InMemoryPerfSink.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PerfSink, IPerfMetric, IPerfRollup } from "@spinajs/log";
|
|
2
|
+
import { Counter, Histogram } from "prom-client";
|
|
3
|
+
import { Metrics } from "./metrics.js";
|
|
4
|
+
/** Duration histogram buckets ( ms ), mirroring the http telemetry middleware. */
|
|
5
|
+
export declare const PERF_DURATION_BUCKETS_MS: number[];
|
|
6
|
+
/**
|
|
7
|
+
* {@link PerfSink} that forwards perf measurements to the shared prom-client
|
|
8
|
+
* {@link Metrics} registry — the prom side of the dual-sink. Span durations go
|
|
9
|
+
* to `perf_span_duration_ms{name}`; counter/value events increment
|
|
10
|
+
* `perf_events_total{name}`; per-request scope totals go to their own
|
|
11
|
+
* `perf_scope_total_ms{name}` histogram so they don't conflate with the
|
|
12
|
+
* per-span distribution. Labels are restricted to `name` ( plus the metric's
|
|
13
|
+
* own low-cardinality labels are folded into the name label space via a single
|
|
14
|
+
* `name` label to stay bounded ).
|
|
15
|
+
*/
|
|
16
|
+
export declare class PromMetricSink extends PerfSink {
|
|
17
|
+
protected metrics: Metrics;
|
|
18
|
+
protected duration: Histogram<string>;
|
|
19
|
+
protected events: Counter<string>;
|
|
20
|
+
protected scopeTotal: Histogram<string>;
|
|
21
|
+
private defined;
|
|
22
|
+
private ensure;
|
|
23
|
+
collect(metric: IPerfMetric): void;
|
|
24
|
+
onScopeEnd(rollup: IPerfRollup): void;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=PromMetricSink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PromMetricSink.d.ts","sourceRoot":"","sources":["../../src/PromMetricSink.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,OAAO,EAAa,MAAM,cAAc,CAAC;AAElD,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,UAA6D,CAAC;AAEnG;;;;;;;;;GASG;AACH,qBAEa,cAAe,SAAQ,QAAQ;IAC1C,SAAS,CAAC,OAAO,EAAG,OAAO,CAAC;IAC5B,SAAS,CAAC,QAAQ,EAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,SAAS,CAAC,MAAM,EAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,SAAS,CAAC,UAAU,EAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO,CAAC,MAAM;IAcP,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IASlC,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;CAQ7C"}
|