@smithers-orchestrator/observability 0.24.2 → 0.25.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/observability",
3
- "version": "0.24.2",
3
+ "version": "0.25.1",
4
4
  "description": "Concrete Smithers metrics, logging, tracing, and observability integrations",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,8 +34,7 @@
34
34
  "@effect/opentelemetry": "^0.63.0",
35
35
  "@effect/platform": "^0.96.0",
36
36
  "@effect/platform-bun": "^0.89.0",
37
- "effect": "^3.21.1",
38
- "@smithers-orchestrator/agents": "0.24.2"
37
+ "effect": "^3.21.1"
39
38
  },
40
39
  "devDependencies": {
41
40
  "@types/bun": "latest",
@@ -7,4 +7,5 @@ export type ResolvedSmithersObservabilityOptions = {
7
7
  readonly serviceName: string;
8
8
  readonly logFormat: SmithersLogFormat;
9
9
  readonly logLevel: LogLevel.LogLevel;
10
+ readonly installLogger: boolean;
10
11
  };
@@ -121,7 +121,19 @@ export type SmithersEvent =
121
121
  after: RunState;
122
122
  timestampMs: number;
123
123
  }
124
- | { type: "RunFinished"; runId: string; timestampMs: number }
124
+ | {
125
+ type: "RunFinished";
126
+ runId: string;
127
+ timestampMs: number;
128
+ /**
129
+ * Tasks that ended `failed` but were tolerated (continueOnFail / transient
130
+ * agent failures) so the run still finished. Present only when `> 0` — the
131
+ * run "succeeded" while these children failed. See `docs/runtime/run-state.mdx`.
132
+ */
133
+ failedChildren?: number;
134
+ /** Task state keys (`nodeId::iteration`) counted by `failedChildren`. */
135
+ failedChildKeys?: readonly string[];
136
+ }
125
137
  | { type: "RunFailed"; runId: string; error: unknown; timestampMs: number }
126
138
  | { type: "RunCancelled"; runId: string; timestampMs: number }
127
139
  | {
@@ -229,6 +241,11 @@ export type SmithersEvent =
229
241
  runId: string;
230
242
  frameNo: number;
231
243
  xmlHash: string;
244
+ trigger?: {
245
+ reason: string;
246
+ nodeId?: string;
247
+ iteration?: number;
248
+ };
232
249
  timestampMs: number;
233
250
  }
234
251
  | {
@@ -7,4 +7,5 @@ export type SmithersObservabilityOptions = {
7
7
  readonly serviceName?: string;
8
8
  readonly logFormat?: SmithersLogFormat;
9
9
  readonly logLevel?: LogLevel.LogLevel | string;
10
+ readonly installLogger?: boolean;
10
11
  };
@@ -5,6 +5,19 @@ import { mergeCorrelationContext } from "./mergeCorrelationContext.js";
5
5
  /** @typedef {import("./CorrelationPatch.ts").CorrelationPatch} CorrelationPatch */
6
6
 
7
7
  /**
8
+ * Bridge the Effect-tracked correlation context onto the imperative
9
+ * AsyncLocalStorage store so plain (non-Effect) `getCurrentCorrelationContext()`
10
+ * reads — e.g. from the imperative logger — see the active run/node correlation
11
+ * while the effect executes.
12
+ *
13
+ * IMPORTANT: run the resulting effect with `Effect.runPromise`/`runFork`, never
14
+ * `Effect.runSync`. The acquire step calls `AsyncLocalStorage.enterWith()`, which
15
+ * mutates the *caller's* async context. Under `runSync` the caller is whatever
16
+ * synchronous context invoked it (e.g. a test-runner's root context); enabling
17
+ * ALS async-hooks there leaks into every subsequent timer/promise on that
18
+ * context. `runPromise`/`runFork` execute on an ephemeral fiber context, keeping
19
+ * the enterWith scoped to that fiber.
20
+ *
8
21
  * @template A, E, R
9
22
  * @param {Effect.Effect<A, E, R>} effect
10
23
  * @param {CorrelationPatch} patch
@@ -1,16 +1,7 @@
1
- import { Context, Effect, Layer } from "effect";
2
- import { renderPrometheusSamples, toPrometheusMetricName, } from "./_corePrometheus.js";
3
- /** @typedef {import("./MetricName.ts").MetricName} MetricName */
1
+ import { Context } from "effect";
2
+ import { toPrometheusMetricName } from "./_corePrometheus.js";
4
3
  /** @typedef {import("./SmithersMetricType.ts").SmithersMetricType} SmithersMetricType */
5
- /** @typedef {import("./SmithersMetricUnit.ts").SmithersMetricUnit} SmithersMetricUnit */
6
- /** @typedef {import("./_corePrometheusShape.ts").MetricLabels} MetricLabels */
7
- /** @typedef {import("./_corePrometheusShape.ts").PrometheusSample} PrometheusSample */
8
- /** @typedef {import("./_coreMetricsShape.ts").CounterEntry} CounterEntry */
9
- /** @typedef {import("./_coreMetricsShape.ts").GaugeEntry} GaugeEntry */
10
- /** @typedef {import("./_coreMetricsShape.ts").HistogramEntry} HistogramEntry */
11
- /** @typedef {import("./_coreMetricsShape.ts").MetricsSnapshot} MetricsSnapshot */
12
4
  /** @typedef {import("./_coreMetricsShape.ts").MetricsServiceShape} MetricsServiceShape */
13
- /** @typedef {import("./_coreMetricsShape.ts").SmithersMetricEvent} SmithersMetricEvent */
14
5
  /** @typedef {import("./SmithersMetricDefinition.ts").SmithersMetricDefinition} SmithersMetricDefinition */
15
6
 
16
7
  /**
@@ -204,307 +195,3 @@ export const smithersMetrics = Object.freeze(Object.fromEntries(smithersMetricCa
204
195
  const _MetricsServiceBase = /** @type {Context.TagClass<MetricsService, "MetricsService", MetricsServiceShape>} */ (/** @type {unknown} */ (Context.Tag("MetricsService")()));
205
196
  export class MetricsService extends _MetricsServiceBase {
206
197
  }
207
- const DEFAULT_HISTOGRAM_BUCKETS = [
208
- 1,
209
- 5,
210
- 10,
211
- 25,
212
- 50,
213
- 100,
214
- 250,
215
- 500,
216
- 1_000,
217
- 2_500,
218
- 5_000,
219
- 10_000,
220
- 30_000,
221
- ];
222
- /**
223
- * @param {MetricLabels} [labels]
224
- * @returns {string}
225
- */
226
- function labelsKey(labels = {}) {
227
- return JSON.stringify(Object.entries(labels).sort(([left], [right]) => left.localeCompare(right)));
228
- }
229
- /**
230
- * @param {string} name
231
- * @param {MetricLabels} [labels]
232
- * @returns {string}
233
- */
234
- function metricKey(name, labels) {
235
- return `${name}|${labelsKey(labels)}`;
236
- }
237
- /**
238
- * @param {MetricLabels} [labels]
239
- * @returns {MetricLabels}
240
- */
241
- function cloneLabels(labels = {}) {
242
- return Object.freeze({ ...labels });
243
- }
244
- /**
245
- * @returns {Context.Tag.Service<MetricsService>}
246
- */
247
- export function makeInMemoryMetricsService() {
248
- const registry = new Map();
249
- const processStartMs = Date.now();
250
- const asyncExternalWaitCounts = {
251
- approval: 0,
252
- event: 0,
253
- };
254
- /**
255
- * @param {string} name
256
- * @param {MetricLabels} [labels]
257
- * @returns {CounterEntry}
258
- */
259
- function upsertCounter(name, labels) {
260
- const key = metricKey(name, labels);
261
- const existing = registry.get(key);
262
- if (existing?.type === "counter")
263
- return existing;
264
- const created = {
265
- type: "counter",
266
- value: 0,
267
- labels: cloneLabels(labels),
268
- };
269
- registry.set(key, created);
270
- return created;
271
- }
272
- /**
273
- * @param {string} name
274
- * @param {MetricLabels} [labels]
275
- * @returns {GaugeEntry}
276
- */
277
- function upsertGauge(name, labels) {
278
- const key = metricKey(name, labels);
279
- const existing = registry.get(key);
280
- if (existing?.type === "gauge")
281
- return existing;
282
- const created = {
283
- type: "gauge",
284
- value: 0,
285
- labels: cloneLabels(labels),
286
- };
287
- registry.set(key, created);
288
- return created;
289
- }
290
- /**
291
- * @param {string} name
292
- * @param {MetricLabels} [labels]
293
- * @returns {HistogramEntry}
294
- */
295
- function upsertHistogram(name, labels) {
296
- const key = metricKey(name, labels);
297
- const existing = registry.get(key);
298
- if (existing?.type === "histogram")
299
- return existing;
300
- const created = {
301
- type: "histogram",
302
- sum: 0,
303
- count: 0,
304
- labels: cloneLabels(labels),
305
- buckets: new Map(DEFAULT_HISTOGRAM_BUCKETS.map((bucket) => [bucket, 0])),
306
- };
307
- registry.set(key, created);
308
- return created;
309
- }
310
- /**
311
- * @returns {PrometheusSample[]}
312
- */
313
- function samples() {
314
- return [...registry.entries()].map(([key, entry]) => {
315
- const name = key.slice(0, key.indexOf("|"));
316
- if (entry.type === "histogram") {
317
- return {
318
- name,
319
- type: entry.type,
320
- labels: entry.labels,
321
- buckets: new Map(entry.buckets),
322
- sum: entry.sum,
323
- count: entry.count,
324
- };
325
- }
326
- return {
327
- name,
328
- type: entry.type,
329
- labels: entry.labels,
330
- value: entry.value,
331
- };
332
- });
333
- }
334
- const service = {
335
- increment: (name, labels) => service.incrementBy(name, 1, labels),
336
- incrementBy: (name, value, labels) => Effect.sync(() => {
337
- const key = metricKey(name, labels);
338
- const existing = registry.get(key);
339
- const definition = smithersMetricCatalogByName.get(name);
340
- if (existing?.type === "gauge" || definition?.type === "gauge") {
341
- upsertGauge(name, labels).value += value;
342
- return;
343
- }
344
- upsertCounter(name, labels).value += value;
345
- }),
346
- gauge: (name, value, labels) => Effect.sync(() => {
347
- upsertGauge(name, labels).value = value;
348
- }),
349
- histogram: (name, value, labels) => Effect.sync(() => {
350
- const entry = upsertHistogram(name, labels);
351
- entry.count += 1;
352
- entry.sum += value;
353
- for (const boundary of DEFAULT_HISTOGRAM_BUCKETS) {
354
- if (value <= boundary) {
355
- entry.buckets.set(boundary, (entry.buckets.get(boundary) ?? 0) + 1);
356
- }
357
- }
358
- }),
359
- recordEvent: (event) => {
360
- const eventType = String(event.type);
361
- const countEvent = service.increment("smithers.events.emitted_total", {
362
- type: eventType,
363
- });
364
- switch (event.type) {
365
- case "RunStarted":
366
- return Effect.all([
367
- countEvent,
368
- service.increment("smithers.runs.total"),
369
- service.incrementBy("smithers.runs.active", 1),
370
- ], { discard: true });
371
- case "RunFinished":
372
- return Effect.all([
373
- countEvent,
374
- service.incrementBy("smithers.runs.active", -1),
375
- service.increment("smithers.runs.finished_total"),
376
- ], { discard: true });
377
- case "RunFailed":
378
- return Effect.all([
379
- countEvent,
380
- service.incrementBy("smithers.runs.active", -1),
381
- service.increment("smithers.runs.failed_total"),
382
- service.increment("smithers.errors.total"),
383
- ], { discard: true });
384
- case "RunCancelled":
385
- return Effect.all([
386
- countEvent,
387
- service.incrementBy("smithers.runs.active", -1),
388
- service.increment("smithers.runs.cancelled_total"),
389
- ], { discard: true });
390
- case "RunContinuedAsNew":
391
- return Effect.all([countEvent, service.increment("smithers.runs.continued_total")], { discard: true });
392
- case "NodeStarted":
393
- return Effect.all([
394
- countEvent,
395
- service.increment("smithers.nodes.started"),
396
- service.incrementBy("smithers.nodes.active", 1),
397
- ], { discard: true });
398
- case "NodeFinished":
399
- return Effect.all([
400
- countEvent,
401
- service.increment("smithers.nodes.finished"),
402
- service.incrementBy("smithers.nodes.active", -1),
403
- typeof event.durationMs === "number"
404
- ? service.histogram("smithers.node.duration_ms", event.durationMs)
405
- : Effect.void,
406
- ], { discard: true });
407
- case "NodeFailed":
408
- return Effect.all([
409
- countEvent,
410
- service.increment("smithers.nodes.failed"),
411
- service.increment("smithers.errors.total"),
412
- service.incrementBy("smithers.nodes.active", -1),
413
- ], { discard: true });
414
- case "CacheHit":
415
- return Effect.all([countEvent, service.increment("smithers.cache.hits")], { discard: true });
416
- case "CacheMiss":
417
- return Effect.all([countEvent, service.increment("smithers.cache.misses")], { discard: true });
418
- case "ApprovalRequested":
419
- return Effect.all([
420
- countEvent,
421
- service.increment("smithers.approvals.requested"),
422
- service.incrementBy("smithers.approval.pending", 1),
423
- ], { discard: true });
424
- case "ApprovalResolved": {
425
- const approved = event.approved === true || event.status === "approved";
426
- return Effect.all([
427
- countEvent,
428
- service.increment(approved
429
- ? "smithers.approvals.granted"
430
- : "smithers.approvals.denied"),
431
- service.incrementBy("smithers.approval.pending", -1),
432
- ], { discard: true });
433
- }
434
- case "TimerCreated":
435
- return Effect.all([
436
- countEvent,
437
- service.increment("smithers.timers.created"),
438
- service.incrementBy("smithers.timers.pending", 1),
439
- ], { discard: true });
440
- case "TimerFired":
441
- return Effect.all([
442
- countEvent,
443
- service.increment("smithers.timers.fired"),
444
- service.incrementBy("smithers.timers.pending", -1),
445
- ], { discard: true });
446
- case "TaskHeartbeat":
447
- return Effect.all([countEvent, service.increment("smithers.heartbeats.total")], { discard: true });
448
- case "TaskHeartbeatTimeout":
449
- return Effect.all([
450
- countEvent,
451
- service.increment("smithers.heartbeats.timeout_total"),
452
- service.increment("smithers.errors.total"),
453
- ], { discard: true });
454
- case "TokenUsageReported": {
455
- const effects = [countEvent];
456
- const labels = {
457
- ...(typeof event.agent === "string" ? { agent: event.agent } : {}),
458
- ...(typeof event.model === "string" ? { model: event.model } : {}),
459
- };
460
- /**
461
- * @param {string} name
462
- * @param {unknown} value
463
- */
464
- const push = (name, value) => {
465
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
466
- effects.push(service.incrementBy(name, value, labels));
467
- }
468
- };
469
- push("smithers.tokens.input_total", event.inputTokens);
470
- push("smithers.tokens.output_total", event.outputTokens);
471
- push("smithers.tokens.cache_read_total", event.cacheReadTokens);
472
- push("smithers.tokens.cache_write_total", event.cacheWriteTokens);
473
- push("smithers.tokens.reasoning_total", event.reasoningTokens);
474
- return Effect.all(effects, { discard: true });
475
- }
476
- default:
477
- return countEvent;
478
- }
479
- },
480
- updateProcessMetrics: () => Effect.sync(() => {
481
- const uptimeS = (Date.now() - processStartMs) / 1000;
482
- const mem = process.memoryUsage();
483
- upsertGauge("smithers.process.uptime_seconds").value = uptimeS;
484
- upsertGauge("smithers.process.memory_rss_bytes").value = mem.rss;
485
- upsertGauge("smithers.process.heap_used_bytes").value = mem.heapUsed;
486
- }),
487
- updateAsyncExternalWaitPending: (kind, delta) => Effect.sync(() => {
488
- asyncExternalWaitCounts[kind] = Math.max(0, asyncExternalWaitCounts[kind] + delta);
489
- upsertGauge("smithers.external_wait.async_pending", { kind }).value =
490
- asyncExternalWaitCounts[kind];
491
- }),
492
- renderPrometheus: () => Effect.sync(() => renderPrometheusSamples(samples())),
493
- snapshot: () => Effect.sync(() => new Map(registry)),
494
- };
495
- return service;
496
- }
497
- /** @type {Layer.Layer<MetricsService, never, never>} */
498
- export const MetricsServiceLive = Layer.sync(MetricsService, makeInMemoryMetricsService);
499
- /** @type {Layer.Layer<MetricsService, never, never>} */
500
- export const MetricsServiceNoop = Layer.succeed(MetricsService, {
501
- increment: () => Effect.void,
502
- incrementBy: () => Effect.void,
503
- gauge: () => Effect.void,
504
- histogram: () => Effect.void,
505
- recordEvent: () => Effect.void,
506
- updateProcessMetrics: () => Effect.void,
507
- updateAsyncExternalWaitPending: () => Effect.void,
508
- renderPrometheus: () => Effect.succeed(""),
509
- snapshot: () => Effect.succeed(new Map()),
510
- });
@@ -1,5 +1,6 @@
1
1
  import { Context, Effect, Layer } from "effect";
2
2
  import { correlationContextToLogAnnotations, getCurrentCorrelationContext, withCorrelationContext, } from "./_coreCorrelation/index.js";
3
+ import { smithersSpanAttributeAliases } from "./_smithersSpanAttributeAliases.js";
3
4
  /** @typedef {import("./SmithersLogFormat.ts").SmithersLogFormat} SmithersLogFormat */
4
5
  /** @typedef {import("./_coreTracingShape.ts").SmithersSpanAttributesInput} SmithersSpanAttributesInput */
5
6
  /** @typedef {import("./_coreTracingShape.ts").TracingServiceShape} TracingServiceShape */
@@ -30,25 +31,7 @@ export function getCurrentSmithersTraceAnnotations() {
30
31
  * @returns {Record<string, unknown>}
31
32
  */
32
33
  export function makeSmithersSpanAttributes(attributes = {}) {
33
- const aliases = {
34
- runId: "smithers.run_id",
35
- run_id: "smithers.run_id",
36
- workflowName: "smithers.workflow_name",
37
- workflow_name: "smithers.workflow_name",
38
- nodeId: "smithers.node_id",
39
- node_id: "smithers.node_id",
40
- iteration: "smithers.iteration",
41
- attempt: "smithers.attempt",
42
- nodeLabel: "smithers.node_label",
43
- node_label: "smithers.node_label",
44
- toolName: "smithers.tool_name",
45
- tool_name: "smithers.tool_name",
46
- agent: "smithers.agent",
47
- model: "smithers.model",
48
- status: "smithers.status",
49
- waitReason: "smithers.wait_reason",
50
- wait_reason: "smithers.wait_reason",
51
- };
34
+ const aliases = smithersSpanAttributeAliases;
52
35
  const result = {};
53
36
  for (const [key, value] of Object.entries(attributes)) {
54
37
  if (value !== undefined) {
@@ -114,18 +97,22 @@ export function annotateSmithersTrace(attributes = {}) {
114
97
  */
115
98
  export function withSmithersSpan(name, effect, attributes) {
116
99
  const spanAttributes = makeSmithersSpanAttributes(attributes);
117
- const annotations = correlationContextToLogAnnotations(getCurrentCorrelationContext());
118
- let program = effect;
119
- if (Object.keys(spanAttributes).length > 0) {
120
- program = program.pipe(Effect.annotateSpans(spanAttributes));
121
- }
122
- if (hasAttributes(attributes)) {
123
- program = program.pipe(Effect.annotateLogs(attributes));
124
- }
125
- if (annotations) {
126
- program = program.pipe(Effect.annotateLogs(annotations));
127
- }
128
- return program.pipe(Effect.withLogSpan(name), Effect.withSpan(inferSmithersSpanName(name, attributes)));
100
+ return Effect.suspend(() => {
101
+ const annotations = correlationContextToLogAnnotations(getCurrentCorrelationContext());
102
+ let program = effect;
103
+ if (Object.keys(spanAttributes).length > 0) {
104
+ program = program.pipe(Effect.annotateSpans(spanAttributes));
105
+ }
106
+ if (hasAttributes(attributes)) {
107
+ program = program.pipe(Effect.annotateLogs(attributes));
108
+ }
109
+ if (annotations) {
110
+ program = program.pipe(Effect.annotateLogs(annotations));
111
+ }
112
+ return program.pipe(Effect.withLogSpan(name), Effect.withSpan(inferSmithersSpanName(name, attributes), {
113
+ attributes: spanAttributes,
114
+ }));
115
+ });
129
116
  }
130
117
  /** @type {Layer.Layer<TracingService, never, never>} */
131
118
  export const TracingServiceLive = Layer.succeed(TracingService, {
@@ -1,6 +1,3 @@
1
- import { extractTextFromJsonValue } from "@smithers-orchestrator/agents/BaseCliAgent";
2
- import { normalizeTokenUsage } from "@smithers-orchestrator/agents/BaseCliAgent";
3
-
4
1
  /**
5
2
  * @typedef {import('./agentTrace.ts').AgentFamily} AgentFamily
6
3
  * @typedef {import('./agentTrace.ts').CanonicalAgentTraceEventKind} CanonicalAgentTraceEventKind
@@ -27,6 +24,77 @@ import { normalizeTokenUsage } from "@smithers-orchestrator/agents/BaseCliAgent"
27
24
  * }} NormalizedTraceBatch
28
25
  */
29
26
 
27
+ /**
28
+ * @param {unknown} value
29
+ * @returns {string | undefined}
30
+ */
31
+ function extractTextFromJsonValue(value) {
32
+ if (typeof value === "string") return value;
33
+ if (Array.isArray(value)) {
34
+ const text = value.map((item) => extractTextFromJsonValue(item) ?? "").join("");
35
+ return text || undefined;
36
+ }
37
+ if (!value || typeof value !== "object") return undefined;
38
+ const record = /** @type {Record<string, unknown>} */ (value);
39
+ if (typeof record.text === "string") return record.text;
40
+ if (typeof record.content === "string") return record.content;
41
+ if (typeof record.output_text === "string") return record.output_text;
42
+ if (Array.isArray(record.content)) {
43
+ const text = record.content.map((part) => extractTextFromJsonValue(part) ?? "").join("");
44
+ if (text) return text;
45
+ }
46
+ if (record.type === "text" && record.part) return extractTextFromJsonValue(record.part);
47
+ for (const field of ["response", "message", "result", "output", "data", "item"]) {
48
+ const text = extractTextFromJsonValue(record[field]);
49
+ if (text) return text;
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ /** @type {Record<string, ReadonlyArray<ReadonlyArray<string>>>} */
55
+ const _usageFieldAliases = {
56
+ inputTokens: [["inputTokens"], ["promptTokens"], ["prompt_tokens"], ["input_tokens"], ["input"], ["models", "gemini", "tokens", "input"]],
57
+ outputTokens: [["outputTokens"], ["completionTokens"], ["completion_tokens"], ["output_tokens"], ["output"], ["models", "gemini", "tokens", "output"]],
58
+ cacheReadTokens: [["cacheReadTokens"], ["cache_read_input_tokens"], ["cached_input_tokens"], ["cache_read_tokens"], ["inputTokenDetails", "cacheReadTokens"]],
59
+ cacheWriteTokens: [["cacheWriteTokens"], ["cache_write_input_tokens"], ["cache_creation_input_tokens"], ["cache_write_tokens"], ["inputTokenDetails", "cacheWriteTokens"]],
60
+ reasoningTokens: [["reasoningTokens"], ["reasoning_tokens"], ["outputTokenDetails", "reasoningTokens"]],
61
+ totalTokens: [["totalTokens"], ["total_tokens"]],
62
+ };
63
+
64
+ /**
65
+ * @param {unknown} value
66
+ * @param {ReadonlyArray<string>} path
67
+ * @returns {unknown}
68
+ */
69
+ function _readUsagePath(value, path) {
70
+ let current = value;
71
+ for (const segment of path) {
72
+ if (!current || typeof current !== "object") return undefined;
73
+ current = /** @type {Record<string, unknown>} */ (current)[segment];
74
+ }
75
+ return current;
76
+ }
77
+
78
+ /**
79
+ * @param {unknown} usage
80
+ * @returns {Record<string, number> | null}
81
+ */
82
+ function normalizeTokenUsage(usage) {
83
+ if (!usage || typeof usage !== "object") return null;
84
+ /** @type {Record<string, number>} */
85
+ const normalized = {};
86
+ for (const [field, aliases] of Object.entries(_usageFieldAliases)) {
87
+ for (const path of aliases) {
88
+ const value = _readUsagePath(usage, path);
89
+ if (typeof value === "number") {
90
+ normalized[field] = value;
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ return Object.values(normalized).some((v) => Number.isFinite(v) && v > 0) ? normalized : null;
96
+ }
97
+
30
98
  /** @type {Record<string, MappedStructuredEvent>} */
31
99
  const piSimpleEventMap = {
32
100
  session: { kind: "session.start", payloadKind: "pi" },
@@ -27,7 +27,8 @@ const rules = [
27
27
  {
28
28
  id: "secret-ish",
29
29
  pattern: /\b(?:api[_-]?key|token|secret|password)=([^\s"']+)/gi,
30
- replace: "",
30
+ // No `replace` field: redactValue special-cases this rule by id and
31
+ // rewrites the captured value itself, so a top-level replace is never read.
31
32
  },
32
33
  ];
33
34
 
@@ -45,5 +45,11 @@ function makeService(options) {
45
45
  */
46
46
  export function createSmithersObservabilityLayer(options = {}) {
47
47
  const resolved = resolveSmithersObservabilityOptions(options);
48
- return Layer.mergeAll(BunContext.layer, Logger.replace(Logger.defaultLogger, resolveLogger(resolved.logFormat)), Logger.minimumLogLevel(resolved.logLevel), createSmithersOtelLayer(resolved), MetricsServiceLive, TracingServiceLive, Layer.succeed(SmithersObservability, makeService(resolved)));
48
+ const loggerLayers = resolved.installLogger
49
+ ? [
50
+ Logger.replace(Logger.defaultLogger, resolveLogger(resolved.logFormat)),
51
+ Logger.minimumLogLevel(resolved.logLevel),
52
+ ]
53
+ : [];
54
+ return Layer.mergeAll(BunContext.layer, ...loggerLayers, createSmithersOtelLayer(resolved), MetricsServiceLive, TracingServiceLive, Layer.succeed(SmithersObservability, makeService(resolved)));
49
55
  }
package/src/index.d.ts CHANGED
@@ -13,6 +13,7 @@ type ResolvedSmithersObservabilityOptions$2 = {
13
13
  readonly serviceName: string;
14
14
  readonly logFormat: SmithersLogFormat$1;
15
15
  readonly logLevel: LogLevel.LogLevel;
16
+ readonly installLogger: boolean;
16
17
  };
17
18
 
18
19
  type SmithersObservabilityService$1 = {
@@ -27,6 +28,7 @@ type SmithersObservabilityOptions$4 = {
27
28
  readonly serviceName?: string;
28
29
  readonly logFormat?: SmithersLogFormat$1;
29
30
  readonly logLevel?: LogLevel.LogLevel | string;
31
+ readonly installLogger?: boolean;
30
32
  };
31
33
 
32
34
  type MetricLabels$1 = Readonly<Record<string, string | number | boolean>>;
@@ -47,6 +49,89 @@ type SmithersMetricDefinition$2 = {
47
49
  readonly boundaries?: readonly number[];
48
50
  };
49
51
 
52
+ type AgentFamily = "pi" | "codex" | "claude-code" | "antigravity" | "gemini" | "kimi" | "openai" | "anthropic" | "amp" | "forge" | "unknown";
53
+ type AgentCaptureMode = "sdk-events" | "rpc-events" | "cli-json-stream" | "cli-json" | "cli-text" | "artifact-import";
54
+ type TraceCompleteness = "full-observed" | "partial-observed" | "final-only" | "capture-failed";
55
+ type CanonicalAgentTraceEventKind = "session.start" | "session.end" | "turn.start" | "turn.end" | "message.start" | "message.update" | "message.end" | "assistant.text.delta" | "assistant.thinking.delta" | "assistant.message.final" | "tool.execution.start" | "tool.execution.update" | "tool.execution.end" | "tool.result" | "retry.start" | "retry.end" | "compaction.start" | "compaction.end" | "stderr" | "stdout" | "usage" | "capture.warning" | "capture.error" | "artifact.created";
56
+ type CanonicalAgentTraceEventPhase = "agent" | "turn" | "message" | "tool" | "session" | "capture" | "artifact";
57
+ type CanonicalAgentTraceEvent = {
58
+ traceVersion: "1";
59
+ runId: string;
60
+ workflowPath?: string;
61
+ workflowHash?: string;
62
+ nodeId: string;
63
+ iteration: number;
64
+ attempt: number;
65
+ timestampMs: number;
66
+ event: {
67
+ sequence: number;
68
+ kind: CanonicalAgentTraceEventKind;
69
+ phase: CanonicalAgentTraceEventPhase;
70
+ };
71
+ source: {
72
+ agentFamily: AgentFamily;
73
+ captureMode: AgentCaptureMode;
74
+ rawType?: string;
75
+ rawEventId?: string;
76
+ observed: boolean;
77
+ };
78
+ traceCompleteness: TraceCompleteness;
79
+ payload: Record<string, unknown> | null;
80
+ raw: unknown;
81
+ redaction: {
82
+ applied: boolean;
83
+ ruleIds: string[];
84
+ };
85
+ annotations: Record<string, string | number | boolean>;
86
+ };
87
+ type AgentTraceSummary = {
88
+ traceVersion: "1";
89
+ runId: string;
90
+ workflowPath?: string;
91
+ workflowHash?: string;
92
+ nodeId: string;
93
+ iteration: number;
94
+ attempt: number;
95
+ traceStartedAtMs: number;
96
+ traceFinishedAtMs: number;
97
+ agentFamily: AgentFamily;
98
+ agentId?: string;
99
+ model?: string;
100
+ captureMode: AgentCaptureMode;
101
+ traceCompleteness: TraceCompleteness;
102
+ unsupportedEventKinds: CanonicalAgentTraceEventKind[];
103
+ missingExpectedEventKinds: CanonicalAgentTraceEventKind[];
104
+ rawArtifactRefs: string[];
105
+ };
106
+ type AgentSessionTranscriptEvent = {
107
+ transcriptVersion: "1";
108
+ runId: string;
109
+ workflowPath?: string;
110
+ workflowHash?: string;
111
+ nodeId: string;
112
+ iteration: number;
113
+ attempt: number;
114
+ timestampMs: number;
115
+ event: {
116
+ sequence: number;
117
+ rowType: string;
118
+ };
119
+ source: {
120
+ agentFamily: AgentFamily;
121
+ captureMode: AgentCaptureMode;
122
+ ingestSource: "live" | "artifact";
123
+ observedLive: boolean;
124
+ providerSessionId?: string;
125
+ providerThreadId?: string;
126
+ };
127
+ raw: unknown;
128
+ redaction: {
129
+ applied: boolean;
130
+ ruleIds: string[];
131
+ };
132
+ annotations: Record<string, string | number | boolean>;
133
+ };
134
+
50
135
  type RunStatus = "running" | "waiting-approval" | "waiting-event" | "waiting-timer" | "finished" | "continued" | "failed" | "cancelled";
51
136
  type RunState = "running" | "waiting-approval" | "waiting-event" | "waiting-timer" | "recovering" | "stale" | "orphaned" | "failed" | "cancelled" | "succeeded" | "unknown";
52
137
  type AgentCliActionKind = "turn" | "command" | "tool" | "file_change" | "web_search" | "todo_list" | "reasoning" | "warning" | "note";
@@ -128,6 +213,14 @@ type SmithersEvent$2 = {
128
213
  type: "RunFinished";
129
214
  runId: string;
130
215
  timestampMs: number;
216
+ /**
217
+ * Tasks that ended `failed` but were tolerated (continueOnFail / transient
218
+ * agent failures) so the run still finished. Present only when `> 0` — the
219
+ * run "succeeded" while these children failed. See `docs/runtime/run-state.mdx`.
220
+ */
221
+ failedChildren?: number;
222
+ /** Task state keys (`nodeId::iteration`) counted by `failedChildren`. */
223
+ failedChildKeys?: readonly string[];
131
224
  } | {
132
225
  type: "RunFailed";
133
226
  runId: string;
@@ -230,6 +323,11 @@ type SmithersEvent$2 = {
230
323
  runId: string;
231
324
  frameNo: number;
232
325
  xmlHash: string;
326
+ trigger?: {
327
+ reason: string;
328
+ nodeId?: string;
329
+ iteration?: number;
330
+ };
233
331
  timestampMs: number;
234
332
  } | {
235
333
  type: "NodePending";
@@ -565,6 +663,30 @@ type SmithersEvent$2 = {
565
663
  runId: string;
566
664
  timerId: string;
567
665
  timestampMs: number;
666
+ } | {
667
+ type: "AgentTraceEvent";
668
+ runId: string;
669
+ nodeId: string;
670
+ iteration: number;
671
+ attempt: number;
672
+ trace: CanonicalAgentTraceEvent;
673
+ timestampMs: number;
674
+ } | {
675
+ type: "AgentTraceSummary";
676
+ runId: string;
677
+ nodeId: string;
678
+ iteration: number;
679
+ attempt: number;
680
+ summary: AgentTraceSummary;
681
+ timestampMs: number;
682
+ } | {
683
+ type: "AgentSessionEvent";
684
+ runId: string;
685
+ nodeId: string;
686
+ iteration: number;
687
+ attempt: number;
688
+ transcript: AgentSessionTranscriptEvent;
689
+ timestampMs: number;
568
690
  };
569
691
 
570
692
  type MetricName = string;
@@ -995,11 +1117,40 @@ declare function correlationContextToLogAnnotations(context?: CorrelationContext
995
1117
  type CorrelationContext$1 = CorrelationContext$5;
996
1118
 
997
1119
  /**
1120
+ * Temporary compatibility shim for legacy, non-Effect callers.
1121
+ *
1122
+ * Unlike the FiberRef-based core implementation
1123
+ * ({@link import("./_coreCorrelation/updateCurrentCorrelationContext.js").updateCurrentCorrelationContext}),
1124
+ * which returns an Effect and sets a fresh merged context on the
1125
+ * `correlationContextFiberRef`, this shim runs synchronously and applies the
1126
+ * patch by **mutating the current context object in place** via
1127
+ * `Object.assign(current, next)`. Any references already holding the current
1128
+ * context object will observe the mutation. This in-place semantics is
1129
+ * intentional and exists only to preserve behavior for callers that captured a
1130
+ * context reference before the Effect-based API existed.
1131
+ *
1132
+ * If there is no current context, the patch is a no-op (nothing is created).
1133
+ *
1134
+ * @deprecated Prefer the Effect-returning
1135
+ * `updateCurrentCorrelationContext` from
1136
+ * `@smithers-orchestrator/observability` (the `_coreCorrelation` version),
1137
+ * which does not mutate shared state. This shim will be removed once legacy
1138
+ * callers migrate.
1139
+ *
998
1140
  * @param {CorrelationPatch} patch
1141
+ * @returns {void}
999
1142
  */
1000
1143
  declare function updateCurrentCorrelationContext(patch: CorrelationPatch$1): void;
1001
1144
  type CorrelationPatch$1 = CorrelationPatch$5;
1002
1145
 
1146
+ /**
1147
+ * Install the Effect runtime used by fire-and-forget observability logs.
1148
+ * Returns a restore function so tests and embedded hosts can scope overrides.
1149
+ *
1150
+ * @param {SmithersLogRunner | null} runner
1151
+ * @returns {() => void}
1152
+ */
1153
+ declare function setSmithersLogRunner(runner: SmithersLogRunner | null): () => void;
1003
1154
  /**
1004
1155
  * @param {string} message
1005
1156
  * @param {LogAnnotations} [annotations]
@@ -1024,6 +1175,10 @@ declare function logWarning(message: string, annotations?: LogAnnotations, span?
1024
1175
  * @param {string} [span]
1025
1176
  */
1026
1177
  declare function logError(message: string, annotations?: LogAnnotations, span?: string): void;
1178
+ type SmithersLogRunner = {
1179
+ runFork: (effect: Effect.Effect<void, never, never>) => unknown;
1180
+ runPromise: (effect: Effect.Effect<void, never, never>) => Promise<void>;
1181
+ };
1027
1182
  type LogAnnotations = Record<string, unknown> | undefined;
1028
1183
 
1029
1184
  type CorrelationContext = CorrelationContext$5;
@@ -1039,4 +1194,4 @@ type SmithersMetricDefinition = SmithersMetricDefinition$2;
1039
1194
  type SmithersObservabilityOptions = SmithersObservabilityOptions$4;
1040
1195
  type SmithersObservabilityService = SmithersObservabilityService$1;
1041
1196
 
1042
- export { type CorrelationContext, CorrelationContextLive, type CorrelationContextPatch, CorrelationContextService, type CorrelationPatch, type MetricLabels, MetricsService, MetricsServiceLive, type MetricsServiceShape, type MetricsSnapshot, type ResolvedSmithersObservabilityOptions, type SmithersEvent, type SmithersLogFormat, type SmithersMetricDefinition, SmithersObservability, type SmithersObservabilityOptions, type SmithersObservabilityService, TracingService, TracingServiceLive, activeNodes, activeRuns, annotateSmithersTrace, approvalPending, approvalWaitDuration, approvalsDenied, approvalsGranted, approvalsRequested, attemptDuration, cacheHits, cacheMisses, correlationContextFiberRef, correlationContextToLogAnnotations, createSmithersObservabilityLayer, createSmithersOtelLayer, createSmithersRuntimeLayer, dbQueryDuration, dbRetries, dbTransactionDuration, dbTransactionRetries, dbTransactionRollbacks, errorsTotal, eventsEmittedTotal, externalWaitAsyncPending, getCurrentCorrelationContext, getCurrentCorrelationContextEffect, getCurrentSmithersTraceAnnotations, getCurrentSmithersTraceSpan, hotReloadDuration, hotReloadFailures, hotReloads, httpRequestDuration, httpRequests, logDebug, logError, logInfo, logWarning, makeSmithersSpanAttributes, mergeCorrelationContext, metricsServiceAdapter, nodeDuration, nodeRetriesTotal, nodesFailed, nodesFinished, nodesStarted, processHeapUsedBytes, processMemoryRssBytes, processUptimeSeconds, prometheusContentType, promptSizeBytes, renderPrometheusMetrics, replaysStarted, resolveSmithersObservabilityOptions, responseSizeBytes, rewindDurationMs, rewindFramesDeleted, rewindRollbackTotal, rewindSandboxesReverted, rewindTotal, runDuration, runForksCreated, runWithCorrelationContext, runsAncestryDepth, runsCancelledTotal, runsCarriedStateBytes, runsContinuedTotal, runsFailedTotal, runsFinishedTotal, runsResumedTotal, runsTotal, sandboxActive, sandboxBundleSizeBytes, sandboxCompletedTotal, sandboxCreatedTotal, sandboxDurationMs, sandboxPatchCount, sandboxTransportDurationMs, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, scorerEventsFailed, scorerEventsFinished, scorerEventsStarted, smithersMetricCatalog, smithersMetrics, smithersSpanNames, snapshotDuration, snapshotsCaptured, timerDelayDuration, timersCancelled, timersCreated, timersFired, timersPending, toPrometheusMetricName, tokensCacheReadTotal, tokensCacheWriteTotal, tokensContextWindowBucketTotal, tokensContextWindowPerCall, tokensInputPerCall, tokensInputTotal, tokensOutputPerCall, tokensOutputTotal, tokensReasoningTotal, toolCallErrorsTotal, toolCallsTotal, toolDuration, toolOutputTruncatedTotal, trackEvent as trackSmithersEvent, updateCurrentCorrelationContext, updateProcessMetrics, vcsDuration, withCorrelationContext, withCurrentCorrelationContext, withSmithersSpan };
1197
+ export { type CorrelationContext, CorrelationContextLive, type CorrelationContextPatch, CorrelationContextService, type CorrelationPatch, type MetricLabels, MetricsService, MetricsServiceLive, type MetricsServiceShape, type MetricsSnapshot, type ResolvedSmithersObservabilityOptions, type SmithersEvent, type SmithersLogFormat, type SmithersMetricDefinition, SmithersObservability, type SmithersObservabilityOptions, type SmithersObservabilityService, TracingService, TracingServiceLive, activeNodes, activeRuns, annotateSmithersTrace, approvalPending, approvalWaitDuration, approvalsDenied, approvalsGranted, approvalsRequested, attemptDuration, cacheHits, cacheMisses, correlationContextFiberRef, correlationContextToLogAnnotations, createSmithersObservabilityLayer, createSmithersOtelLayer, createSmithersRuntimeLayer, dbQueryDuration, dbRetries, dbTransactionDuration, dbTransactionRetries, dbTransactionRollbacks, errorsTotal, eventsEmittedTotal, externalWaitAsyncPending, getCurrentCorrelationContext, getCurrentCorrelationContextEffect, getCurrentSmithersTraceAnnotations, getCurrentSmithersTraceSpan, hotReloadDuration, hotReloadFailures, hotReloads, httpRequestDuration, httpRequests, logDebug, logError, logInfo, logWarning, makeSmithersSpanAttributes, mergeCorrelationContext, metricsServiceAdapter, nodeDuration, nodeRetriesTotal, nodesFailed, nodesFinished, nodesStarted, processHeapUsedBytes, processMemoryRssBytes, processUptimeSeconds, prometheusContentType, promptSizeBytes, renderPrometheusMetrics, replaysStarted, resolveSmithersObservabilityOptions, responseSizeBytes, rewindDurationMs, rewindFramesDeleted, rewindRollbackTotal, rewindSandboxesReverted, rewindTotal, runDuration, runForksCreated, runWithCorrelationContext, runsAncestryDepth, runsCancelledTotal, runsCarriedStateBytes, runsContinuedTotal, runsFailedTotal, runsFinishedTotal, runsResumedTotal, runsTotal, sandboxActive, sandboxBundleSizeBytes, sandboxCompletedTotal, sandboxCreatedTotal, sandboxDurationMs, sandboxPatchCount, sandboxTransportDurationMs, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, scorerEventsFailed, scorerEventsFinished, scorerEventsStarted, setSmithersLogRunner, smithersMetricCatalog, smithersMetrics, smithersSpanNames, snapshotDuration, snapshotsCaptured, timerDelayDuration, timersCancelled, timersCreated, timersFired, timersPending, toPrometheusMetricName, tokensCacheReadTotal, tokensCacheWriteTotal, tokensContextWindowBucketTotal, tokensContextWindowPerCall, tokensInputPerCall, tokensInputTotal, tokensOutputPerCall, tokensOutputTotal, tokensReasoningTotal, toolCallErrorsTotal, toolCallsTotal, toolDuration, toolOutputTruncatedTotal, trackEvent as trackSmithersEvent, updateCurrentCorrelationContext, updateProcessMetrics, vcsDuration, withCorrelationContext, withCurrentCorrelationContext, withSmithersSpan };
package/src/index.js CHANGED
@@ -32,4 +32,4 @@ export { rewindTotal, rewindRollbackTotal, rewindDurationMs, rewindFramesDeleted
32
32
  export { activeNodes, activeRuns, approvalPending, externalWaitAsyncPending, approvalsDenied, approvalsGranted, approvalsRequested, approvalWaitDuration, timerDelayDuration, timersCancelled, timersCreated, timersFired, timersPending, attemptDuration, cacheHits, cacheMisses, dbQueryDuration, dbRetries, dbTransactionDuration, dbTransactionRetries, dbTransactionRollbacks, errorsTotal, eventsEmittedTotal, hotReloadDuration, hotReloadFailures, hotReloads, httpRequestDuration, httpRequests, nodeDuration, nodeRetriesTotal, nodesFailed, nodesFinished, nodesStarted, processHeapUsedBytes, processMemoryRssBytes, processUptimeSeconds, promptSizeBytes, responseSizeBytes, runDuration, runsCancelledTotal, runsContinuedTotal, runsFailedTotal, runsFinishedTotal, runsResumedTotal, runsAncestryDepth, runsCarriedStateBytes, sandboxActive, sandboxBundleSizeBytes, sandboxCompletedTotal, sandboxCreatedTotal, sandboxDurationMs, sandboxPatchCount, sandboxTransportDurationMs, runsTotal, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, tokensCacheReadTotal, tokensCacheWriteTotal, tokensContextWindowBucketTotal, tokensContextWindowPerCall, tokensInputPerCall, tokensInputTotal, tokensOutputPerCall, tokensOutputTotal, tokensReasoningTotal, toolCallErrorsTotal, toolCallsTotal, toolDuration, toolOutputTruncatedTotal, scorerEventsStarted, scorerEventsFinished, scorerEventsFailed, snapshotsCaptured, runForksCreated, replaysStarted, snapshotDuration, trackEvent as trackSmithersEvent, updateProcessMetrics, vcsDuration, toPrometheusMetricName, smithersMetricCatalog, metricsServiceAdapter, } from "./metrics/index.js";
33
33
  export { correlationContextFiberRef, correlationContextToLogAnnotations, CorrelationContextLive, CorrelationContextService, getCurrentCorrelationContext, getCurrentCorrelationContextEffect, mergeCorrelationContext, runWithCorrelationContext, withCorrelationContext, withCurrentCorrelationContext, } from "./correlation.js";
34
34
  export { updateCurrentCorrelationContext } from "./correlation.js";
35
- export { logDebug, logInfo, logWarning, logError, } from "./logging.js";
35
+ export { logDebug, logInfo, logWarning, logError, setSmithersLogRunner, } from "./logging.js";
package/src/logging.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Logger, LogLevel } from "effect";
2
2
  import { getCurrentSmithersTraceAnnotations } from "./getCurrentSmithersTraceAnnotations.js";
3
3
  import { correlationContextToLogAnnotations, getCurrentCorrelationContext, withCurrentCorrelationContext, } from "./correlation.js";
4
4
  /**
@@ -11,6 +11,7 @@ const LOG_LEVEL_DEBUG = 1;
11
11
  const LOG_LEVEL_INFO = 2;
12
12
  const LOG_LEVEL_WARNING = 3;
13
13
  const LOG_LEVEL_ERROR = 4;
14
+ const LOG_RUNNER_KEY = Symbol.for("smithers.observability.logRunner");
14
15
 
15
16
  /** @returns {number} */
16
17
  function resolveMinLevel() {
@@ -31,6 +32,58 @@ function resolveMinLevel() {
31
32
 
32
33
  const minLevel = resolveMinLevel();
33
34
 
35
+ /**
36
+ * @typedef {{
37
+ * runFork: (effect: Effect.Effect<void, never, never>) => unknown;
38
+ * runPromise: (effect: Effect.Effect<void, never, never>) => Promise<void>;
39
+ * }} SmithersLogRunner
40
+ */
41
+
42
+ /** @returns {{ runner: SmithersLogRunner | null }} */
43
+ function getRunnerState() {
44
+ const globalState = /** @type {typeof globalThis & { [LOG_RUNNER_KEY]?: { runner: SmithersLogRunner | null } }} */ (globalThis);
45
+ globalState[LOG_RUNNER_KEY] ??= { runner: null };
46
+ return globalState[LOG_RUNNER_KEY];
47
+ }
48
+
49
+ /** @type {SmithersLogRunner} */
50
+ const defaultRunner = {
51
+ runFork: (effect) => Effect.runFork(effect),
52
+ runPromise: (effect) => Effect.runPromise(effect),
53
+ };
54
+
55
+ /** @returns {SmithersLogRunner} */
56
+ function getLogRunner() {
57
+ return getRunnerState().runner ?? defaultRunner;
58
+ }
59
+
60
+ /**
61
+ * Install the Effect runtime used by fire-and-forget observability logs.
62
+ * Returns a restore function so tests and embedded hosts can scope overrides.
63
+ *
64
+ * @param {SmithersLogRunner | null} runner
65
+ * @returns {() => void}
66
+ */
67
+ export function setSmithersLogRunner(runner) {
68
+ const state = getRunnerState();
69
+ const previous = state.runner;
70
+ state.runner = runner;
71
+ return () => {
72
+ state.runner = previous;
73
+ };
74
+ }
75
+
76
+ /** @param {number} level */
77
+ function toEffectLogLevel(level) {
78
+ switch (level) {
79
+ case LOG_LEVEL_DEBUG: return LogLevel.Debug;
80
+ case LOG_LEVEL_INFO: return LogLevel.Info;
81
+ case LOG_LEVEL_WARNING: return LogLevel.Warning;
82
+ case LOG_LEVEL_ERROR: return LogLevel.Error;
83
+ default: return LogLevel.All;
84
+ }
85
+ }
86
+
34
87
  /**
35
88
  * @param {Effect.Effect<void, never, never>} effect
36
89
  * @param {LogAnnotations} [annotations]
@@ -66,7 +119,12 @@ function buildLogProgram(effect, annotations, span) {
66
119
  function emitLog(effect, annotations, span, level = LOG_LEVEL_INFO) {
67
120
  if (level < minLevel) return;
68
121
  const program = buildLogProgram(effect, annotations, span);
69
- if (program) void Effect.runFork(program);
122
+ if (!program) return;
123
+ try {
124
+ void getLogRunner().runFork(program.pipe(Logger.withMinimumLogLevel(toEffectLogLevel(level))));
125
+ } catch {
126
+ // Logging must never break the caller.
127
+ }
70
128
  }
71
129
 
72
130
  /**
@@ -79,7 +137,12 @@ function emitLog(effect, annotations, span, level = LOG_LEVEL_INFO) {
79
137
  async function emitLogAwait(effect, annotations, span, level = LOG_LEVEL_INFO) {
80
138
  if (level < minLevel) return;
81
139
  const program = buildLogProgram(effect, annotations, span);
82
- if (program) await Effect.runPromise(program);
140
+ if (!program) return;
141
+ try {
142
+ await getLogRunner().runPromise(program.pipe(Logger.withMinimumLogLevel(toEffectLogLevel(level))));
143
+ } catch {
144
+ // Logging must never break the caller.
145
+ }
83
146
  }
84
147
  /**
85
148
  * @param {string} message
@@ -0,0 +1,2 @@
1
+ import { Metric } from "effect";
2
+ export const gatewayRunEventBackpressureDisconnectTotal = Metric.counter("smithers.gateway.run_event_backpressure_disconnect_total");
@@ -92,6 +92,7 @@ export { gatewayWebhooksRejectedTotal } from "./gatewayWebhooksRejectedTotal.js"
92
92
  export { devtoolsSubscribeTotal } from "./devtoolsSubscribeTotal.js";
93
93
  export { devtoolsEventTotal } from "./devtoolsEventTotal.js";
94
94
  export { devtoolsBackpressureDisconnectTotal } from "./devtoolsBackpressureDisconnectTotal.js";
95
+ export { gatewayRunEventBackpressureDisconnectTotal } from "./gatewayRunEventBackpressureDisconnectTotal.js";
95
96
  export { eventsEmittedTotal } from "./eventsEmittedTotal.js";
96
97
  export { taskHeartbeatsTotal } from "./taskHeartbeatsTotal.js";
97
98
  export { taskHeartbeatTimeoutTotal } from "./taskHeartbeatTimeoutTotal.js";
@@ -75,5 +75,6 @@ export function resolveSmithersObservabilityOptions(options = {}) {
75
75
  ? resolveLogFormat(options.logFormat)
76
76
  : resolveLogFormat(process.env.SMITHERS_LOG_FORMAT),
77
77
  logLevel: resolveLogLevel(options.logLevel ?? process.env.SMITHERS_LOG_LEVEL),
78
+ installLogger: options.installLogger !== false,
78
79
  };
79
80
  }