@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
@@ -4,46 +4,68 @@
4
4
  * Factory functions for creating structured log records.
5
5
  */
6
6
  import { LogLevel } from "../types.js";
7
+ import { logLevelToName } from "../logLevel/index.js";
8
+ /**
9
+ * Serializes a thrown value into something a JSON transport can carry.
10
+ *
11
+ * `Error`'s own fields are non-enumerable, so an error placed in a log
12
+ * context stringifies to `{}` — this is what turns it back into data.
13
+ * `cause` chains are followed, with a depth cap so a self-referential cause
14
+ * cannot recurse forever.
15
+ */
16
+ export function serializeError(error, depth = 0) {
17
+ if (error instanceof Error) {
18
+ const cause = error.cause !== undefined && depth < 4
19
+ ? serializeError(error.cause, depth + 1)
20
+ : undefined;
21
+ return {
22
+ name: error.name,
23
+ message: error.message,
24
+ stack: error.stack,
25
+ cause,
26
+ };
27
+ }
28
+ if (typeof error === "object" && error !== null) {
29
+ const record = error;
30
+ const name = typeof record["name"] === "string" ? record["name"] : "Error";
31
+ const message = typeof record["message"] === "string"
32
+ ? record["message"]
33
+ : safeStringify(error);
34
+ return { name, message };
35
+ }
36
+ return { name: "Error", message: String(error) };
37
+ }
38
+ function safeStringify(value) {
39
+ try {
40
+ return JSON.stringify(value) ?? String(value);
41
+ }
42
+ catch {
43
+ return String(value);
44
+ }
45
+ }
7
46
  /** Creates a structured log record. */
8
47
  export function createLogRecord(options) {
9
- const error = options.error
10
- ? {
11
- name: options.error.name,
12
- message: options.error.message,
13
- stack: options.error.stack,
14
- cause: options.error.cause,
15
- }
16
- : undefined;
17
48
  return {
18
49
  level: options.level,
19
- levelName: logLevelToNameInternal(options.level),
50
+ levelName: logLevelToName(options.level),
20
51
  message: options.message,
21
- timestamp: new Date(),
52
+ timestamp: options.timestamp ?? new Date(),
22
53
  loggerName: options.loggerName,
23
54
  context: options.context,
24
- error,
55
+ error: options.error === undefined ? undefined : serializeError(options.error),
56
+ traceId: options.traceId,
57
+ spanId: options.spanId,
25
58
  };
26
59
  }
27
60
  /** Creates a log record for an error. */
28
- export function createErrorLogRecord(error, level, loggerName) {
61
+ export function createErrorLogRecord(error, level, loggerName, context) {
62
+ const serialized = serializeError(error);
29
63
  return createLogRecord({
30
64
  level,
31
- message: error.message,
65
+ message: serialized.message,
32
66
  loggerName,
33
- context: { stack: error.stack },
67
+ context,
34
68
  error,
35
69
  });
36
70
  }
37
- function logLevelToNameInternal(level) {
38
- const names = {
39
- [LogLevel.TRACE]: "trace",
40
- [LogLevel.DEBUG]: "debug",
41
- [LogLevel.INFO]: "info",
42
- [LogLevel.WARN]: "warn",
43
- [LogLevel.ERROR]: "error",
44
- [LogLevel.FATAL]: "fatal",
45
- [LogLevel.OFF]: "off",
46
- };
47
- return names[level] ?? "off";
48
- }
49
71
  //# sourceMappingURL=logRecord.core.js.map
@@ -2,26 +2,34 @@
2
2
  * @zudojs/observability — Logger Core
3
3
  *
4
4
  * Structured logger implementation with level filtering, transport support,
5
- * child loggers, and persistent context.
5
+ * child loggers, persistent context, trace correlation and redaction.
6
6
  */
7
7
  import type { Logger, LoggerOptions } from "../types.js";
8
8
  import { LogLevel } from "../types.js";
9
9
  /**
10
10
  * Core structured logger with level filtering, child loggers,
11
11
  * persistent context, and transport support.
12
+ *
13
+ * Every record is stamped with the ambient `traceId`/`spanId` when a
14
+ * propagation context is active, so logs and traces line up without the
15
+ * caller threading IDs through by hand.
12
16
  */
13
17
  export declare class StructuredLogger implements Logger {
14
18
  readonly name: string;
15
- readonly level: LogLevel;
19
+ private currentLevel;
16
20
  private readonly context;
17
21
  private readonly transport;
22
+ private readonly correlate;
23
+ private readonly redact?;
18
24
  constructor(options: LoggerOptions);
19
- trace(message: string, context?: Record<string, unknown>): void;
20
- debug(message: string, context?: Record<string, unknown>): void;
21
- info(message: string, context?: Record<string, unknown>): void;
22
- warn(message: string, context?: Record<string, unknown>): void;
23
- error(message: string, context?: Record<string, unknown>): void;
24
- fatal(message: string, context?: Record<string, unknown>): void;
25
+ get level(): LogLevel;
26
+ setLevel(level: LogLevel): void;
27
+ trace(message: string, context?: Record<string, unknown>, error?: unknown): void;
28
+ debug(message: string, context?: Record<string, unknown>, error?: unknown): void;
29
+ info(message: string, context?: Record<string, unknown>, error?: unknown): void;
30
+ warn(message: string, context?: Record<string, unknown>, error?: unknown): void;
31
+ error(message: string, context?: Record<string, unknown>, error?: unknown): void;
32
+ fatal(message: string, context?: Record<string, unknown>, error?: unknown): void;
25
33
  child(name: string, context?: Record<string, unknown>): Logger;
26
34
  isLevelEnabled(level: LogLevel): boolean;
27
35
  flush(): Promise<void>;
@@ -2,11 +2,12 @@
2
2
  * @zudojs/observability — Logger Core
3
3
  *
4
4
  * Structured logger implementation with level filtering, transport support,
5
- * child loggers, and persistent context.
5
+ * child loggers, persistent context, trace correlation and redaction.
6
6
  */
7
7
  import { LogLevel } from "../types.js";
8
- import { shouldLog, logLevelToName } from "../logLevel/index.js";
8
+ import { shouldLog } from "../logLevel/index.js";
9
9
  import { createLogRecord } from "../logRecord/index.js";
10
+ import { getCurrentContext } from "../propagation/index.js";
10
11
  /** Default no-op transport that discards all records. */
11
12
  const noopTransport = {
12
13
  name: "noop",
@@ -15,63 +16,93 @@ const noopTransport = {
15
16
  /**
16
17
  * Core structured logger with level filtering, child loggers,
17
18
  * persistent context, and transport support.
19
+ *
20
+ * Every record is stamped with the ambient `traceId`/`spanId` when a
21
+ * propagation context is active, so logs and traces line up without the
22
+ * caller threading IDs through by hand.
18
23
  */
19
24
  export class StructuredLogger {
20
25
  name;
21
- level;
26
+ currentLevel;
22
27
  context;
23
28
  transport;
29
+ correlate;
30
+ redact;
24
31
  constructor(options) {
25
32
  this.name = options.name;
26
- this.level = options.level ?? LogLevel.INFO;
33
+ this.currentLevel = options.level ?? LogLevel.INFO;
27
34
  this.context = { ...(options.context ?? {}) };
28
35
  this.transport = options.transport ?? noopTransport;
36
+ this.correlate = options.correlate ?? true;
37
+ this.redact = options.redact;
38
+ }
39
+ get level() {
40
+ return this.currentLevel;
41
+ }
42
+ setLevel(level) {
43
+ this.currentLevel = level;
29
44
  }
30
- trace(message, context) {
31
- this.log(LogLevel.TRACE, message, context);
45
+ trace(message, context, error) {
46
+ this.log(LogLevel.TRACE, message, context, error);
32
47
  }
33
- debug(message, context) {
34
- this.log(LogLevel.DEBUG, message, context);
48
+ debug(message, context, error) {
49
+ this.log(LogLevel.DEBUG, message, context, error);
35
50
  }
36
- info(message, context) {
37
- this.log(LogLevel.INFO, message, context);
51
+ info(message, context, error) {
52
+ this.log(LogLevel.INFO, message, context, error);
38
53
  }
39
- warn(message, context) {
40
- this.log(LogLevel.WARN, message, context);
54
+ warn(message, context, error) {
55
+ this.log(LogLevel.WARN, message, context, error);
41
56
  }
42
- error(message, context) {
43
- this.log(LogLevel.ERROR, message, context);
57
+ error(message, context, error) {
58
+ this.log(LogLevel.ERROR, message, context, error);
44
59
  }
45
- fatal(message, context) {
46
- this.log(LogLevel.FATAL, message, context);
60
+ fatal(message, context, error) {
61
+ this.log(LogLevel.FATAL, message, context, error);
47
62
  }
48
63
  child(name, context) {
49
64
  const childName = `${this.name}.${name}`;
50
65
  const childContext = { ...this.context, ...(context ?? {}) };
51
66
  return new StructuredLogger({
52
67
  name: childName,
53
- level: this.level,
68
+ level: this.currentLevel,
54
69
  context: childContext,
55
70
  transport: this.transport,
71
+ correlate: this.correlate,
72
+ redact: this.redact,
56
73
  });
57
74
  }
58
75
  isLevelEnabled(level) {
59
- return shouldLog(this.level, level);
76
+ return shouldLog(this.currentLevel, level);
60
77
  }
61
78
  async flush() {
62
- // Transport-specific flush; default is no-op
79
+ await this.transport.flush?.();
63
80
  }
64
- log(level, message, context) {
81
+ log(level, message, context, error) {
65
82
  if (!this.isLevelEnabled(level))
66
83
  return;
67
- const mergedContext = { ...this.context, ...(context ?? {}) };
84
+ const merged = { ...this.context, ...(context ?? {}) };
85
+ const redacted = this.redact ? this.redact(merged) : merged;
86
+ const propagation = this.correlate ? getCurrentContext() : undefined;
68
87
  const record = createLogRecord({
69
88
  level,
70
89
  message,
71
90
  loggerName: this.name,
72
- context: Object.keys(mergedContext).length > 0 ? mergedContext : undefined,
91
+ context: Object.keys(redacted).length > 0 ? redacted : undefined,
92
+ error,
93
+ traceId: propagation?.traceId,
94
+ spanId: propagation?.spanId,
73
95
  });
74
- this.transport.write(record);
96
+ // A transport must never take the caller down with it. A failing sink is
97
+ // reported through the transport's own channel, not raised here.
98
+ try {
99
+ const written = this.transport.write(record);
100
+ if (written instanceof Promise)
101
+ written.catch(() => { });
102
+ }
103
+ catch {
104
+ // Intentionally swallowed: logging is not allowed to throw.
105
+ }
75
106
  }
76
107
  }
77
108
  /** Creates a structured logger. */
@@ -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