@zudojs/observability 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +260 -13
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors/observabilityError.core.d.ts +32 -0
- package/dist/errors/observabilityError.core.js +56 -0
- package/dist/exporter/exporter.console.d.ts +51 -3
- package/dist/exporter/exporter.console.js +110 -23
- package/dist/exporter/index.d.ts +2 -2
- package/dist/exporter/index.js +2 -2
- package/dist/index.d.ts +17 -11
- package/dist/index.js +20 -12
- package/dist/internal/ids.core.d.ts +26 -0
- package/dist/internal/ids.core.js +51 -0
- package/dist/internal/index.d.ts +5 -0
- package/dist/internal/index.js +5 -0
- package/dist/logLevel/index.d.ts +1 -1
- package/dist/logLevel/index.js +1 -1
- package/dist/logLevel/logLevel.type.d.ts +8 -2
- package/dist/logLevel/logLevel.type.js +27 -20
- package/dist/logRecord/index.d.ts +1 -1
- package/dist/logRecord/index.js +1 -1
- package/dist/logRecord/logRecord.core.d.ts +15 -3
- package/dist/logRecord/logRecord.core.js +48 -26
- package/dist/logger/logger.core.d.ts +16 -8
- package/dist/logger/logger.core.js +54 -23
- package/dist/metrics/counter/counter.core.d.ts +4 -1
- package/dist/metrics/counter/counter.core.js +11 -3
- package/dist/metrics/gauge/gauge.core.d.ts +1 -0
- package/dist/metrics/gauge/gauge.core.js +9 -0
- package/dist/metrics/histogram/histogram.core.d.ts +22 -11
- package/dist/metrics/histogram/histogram.core.js +102 -7
- package/dist/metrics/histogram/index.d.ts +1 -1
- package/dist/metrics/histogram/index.js +1 -1
- package/dist/metrics/index.d.ts +4 -3
- package/dist/metrics/index.js +4 -3
- package/dist/metrics/metrics.reader.d.ts +39 -0
- package/dist/metrics/metrics.reader.js +81 -0
- package/dist/metrics/metrics.registry.d.ts +55 -4
- package/dist/metrics/metrics.registry.js +136 -49
- package/dist/noop/index.d.ts +1 -1
- package/dist/noop/index.js +1 -1
- package/dist/noop/noopObservability.core.d.ts +14 -2
- package/dist/noop/noopObservability.core.js +48 -11
- package/dist/observability/observability.core.d.ts +57 -3
- package/dist/observability/observability.core.js +206 -50
- package/dist/processor/index.d.ts +4 -3
- package/dist/processor/index.js +4 -3
- package/dist/processor/processor.batch.d.ts +66 -11
- package/dist/processor/processor.batch.js +130 -29
- package/dist/processor/processor.log.d.ts +56 -0
- package/dist/processor/processor.log.js +119 -0
- package/dist/propagation/index.d.ts +1 -1
- package/dist/propagation/index.js +1 -1
- package/dist/propagation/propagation.core.d.ts +26 -6
- package/dist/propagation/propagation.core.js +35 -19
- package/dist/redaction/index.d.ts +1 -1
- package/dist/redaction/index.js +1 -1
- package/dist/redaction/redaction.core.d.ts +37 -5
- package/dist/redaction/redaction.core.js +180 -36
- package/dist/sampling/index.d.ts +1 -1
- package/dist/sampling/index.js +1 -1
- package/dist/sampling/sampler.type.d.ts +38 -6
- package/dist/sampling/sampler.type.js +64 -23
- package/dist/tracing/index.d.ts +2 -2
- package/dist/tracing/index.js +2 -2
- package/dist/tracing/span/index.d.ts +1 -1
- package/dist/tracing/span/index.js +1 -1
- package/dist/tracing/span/span.core.d.ts +51 -8
- package/dist/tracing/span/span.core.js +90 -10
- package/dist/tracing/span/spanContext.type.d.ts +12 -3
- package/dist/tracing/span/spanContext.type.js +19 -16
- package/dist/tracing/tracer/index.d.ts +2 -2
- package/dist/tracing/tracer/index.js +2 -2
- package/dist/tracing/tracer/tracer.core.d.ts +44 -14
- package/dist/tracing/tracer/tracer.core.js +81 -25
- package/dist/types/config.types.d.ts +138 -0
- package/dist/types/config.types.js +5 -0
- package/dist/types/logging.types.d.ts +94 -0
- package/dist/types/logging.types.js +20 -0
- package/dist/types/metrics.types.d.ts +93 -0
- package/dist/types/metrics.types.js +5 -0
- package/dist/types/tracing.types.d.ts +115 -0
- package/dist/types/tracing.types.js +28 -0
- package/package.json +23 -14
- package/dist/exporter/exporter.console.d.ts.map +0 -1
- package/dist/exporter/exporter.console.js.map +0 -1
- package/dist/exporter/index.d.ts.map +0 -1
- package/dist/exporter/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logLevel/index.d.ts.map +0 -1
- package/dist/logLevel/index.js.map +0 -1
- package/dist/logLevel/logLevel.type.d.ts.map +0 -1
- package/dist/logLevel/logLevel.type.js.map +0 -1
- package/dist/logRecord/index.d.ts.map +0 -1
- package/dist/logRecord/index.js.map +0 -1
- package/dist/logRecord/logRecord.core.d.ts.map +0 -1
- package/dist/logRecord/logRecord.core.js.map +0 -1
- package/dist/logger/index.d.ts.map +0 -1
- package/dist/logger/index.js.map +0 -1
- package/dist/logger/logger.core.d.ts.map +0 -1
- package/dist/logger/logger.core.js.map +0 -1
- package/dist/metrics/counter/counter.core.d.ts.map +0 -1
- package/dist/metrics/counter/counter.core.js.map +0 -1
- package/dist/metrics/counter/index.d.ts.map +0 -1
- package/dist/metrics/counter/index.js.map +0 -1
- package/dist/metrics/gauge/gauge.core.d.ts.map +0 -1
- package/dist/metrics/gauge/gauge.core.js.map +0 -1
- package/dist/metrics/gauge/index.d.ts.map +0 -1
- package/dist/metrics/gauge/index.js.map +0 -1
- package/dist/metrics/histogram/histogram.core.d.ts.map +0 -1
- package/dist/metrics/histogram/histogram.core.js.map +0 -1
- package/dist/metrics/histogram/index.d.ts.map +0 -1
- package/dist/metrics/histogram/index.js.map +0 -1
- package/dist/metrics/index.d.ts.map +0 -1
- package/dist/metrics/index.js.map +0 -1
- package/dist/metrics/metrics.registry.d.ts.map +0 -1
- package/dist/metrics/metrics.registry.js.map +0 -1
- package/dist/noop/index.d.ts.map +0 -1
- package/dist/noop/index.js.map +0 -1
- package/dist/noop/noopObservability.core.d.ts.map +0 -1
- package/dist/noop/noopObservability.core.js.map +0 -1
- package/dist/observability/index.d.ts.map +0 -1
- package/dist/observability/index.js.map +0 -1
- package/dist/observability/observability.core.d.ts.map +0 -1
- package/dist/observability/observability.core.js.map +0 -1
- package/dist/processor/index.d.ts.map +0 -1
- package/dist/processor/index.js.map +0 -1
- package/dist/processor/processor.batch.d.ts.map +0 -1
- package/dist/processor/processor.batch.js.map +0 -1
- package/dist/propagation/index.d.ts.map +0 -1
- package/dist/propagation/index.js.map +0 -1
- package/dist/propagation/propagation.core.d.ts.map +0 -1
- package/dist/propagation/propagation.core.js.map +0 -1
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/redaction.core.d.ts.map +0 -1
- package/dist/redaction/redaction.core.js.map +0 -1
- package/dist/sampling/index.d.ts.map +0 -1
- package/dist/sampling/index.js.map +0 -1
- package/dist/sampling/sampler.type.d.ts.map +0 -1
- package/dist/sampling/sampler.type.js.map +0 -1
- package/dist/tracing/index.d.ts.map +0 -1
- package/dist/tracing/index.js.map +0 -1
- package/dist/tracing/span/index.d.ts.map +0 -1
- package/dist/tracing/span/index.js.map +0 -1
- package/dist/tracing/span/span.core.d.ts.map +0 -1
- package/dist/tracing/span/span.core.js.map +0 -1
- package/dist/tracing/span/spanContext.type.d.ts.map +0 -1
- package/dist/tracing/span/spanContext.type.js.map +0 -1
- package/dist/tracing/tracer/index.d.ts.map +0 -1
- package/dist/tracing/tracer/index.js.map +0 -1
- package/dist/tracing/tracer/tracer.core.d.ts.map +0 -1
- package/dist/tracing/tracer/tracer.core.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -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:
|
|
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:
|
|
65
|
+
message: serialized.message,
|
|
32
66
|
loggerName,
|
|
33
|
-
context
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
|
5
|
+
* child loggers, persistent context, trace correlation and redaction.
|
|
6
6
|
*/
|
|
7
7
|
import { LogLevel } from "../types.js";
|
|
8
|
-
import { shouldLog
|
|
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
|
-
|
|
26
|
+
currentLevel;
|
|
22
27
|
context;
|
|
23
28
|
transport;
|
|
29
|
+
correlate;
|
|
30
|
+
redact;
|
|
24
31
|
constructor(options) {
|
|
25
32
|
this.name = options.name;
|
|
26
|
-
this.
|
|
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.
|
|
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.
|
|
76
|
+
return shouldLog(this.currentLevel, level);
|
|
60
77
|
}
|
|
61
78
|
async flush() {
|
|
62
|
-
|
|
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
|
|
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(
|
|
91
|
+
context: Object.keys(redacted).length > 0 ? redacted : undefined,
|
|
92
|
+
error,
|
|
93
|
+
traceId: propagation?.traceId,
|
|
94
|
+
spanId: propagation?.spanId,
|
|
73
95
|
});
|
|
74
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
20
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
15
|
+
export declare const DEFAULT_BUCKET_BOUNDARIES: readonly number[];
|
|
8
16
|
/**
|
|
9
|
-
* In-memory histogram
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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:
|
|
34
|
-
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
|
package/dist/metrics/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/observability — Metrics
|
|
3
3
|
*
|
|
4
|
-
* Counters, gauges, histograms, and the
|
|
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
|
package/dist/metrics/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/observability — Metrics
|
|
3
3
|
*
|
|
4
|
-
* Counters, gauges, histograms, and the
|
|
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
|