@objectstack/observability 5.0.0
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/LICENSE +202 -0
- package/README.md +69 -0
- package/dist/index.cjs +457 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +375 -0
- package/dist/index.d.ts +375 -0
- package/dist/index.js +418 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { Logger } from '@objectstack/spec/contracts';
|
|
2
|
+
export { Logger } from '@objectstack/spec/contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Observability contracts for ObjectStack.
|
|
6
|
+
*
|
|
7
|
+
* Three orthogonal concerns live here:
|
|
8
|
+
*
|
|
9
|
+
* - **MetricsRegistry** — counters / histograms / gauges, Prometheus-style names.
|
|
10
|
+
* - **ErrorReporter** — APM-style exception capture (Sentry / Datadog / Rollbar).
|
|
11
|
+
* - **Logger** — structured log records (re-exported from `@objectstack/spec`).
|
|
12
|
+
*
|
|
13
|
+
* Implementations of these contracts live in this same package
|
|
14
|
+
* (see `metrics-exporters.ts`, `error-exporters.ts`, `loggers.ts`). The
|
|
15
|
+
* runtime, services, and host applications only depend on the contracts
|
|
16
|
+
* so any backend can be wired at deployment time.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Metrics registry contract.
|
|
20
|
+
*
|
|
21
|
+
* Hosts plug in whatever metrics backend they want (Prometheus via
|
|
22
|
+
* `prom-client`, OTel via `@opentelemetry/api-metrics`, Cloudflare
|
|
23
|
+
* Workers Analytics Engine, StatsD, CloudWatch, …) without the
|
|
24
|
+
* framework taking a hard dep on any of them.
|
|
25
|
+
*
|
|
26
|
+
* Naming follows Prometheus conventions:
|
|
27
|
+
*
|
|
28
|
+
* - snake_case names
|
|
29
|
+
* - unit suffix (`_ms`, `_seconds`, `_bytes`, `_total` for counters)
|
|
30
|
+
*
|
|
31
|
+
* Labels are arbitrary string maps; backends should map them to their
|
|
32
|
+
* native label/tag concept. **Keep cardinality low** — never label by
|
|
33
|
+
* raw url path, user id, or tenant id without bucketing.
|
|
34
|
+
*
|
|
35
|
+
* All methods are fire-and-forget; implementations MUST NOT throw on
|
|
36
|
+
* the hot path. Use `NoopMetricsRegistry` when metrics are disabled.
|
|
37
|
+
*/
|
|
38
|
+
interface MetricsRegistry {
|
|
39
|
+
/** Monotonic counter. `value` defaults to 1. */
|
|
40
|
+
counter(name: string, labels?: Record<string, string>, value?: number): void;
|
|
41
|
+
/** Histogram / timing in arbitrary units (typically ms). */
|
|
42
|
+
histogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
43
|
+
/** Point-in-time gauge. */
|
|
44
|
+
gauge(name: string, value: number, labels?: Record<string, string>): void;
|
|
45
|
+
}
|
|
46
|
+
/** Recorded metric sample (used by InMemoryMetricsRegistry and OTLP exporter). */
|
|
47
|
+
interface MetricSample {
|
|
48
|
+
name: string;
|
|
49
|
+
kind: 'counter' | 'histogram' | 'gauge';
|
|
50
|
+
value: number;
|
|
51
|
+
labels: Record<string, string>;
|
|
52
|
+
/** Wall-clock timestamp (ms epoch). */
|
|
53
|
+
at: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Error reporter contract.
|
|
57
|
+
*
|
|
58
|
+
* Production deployments wire this to Sentry, Datadog APM, Rollbar,
|
|
59
|
+
* etc. The runtime calls `captureException` when a route handler
|
|
60
|
+
* results in a 5xx response so the host's APM gets the stack trace
|
|
61
|
+
* without each plugin/route needing to import the vendor SDK.
|
|
62
|
+
*
|
|
63
|
+
* Implementations MUST NOT throw — error reporting failures should be
|
|
64
|
+
* swallowed so the original error reaches the client unmolested.
|
|
65
|
+
*
|
|
66
|
+
* 4xx responses are intentionally NOT captured here. Client errors
|
|
67
|
+
* flood APM systems with noise. Use metrics counters
|
|
68
|
+
* (`http_requests_total{status="4xx"}`) for them, not error reporting.
|
|
69
|
+
*/
|
|
70
|
+
interface ErrorReporter {
|
|
71
|
+
/**
|
|
72
|
+
* Capture a thrown error with optional context. Context typically
|
|
73
|
+
* includes `requestId`, `method`, `route`, `userId`, `orgId`.
|
|
74
|
+
*
|
|
75
|
+
* The reporter is responsible for redacting sensitive fields from
|
|
76
|
+
* `context` (the runtime does not know what is sensitive in the
|
|
77
|
+
* caller's deployment).
|
|
78
|
+
*/
|
|
79
|
+
captureException(error: unknown, context?: Record<string, unknown>): void;
|
|
80
|
+
}
|
|
81
|
+
/** Recorded error (used by InMemoryErrorReporter). */
|
|
82
|
+
interface CapturedError {
|
|
83
|
+
error: unknown;
|
|
84
|
+
context: Record<string, unknown>;
|
|
85
|
+
at: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Semantic conventions — canonical metric names emitted by the
|
|
90
|
+
* framework. Listed here so hosts can wire alerts/dashboards against
|
|
91
|
+
* a stable namespace and so call sites don't sprinkle string
|
|
92
|
+
* literals through the code base.
|
|
93
|
+
*
|
|
94
|
+
* Naming follows Prometheus conventions:
|
|
95
|
+
*
|
|
96
|
+
* - snake_case identifiers.
|
|
97
|
+
* - `_total` suffix for monotonic counters.
|
|
98
|
+
* - `_ms`, `_seconds`, `_bytes` suffixes for histograms / gauges
|
|
99
|
+
* with units.
|
|
100
|
+
*
|
|
101
|
+
* Groups roughly mirror the framework subsystems that emit them.
|
|
102
|
+
* Cloud-specific metrics (DO restarts, Workers Analytics Engine
|
|
103
|
+
* writes, …) do NOT belong here — they are deployment-specific and
|
|
104
|
+
* stay in the deployment repo.
|
|
105
|
+
*/
|
|
106
|
+
declare const SEMCONV: {
|
|
107
|
+
/** Counter, labels: `method`, `route`, `status`. */
|
|
108
|
+
readonly httpRequestsTotal: "http_requests_total";
|
|
109
|
+
/** Histogram (ms), labels: `method`, `route`. */
|
|
110
|
+
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
111
|
+
/**
|
|
112
|
+
* Counter, labels: `method`, `route`. Incremented when an
|
|
113
|
+
* in-flight handler throws after the response is sent.
|
|
114
|
+
*/
|
|
115
|
+
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
116
|
+
/** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
|
|
117
|
+
readonly storageOperationsTotal: "storage_operations_total";
|
|
118
|
+
/** Histogram (ms), labels: `adapter`, `op`. */
|
|
119
|
+
readonly storageOperationDurationMs: "storage_operation_duration_ms";
|
|
120
|
+
/** Counter, labels: `adapter`, `op`, `errorClass`. */
|
|
121
|
+
readonly storageErrorsTotal: "storage_errors_total";
|
|
122
|
+
/** Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`). */
|
|
123
|
+
readonly cacheLookupsTotal: "cache_lookups_total";
|
|
124
|
+
/** Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). */
|
|
125
|
+
readonly cacheWritesTotal: "cache_writes_total";
|
|
126
|
+
/** Counter, labels: `adapter`, `op`, `errorClass`. */
|
|
127
|
+
readonly cacheErrorsTotal: "cache_errors_total";
|
|
128
|
+
/** Counter, labels: `result` (`ok`|`miss`|`error`). */
|
|
129
|
+
readonly registryLookupsTotal: "registry_lookups_total";
|
|
130
|
+
/** Histogram (ms). */
|
|
131
|
+
readonly registryLookupDurationMs: "registry_lookup_duration_ms";
|
|
132
|
+
/** Counter, labels: `source` (`r2`|`http`|`local`), `result` (`hit`|`miss`|`error`). */
|
|
133
|
+
readonly registrySourceFetchesTotal: "registry_source_fetches_total";
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Backwards-compat alias. `RUNTIME_METRICS` was the original (HTTP-only)
|
|
137
|
+
* constant name shipped from `@objectstack/runtime`; we keep it here so
|
|
138
|
+
* existing code reading `RUNTIME_METRICS.httpRequestsTotal` continues
|
|
139
|
+
* to work after the constants moved into this package.
|
|
140
|
+
*/
|
|
141
|
+
declare const RUNTIME_METRICS: {
|
|
142
|
+
readonly httpRequestsTotal: "http_requests_total";
|
|
143
|
+
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
144
|
+
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* No-op metrics registry — the default. Discards every observation.
|
|
149
|
+
* Production deployments should swap this for a real registry; tests
|
|
150
|
+
* can use {@link InMemoryMetricsRegistry} to assert emissions.
|
|
151
|
+
*/
|
|
152
|
+
declare class NoopMetricsRegistry implements MetricsRegistry {
|
|
153
|
+
counter(): void;
|
|
154
|
+
histogram(): void;
|
|
155
|
+
gauge(): void;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* In-memory registry used for tests and local inspection. Stores
|
|
159
|
+
* every observation in insertion order; query via the helpers below
|
|
160
|
+
* or read {@link samples} directly.
|
|
161
|
+
*
|
|
162
|
+
* Not intended for production — unbounded growth.
|
|
163
|
+
*/
|
|
164
|
+
declare class InMemoryMetricsRegistry implements MetricsRegistry {
|
|
165
|
+
readonly samples: MetricSample[];
|
|
166
|
+
counter(name: string, labels?: Record<string, string>, value?: number): void;
|
|
167
|
+
histogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
168
|
+
gauge(name: string, value: number, labels?: Record<string, string>): void;
|
|
169
|
+
/** Sum of counter increments matching `name` and optionally a label subset. */
|
|
170
|
+
totalCounter(name: string, labelMatch?: Record<string, string>): number;
|
|
171
|
+
/** Raw histogram observations matching `name` and optionally a label subset. */
|
|
172
|
+
histogramValues(name: string, labelMatch?: Record<string, string>): number[];
|
|
173
|
+
/** Last gauge value matching `name` and optionally a label subset, or undefined. */
|
|
174
|
+
lastGauge(name: string, labelMatch?: Record<string, string>): number | undefined;
|
|
175
|
+
/** Clear all recorded samples. */
|
|
176
|
+
reset(): void;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Console metrics registry — prints one line per observation. Useful
|
|
180
|
+
* during local development to confirm that instrumentation is firing.
|
|
181
|
+
*
|
|
182
|
+
* Not intended for production: writing every observation to stdout
|
|
183
|
+
* defeats Prometheus / OTLP pipelines and dominates request latency.
|
|
184
|
+
*/
|
|
185
|
+
declare class ConsoleMetricsRegistry implements MetricsRegistry {
|
|
186
|
+
private readonly opts;
|
|
187
|
+
constructor(opts?: {
|
|
188
|
+
sink?: (line: string) => void;
|
|
189
|
+
prefix?: string;
|
|
190
|
+
});
|
|
191
|
+
counter(name: string, labels?: Record<string, string>, value?: number): void;
|
|
192
|
+
histogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
193
|
+
gauge(name: string, value: number, labels?: Record<string, string>): void;
|
|
194
|
+
private emit;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Configuration for {@link OtlpHttpMetricsRegistry}.
|
|
198
|
+
*/
|
|
199
|
+
interface OtlpHttpExporterOptions {
|
|
200
|
+
/**
|
|
201
|
+
* OTLP/HTTP metrics endpoint, e.g. `http://otel-collector:4318/v1/metrics`.
|
|
202
|
+
* The path is appended automatically if missing.
|
|
203
|
+
*/
|
|
204
|
+
endpoint: string;
|
|
205
|
+
/** Optional headers (Authorization, x-tenant, …). */
|
|
206
|
+
headers?: Record<string, string>;
|
|
207
|
+
/**
|
|
208
|
+
* Resource attributes — `service.name`, `service.namespace`,
|
|
209
|
+
* `deployment.environment`, etc. Merged into the OTLP `resource`
|
|
210
|
+
* block on every export.
|
|
211
|
+
*/
|
|
212
|
+
resource?: Record<string, string>;
|
|
213
|
+
/**
|
|
214
|
+
* Custom fetch implementation. Defaults to the global `fetch`.
|
|
215
|
+
* Allows Workers / undici / node-fetch substitution and test
|
|
216
|
+
* doubles.
|
|
217
|
+
*/
|
|
218
|
+
fetch?: typeof fetch;
|
|
219
|
+
/**
|
|
220
|
+
* Called when an export attempt fails. Default: silently swallow
|
|
221
|
+
* (per the contract that metric emission must not throw / log
|
|
222
|
+
* loudly on the hot path).
|
|
223
|
+
*/
|
|
224
|
+
onError?: (error: unknown) => void;
|
|
225
|
+
/**
|
|
226
|
+
* Maximum number of samples buffered before {@link OtlpHttpMetricsRegistry.flush}
|
|
227
|
+
* is called automatically. Defaults to 1024.
|
|
228
|
+
*/
|
|
229
|
+
maxBufferSize?: number;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* OTLP/HTTP metrics exporter.
|
|
233
|
+
*
|
|
234
|
+
* Buffers samples in memory and serialises them to the OpenTelemetry
|
|
235
|
+
* Protocol JSON encoding when {@link flush} is called (manually or
|
|
236
|
+
* automatically once the buffer hits the configured size).
|
|
237
|
+
*
|
|
238
|
+
* Intentionally does **not** start an interval timer in the constructor:
|
|
239
|
+
* (a) it makes the exporter usable on Cloudflare Workers where
|
|
240
|
+
* `setInterval` is restricted, and (b) it keeps unit tests deterministic.
|
|
241
|
+
* Long-running hosts should call `flush()` on a schedule
|
|
242
|
+
* (e.g. `setInterval(() => reg.flush(), 10_000)` on Node, or
|
|
243
|
+
* `ctx.waitUntil(reg.flush())` from a Workers fetch handler).
|
|
244
|
+
*
|
|
245
|
+
* Only counters, histograms, and gauges are emitted — no support for
|
|
246
|
+
* exemplars or aggregation temporality switches (the Collector handles
|
|
247
|
+
* those on the upstream side).
|
|
248
|
+
*/
|
|
249
|
+
declare class OtlpHttpMetricsRegistry implements MetricsRegistry {
|
|
250
|
+
private buffer;
|
|
251
|
+
private readonly endpoint;
|
|
252
|
+
private readonly headers;
|
|
253
|
+
private readonly resource;
|
|
254
|
+
private readonly maxBufferSize;
|
|
255
|
+
private readonly fetchImpl;
|
|
256
|
+
private readonly onError;
|
|
257
|
+
constructor(options: OtlpHttpExporterOptions);
|
|
258
|
+
counter(name: string, labels?: Record<string, string>, value?: number): void;
|
|
259
|
+
histogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
260
|
+
gauge(name: string, value: number, labels?: Record<string, string>): void;
|
|
261
|
+
private record;
|
|
262
|
+
/** Snapshot the current buffer (for tests). */
|
|
263
|
+
peek(): readonly MetricSample[];
|
|
264
|
+
/**
|
|
265
|
+
* Send the buffered samples to the OTLP endpoint and clear the
|
|
266
|
+
* buffer. Safe to call concurrently — each invocation takes a
|
|
267
|
+
* snapshot before clearing.
|
|
268
|
+
*/
|
|
269
|
+
flush(): Promise<void>;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** No-op reporter — the default. */
|
|
273
|
+
declare class NoopErrorReporter implements ErrorReporter {
|
|
274
|
+
captureException(): void;
|
|
275
|
+
}
|
|
276
|
+
/** In-memory reporter for tests. */
|
|
277
|
+
declare class InMemoryErrorReporter implements ErrorReporter {
|
|
278
|
+
readonly captured: CapturedError[];
|
|
279
|
+
captureException(error: unknown, context?: Record<string, unknown>): void;
|
|
280
|
+
reset(): void;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Console error reporter — writes a structured JSON line to stderr per
|
|
284
|
+
* captured exception. Convenient default for local development and for
|
|
285
|
+
* any deployment that ships stderr to a log aggregator (e.g. Loki,
|
|
286
|
+
* fluent-bit) but does not have a dedicated APM.
|
|
287
|
+
*
|
|
288
|
+
* Stack traces are included when the captured value is an `Error`.
|
|
289
|
+
*/
|
|
290
|
+
declare class ConsoleErrorReporter implements ErrorReporter {
|
|
291
|
+
private readonly opts;
|
|
292
|
+
constructor(opts?: {
|
|
293
|
+
sink?: (line: string) => void;
|
|
294
|
+
});
|
|
295
|
+
captureException(error: unknown, context?: Record<string, unknown>): void;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Recognised log levels in increasing severity order. */
|
|
299
|
+
declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error", "fatal"];
|
|
300
|
+
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
301
|
+
/** No-op logger — discards every message. */
|
|
302
|
+
declare class NoopLogger implements Logger {
|
|
303
|
+
debug(): void;
|
|
304
|
+
info(): void;
|
|
305
|
+
warn(): void;
|
|
306
|
+
error(): void;
|
|
307
|
+
fatal(): void;
|
|
308
|
+
child(): Logger;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Console logger — pretty-printed messages for local development.
|
|
312
|
+
*
|
|
313
|
+
* Not suitable for production where structured JSON is required;
|
|
314
|
+
* use {@link JsonLogger} there instead.
|
|
315
|
+
*/
|
|
316
|
+
declare class ConsoleLogger implements Logger {
|
|
317
|
+
private readonly opts;
|
|
318
|
+
constructor(opts?: {
|
|
319
|
+
level?: LogLevel;
|
|
320
|
+
context?: Record<string, unknown>;
|
|
321
|
+
sink?: {
|
|
322
|
+
log: (s: string) => void;
|
|
323
|
+
error: (s: string) => void;
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
private get threshold();
|
|
327
|
+
private get sink();
|
|
328
|
+
private get context();
|
|
329
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
330
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
331
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
332
|
+
error(message: string, error?: Error, meta?: Record<string, unknown>): void;
|
|
333
|
+
fatal(message: string, error?: Error, meta?: Record<string, unknown>): void;
|
|
334
|
+
child(context: Record<string, unknown>): Logger;
|
|
335
|
+
private emit;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* JSON logger — one JSON object per line on stdout (errors on stderr).
|
|
339
|
+
*
|
|
340
|
+
* Matches the shape that Loki / fluent-bit / Cloudflare Logpush ingest
|
|
341
|
+
* by default. Every record contains `ts`, `level`, `msg`, and any
|
|
342
|
+
* accumulated child-context plus per-call `meta`.
|
|
343
|
+
*
|
|
344
|
+
* Use this in production. Use {@link ConsoleLogger} during development
|
|
345
|
+
* for human-friendly output.
|
|
346
|
+
*/
|
|
347
|
+
declare class JsonLogger implements Logger {
|
|
348
|
+
private readonly opts;
|
|
349
|
+
constructor(opts?: {
|
|
350
|
+
level?: LogLevel;
|
|
351
|
+
context?: Record<string, unknown>;
|
|
352
|
+
sink?: {
|
|
353
|
+
log: (s: string) => void;
|
|
354
|
+
error: (s: string) => void;
|
|
355
|
+
};
|
|
356
|
+
/** Optional fields injected into every record (`service`, `env`, …). */
|
|
357
|
+
base?: Record<string, unknown>;
|
|
358
|
+
/** Wall clock for tests. */
|
|
359
|
+
now?: () => Date;
|
|
360
|
+
});
|
|
361
|
+
private get threshold();
|
|
362
|
+
private get sink();
|
|
363
|
+
private get context();
|
|
364
|
+
private get base();
|
|
365
|
+
private get now();
|
|
366
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
367
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
368
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
369
|
+
error(message: string, error?: Error, meta?: Record<string, unknown>): void;
|
|
370
|
+
fatal(message: string, error?: Error, meta?: Record<string, unknown>): void;
|
|
371
|
+
child(context: Record<string, unknown>): Logger;
|
|
372
|
+
private emit;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, RUNTIME_METRICS, SEMCONV };
|