@zudojs/observability 0.0.1 → 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.
Files changed (156) hide show
  1. package/README.md +260 -13
  2. package/dist/errors/index.d.ts +5 -0
  3. package/dist/errors/index.js +5 -0
  4. package/dist/errors/observabilityError.core.d.ts +32 -0
  5. package/dist/errors/observabilityError.core.js +56 -0
  6. package/dist/exporter/exporter.console.d.ts +51 -3
  7. package/dist/exporter/exporter.console.js +110 -23
  8. package/dist/exporter/index.d.ts +2 -2
  9. package/dist/exporter/index.js +2 -2
  10. package/dist/index.d.ts +17 -11
  11. package/dist/index.js +20 -12
  12. package/dist/internal/ids.core.d.ts +26 -0
  13. package/dist/internal/ids.core.js +51 -0
  14. package/dist/internal/index.d.ts +5 -0
  15. package/dist/internal/index.js +5 -0
  16. package/dist/logLevel/index.d.ts +1 -1
  17. package/dist/logLevel/index.js +1 -1
  18. package/dist/logLevel/logLevel.type.d.ts +8 -2
  19. package/dist/logLevel/logLevel.type.js +27 -20
  20. package/dist/logRecord/index.d.ts +1 -1
  21. package/dist/logRecord/index.js +1 -1
  22. package/dist/logRecord/logRecord.core.d.ts +15 -3
  23. package/dist/logRecord/logRecord.core.js +48 -26
  24. package/dist/logger/logger.core.d.ts +16 -8
  25. package/dist/logger/logger.core.js +54 -23
  26. package/dist/metrics/counter/counter.core.d.ts +4 -1
  27. package/dist/metrics/counter/counter.core.js +11 -3
  28. package/dist/metrics/gauge/gauge.core.d.ts +1 -0
  29. package/dist/metrics/gauge/gauge.core.js +9 -0
  30. package/dist/metrics/histogram/histogram.core.d.ts +22 -11
  31. package/dist/metrics/histogram/histogram.core.js +102 -7
  32. package/dist/metrics/histogram/index.d.ts +1 -1
  33. package/dist/metrics/histogram/index.js +1 -1
  34. package/dist/metrics/index.d.ts +4 -3
  35. package/dist/metrics/index.js +4 -3
  36. package/dist/metrics/metrics.reader.d.ts +39 -0
  37. package/dist/metrics/metrics.reader.js +81 -0
  38. package/dist/metrics/metrics.registry.d.ts +55 -4
  39. package/dist/metrics/metrics.registry.js +136 -49
  40. package/dist/noop/index.d.ts +1 -1
  41. package/dist/noop/index.js +1 -1
  42. package/dist/noop/noopObservability.core.d.ts +14 -2
  43. package/dist/noop/noopObservability.core.js +48 -11
  44. package/dist/observability/observability.core.d.ts +57 -3
  45. package/dist/observability/observability.core.js +206 -50
  46. package/dist/processor/index.d.ts +4 -3
  47. package/dist/processor/index.js +4 -3
  48. package/dist/processor/processor.batch.d.ts +66 -11
  49. package/dist/processor/processor.batch.js +130 -29
  50. package/dist/processor/processor.log.d.ts +56 -0
  51. package/dist/processor/processor.log.js +119 -0
  52. package/dist/propagation/index.d.ts +1 -1
  53. package/dist/propagation/index.js +1 -1
  54. package/dist/propagation/propagation.core.d.ts +26 -6
  55. package/dist/propagation/propagation.core.js +35 -19
  56. package/dist/redaction/index.d.ts +1 -1
  57. package/dist/redaction/index.js +1 -1
  58. package/dist/redaction/redaction.core.d.ts +37 -5
  59. package/dist/redaction/redaction.core.js +180 -36
  60. package/dist/sampling/index.d.ts +1 -1
  61. package/dist/sampling/index.js +1 -1
  62. package/dist/sampling/sampler.type.d.ts +38 -6
  63. package/dist/sampling/sampler.type.js +64 -23
  64. package/dist/tracing/index.d.ts +2 -2
  65. package/dist/tracing/index.js +2 -2
  66. package/dist/tracing/span/index.d.ts +1 -1
  67. package/dist/tracing/span/index.js +1 -1
  68. package/dist/tracing/span/span.core.d.ts +51 -8
  69. package/dist/tracing/span/span.core.js +90 -10
  70. package/dist/tracing/span/spanContext.type.d.ts +12 -3
  71. package/dist/tracing/span/spanContext.type.js +19 -16
  72. package/dist/tracing/tracer/index.d.ts +2 -2
  73. package/dist/tracing/tracer/index.js +2 -2
  74. package/dist/tracing/tracer/tracer.core.d.ts +44 -14
  75. package/dist/tracing/tracer/tracer.core.js +81 -25
  76. package/dist/types/config.types.d.ts +138 -0
  77. package/dist/types/config.types.js +5 -0
  78. package/dist/types/logging.types.d.ts +94 -0
  79. package/dist/types/logging.types.js +20 -0
  80. package/dist/types/metrics.types.d.ts +93 -0
  81. package/dist/types/metrics.types.js +5 -0
  82. package/dist/types/tracing.types.d.ts +115 -0
  83. package/dist/types/tracing.types.js +28 -0
  84. package/package.json +17 -8
  85. package/dist/exporter/exporter.console.d.ts.map +0 -1
  86. package/dist/exporter/exporter.console.js.map +0 -1
  87. package/dist/exporter/index.d.ts.map +0 -1
  88. package/dist/exporter/index.js.map +0 -1
  89. package/dist/index.d.ts.map +0 -1
  90. package/dist/index.js.map +0 -1
  91. package/dist/logLevel/index.d.ts.map +0 -1
  92. package/dist/logLevel/index.js.map +0 -1
  93. package/dist/logLevel/logLevel.type.d.ts.map +0 -1
  94. package/dist/logLevel/logLevel.type.js.map +0 -1
  95. package/dist/logRecord/index.d.ts.map +0 -1
  96. package/dist/logRecord/index.js.map +0 -1
  97. package/dist/logRecord/logRecord.core.d.ts.map +0 -1
  98. package/dist/logRecord/logRecord.core.js.map +0 -1
  99. package/dist/logger/index.d.ts.map +0 -1
  100. package/dist/logger/index.js.map +0 -1
  101. package/dist/logger/logger.core.d.ts.map +0 -1
  102. package/dist/logger/logger.core.js.map +0 -1
  103. package/dist/metrics/counter/counter.core.d.ts.map +0 -1
  104. package/dist/metrics/counter/counter.core.js.map +0 -1
  105. package/dist/metrics/counter/index.d.ts.map +0 -1
  106. package/dist/metrics/counter/index.js.map +0 -1
  107. package/dist/metrics/gauge/gauge.core.d.ts.map +0 -1
  108. package/dist/metrics/gauge/gauge.core.js.map +0 -1
  109. package/dist/metrics/gauge/index.d.ts.map +0 -1
  110. package/dist/metrics/gauge/index.js.map +0 -1
  111. package/dist/metrics/histogram/histogram.core.d.ts.map +0 -1
  112. package/dist/metrics/histogram/histogram.core.js.map +0 -1
  113. package/dist/metrics/histogram/index.d.ts.map +0 -1
  114. package/dist/metrics/histogram/index.js.map +0 -1
  115. package/dist/metrics/index.d.ts.map +0 -1
  116. package/dist/metrics/index.js.map +0 -1
  117. package/dist/metrics/metrics.registry.d.ts.map +0 -1
  118. package/dist/metrics/metrics.registry.js.map +0 -1
  119. package/dist/noop/index.d.ts.map +0 -1
  120. package/dist/noop/index.js.map +0 -1
  121. package/dist/noop/noopObservability.core.d.ts.map +0 -1
  122. package/dist/noop/noopObservability.core.js.map +0 -1
  123. package/dist/observability/index.d.ts.map +0 -1
  124. package/dist/observability/index.js.map +0 -1
  125. package/dist/observability/observability.core.d.ts.map +0 -1
  126. package/dist/observability/observability.core.js.map +0 -1
  127. package/dist/processor/index.d.ts.map +0 -1
  128. package/dist/processor/index.js.map +0 -1
  129. package/dist/processor/processor.batch.d.ts.map +0 -1
  130. package/dist/processor/processor.batch.js.map +0 -1
  131. package/dist/propagation/index.d.ts.map +0 -1
  132. package/dist/propagation/index.js.map +0 -1
  133. package/dist/propagation/propagation.core.d.ts.map +0 -1
  134. package/dist/propagation/propagation.core.js.map +0 -1
  135. package/dist/redaction/index.d.ts.map +0 -1
  136. package/dist/redaction/index.js.map +0 -1
  137. package/dist/redaction/redaction.core.d.ts.map +0 -1
  138. package/dist/redaction/redaction.core.js.map +0 -1
  139. package/dist/sampling/index.d.ts.map +0 -1
  140. package/dist/sampling/index.js.map +0 -1
  141. package/dist/sampling/sampler.type.d.ts.map +0 -1
  142. package/dist/sampling/sampler.type.js.map +0 -1
  143. package/dist/tracing/index.d.ts.map +0 -1
  144. package/dist/tracing/index.js.map +0 -1
  145. package/dist/tracing/span/index.d.ts.map +0 -1
  146. package/dist/tracing/span/index.js.map +0 -1
  147. package/dist/tracing/span/span.core.d.ts.map +0 -1
  148. package/dist/tracing/span/span.core.js.map +0 -1
  149. package/dist/tracing/span/spanContext.type.d.ts.map +0 -1
  150. package/dist/tracing/span/spanContext.type.js.map +0 -1
  151. package/dist/tracing/tracer/index.d.ts.map +0 -1
  152. package/dist/tracing/tracer/index.js.map +0 -1
  153. package/dist/tracing/tracer/tracer.core.d.ts.map +0 -1
  154. package/dist/tracing/tracer/tracer.core.js.map +0 -1
  155. package/dist/types.d.ts.map +0 -1
  156. package/dist/types.js.map +0 -1
@@ -6,7 +6,10 @@
6
6
  import type { Counter } from "../../types.js";
7
7
  /**
8
8
  * In-memory counter. Increments monotonically.
9
- * Designed to be safe and cheap — no async, no locks.
9
+ *
10
+ * Invalid increments throw rather than being dropped: a counter that quietly
11
+ * ignored a negative delta would report a total nobody can reconcile with the
12
+ * code that produced it.
10
13
  */
11
14
  export declare class DefaultCounter implements Counter {
12
15
  readonly name: string;
@@ -3,9 +3,13 @@
3
3
  *
4
4
  * Monotonically increasing counter for tracking event counts.
5
5
  */
6
+ import { MetricValueError } from "../../errors/index.js";
6
7
  /**
7
8
  * In-memory counter. Increments monotonically.
8
- * Designed to be safe and cheap — no async, no locks.
9
+ *
10
+ * Invalid increments throw rather than being dropped: a counter that quietly
11
+ * ignored a negative delta would report a total nobody can reconcile with the
12
+ * code that produced it.
9
13
  */
10
14
  export class DefaultCounter {
11
15
  name;
@@ -16,8 +20,12 @@ export class DefaultCounter {
16
20
  this.labels = labels;
17
21
  }
18
22
  increment(value = 1) {
19
- if (value < 0)
20
- return;
23
+ if (!Number.isFinite(value)) {
24
+ throw new MetricValueError(this.name, value, "must be finite");
25
+ }
26
+ if (value < 0) {
27
+ throw new MetricValueError(this.name, value, "a counter cannot decrease; use a gauge");
28
+ }
21
29
  this.value += value;
22
30
  }
23
31
  getValue() {
@@ -12,6 +12,7 @@ export declare class DefaultGauge implements Gauge {
12
12
  readonly labels?: Record<string, string>;
13
13
  private value;
14
14
  constructor(name: string, labels?: Record<string, string>);
15
+ private assertFinite;
15
16
  setValue(value: number): void;
16
17
  increment(value?: number): void;
17
18
  decrement(value?: number): void;
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * A value that can go up and down, for tracking current state.
5
5
  */
6
+ import { MetricValueError } from "../../errors/index.js";
6
7
  /**
7
8
  * In-memory gauge. Tracks a value that can be set, incremented, or decremented.
8
9
  */
@@ -14,13 +15,21 @@ export class DefaultGauge {
14
15
  this.name = name;
15
16
  this.labels = labels;
16
17
  }
18
+ assertFinite(value) {
19
+ if (!Number.isFinite(value)) {
20
+ throw new MetricValueError(this.name, value, "must be finite");
21
+ }
22
+ }
17
23
  setValue(value) {
24
+ this.assertFinite(value);
18
25
  this.value = value;
19
26
  }
20
27
  increment(value = 1) {
28
+ this.assertFinite(value);
21
29
  this.value += value;
22
30
  }
23
31
  decrement(value = 1) {
32
+ this.assertFinite(value);
24
33
  this.value -= value;
25
34
  }
26
35
  getValue() {
@@ -2,29 +2,40 @@
2
2
  * @zudojs/observability — Histogram
3
3
  *
4
4
  * Distribution of observed values for tracking latencies, sizes, etc.
5
- * Memory-bounded by keeping only summary statistics.
5
+ *
6
+ * Memory is bounded by fixed bucket boundaries rather than by keeping the
7
+ * observations: count, sum, min and max alone cannot answer "what is the p95",
8
+ * which is the question a latency histogram exists to answer.
9
+ */
10
+ import type { Histogram, HistogramValue } from "../../types.js";
11
+ /**
12
+ * Default boundaries, in milliseconds, covering sub-millisecond calls through
13
+ * ten-second ones. Pass your own when the unit is not latency.
6
14
  */
7
- import type { Histogram } from "../../types.js";
15
+ export declare const DEFAULT_BUCKET_BOUNDARIES: readonly number[];
8
16
  /**
9
- * In-memory histogram. Tracks count, sum, min, and max of observed values.
17
+ * In-memory histogram with cumulative buckets and interpolated quantiles.
10
18
  */
11
19
  export declare class DefaultHistogram implements Histogram {
12
20
  readonly name: string;
13
21
  readonly labels?: Record<string, string>;
22
+ private readonly boundaries;
23
+ /** Counts per bucket; one slot longer than `boundaries` for the overflow. */
24
+ private counts;
14
25
  private count;
15
26
  private sum;
16
27
  private min;
17
28
  private max;
18
- constructor(name: string, labels?: Record<string, string>);
29
+ constructor(name: string, labels?: Record<string, string>, boundaries?: readonly number[]);
19
30
  record(value: number): void;
20
- getValue(): {
21
- readonly count: number;
22
- readonly sum: number;
23
- readonly min: number;
24
- readonly max: number;
25
- };
31
+ getValue(): HistogramValue;
32
+ /**
33
+ * Estimates a quantile by linear interpolation inside the bucket the
34
+ * quantile falls in, clamped to the observed min and max.
35
+ */
36
+ percentile(q: number): number;
26
37
  reset(): void;
27
38
  }
28
39
  /** Creates a histogram. */
29
- export declare function createHistogram(name: string, labels?: Record<string, string>): DefaultHistogram;
40
+ export declare function createHistogram(name: string, labels?: Record<string, string>, boundaries?: readonly number[]): DefaultHistogram;
30
41
  //# sourceMappingURL=histogram.core.d.ts.map
@@ -2,47 +2,142 @@
2
2
  * @zudojs/observability — Histogram
3
3
  *
4
4
  * Distribution of observed values for tracking latencies, sizes, etc.
5
- * Memory-bounded by keeping only summary statistics.
5
+ *
6
+ * Memory is bounded by fixed bucket boundaries rather than by keeping the
7
+ * observations: count, sum, min and max alone cannot answer "what is the p95",
8
+ * which is the question a latency histogram exists to answer.
9
+ */
10
+ import { MetricValueError } from "../../errors/index.js";
11
+ /**
12
+ * Default boundaries, in milliseconds, covering sub-millisecond calls through
13
+ * ten-second ones. Pass your own when the unit is not latency.
14
+ */
15
+ export const DEFAULT_BUCKET_BOUNDARIES = [
16
+ 1, 2, 5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000,
17
+ ];
18
+ /**
19
+ * Validates and canonicalises bucket boundaries.
20
+ *
21
+ * A `NaN` boundary makes `sort` leave the list in an arbitrary order, and
22
+ * every quantile computed from it comes back `NaN` — a latency histogram that
23
+ * reports nothing while looking like it works. Duplicates create zero-width
24
+ * buckets that skew interpolation, and an empty list leaves a single overflow
25
+ * bucket that cannot answer a percentile at all.
6
26
  */
27
+ function normalizeBoundaries(name, boundaries) {
28
+ for (const boundary of boundaries) {
29
+ if (!Number.isFinite(boundary)) {
30
+ throw new MetricValueError(name, boundary, "histogram bucket boundaries must all be finite numbers");
31
+ }
32
+ }
33
+ const sorted = [...new Set(boundaries)].sort((a, b) => a - b);
34
+ if (sorted.length === 0) {
35
+ throw new MetricValueError(name, 0, "a histogram needs at least one bucket boundary");
36
+ }
37
+ return sorted;
38
+ }
7
39
  /**
8
- * In-memory histogram. Tracks count, sum, min, and max of observed values.
40
+ * In-memory histogram with cumulative buckets and interpolated quantiles.
9
41
  */
10
42
  export class DefaultHistogram {
11
43
  name;
12
44
  labels;
45
+ boundaries;
46
+ /** Counts per bucket; one slot longer than `boundaries` for the overflow. */
47
+ counts;
13
48
  count = 0;
14
49
  sum = 0;
15
50
  min = Infinity;
16
51
  max = -Infinity;
17
- constructor(name, labels) {
52
+ constructor(name, labels, boundaries = DEFAULT_BUCKET_BOUNDARIES) {
18
53
  this.name = name;
19
54
  this.labels = labels;
55
+ this.boundaries = normalizeBoundaries(name, boundaries);
56
+ this.counts = new Array(this.boundaries.length + 1).fill(0);
20
57
  }
21
58
  record(value) {
59
+ if (!Number.isFinite(value)) {
60
+ throw new MetricValueError(this.name, value, "must be finite");
61
+ }
22
62
  this.count++;
23
63
  this.sum += value;
24
64
  if (value < this.min)
25
65
  this.min = value;
26
66
  if (value > this.max)
27
67
  this.max = value;
68
+ let bucket = this.boundaries.length;
69
+ for (let i = 0; i < this.boundaries.length; i++) {
70
+ if (value <= this.boundaries[i]) {
71
+ bucket = i;
72
+ break;
73
+ }
74
+ }
75
+ this.counts[bucket] += 1;
28
76
  }
29
77
  getValue() {
78
+ const empty = this.count === 0;
79
+ let cumulative = 0;
80
+ const buckets = this.boundaries.map((le, i) => {
81
+ cumulative += this.counts[i];
82
+ return { le, count: cumulative };
83
+ });
30
84
  return {
31
85
  count: this.count,
32
86
  sum: this.sum,
33
- min: this.count === 0 ? 0 : this.min,
34
- max: this.count === 0 ? 0 : this.max,
87
+ min: empty ? 0 : this.min,
88
+ max: empty ? 0 : this.max,
89
+ mean: empty ? 0 : this.sum / this.count,
90
+ buckets,
91
+ p50: this.percentile(0.5),
92
+ p90: this.percentile(0.9),
93
+ p95: this.percentile(0.95),
94
+ p99: this.percentile(0.99),
35
95
  };
36
96
  }
97
+ /**
98
+ * Estimates a quantile by linear interpolation inside the bucket the
99
+ * quantile falls in, clamped to the observed min and max.
100
+ */
101
+ percentile(q) {
102
+ if (!Number.isFinite(q) || q < 0 || q > 1) {
103
+ throw new MetricValueError(this.name, q, "quantile must be in [0, 1]");
104
+ }
105
+ if (this.count === 0)
106
+ return 0;
107
+ const target = q * this.count;
108
+ let cumulative = 0;
109
+ let lowerBound = this.min;
110
+ for (let i = 0; i < this.counts.length; i++) {
111
+ const inBucket = this.counts[i];
112
+ if (inBucket === 0) {
113
+ if (i < this.boundaries.length)
114
+ lowerBound = this.boundaries[i];
115
+ continue;
116
+ }
117
+ const upperBound = i < this.boundaries.length ? this.boundaries[i] : this.max;
118
+ if (cumulative + inBucket >= target) {
119
+ const within = (target - cumulative) / inBucket;
120
+ const low = Math.max(lowerBound, this.min);
121
+ const high = Math.min(upperBound, this.max);
122
+ if (high <= low)
123
+ return high;
124
+ return low + within * (high - low);
125
+ }
126
+ cumulative += inBucket;
127
+ lowerBound = upperBound;
128
+ }
129
+ return this.max;
130
+ }
37
131
  reset() {
38
132
  this.count = 0;
39
133
  this.sum = 0;
40
134
  this.min = Infinity;
41
135
  this.max = -Infinity;
136
+ this.counts = new Array(this.boundaries.length + 1).fill(0);
42
137
  }
43
138
  }
44
139
  /** Creates a histogram. */
45
- export function createHistogram(name, labels) {
46
- return new DefaultHistogram(name, labels);
140
+ export function createHistogram(name, labels, boundaries) {
141
+ return new DefaultHistogram(name, labels, boundaries);
47
142
  }
48
143
  //# sourceMappingURL=histogram.core.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Distribution of observed values for latencies and sizes.
5
5
  */
6
- export { DefaultHistogram, createHistogram } from "./histogram.core.js";
6
+ export { DefaultHistogram, createHistogram, DEFAULT_BUCKET_BOUNDARIES, } from "./histogram.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Distribution of observed values for latencies and sizes.
5
5
  */
6
- export { DefaultHistogram, createHistogram } from "./histogram.core.js";
6
+ export { DefaultHistogram, createHistogram, DEFAULT_BUCKET_BOUNDARIES, } from "./histogram.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * @zudojs/observability — Metrics
3
3
  *
4
- * Counters, gauges, histograms, and the metrics registry.
4
+ * Counters, gauges, histograms, the registry, and the periodic reader.
5
5
  */
6
6
  export { DefaultCounter, createCounter } from "./counter/index.js";
7
7
  export { DefaultGauge, createGauge } from "./gauge/index.js";
8
- export { DefaultHistogram, createHistogram } from "./histogram/index.js";
9
- export { DefaultMetricsRegistry, createMetricsRegistry, } from "./metrics.registry.js";
8
+ export { DefaultHistogram, createHistogram, DEFAULT_BUCKET_BOUNDARIES, } from "./histogram/index.js";
9
+ export { DefaultMetricsRegistry, createMetricsRegistry, metricKey, type MetricsRegistryOptions, } from "./metrics.registry.js";
10
+ export { PeriodicMetricReader, createPeriodicMetricReader, type PeriodicMetricReaderOptions, } from "./metrics.reader.js";
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * @zudojs/observability — Metrics
3
3
  *
4
- * Counters, gauges, histograms, and the metrics registry.
4
+ * Counters, gauges, histograms, the registry, and the periodic reader.
5
5
  */
6
6
  export { DefaultCounter, createCounter } from "./counter/index.js";
7
7
  export { DefaultGauge, createGauge } from "./gauge/index.js";
8
- export { DefaultHistogram, createHistogram } from "./histogram/index.js";
9
- export { DefaultMetricsRegistry, createMetricsRegistry, } from "./metrics.registry.js";
8
+ export { DefaultHistogram, createHistogram, DEFAULT_BUCKET_BOUNDARIES, } from "./histogram/index.js";
9
+ export { DefaultMetricsRegistry, createMetricsRegistry, metricKey, } from "./metrics.registry.js";
10
+ export { PeriodicMetricReader, createPeriodicMetricReader, } from "./metrics.reader.js";
10
11
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,39 @@
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
+ import type { MetricExporter, MetricsRegistry } from "../types.js";
10
+ /** Options for {@link PeriodicMetricReader}. */
11
+ export interface PeriodicMetricReaderOptions {
12
+ readonly registry: MetricsRegistry;
13
+ readonly exporter: MetricExporter;
14
+ /** Export interval in ms. Default: 60,000. `0` disables the timer. */
15
+ readonly intervalMs?: number;
16
+ /** Reports export failures. */
17
+ readonly onError?: (error: unknown, source: string) => void;
18
+ }
19
+ /** Snapshots a registry on an interval and exports the result. */
20
+ export declare class PeriodicMetricReader {
21
+ private readonly registry;
22
+ private readonly exporter;
23
+ private readonly intervalMs;
24
+ private readonly onError?;
25
+ private timer?;
26
+ private started;
27
+ private shuttingDown;
28
+ private inFlight?;
29
+ constructor(options: PeriodicMetricReaderOptions);
30
+ /** Begins periodic export. Calling it twice is a no-op. */
31
+ start(): void;
32
+ /** Exports one snapshot immediately. */
33
+ collect(): Promise<void>;
34
+ /** Exports a final snapshot and stops. Safe to call more than once. */
35
+ shutdown(): Promise<void>;
36
+ }
37
+ /** Creates a periodic metric reader. */
38
+ export declare function createPeriodicMetricReader(options: PeriodicMetricReaderOptions): PeriodicMetricReader;
39
+ //# sourceMappingURL=metrics.reader.d.ts.map
@@ -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
- getCounter(name: string): Counter | undefined;
16
- getGauge(name: string): Gauge | undefined;
17
- getHistogram(name: string): Histogram | undefined;
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