@zudojs/observability 1.1.0 → 1.1.1

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.
@@ -143,6 +143,35 @@ function buildResourceAttributes(config) {
143
143
  ...(config.resource ?? {}),
144
144
  };
145
145
  }
146
+ /** Shortest interval between two repeat queue-overflow reports. */
147
+ const DROP_REPORT_INTERVAL_MS = 60_000;
148
+ /**
149
+ * Rate-limits a processor's `onDrop` callback.
150
+ *
151
+ * `onDrop` fires once per record the bounded queue refuses, on the
152
+ * synchronous `logger.info()` path, and the report below allocates an
153
+ * `Error` (so, a stack capture) and re-enters the caller's `onError` —
154
+ * which usually writes to the sink that is already stalled. The bounded
155
+ * queue is the control that makes a stalled exporter survivable; reporting
156
+ * every drop turned it into a second, louder failure.
157
+ *
158
+ * Handled the same way an over-cardinality metric is a few lines below: the
159
+ * first occurrence is reported immediately, then at most one per interval.
160
+ * The count handed to `report` is the processor's running total, so
161
+ * whoever does get notified still sees how many records were lost.
162
+ */
163
+ function throttleDropReports(report) {
164
+ let lastReportedAt;
165
+ return (dropped) => {
166
+ const at = Date.now();
167
+ if (lastReportedAt !== undefined &&
168
+ at - lastReportedAt < DROP_REPORT_INTERVAL_MS) {
169
+ return;
170
+ }
171
+ lastReportedAt = at;
172
+ report(dropped);
173
+ };
174
+ }
146
175
  function buildPipeline(config) {
147
176
  const useConsole = config.useConsoleExporters ?? true;
148
177
  /* ── Logging ─────────────────────────────────────────────────────────── */
@@ -153,7 +182,7 @@ function buildPipeline(config) {
153
182
  batchSize: config.logBatchSize,
154
183
  flushIntervalMs: config.logFlushIntervalMs,
155
184
  onError: config.onError,
156
- onDrop: (dropped) => config.onError?.(new Error(`Dropped ${dropped} log records: queue full`), "BatchLogProcessor"),
185
+ onDrop: throttleDropReports((dropped) => config.onError?.(new Error(`Dropped ${dropped} log records: queue full`), "BatchLogProcessor")),
157
186
  });
158
187
  // Redaction is applied by the logger, so every transport and exporter
159
188
  // downstream sees redacted records — configuring it and not wiring it here
@@ -179,7 +208,7 @@ function buildPipeline(config) {
179
208
  new BatchSpanProcessor({
180
209
  exporter: ownSpanExporter(),
181
210
  onError: config.onError,
182
- onDrop: (dropped) => config.onError?.(new Error(`Dropped ${dropped} spans: queue full`), "BatchSpanProcessor"),
211
+ onDrop: throttleDropReports((dropped) => config.onError?.(new Error(`Dropped ${dropped} spans: queue full`), "BatchSpanProcessor")),
183
212
  }),
184
213
  ];
185
214
  // When the caller supplied their own processors, they own the exporter's
@@ -6,6 +6,23 @@
6
6
  */
7
7
  import { SpanKind, SpanStatus } from "../../types.js";
8
8
  import { createSpanContext, createChildSpanContext, } from "./spanContext.type.js";
9
+ /**
10
+ * Stores one attribute on a bag whose keys come from instrumentation.
11
+ *
12
+ * `bag[key] = value` invokes the inherited `__proto__` setter for that one
13
+ * key: the attribute never lands, and an object value replaces the bag's
14
+ * prototype. A poisoned prototype then defeats `maxAttributes`, because the
15
+ * cap is only applied to keys the bag does not already answer for. Matches
16
+ * the same guard in `redaction.core.ts`.
17
+ */
18
+ function defineAttribute(target, key, value) {
19
+ Object.defineProperty(target, key, {
20
+ value,
21
+ enumerable: true,
22
+ writable: true,
23
+ configurable: true,
24
+ });
25
+ }
9
26
  /** Defaults matching the OpenTelemetry specification. */
10
27
  const DEFAULT_LIMITS = {
11
28
  maxAttributes: 128,
@@ -81,12 +98,12 @@ export class DefaultSpan {
81
98
  setAttribute(key, value) {
82
99
  if (this.ended || !this.recording)
83
100
  return;
84
- if (!(key in this.attributes) &&
101
+ if (!Object.prototype.hasOwnProperty.call(this.attributes, key) &&
85
102
  Object.keys(this.attributes).length >= this.limits.maxAttributes) {
86
103
  this.droppedAttributes++;
87
104
  return;
88
105
  }
89
- this.attributes[key] = this.sanitize(key, value);
106
+ defineAttribute(this.attributes, key, this.sanitize(key, value));
90
107
  }
91
108
  addEvent(name, attributes) {
92
109
  if (this.ended || !this.recording)
@@ -104,7 +121,7 @@ export class DefaultSpan {
104
121
  this.droppedAttributes++;
105
122
  continue;
106
123
  }
107
- eventAttributes[key] = this.sanitize(key, value);
124
+ defineAttribute(eventAttributes, key, this.sanitize(key, value));
108
125
  kept++;
109
126
  }
110
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/observability",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Structured logging, metrics, tracing, context propagation, and exporters for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -26,7 +26,7 @@
26
26
  "!dist/.tsbuildinfo"
27
27
  ],
28
28
  "dependencies": {
29
- "@zudojs/errors": "1.1.0"
29
+ "@zudojs/errors": "1.2.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "typescript": "7.0.2",