@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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -13
  3. package/dist/errors/index.d.ts +5 -0
  4. package/dist/errors/index.js +5 -0
  5. package/dist/errors/observabilityError.core.d.ts +32 -0
  6. package/dist/errors/observabilityError.core.js +56 -0
  7. package/dist/exporter/exporter.console.d.ts +51 -3
  8. package/dist/exporter/exporter.console.js +110 -23
  9. package/dist/exporter/index.d.ts +2 -2
  10. package/dist/exporter/index.js +2 -2
  11. package/dist/index.d.ts +17 -11
  12. package/dist/index.js +20 -12
  13. package/dist/internal/ids.core.d.ts +26 -0
  14. package/dist/internal/ids.core.js +51 -0
  15. package/dist/internal/index.d.ts +5 -0
  16. package/dist/internal/index.js +5 -0
  17. package/dist/logLevel/index.d.ts +1 -1
  18. package/dist/logLevel/index.js +1 -1
  19. package/dist/logLevel/logLevel.type.d.ts +8 -2
  20. package/dist/logLevel/logLevel.type.js +27 -20
  21. package/dist/logRecord/index.d.ts +1 -1
  22. package/dist/logRecord/index.js +1 -1
  23. package/dist/logRecord/logRecord.core.d.ts +15 -3
  24. package/dist/logRecord/logRecord.core.js +48 -26
  25. package/dist/logger/logger.core.d.ts +16 -8
  26. package/dist/logger/logger.core.js +54 -23
  27. package/dist/metrics/counter/counter.core.d.ts +4 -1
  28. package/dist/metrics/counter/counter.core.js +11 -3
  29. package/dist/metrics/gauge/gauge.core.d.ts +1 -0
  30. package/dist/metrics/gauge/gauge.core.js +9 -0
  31. package/dist/metrics/histogram/histogram.core.d.ts +22 -11
  32. package/dist/metrics/histogram/histogram.core.js +102 -7
  33. package/dist/metrics/histogram/index.d.ts +1 -1
  34. package/dist/metrics/histogram/index.js +1 -1
  35. package/dist/metrics/index.d.ts +4 -3
  36. package/dist/metrics/index.js +4 -3
  37. package/dist/metrics/metrics.reader.d.ts +39 -0
  38. package/dist/metrics/metrics.reader.js +81 -0
  39. package/dist/metrics/metrics.registry.d.ts +55 -4
  40. package/dist/metrics/metrics.registry.js +136 -49
  41. package/dist/noop/index.d.ts +1 -1
  42. package/dist/noop/index.js +1 -1
  43. package/dist/noop/noopObservability.core.d.ts +14 -2
  44. package/dist/noop/noopObservability.core.js +48 -11
  45. package/dist/observability/observability.core.d.ts +57 -3
  46. package/dist/observability/observability.core.js +206 -50
  47. package/dist/processor/index.d.ts +4 -3
  48. package/dist/processor/index.js +4 -3
  49. package/dist/processor/processor.batch.d.ts +66 -11
  50. package/dist/processor/processor.batch.js +130 -29
  51. package/dist/processor/processor.log.d.ts +56 -0
  52. package/dist/processor/processor.log.js +119 -0
  53. package/dist/propagation/index.d.ts +1 -1
  54. package/dist/propagation/index.js +1 -1
  55. package/dist/propagation/propagation.core.d.ts +26 -6
  56. package/dist/propagation/propagation.core.js +35 -19
  57. package/dist/redaction/index.d.ts +1 -1
  58. package/dist/redaction/index.js +1 -1
  59. package/dist/redaction/redaction.core.d.ts +37 -5
  60. package/dist/redaction/redaction.core.js +180 -36
  61. package/dist/sampling/index.d.ts +1 -1
  62. package/dist/sampling/index.js +1 -1
  63. package/dist/sampling/sampler.type.d.ts +38 -6
  64. package/dist/sampling/sampler.type.js +64 -23
  65. package/dist/tracing/index.d.ts +2 -2
  66. package/dist/tracing/index.js +2 -2
  67. package/dist/tracing/span/index.d.ts +1 -1
  68. package/dist/tracing/span/index.js +1 -1
  69. package/dist/tracing/span/span.core.d.ts +51 -8
  70. package/dist/tracing/span/span.core.js +90 -10
  71. package/dist/tracing/span/spanContext.type.d.ts +12 -3
  72. package/dist/tracing/span/spanContext.type.js +19 -16
  73. package/dist/tracing/tracer/index.d.ts +2 -2
  74. package/dist/tracing/tracer/index.js +2 -2
  75. package/dist/tracing/tracer/tracer.core.d.ts +44 -14
  76. package/dist/tracing/tracer/tracer.core.js +81 -25
  77. package/dist/types/config.types.d.ts +138 -0
  78. package/dist/types/config.types.js +5 -0
  79. package/dist/types/logging.types.d.ts +94 -0
  80. package/dist/types/logging.types.js +20 -0
  81. package/dist/types/metrics.types.d.ts +93 -0
  82. package/dist/types/metrics.types.js +5 -0
  83. package/dist/types/tracing.types.d.ts +115 -0
  84. package/dist/types/tracing.types.js +28 -0
  85. package/package.json +23 -14
  86. package/dist/exporter/exporter.console.d.ts.map +0 -1
  87. package/dist/exporter/exporter.console.js.map +0 -1
  88. package/dist/exporter/index.d.ts.map +0 -1
  89. package/dist/exporter/index.js.map +0 -1
  90. package/dist/index.d.ts.map +0 -1
  91. package/dist/index.js.map +0 -1
  92. package/dist/logLevel/index.d.ts.map +0 -1
  93. package/dist/logLevel/index.js.map +0 -1
  94. package/dist/logLevel/logLevel.type.d.ts.map +0 -1
  95. package/dist/logLevel/logLevel.type.js.map +0 -1
  96. package/dist/logRecord/index.d.ts.map +0 -1
  97. package/dist/logRecord/index.js.map +0 -1
  98. package/dist/logRecord/logRecord.core.d.ts.map +0 -1
  99. package/dist/logRecord/logRecord.core.js.map +0 -1
  100. package/dist/logger/index.d.ts.map +0 -1
  101. package/dist/logger/index.js.map +0 -1
  102. package/dist/logger/logger.core.d.ts.map +0 -1
  103. package/dist/logger/logger.core.js.map +0 -1
  104. package/dist/metrics/counter/counter.core.d.ts.map +0 -1
  105. package/dist/metrics/counter/counter.core.js.map +0 -1
  106. package/dist/metrics/counter/index.d.ts.map +0 -1
  107. package/dist/metrics/counter/index.js.map +0 -1
  108. package/dist/metrics/gauge/gauge.core.d.ts.map +0 -1
  109. package/dist/metrics/gauge/gauge.core.js.map +0 -1
  110. package/dist/metrics/gauge/index.d.ts.map +0 -1
  111. package/dist/metrics/gauge/index.js.map +0 -1
  112. package/dist/metrics/histogram/histogram.core.d.ts.map +0 -1
  113. package/dist/metrics/histogram/histogram.core.js.map +0 -1
  114. package/dist/metrics/histogram/index.d.ts.map +0 -1
  115. package/dist/metrics/histogram/index.js.map +0 -1
  116. package/dist/metrics/index.d.ts.map +0 -1
  117. package/dist/metrics/index.js.map +0 -1
  118. package/dist/metrics/metrics.registry.d.ts.map +0 -1
  119. package/dist/metrics/metrics.registry.js.map +0 -1
  120. package/dist/noop/index.d.ts.map +0 -1
  121. package/dist/noop/index.js.map +0 -1
  122. package/dist/noop/noopObservability.core.d.ts.map +0 -1
  123. package/dist/noop/noopObservability.core.js.map +0 -1
  124. package/dist/observability/index.d.ts.map +0 -1
  125. package/dist/observability/index.js.map +0 -1
  126. package/dist/observability/observability.core.d.ts.map +0 -1
  127. package/dist/observability/observability.core.js.map +0 -1
  128. package/dist/processor/index.d.ts.map +0 -1
  129. package/dist/processor/index.js.map +0 -1
  130. package/dist/processor/processor.batch.d.ts.map +0 -1
  131. package/dist/processor/processor.batch.js.map +0 -1
  132. package/dist/propagation/index.d.ts.map +0 -1
  133. package/dist/propagation/index.js.map +0 -1
  134. package/dist/propagation/propagation.core.d.ts.map +0 -1
  135. package/dist/propagation/propagation.core.js.map +0 -1
  136. package/dist/redaction/index.d.ts.map +0 -1
  137. package/dist/redaction/index.js.map +0 -1
  138. package/dist/redaction/redaction.core.d.ts.map +0 -1
  139. package/dist/redaction/redaction.core.js.map +0 -1
  140. package/dist/sampling/index.d.ts.map +0 -1
  141. package/dist/sampling/index.js.map +0 -1
  142. package/dist/sampling/sampler.type.d.ts.map +0 -1
  143. package/dist/sampling/sampler.type.js.map +0 -1
  144. package/dist/tracing/index.d.ts.map +0 -1
  145. package/dist/tracing/index.js.map +0 -1
  146. package/dist/tracing/span/index.d.ts.map +0 -1
  147. package/dist/tracing/span/index.js.map +0 -1
  148. package/dist/tracing/span/span.core.d.ts.map +0 -1
  149. package/dist/tracing/span/span.core.js.map +0 -1
  150. package/dist/tracing/span/spanContext.type.d.ts.map +0 -1
  151. package/dist/tracing/span/spanContext.type.js.map +0 -1
  152. package/dist/tracing/tracer/index.d.ts.map +0 -1
  153. package/dist/tracing/tracer/index.js.map +0 -1
  154. package/dist/tracing/tracer/tracer.core.d.ts.map +0 -1
  155. package/dist/tracing/tracer/tracer.core.js.map +0 -1
  156. package/dist/types.d.ts.map +0 -1
  157. 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
- 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
@@ -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
- function metricKey(type, name, labels) {
10
- const labelStr = labels
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
- .sort(([a], [b]) => a.localeCompare(b))
13
- .map(([k, v]) => `${k}=${v}`)
14
- .join(",")
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 key = metricKey("counter", name, labels);
25
- let entry = this.metrics.get(key);
26
- if (!entry || entry.type !== "counter") {
27
- entry = { type: "counter", metric: new DefaultCounter(name, labels) };
28
- this.metrics.set(key, entry);
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 key = metricKey("gauge", name, labels);
34
- let entry = this.metrics.get(key);
35
- if (!entry || entry.type !== "gauge") {
36
- entry = { type: "gauge", metric: new DefaultGauge(name, labels) };
37
- this.metrics.set(key, entry);
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 key = metricKey("histogram", name, labels);
43
- let entry = this.metrics.get(key);
44
- if (!entry || entry.type !== "histogram") {
45
- entry = { type: "histogram", metric: new DefaultHistogram(name, labels) };
46
- this.metrics.set(key, entry);
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
- getCounter(name) {
51
- for (const entry of this.metrics.values()) {
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
- getGauge(name) {
58
- for (const entry of this.metrics.values()) {
59
- if (entry.type === "gauge" && entry.metric.name === name)
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
- getHistogram(name) {
65
- for (const entry of this.metrics.values()) {
66
- if (entry.type === "histogram" && entry.metric.name === name)
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
- getAll() {
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
- snapshots.push({
75
- name: entry.metric.name,
76
- type: entry.type,
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
@@ -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
@@ -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: () => ({ count: 0, sum: 0, min: 0, max: 0 }),
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: { traceId: "", spanId: "" },
60
- startTime: new Date(),
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: () => noopContext,
108
+ current: () => undefined,
80
109
  run: async (_ctx, fn) => fn(),
81
- derive: () => noopContext,
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
- private readonly shutdownHooks;
22
- constructor(config: ObservabilityConfig);
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