@zudojs/observability 1.1.1 → 1.2.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/README.md CHANGED
@@ -33,8 +33,7 @@ const obs = createObservability({
33
33
  environment: "production",
34
34
  logLevel: LogLevel.INFO,
35
35
 
36
- // Opt in to redaction — see "Redaction" below.
37
- redaction: {},
36
+ // Redaction is on by default — see "Redaction" below.
38
37
 
39
38
  // Sample 10% of traces. The decision is derived from the trace ID, so a
40
39
  // trace is never sampled in half across services.
@@ -82,8 +81,17 @@ Never pass a raw level number between the two; convert with
82
81
 
83
82
  ## Redaction
84
83
 
85
- Redaction is off unless you configure it, and on once you do. The logger
86
- applies it to every record before any transport sees it, and the tracer
84
+ Redaction is **on by default**, as it is in `@zudojs/logger`: with no
85
+ `redaction` option, every name the logger's default matcher redacts
86
+ (`DEFAULT_LOGGER_SECRET_FIELDS` — password, passphrase, secret, token, jwt,
87
+ bearer, auth, authorization, cookie, session, sid, credential, api key,
88
+ private key, client secret, card number, cvv, ssn, pin, otp and more) is
89
+ redacted here too, along with this package's own `DEFAULT_SENSITIVE_FIELDS`.
90
+ Pass a config to change the rules, or `redaction: false` to turn it off.
91
+ Before 1.2 redaction was off unless configured, so a `password` field was
92
+ exported in the clear.
93
+
94
+ The logger applies it to every record before any transport sees it, and the tracer
87
95
  applies it to every span attribute and span event attribute before any
88
96
  processor or exporter sees it — a `Bearer` token attached to a span is
89
97
  redacted the same way one written to a log field is.
@@ -92,7 +100,7 @@ redacted the same way one written to a log field is.
92
100
  const obs = createObservability({
93
101
  serviceName: "api",
94
102
  redaction: {
95
- // Defaults cover passwords, tokens, cookies, keys and card numbers.
103
+ // Setting `fields` replaces the default list (and the logger's rules).
96
104
  fields: ["password", "token", "ssn"],
97
105
  patterns: [/^x-.*-secret$/i],
98
106
  // "contains" (the default) matches on word boundaries: "userPassword"
@@ -119,6 +127,10 @@ being flattened to `{}`.
119
127
  `redactObject`, `redactValue` and `createStructureRedactor` are exported for
120
128
  use outside the logger.
121
129
 
130
+ ```typescript
131
+ createObservability({ serviceName: "api", redaction: false }); // opt out
132
+ ```
133
+
122
134
  ## Metrics
123
135
 
124
136
  Counters, gauges and histograms, keyed by name **and** labels.
@@ -165,8 +177,9 @@ name as two types is rejected wholesale by OTLP and Prometheus.
165
177
 
166
178
  Metrics are exported by a `PeriodicMetricReader`, which the facade starts for
167
179
  you when a `metricExporter` is configured. `metricExportIntervalMs: 0`
168
- disables the periodic export while still collecting a final snapshot on
169
- `flush()` and `shutdown()`:
180
+ disables the periodic export while still collecting a snapshot on `flush()`
181
+ and a final one on `shutdown()`. `shutdown()` exports that final snapshot
182
+ exactly once (before 1.2 it was exported twice):
170
183
 
171
184
  ```typescript
172
185
  const obs = createObservability({
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * const obs = createObservability({
17
17
  * serviceName: "my-api",
18
18
  * logLevel: LogLevel.INFO,
19
- * redaction: {}, // opt in to redaction
19
+ * // Redaction is on by default; `redaction: false` turns it off.
20
20
  * sampler: createProbabilitySampler(0.1),
21
21
  * });
22
22
  *
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * const obs = createObservability({
17
17
  * serviceName: "my-api",
18
18
  * logLevel: LogLevel.INFO,
19
- * redaction: {}, // opt in to redaction
19
+ * // Redaction is on by default; `redaction: false` turns it off.
20
20
  * sampler: createProbabilitySampler(0.1),
21
21
  * });
22
22
  *
@@ -23,7 +23,7 @@ interface TelemetryPipeline {
23
23
  readonly metricReader: PeriodicMetricReader;
24
24
  readonly config: ObservabilityConfig;
25
25
  readonly sampler: ObservabilityConfig["sampler"];
26
- /** Redacts span attributes, when `config.redaction` is set. */
26
+ /** Redacts span attributes, unless `config.redaction` is `false`. */
27
27
  readonly redactAttribute?: (key: string, value: unknown) => unknown;
28
28
  }
29
29
  /**
@@ -64,6 +64,11 @@ export declare class DefaultObservability implements Observability {
64
64
  * when the caller went on to exit.
65
65
  */
66
66
  flush(): Promise<void>;
67
+ /**
68
+ * Drains the log and span buffers, and — when `includeMetrics` — exports a
69
+ * metric snapshot. Shutdown passes `false`: the reader's own `shutdown()`
70
+ * exports the final snapshot, and collecting here as well sent it twice.
71
+ */
67
72
  private drain;
68
73
  private reportFailures;
69
74
  /**
@@ -79,9 +79,14 @@ export class DefaultObservability {
79
79
  * when the caller went on to exit.
80
80
  */
81
81
  async flush() {
82
- await this.drain();
82
+ await this.drain(true);
83
83
  }
84
- async drain() {
84
+ /**
85
+ * Drains the log and span buffers, and — when `includeMetrics` — exports a
86
+ * metric snapshot. Shutdown passes `false`: the reader's own `shutdown()`
87
+ * exports the final snapshot, and collecting here as well sent it twice.
88
+ */
89
+ async drain(includeMetrics) {
85
90
  const tasks = [];
86
91
  if (this.pipeline.logProcessor) {
87
92
  tasks.push(this.pipeline.logProcessor.flush());
@@ -90,7 +95,8 @@ export class DefaultObservability {
90
95
  if (processor.forceFlush)
91
96
  tasks.push(processor.forceFlush());
92
97
  }
93
- tasks.push(this.pipeline.metricReader.collect());
98
+ if (includeMetrics)
99
+ tasks.push(this.pipeline.metricReader.collect());
94
100
  await this.reportFailures(await Promise.allSettled(tasks), "flush");
95
101
  }
96
102
  async reportFailures(results, source) {
@@ -115,8 +121,9 @@ export class DefaultObservability {
115
121
  }
116
122
  async performShutdown() {
117
123
  // Drain first: whatever is still queued should reach the backend before
118
- // the exporters close.
119
- await this.drain();
124
+ // the exporters close. Metrics are left to the reader's shutdown, which
125
+ // exports the final snapshot exactly once.
126
+ await this.drain(false);
120
127
  const steps = [];
121
128
  steps.push(this.pipeline.metricReader.shutdown());
122
129
  if (this.pipeline.logProcessor) {
@@ -186,10 +193,10 @@ function buildPipeline(config) {
186
193
  });
187
194
  // Redaction is applied by the logger, so every transport and exporter
188
195
  // downstream sees redacted records — configuring it and not wiring it here
189
- // is what made the "credentials are never logged" promise untrue.
190
- const redactor = config.redaction
191
- ? createStructureRedactor(config.redaction)
192
- : undefined;
196
+ // is what made the "credentials are never logged" promise untrue. It is on
197
+ // unless the caller opts out with `redaction: false`, as in @zudojs/logger.
198
+ const redaction = resolveRedaction(config);
199
+ const redactor = redaction ? createStructureRedactor(redaction) : undefined;
193
200
  const logger = new StructuredLogger({
194
201
  name: config.serviceName,
195
202
  level: config.logLevel ?? LogLevel.INFO,
@@ -244,8 +251,8 @@ function buildPipeline(config) {
244
251
  onError: config.onError,
245
252
  });
246
253
  metricReader.start();
247
- const redactAttribute = config.redaction
248
- ? buildAttributeRedactor(config.redaction)
254
+ const redactAttribute = redaction
255
+ ? buildAttributeRedactor(redaction)
249
256
  : undefined;
250
257
  return {
251
258
  logger,
@@ -259,6 +266,15 @@ function buildPipeline(config) {
259
266
  redactAttribute,
260
267
  };
261
268
  }
269
+ /**
270
+ * The redaction rules in force: the caller's, the defaults when none were
271
+ * given, or none at all for an explicit `redaction: false`.
272
+ */
273
+ function resolveRedaction(config) {
274
+ if (config.redaction === false)
275
+ return undefined;
276
+ return config.redaction ?? {};
277
+ }
262
278
  /**
263
279
  * Builds the span-attribute redactor.
264
280
  *
@@ -16,6 +16,13 @@
16
16
  * - matching is substring-based by default, so `userPassword` and
17
17
  * `x-api-key` are caught, not just the exact names in the list.
18
18
  */
19
+ import { createDefaultSecretFieldMatcher } from "@zudojs/logger";
20
+ /**
21
+ * @zudojs/logger's default secret-name matcher. The default rules here
22
+ * include it, so a field the logger redacts is never exported in the clear
23
+ * by this package.
24
+ */
25
+ const isLoggerSecretField = createDefaultSecretFieldMatcher();
19
26
  /** Default sensitive field names, matched case-insensitively. */
20
27
  export const DEFAULT_SENSITIVE_FIELDS = [
21
28
  "password",
@@ -92,10 +99,13 @@ function compile(config) {
92
99
  const matchMode = config?.matchMode ?? "contains";
93
100
  const exact = new Set(fields);
94
101
  const normalizedFields = new Set(fields.map((field) => field.replace(/[^a-z0-9]/g, "")).filter(Boolean));
102
+ const withLoggerDefaults = config?.fields === undefined && matchMode === "contains";
95
103
  const isSensitive = (key) => {
96
104
  const lower = key.toLowerCase();
97
105
  if (exact.has(lower))
98
106
  return true;
107
+ if (withLoggerDefaults && isLoggerSecretField(key))
108
+ return true;
99
109
  if (matchMode === "contains") {
100
110
  for (const candidate of wordJoins(key)) {
101
111
  if (normalizedFields.has(candidate))
@@ -51,7 +51,8 @@ export type RedactionMatchMode = "exact" | "contains";
51
51
  export interface RedactionConfig {
52
52
  /**
53
53
  * Field names to redact (case-insensitive). Defaults to a built-in list
54
- * covering passwords, tokens, cookies, keys and card numbers.
54
+ * covering passwords, tokens, cookies, keys and card numbers, plus every
55
+ * name @zudojs/logger's default matcher redacts. Setting it replaces both.
55
56
  */
56
57
  readonly fields?: readonly string[];
57
58
  /**
@@ -101,9 +102,13 @@ export interface ObservabilityConfig {
101
102
  readonly processors?: readonly SpanProcessor[];
102
103
  /**
103
104
  * Redacts sensitive fields from log contexts *and* from span attributes and
104
- * span event attributes. Omit it and neither is redacted.
105
+ * span event attributes.
106
+ *
107
+ * On by default: omitting it applies the default rules, which include
108
+ * every name @zudojs/logger redacts. Pass a config to change the rules, or
109
+ * `false` to turn redaction off.
105
110
  */
106
- readonly redaction?: RedactionConfig;
111
+ readonly redaction?: RedactionConfig | false;
107
112
  /**
108
113
  * Metrics registry tuning: the series cap that bounds cardinality, and the
109
114
  * histogram bucket boundaries. Without this the defaults were unreachable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/observability",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Structured logging, metrics, tracing, context propagation, and exporters for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -26,12 +26,13 @@
26
26
  "!dist/.tsbuildinfo"
27
27
  ],
28
28
  "dependencies": {
29
- "@zudojs/errors": "1.2.0"
29
+ "@zudojs/errors": "1.3.0",
30
+ "@zudojs/logger": "1.4.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "typescript": "7.0.2",
33
- "vitest": "^4.1.11",
34
- "@types/node": "^26.4.1"
34
+ "vitest": "^5.0.1",
35
+ "@types/node": "^26.6.2"
35
36
  },
36
37
  "engines": {
37
38
  "node": ">=24.0.0"
@@ -46,7 +47,7 @@
46
47
  "tracing",
47
48
  "logs"
48
49
  ],
49
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
50
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-observability",
50
51
  "bugs": {
51
52
  "url": "https://github.com/oyinlola-tech/zudo/issues"
52
53
  },