@zudojs/observability 0.1.0 → 1.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 +21 -0
- package/README.md +260 -13
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors/observabilityError.core.d.ts +32 -0
- package/dist/errors/observabilityError.core.js +56 -0
- package/dist/exporter/exporter.console.d.ts +51 -3
- package/dist/exporter/exporter.console.js +110 -23
- package/dist/exporter/index.d.ts +2 -2
- package/dist/exporter/index.js +2 -2
- package/dist/index.d.ts +17 -11
- package/dist/index.js +20 -12
- package/dist/internal/ids.core.d.ts +26 -0
- package/dist/internal/ids.core.js +51 -0
- package/dist/internal/index.d.ts +5 -0
- package/dist/internal/index.js +5 -0
- package/dist/logLevel/index.d.ts +1 -1
- package/dist/logLevel/index.js +1 -1
- package/dist/logLevel/logLevel.type.d.ts +8 -2
- package/dist/logLevel/logLevel.type.js +27 -20
- package/dist/logRecord/index.d.ts +1 -1
- package/dist/logRecord/index.js +1 -1
- package/dist/logRecord/logRecord.core.d.ts +15 -3
- package/dist/logRecord/logRecord.core.js +48 -26
- package/dist/logger/logger.core.d.ts +16 -8
- package/dist/logger/logger.core.js +54 -23
- package/dist/metrics/counter/counter.core.d.ts +4 -1
- package/dist/metrics/counter/counter.core.js +11 -3
- package/dist/metrics/gauge/gauge.core.d.ts +1 -0
- package/dist/metrics/gauge/gauge.core.js +9 -0
- package/dist/metrics/histogram/histogram.core.d.ts +22 -11
- package/dist/metrics/histogram/histogram.core.js +102 -7
- package/dist/metrics/histogram/index.d.ts +1 -1
- package/dist/metrics/histogram/index.js +1 -1
- package/dist/metrics/index.d.ts +4 -3
- package/dist/metrics/index.js +4 -3
- package/dist/metrics/metrics.reader.d.ts +39 -0
- package/dist/metrics/metrics.reader.js +81 -0
- package/dist/metrics/metrics.registry.d.ts +55 -4
- package/dist/metrics/metrics.registry.js +136 -49
- package/dist/noop/index.d.ts +1 -1
- package/dist/noop/index.js +1 -1
- package/dist/noop/noopObservability.core.d.ts +14 -2
- package/dist/noop/noopObservability.core.js +48 -11
- package/dist/observability/observability.core.d.ts +57 -3
- package/dist/observability/observability.core.js +206 -50
- package/dist/processor/index.d.ts +4 -3
- package/dist/processor/index.js +4 -3
- package/dist/processor/processor.batch.d.ts +66 -11
- package/dist/processor/processor.batch.js +130 -29
- package/dist/processor/processor.log.d.ts +56 -0
- package/dist/processor/processor.log.js +119 -0
- package/dist/propagation/index.d.ts +1 -1
- package/dist/propagation/index.js +1 -1
- package/dist/propagation/propagation.core.d.ts +26 -6
- package/dist/propagation/propagation.core.js +35 -19
- package/dist/redaction/index.d.ts +1 -1
- package/dist/redaction/index.js +1 -1
- package/dist/redaction/redaction.core.d.ts +37 -5
- package/dist/redaction/redaction.core.js +180 -36
- package/dist/sampling/index.d.ts +1 -1
- package/dist/sampling/index.js +1 -1
- package/dist/sampling/sampler.type.d.ts +38 -6
- package/dist/sampling/sampler.type.js +64 -23
- package/dist/tracing/index.d.ts +2 -2
- package/dist/tracing/index.js +2 -2
- package/dist/tracing/span/index.d.ts +1 -1
- package/dist/tracing/span/index.js +1 -1
- package/dist/tracing/span/span.core.d.ts +51 -8
- package/dist/tracing/span/span.core.js +90 -10
- package/dist/tracing/span/spanContext.type.d.ts +12 -3
- package/dist/tracing/span/spanContext.type.js +19 -16
- package/dist/tracing/tracer/index.d.ts +2 -2
- package/dist/tracing/tracer/index.js +2 -2
- package/dist/tracing/tracer/tracer.core.d.ts +44 -14
- package/dist/tracing/tracer/tracer.core.js +81 -25
- package/dist/types/config.types.d.ts +138 -0
- package/dist/types/config.types.js +5 -0
- package/dist/types/logging.types.d.ts +94 -0
- package/dist/types/logging.types.js +20 -0
- package/dist/types/metrics.types.d.ts +93 -0
- package/dist/types/metrics.types.js +5 -0
- package/dist/types/tracing.types.d.ts +115 -0
- package/dist/types/tracing.types.js +28 -0
- package/package.json +23 -14
- package/dist/exporter/exporter.console.d.ts.map +0 -1
- package/dist/exporter/exporter.console.js.map +0 -1
- package/dist/exporter/index.d.ts.map +0 -1
- package/dist/exporter/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logLevel/index.d.ts.map +0 -1
- package/dist/logLevel/index.js.map +0 -1
- package/dist/logLevel/logLevel.type.d.ts.map +0 -1
- package/dist/logLevel/logLevel.type.js.map +0 -1
- package/dist/logRecord/index.d.ts.map +0 -1
- package/dist/logRecord/index.js.map +0 -1
- package/dist/logRecord/logRecord.core.d.ts.map +0 -1
- package/dist/logRecord/logRecord.core.js.map +0 -1
- package/dist/logger/index.d.ts.map +0 -1
- package/dist/logger/index.js.map +0 -1
- package/dist/logger/logger.core.d.ts.map +0 -1
- package/dist/logger/logger.core.js.map +0 -1
- package/dist/metrics/counter/counter.core.d.ts.map +0 -1
- package/dist/metrics/counter/counter.core.js.map +0 -1
- package/dist/metrics/counter/index.d.ts.map +0 -1
- package/dist/metrics/counter/index.js.map +0 -1
- package/dist/metrics/gauge/gauge.core.d.ts.map +0 -1
- package/dist/metrics/gauge/gauge.core.js.map +0 -1
- package/dist/metrics/gauge/index.d.ts.map +0 -1
- package/dist/metrics/gauge/index.js.map +0 -1
- package/dist/metrics/histogram/histogram.core.d.ts.map +0 -1
- package/dist/metrics/histogram/histogram.core.js.map +0 -1
- package/dist/metrics/histogram/index.d.ts.map +0 -1
- package/dist/metrics/histogram/index.js.map +0 -1
- package/dist/metrics/index.d.ts.map +0 -1
- package/dist/metrics/index.js.map +0 -1
- package/dist/metrics/metrics.registry.d.ts.map +0 -1
- package/dist/metrics/metrics.registry.js.map +0 -1
- package/dist/noop/index.d.ts.map +0 -1
- package/dist/noop/index.js.map +0 -1
- package/dist/noop/noopObservability.core.d.ts.map +0 -1
- package/dist/noop/noopObservability.core.js.map +0 -1
- package/dist/observability/index.d.ts.map +0 -1
- package/dist/observability/index.js.map +0 -1
- package/dist/observability/observability.core.d.ts.map +0 -1
- package/dist/observability/observability.core.js.map +0 -1
- package/dist/processor/index.d.ts.map +0 -1
- package/dist/processor/index.js.map +0 -1
- package/dist/processor/processor.batch.d.ts.map +0 -1
- package/dist/processor/processor.batch.js.map +0 -1
- package/dist/propagation/index.d.ts.map +0 -1
- package/dist/propagation/index.js.map +0 -1
- package/dist/propagation/propagation.core.d.ts.map +0 -1
- package/dist/propagation/propagation.core.js.map +0 -1
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/redaction.core.d.ts.map +0 -1
- package/dist/redaction/redaction.core.js.map +0 -1
- package/dist/sampling/index.d.ts.map +0 -1
- package/dist/sampling/index.js.map +0 -1
- package/dist/sampling/sampler.type.d.ts.map +0 -1
- package/dist/sampling/sampler.type.js.map +0 -1
- package/dist/tracing/index.d.ts.map +0 -1
- package/dist/tracing/index.js.map +0 -1
- package/dist/tracing/span/index.d.ts.map +0 -1
- package/dist/tracing/span/index.js.map +0 -1
- package/dist/tracing/span/span.core.d.ts.map +0 -1
- package/dist/tracing/span/span.core.js.map +0 -1
- package/dist/tracing/span/spanContext.type.d.ts.map +0 -1
- package/dist/tracing/span/spanContext.type.js.map +0 -1
- package/dist/tracing/tracer/index.d.ts.map +0 -1
- package/dist/tracing/tracer/index.js.map +0 -1
- package/dist/tracing/tracer/tracer.core.d.ts.map +0 -1
- package/dist/tracing/tracer/tracer.core.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/observability — Metric Reader
|
|
3
|
+
*
|
|
4
|
+
* Metrics are pull-based in-process: the registry holds live numbers and
|
|
5
|
+
* nothing moves them anywhere. This reader is the missing half — it snapshots
|
|
6
|
+
* the registry on an interval and hands the snapshots to an exporter, so a
|
|
7
|
+
* configured `metricExporter` actually receives data.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
10
|
+
/** Snapshots a registry on an interval and exports the result. */
|
|
11
|
+
export class PeriodicMetricReader {
|
|
12
|
+
registry;
|
|
13
|
+
exporter;
|
|
14
|
+
intervalMs;
|
|
15
|
+
onError;
|
|
16
|
+
timer;
|
|
17
|
+
started = false;
|
|
18
|
+
shuttingDown = false;
|
|
19
|
+
inFlight;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.registry = options.registry;
|
|
22
|
+
this.exporter = options.exporter;
|
|
23
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
24
|
+
this.onError = options.onError;
|
|
25
|
+
}
|
|
26
|
+
/** Begins periodic export. Calling it twice is a no-op. */
|
|
27
|
+
start() {
|
|
28
|
+
if (this.started || this.shuttingDown || this.intervalMs <= 0)
|
|
29
|
+
return;
|
|
30
|
+
this.started = true;
|
|
31
|
+
this.timer = setInterval(() => {
|
|
32
|
+
void this.collect();
|
|
33
|
+
}, this.intervalMs);
|
|
34
|
+
if (typeof this.timer === "object" && "unref" in this.timer) {
|
|
35
|
+
this.timer.unref();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Exports one snapshot immediately. */
|
|
39
|
+
async collect() {
|
|
40
|
+
if (this.inFlight) {
|
|
41
|
+
await this.inFlight;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const run = (async () => {
|
|
45
|
+
const snapshots = this.registry.getAll();
|
|
46
|
+
if (snapshots.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
await this.exporter.export(snapshots);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.onError?.(error, "MetricExporter.export");
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
this.inFlight = run;
|
|
56
|
+
try {
|
|
57
|
+
await run;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (this.inFlight === run)
|
|
61
|
+
this.inFlight = undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Exports a final snapshot and stops. Safe to call more than once. */
|
|
65
|
+
async shutdown() {
|
|
66
|
+
if (this.shuttingDown)
|
|
67
|
+
return;
|
|
68
|
+
this.shuttingDown = true;
|
|
69
|
+
if (this.timer !== undefined) {
|
|
70
|
+
clearInterval(this.timer);
|
|
71
|
+
this.timer = undefined;
|
|
72
|
+
}
|
|
73
|
+
await this.collect();
|
|
74
|
+
await this.exporter.shutdown();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Creates a periodic metric reader. */
|
|
78
|
+
export function createPeriodicMetricReader(options) {
|
|
79
|
+
return new PeriodicMetricReader(options);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=metrics.reader.js.map
|
|
@@ -4,20 +4,71 @@
|
|
|
4
4
|
* Central registry for all metrics. Creates and caches metrics by name+labels.
|
|
5
5
|
*/
|
|
6
6
|
import type { Counter, Gauge, Histogram, MetricsRegistry, MetricSnapshot } from "../types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Builds the cache key for one series.
|
|
9
|
+
*
|
|
10
|
+
* Label keys and values are JSON-encoded, so `{ a: "b,c=d" }` and
|
|
11
|
+
* `{ a: "b", c: "d" }` cannot collapse onto the same key the way a bare
|
|
12
|
+
* `k=v` join lets them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function metricKey(type: string, name: string, labels?: Record<string, string>): string;
|
|
15
|
+
/** Options for {@link DefaultMetricsRegistry}. */
|
|
16
|
+
export interface MetricsRegistryOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Maximum number of distinct series held at once. Default: 10,000.
|
|
19
|
+
*
|
|
20
|
+
* A label carrying a user ID or a path with IDs in it turns an unbounded
|
|
21
|
+
* registry into a memory leak, so the cap is on by default and creating a
|
|
22
|
+
* series past it reports the offending metric instead of growing silently.
|
|
23
|
+
*/
|
|
24
|
+
readonly maxSeries?: number;
|
|
25
|
+
/** Bucket boundaries for histograms created by this registry. */
|
|
26
|
+
readonly histogramBoundaries?: readonly number[];
|
|
27
|
+
/** Called when the series cap is hit, once per rejected series. */
|
|
28
|
+
readonly onCardinalityLimit?: (name: string, size: number) => void;
|
|
29
|
+
}
|
|
7
30
|
/**
|
|
8
31
|
* In-memory metrics registry. Creates, caches, and manages metrics.
|
|
9
32
|
*/
|
|
10
33
|
export declare class DefaultMetricsRegistry implements MetricsRegistry {
|
|
11
34
|
private readonly metrics;
|
|
35
|
+
/**
|
|
36
|
+
* The type each metric name was first registered as.
|
|
37
|
+
*
|
|
38
|
+
* The cache key includes the type, so the map alone can never surface a
|
|
39
|
+
* differently-typed entry — the conflict check that reads it was dead code,
|
|
40
|
+
* and `counter("x")` followed by `gauge("x")` produced two same-named series
|
|
41
|
+
* of different types in one document. An OTLP or Prometheus backend rejects
|
|
42
|
+
* that, so the whole scrape is lost rather than one metric.
|
|
43
|
+
*/
|
|
44
|
+
private readonly typeByName;
|
|
45
|
+
private readonly maxSeries;
|
|
46
|
+
private readonly histogramBoundaries?;
|
|
47
|
+
private readonly onCardinalityLimit?;
|
|
48
|
+
/** Series rejected by the cap, reused so callers still get a usable object. */
|
|
49
|
+
private readonly overflow;
|
|
50
|
+
/**
|
|
51
|
+
* Cap on the detached-series cache. Bounded well below `maxSeries` so the
|
|
52
|
+
* documented ceiling is not quietly doubled by the overflow path.
|
|
53
|
+
*/
|
|
54
|
+
private readonly maxOverflow;
|
|
55
|
+
constructor(options?: MetricsRegistryOptions);
|
|
56
|
+
private create;
|
|
57
|
+
private obtain;
|
|
12
58
|
counter(name: string, labels?: Record<string, string>): Counter;
|
|
13
59
|
gauge(name: string, labels?: Record<string, string>): Gauge;
|
|
14
60
|
histogram(name: string, labels?: Record<string, string>): Histogram;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
61
|
+
private lookup;
|
|
62
|
+
getCounter(name: string, labels?: Record<string, string>): Counter | undefined;
|
|
63
|
+
getGauge(name: string, labels?: Record<string, string>): Gauge | undefined;
|
|
64
|
+
getHistogram(name: string, labels?: Record<string, string>): Histogram | undefined;
|
|
65
|
+
getSeries(name: string): readonly MetricSnapshot[];
|
|
66
|
+
private snapshot;
|
|
18
67
|
getAll(): MetricSnapshot[];
|
|
68
|
+
size(): number;
|
|
19
69
|
reset(): void;
|
|
70
|
+
clear(): void;
|
|
20
71
|
}
|
|
21
72
|
/** Creates a metrics registry. */
|
|
22
|
-
export declare function createMetricsRegistry(): DefaultMetricsRegistry;
|
|
73
|
+
export declare function createMetricsRegistry(options?: MetricsRegistryOptions): DefaultMetricsRegistry;
|
|
23
74
|
//# sourceMappingURL=metrics.registry.d.ts.map
|
|
@@ -6,88 +6,175 @@
|
|
|
6
6
|
import { DefaultCounter } from "./counter/counter.core.js";
|
|
7
7
|
import { DefaultGauge } from "./gauge/gauge.core.js";
|
|
8
8
|
import { DefaultHistogram } from "./histogram/histogram.core.js";
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { ObservabilityConfigError } from "../errors/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Builds the cache key for one series.
|
|
12
|
+
*
|
|
13
|
+
* Label keys and values are JSON-encoded, so `{ a: "b,c=d" }` and
|
|
14
|
+
* `{ a: "b", c: "d" }` cannot collapse onto the same key the way a bare
|
|
15
|
+
* `k=v` join lets them.
|
|
16
|
+
*/
|
|
17
|
+
export function metricKey(type, name, labels) {
|
|
18
|
+
const entries = labels
|
|
11
19
|
? Object.entries(labels)
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return `${type}:${name}:${labelStr}`;
|
|
20
|
+
.filter(([, value]) => value !== undefined)
|
|
21
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
22
|
+
: [];
|
|
23
|
+
return `${type}:${name}:${JSON.stringify(entries)}`;
|
|
17
24
|
}
|
|
25
|
+
const DEFAULT_MAX_SERIES = 10_000;
|
|
26
|
+
/** Upper bound on the detached series kept for callers past the cap. */
|
|
27
|
+
const MAX_OVERFLOW_SERIES = 1_024;
|
|
18
28
|
/**
|
|
19
29
|
* In-memory metrics registry. Creates, caches, and manages metrics.
|
|
20
30
|
*/
|
|
21
31
|
export class DefaultMetricsRegistry {
|
|
22
32
|
metrics = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* The type each metric name was first registered as.
|
|
35
|
+
*
|
|
36
|
+
* The cache key includes the type, so the map alone can never surface a
|
|
37
|
+
* differently-typed entry — the conflict check that reads it was dead code,
|
|
38
|
+
* and `counter("x")` followed by `gauge("x")` produced two same-named series
|
|
39
|
+
* of different types in one document. An OTLP or Prometheus backend rejects
|
|
40
|
+
* that, so the whole scrape is lost rather than one metric.
|
|
41
|
+
*/
|
|
42
|
+
typeByName = new Map();
|
|
43
|
+
maxSeries;
|
|
44
|
+
histogramBoundaries;
|
|
45
|
+
onCardinalityLimit;
|
|
46
|
+
/** Series rejected by the cap, reused so callers still get a usable object. */
|
|
47
|
+
overflow = new Map();
|
|
48
|
+
/**
|
|
49
|
+
* Cap on the detached-series cache. Bounded well below `maxSeries` so the
|
|
50
|
+
* documented ceiling is not quietly doubled by the overflow path.
|
|
51
|
+
*/
|
|
52
|
+
maxOverflow;
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.maxSeries = options?.maxSeries ?? DEFAULT_MAX_SERIES;
|
|
55
|
+
this.maxOverflow = Math.max(1, Math.min(this.maxSeries, MAX_OVERFLOW_SERIES));
|
|
56
|
+
this.histogramBoundaries = options?.histogramBoundaries;
|
|
57
|
+
this.onCardinalityLimit = options?.onCardinalityLimit;
|
|
58
|
+
}
|
|
59
|
+
create(type, name, labels) {
|
|
60
|
+
if (type === "counter") {
|
|
61
|
+
return { type, metric: new DefaultCounter(name, labels) };
|
|
62
|
+
}
|
|
63
|
+
if (type === "gauge") {
|
|
64
|
+
return { type, metric: new DefaultGauge(name, labels) };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
type,
|
|
68
|
+
metric: new DefaultHistogram(name, labels, this.histogramBoundaries),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
obtain(type, name, labels) {
|
|
72
|
+
const registeredAs = this.typeByName.get(name);
|
|
73
|
+
if (registeredAs !== undefined && registeredAs !== type) {
|
|
74
|
+
throw new ObservabilityConfigError(`Metric "${name}" is already registered as a ${registeredAs}`, { name, registeredAs, requestedAs: type });
|
|
75
|
+
}
|
|
76
|
+
const key = metricKey(type, name, labels);
|
|
77
|
+
const existing = this.metrics.get(key);
|
|
78
|
+
if (existing)
|
|
79
|
+
return existing;
|
|
80
|
+
if (this.metrics.size >= this.maxSeries) {
|
|
81
|
+
// Past the cap, hand back a detached series so instrumented code keeps
|
|
82
|
+
// working, but never let the registry itself grow.
|
|
83
|
+
const cached = this.overflow.get(key);
|
|
84
|
+
if (cached)
|
|
85
|
+
return cached;
|
|
86
|
+
this.onCardinalityLimit?.(name, this.metrics.size);
|
|
87
|
+
const detached = this.create(type, name, labels);
|
|
88
|
+
if (this.overflow.size < this.maxOverflow)
|
|
89
|
+
this.overflow.set(key, detached);
|
|
90
|
+
return detached;
|
|
91
|
+
}
|
|
92
|
+
const entry = this.create(type, name, labels);
|
|
93
|
+
this.metrics.set(key, entry);
|
|
94
|
+
this.typeByName.set(name, type);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
23
97
|
counter(name, labels) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
98
|
+
const entry = this.obtain("counter", name, labels);
|
|
99
|
+
// `obtain` rejects a type conflict before returning, so this narrowing
|
|
100
|
+
// always succeeds; the throw is a guard, not a cast.
|
|
101
|
+
if (entry.type !== "counter") {
|
|
102
|
+
throw new ObservabilityConfigError(`Metric "${name}" is already registered as a ${entry.type}`, { name, registeredAs: entry.type, requestedAs: "counter" });
|
|
29
103
|
}
|
|
30
104
|
return entry.metric;
|
|
31
105
|
}
|
|
32
106
|
gauge(name, labels) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
107
|
+
const entry = this.obtain("gauge", name, labels);
|
|
108
|
+
// `obtain` rejects a type conflict before returning, so this narrowing
|
|
109
|
+
// always succeeds; the throw is a guard, not a cast.
|
|
110
|
+
if (entry.type !== "gauge") {
|
|
111
|
+
throw new ObservabilityConfigError(`Metric "${name}" is already registered as a ${entry.type}`, { name, registeredAs: entry.type, requestedAs: "gauge" });
|
|
38
112
|
}
|
|
39
113
|
return entry.metric;
|
|
40
114
|
}
|
|
41
115
|
histogram(name, labels) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
116
|
+
const entry = this.obtain("histogram", name, labels);
|
|
117
|
+
// `obtain` rejects a type conflict before returning, so this narrowing
|
|
118
|
+
// always succeeds; the throw is a guard, not a cast.
|
|
119
|
+
if (entry.type !== "histogram") {
|
|
120
|
+
throw new ObservabilityConfigError(`Metric "${name}" is already registered as a ${entry.type}`, { name, registeredAs: entry.type, requestedAs: "histogram" });
|
|
47
121
|
}
|
|
48
122
|
return entry.metric;
|
|
49
123
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (entry.type === "counter" && entry.metric.name === name)
|
|
53
|
-
return entry.metric;
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
124
|
+
lookup(type, name, labels) {
|
|
125
|
+
return this.metrics.get(metricKey(type, name, labels));
|
|
56
126
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return entry.metric;
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
127
|
+
getCounter(name, labels) {
|
|
128
|
+
const entry = this.lookup("counter", name, labels);
|
|
129
|
+
return entry?.type === "counter" ? entry.metric : undefined;
|
|
63
130
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return entry.metric;
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
131
|
+
getGauge(name, labels) {
|
|
132
|
+
const entry = this.lookup("gauge", name, labels);
|
|
133
|
+
return entry?.type === "gauge" ? entry.metric : undefined;
|
|
70
134
|
}
|
|
71
|
-
|
|
135
|
+
getHistogram(name, labels) {
|
|
136
|
+
const entry = this.lookup("histogram", name, labels);
|
|
137
|
+
return entry?.type === "histogram" ? entry.metric : undefined;
|
|
138
|
+
}
|
|
139
|
+
getSeries(name) {
|
|
140
|
+
const timestamp = new Date();
|
|
72
141
|
const snapshots = [];
|
|
73
142
|
for (const entry of this.metrics.values()) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
value: entry.metric.getValue(),
|
|
78
|
-
labels: entry.metric.labels,
|
|
79
|
-
});
|
|
143
|
+
if (entry.metric.name === name) {
|
|
144
|
+
snapshots.push(this.snapshot(entry, timestamp));
|
|
145
|
+
}
|
|
80
146
|
}
|
|
81
147
|
return snapshots;
|
|
82
148
|
}
|
|
149
|
+
snapshot(entry, timestamp) {
|
|
150
|
+
return {
|
|
151
|
+
name: entry.metric.name,
|
|
152
|
+
type: entry.type,
|
|
153
|
+
value: entry.metric.getValue(),
|
|
154
|
+
labels: entry.metric.labels ? { ...entry.metric.labels } : undefined,
|
|
155
|
+
timestamp,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
getAll() {
|
|
159
|
+
const timestamp = new Date();
|
|
160
|
+
return [...this.metrics.values()].map((entry) => this.snapshot(entry, timestamp));
|
|
161
|
+
}
|
|
162
|
+
size() {
|
|
163
|
+
return this.metrics.size;
|
|
164
|
+
}
|
|
83
165
|
reset() {
|
|
84
166
|
for (const entry of this.metrics.values()) {
|
|
85
167
|
entry.metric.reset();
|
|
86
168
|
}
|
|
87
169
|
}
|
|
170
|
+
clear() {
|
|
171
|
+
this.metrics.clear();
|
|
172
|
+
this.overflow.clear();
|
|
173
|
+
this.typeByName.clear();
|
|
174
|
+
}
|
|
88
175
|
}
|
|
89
176
|
/** Creates a metrics registry. */
|
|
90
|
-
export function createMetricsRegistry() {
|
|
91
|
-
return new DefaultMetricsRegistry();
|
|
177
|
+
export function createMetricsRegistry(options) {
|
|
178
|
+
return new DefaultMetricsRegistry(options);
|
|
92
179
|
}
|
|
93
180
|
//# sourceMappingURL=metrics.registry.js.map
|
package/dist/noop/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* No-op implementations that discard all telemetry.
|
|
5
5
|
*/
|
|
6
|
-
export { NoopObservability, createNoopObservability, noopLogger, noopCounter, noopGauge, noopHistogram, noopMetricsRegistry, noopSpan, noopTracer, noopPropagationManager, } from "./noopObservability.core.js";
|
|
6
|
+
export { NoopObservability, createNoopObservability, noopLogger, noopCounter, noopGauge, noopHistogram, noopMetricsRegistry, noopSpan, noopTracer, noopPropagationManager, INVALID_PROPAGATION_CONTEXT, } from "./noopObservability.core.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/noop/index.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* No-op implementations that discard all telemetry.
|
|
5
5
|
*/
|
|
6
|
-
export { NoopObservability, createNoopObservability, noopLogger, noopCounter, noopGauge, noopHistogram, noopMetricsRegistry, noopSpan, noopTracer, noopPropagationManager, } from "./noopObservability.core.js";
|
|
6
|
+
export { NoopObservability, createNoopObservability, noopLogger, noopCounter, noopGauge, noopHistogram, noopMetricsRegistry, noopSpan, noopTracer, noopPropagationManager, INVALID_PROPAGATION_CONTEXT, } from "./noopObservability.core.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -4,12 +4,21 @@
|
|
|
4
4
|
* No-op implementations that discard all telemetry.
|
|
5
5
|
* Allows instrumentation code to remain simple without null checks.
|
|
6
6
|
*/
|
|
7
|
-
import type { Counter, Gauge, Histogram, Logger, MetricsRegistry, Observability, PropagationManager, Span, Tracer } from "../types.js";
|
|
7
|
+
import type { Counter, Gauge, Histogram, Logger, MetricsRegistry, Observability, PropagationContext, PropagationManager, Span, Tracer } from "../types.js";
|
|
8
8
|
declare const noopLogger: Logger;
|
|
9
9
|
declare const noopCounter: Counter;
|
|
10
10
|
declare const noopGauge: Gauge;
|
|
11
11
|
declare const noopHistogram: Histogram;
|
|
12
12
|
declare const noopMetricsRegistry: MetricsRegistry;
|
|
13
|
+
/**
|
|
14
|
+
* A span that records nothing.
|
|
15
|
+
*
|
|
16
|
+
* `startTime` is a getter rather than a captured value: as a module-level
|
|
17
|
+
* constant it would report the moment the process loaded, for every span,
|
|
18
|
+
* forever. The context carries the all-zero IDs the W3C spec reserves for
|
|
19
|
+
* "invalid", so a noop span that reaches an exporter is recognisable rather
|
|
20
|
+
* than looking like a real trace.
|
|
21
|
+
*/
|
|
13
22
|
declare const noopSpan: Span;
|
|
14
23
|
declare const noopTracer: Tracer;
|
|
15
24
|
declare const noopPropagationManager: PropagationManager;
|
|
@@ -22,10 +31,13 @@ export declare class NoopObservability implements Observability {
|
|
|
22
31
|
readonly metrics: MetricsRegistry;
|
|
23
32
|
readonly tracer: Tracer;
|
|
24
33
|
readonly propagation: PropagationManager;
|
|
25
|
-
resource(): Observability;
|
|
34
|
+
resource(_attributes: Record<string, unknown>): Observability;
|
|
35
|
+
flush(): Promise<void>;
|
|
26
36
|
shutdown(): Promise<void>;
|
|
27
37
|
}
|
|
28
38
|
/** Creates a noop observability instance. */
|
|
29
39
|
export declare function createNoopObservability(): NoopObservability;
|
|
40
|
+
/** A propagation context with the reserved invalid IDs. */
|
|
41
|
+
export declare const INVALID_PROPAGATION_CONTEXT: PropagationContext;
|
|
30
42
|
export { noopLogger, noopCounter, noopGauge, noopHistogram, noopMetricsRegistry, noopSpan, noopTracer, noopPropagationManager, };
|
|
31
43
|
//# sourceMappingURL=noopObservability.core.d.ts.map
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* No-op implementations that discard all telemetry.
|
|
5
5
|
* Allows instrumentation code to remain simple without null checks.
|
|
6
6
|
*/
|
|
7
|
-
import { LogLevel } from "../types.js";
|
|
7
|
+
import { LogLevel, TraceFlags } from "../types.js";
|
|
8
|
+
import { createPropagationContext } from "../propagation/index.js";
|
|
8
9
|
/* ─── Noop Logger ─────────────────────────────────────────────────────── */
|
|
9
10
|
const noopLogger = {
|
|
10
11
|
name: "noop",
|
|
@@ -17,6 +18,7 @@ const noopLogger = {
|
|
|
17
18
|
fatal: () => { },
|
|
18
19
|
child: () => noopLogger,
|
|
19
20
|
isLevelEnabled: () => false,
|
|
21
|
+
setLevel: () => { },
|
|
20
22
|
flush: async () => { },
|
|
21
23
|
};
|
|
22
24
|
/* ─── Noop Counter ────────────────────────────────────────────────────── */
|
|
@@ -36,10 +38,23 @@ const noopGauge = {
|
|
|
36
38
|
reset: () => { },
|
|
37
39
|
};
|
|
38
40
|
/* ─── Noop Histogram ──────────────────────────────────────────────────── */
|
|
41
|
+
const EMPTY_HISTOGRAM = {
|
|
42
|
+
count: 0,
|
|
43
|
+
sum: 0,
|
|
44
|
+
min: 0,
|
|
45
|
+
max: 0,
|
|
46
|
+
mean: 0,
|
|
47
|
+
buckets: [],
|
|
48
|
+
p50: 0,
|
|
49
|
+
p90: 0,
|
|
50
|
+
p95: 0,
|
|
51
|
+
p99: 0,
|
|
52
|
+
};
|
|
39
53
|
const noopHistogram = {
|
|
40
54
|
name: "noop",
|
|
41
55
|
record: () => { },
|
|
42
|
-
getValue: () =>
|
|
56
|
+
getValue: () => EMPTY_HISTOGRAM,
|
|
57
|
+
percentile: () => 0,
|
|
43
58
|
reset: () => { },
|
|
44
59
|
};
|
|
45
60
|
/* ─── Noop Metrics Registry ───────────────────────────────────────────── */
|
|
@@ -50,14 +65,32 @@ const noopMetricsRegistry = {
|
|
|
50
65
|
getCounter: () => undefined,
|
|
51
66
|
getGauge: () => undefined,
|
|
52
67
|
getHistogram: () => undefined,
|
|
68
|
+
getSeries: () => [],
|
|
53
69
|
getAll: () => [],
|
|
70
|
+
size: () => 0,
|
|
54
71
|
reset: () => { },
|
|
72
|
+
clear: () => { },
|
|
55
73
|
};
|
|
56
74
|
/* ─── Noop Span ───────────────────────────────────────────────────────── */
|
|
75
|
+
/**
|
|
76
|
+
* A span that records nothing.
|
|
77
|
+
*
|
|
78
|
+
* `startTime` is a getter rather than a captured value: as a module-level
|
|
79
|
+
* constant it would report the moment the process loaded, for every span,
|
|
80
|
+
* forever. The context carries the all-zero IDs the W3C spec reserves for
|
|
81
|
+
* "invalid", so a noop span that reaches an exporter is recognisable rather
|
|
82
|
+
* than looking like a real trace.
|
|
83
|
+
*/
|
|
57
84
|
const noopSpan = {
|
|
58
85
|
name: "noop",
|
|
59
|
-
context: {
|
|
60
|
-
|
|
86
|
+
context: {
|
|
87
|
+
traceId: "0".repeat(32),
|
|
88
|
+
spanId: "0".repeat(16),
|
|
89
|
+
traceFlags: TraceFlags.NONE,
|
|
90
|
+
},
|
|
91
|
+
get startTime() {
|
|
92
|
+
return new Date();
|
|
93
|
+
},
|
|
61
94
|
setAttribute: () => { },
|
|
62
95
|
addEvent: () => { },
|
|
63
96
|
setStatus: () => { },
|
|
@@ -71,14 +104,11 @@ const noopTracer = {
|
|
|
71
104
|
startSpan: () => noopSpan,
|
|
72
105
|
};
|
|
73
106
|
/* ─── Noop Propagation Manager ────────────────────────────────────────── */
|
|
74
|
-
const noopContext = {
|
|
75
|
-
traceId: "",
|
|
76
|
-
spanId: "",
|
|
77
|
-
};
|
|
78
107
|
const noopPropagationManager = {
|
|
79
|
-
current: () =>
|
|
108
|
+
current: () => undefined,
|
|
80
109
|
run: async (_ctx, fn) => fn(),
|
|
81
|
-
|
|
110
|
+
runSync: (_ctx, fn) => fn(),
|
|
111
|
+
derive: (overrides) => createPropagationContext(overrides),
|
|
82
112
|
};
|
|
83
113
|
/* ─── Noop Observability ──────────────────────────────────────────────── */
|
|
84
114
|
/**
|
|
@@ -90,14 +120,21 @@ export class NoopObservability {
|
|
|
90
120
|
metrics = noopMetricsRegistry;
|
|
91
121
|
tracer = noopTracer;
|
|
92
122
|
propagation = noopPropagationManager;
|
|
93
|
-
resource() {
|
|
123
|
+
resource(_attributes) {
|
|
94
124
|
return this;
|
|
95
125
|
}
|
|
126
|
+
async flush() { }
|
|
96
127
|
async shutdown() { }
|
|
97
128
|
}
|
|
98
129
|
/** Creates a noop observability instance. */
|
|
99
130
|
export function createNoopObservability() {
|
|
100
131
|
return new NoopObservability();
|
|
101
132
|
}
|
|
133
|
+
/** A propagation context with the reserved invalid IDs. */
|
|
134
|
+
export const INVALID_PROPAGATION_CONTEXT = Object.freeze({
|
|
135
|
+
traceId: "0".repeat(32),
|
|
136
|
+
spanId: "0".repeat(16),
|
|
137
|
+
traceFlags: TraceFlags.NONE,
|
|
138
|
+
});
|
|
102
139
|
export { noopLogger, noopCounter, noopGauge, noopHistogram, noopMetricsRegistry, noopSpan, noopTracer, noopPropagationManager, };
|
|
103
140
|
//# sourceMappingURL=noopObservability.core.js.map
|
|
@@ -5,7 +5,27 @@
|
|
|
5
5
|
* propagation. Other Zudojs packages depend on this abstraction rather
|
|
6
6
|
* than on specific telemetry implementations.
|
|
7
7
|
*/
|
|
8
|
-
import type { Logger, MetricsRegistry, Observability, ObservabilityConfig, PropagationManager, Tracer } from "../types.js";
|
|
8
|
+
import type { Logger, MetricsRegistry, Observability, ObservabilityConfig, PropagationManager, SpanProcessor, Tracer } from "../types.js";
|
|
9
|
+
import { StructuredLogger } from "../logger/index.js";
|
|
10
|
+
import { DefaultMetricsRegistry, PeriodicMetricReader } from "../metrics/index.js";
|
|
11
|
+
import { AsyncPropagationManager } from "../propagation/index.js";
|
|
12
|
+
import { BatchLogProcessor } from "../processor/index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Everything a facade owns and must tear down, shared by an instance and the
|
|
15
|
+
* scopes it creates through {@link DefaultObservability.resource}.
|
|
16
|
+
*/
|
|
17
|
+
interface TelemetryPipeline {
|
|
18
|
+
readonly logger: StructuredLogger;
|
|
19
|
+
readonly metrics: DefaultMetricsRegistry;
|
|
20
|
+
readonly propagation: AsyncPropagationManager;
|
|
21
|
+
readonly processors: readonly SpanProcessor[];
|
|
22
|
+
readonly logProcessor?: BatchLogProcessor;
|
|
23
|
+
readonly metricReader: PeriodicMetricReader;
|
|
24
|
+
readonly config: ObservabilityConfig;
|
|
25
|
+
readonly sampler: ObservabilityConfig["sampler"];
|
|
26
|
+
/** Redacts span attributes, when `config.redaction` is set. */
|
|
27
|
+
readonly redactAttribute?: (key: string, value: unknown) => unknown;
|
|
28
|
+
}
|
|
9
29
|
/**
|
|
10
30
|
* Default observability implementation.
|
|
11
31
|
*
|
|
@@ -17,12 +37,46 @@ export declare class DefaultObservability implements Observability {
|
|
|
17
37
|
readonly metrics: MetricsRegistry;
|
|
18
38
|
readonly tracer: Tracer;
|
|
19
39
|
readonly propagation: PropagationManager;
|
|
40
|
+
private readonly pipeline;
|
|
20
41
|
private readonly resourceAttributes;
|
|
21
|
-
|
|
22
|
-
|
|
42
|
+
/** Scopes share the parent's pipeline, so only the root tears it down. */
|
|
43
|
+
private readonly ownsPipeline;
|
|
44
|
+
private shutdownPromise?;
|
|
45
|
+
constructor(config: ObservabilityConfig, scope?: {
|
|
46
|
+
readonly pipeline: TelemetryPipeline;
|
|
47
|
+
readonly resourceAttributes: Record<string, unknown>;
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Creates a scope that differs only in its resource attributes.
|
|
51
|
+
*
|
|
52
|
+
* The logger, registry, processors and exporters are shared with the parent:
|
|
53
|
+
* building a second pipeline here silently dropped every configured
|
|
54
|
+
* exporter and log level, and left an orphan flush timer nobody shut down.
|
|
55
|
+
*/
|
|
23
56
|
resource(attributes: Record<string, unknown>): Observability;
|
|
57
|
+
/**
|
|
58
|
+
* Drains every buffer without shutting anything down.
|
|
59
|
+
*
|
|
60
|
+
* A scope shares its parent's pipeline, so draining from one is both safe
|
|
61
|
+
* and what the caller asked for. Returning early because the scope does not
|
|
62
|
+
* *own* the pipeline made `obs.resource({...}).flush()` a silent no-op —
|
|
63
|
+
* the buffered records it was meant to push were still sitting in the queue
|
|
64
|
+
* when the caller went on to exit.
|
|
65
|
+
*/
|
|
66
|
+
flush(): Promise<void>;
|
|
67
|
+
private drain;
|
|
68
|
+
private reportFailures;
|
|
69
|
+
/**
|
|
70
|
+
* Shuts the pipeline down. Idempotent, and one failing step never skips the
|
|
71
|
+
* rest — a half-torn-down telemetry stack is worse than a noisy one.
|
|
72
|
+
*
|
|
73
|
+
* A scope created by {@link DefaultObservability.resource} does not own the
|
|
74
|
+
* pipeline and shutting it down is a no-op; shut down the root instead.
|
|
75
|
+
*/
|
|
24
76
|
shutdown(): Promise<void>;
|
|
77
|
+
private performShutdown;
|
|
25
78
|
}
|
|
26
79
|
/** Creates an observability instance. */
|
|
27
80
|
export declare function createObservability(config: ObservabilityConfig): DefaultObservability;
|
|
81
|
+
export {};
|
|
28
82
|
//# sourceMappingURL=observability.core.d.ts.map
|