@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.
@@ -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 };