autotel-effect 4.0.0 → 4.1.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/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ autotel
2
+ Copyright 2026 Jag Reehal
3
+
4
+ This product is licensed under the Apache License, Version 2.0.
5
+ See the LICENSE file for the full license text.
6
+
7
+ "autotel" is a trademark of Jag Reehal. The Apache License, Version 2.0
8
+ does not grant rights to use the "autotel" name. See TRADEMARKS.md for
9
+ the trademark policy.
package/README.md CHANGED
@@ -73,9 +73,42 @@ In a server, merge the layer where you assemble services:
73
73
  appLayer.pipe(Layer.provideMerge(layer({ serviceName: 'my-api' })));
74
74
  ```
75
75
 
76
- HTTP spans from autotel's `node:http` instrumentation become parents of domain spans from `Effect.withSpan`.
76
+ ### 3. Join the surrounding trace with `withAutotel`
77
77
 
78
- ### 3. Logs come with it
78
+ Effect takes a span's parent from its own `Tracer.ParentSpan`, not from the
79
+ ambient OpenTelemetry context, and marks a span as a root when it has none. So a
80
+ bare `Effect.runPromise` inside an instrumented HTTP handler opens a **new
81
+ trace** — the handler's span and the Effect's span end up in two separate
82
+ traces, with nothing to say they belong together. `withAutotel` hands Effect the
83
+ active autotel span as the parent:
84
+
85
+ ```typescript
86
+ import { withAutotel } from 'autotel-effect';
87
+
88
+ app.get('/api/todos', async () => {
89
+ // inside autotel's `node:http` span
90
+ return Effect.runPromise(withAutotel(program));
91
+ });
92
+ ```
93
+
94
+ It reads the active span when the effect **runs**, so a program assembled once
95
+ at startup still joins the request it is run inside. That is also why this is
96
+ not part of `layer()`: a layer is built once for the whole application, long
97
+ before any request span exists.
98
+
99
+ With it, HTTP spans from autotel's `node:http` instrumentation become parents of
100
+ domain spans from `Effect.withSpan`.
101
+
102
+ ### Don't reach for `trace()` here
103
+
104
+ autotel's `trace(name, fn)` wraps a function and ends its span when that
105
+ function **returns**. A function that returns an `Effect` returns a description
106
+ of work, not the work, so the span closes before anything runs and measures the
107
+ construction rather than the effect. Use Effect's own instrumentation instead —
108
+ `Effect.fn('name')(...)` or `Effect.withSpan('name')` — which is what `layer()`
109
+ routes through autotel.
110
+
111
+ ### 4. Logs come with it
79
112
 
80
113
  One `layer()` call bridges both signals. `Effect.log*` becomes an OpenTelemetry
81
114
  **log record** (so it reaches any OTLP log backend, including autotel-devtools)
@@ -94,6 +127,15 @@ Effect.log('charged').pipe(Effect.annotateLogs({ 'order.id': 'o-1' }));
94
127
  // {"level":"info","msg":"charged","order.id":"o-1","traceId":"...","spanId":"..."}
95
128
  ```
96
129
 
130
+ Rich annotation values are flattened to the attribute shape the rest of autotel
131
+ produces: a nested object becomes dot-notation keys, a `Date` becomes an ISO
132
+ string, an `Error` becomes its message.
133
+
134
+ ```typescript
135
+ Effect.annotateLogs({ order: { id: 7 }, at: new Date(0) });
136
+ // attributes: { 'order.id': 7, at: '1970-01-01T00:00:00.000Z' }
137
+ ```
138
+
97
139
  Errors keep their stack: a `Cause` passed to `Effect.logError` (or an `Error`
98
140
  in the message parts) is logged as `err` rather than stringified into `msg`.
99
141
 
@@ -104,7 +146,7 @@ allows it); `pretty: true` swaps JSON for human-readable stdout; `console: false
104
146
  drops the stdout line and emits only the log record — use it when
105
147
  `captureConsole()` is on, or every Effect log is reported twice.
106
148
 
107
- ### 4. Unit tests: provide nothing
149
+ ### 5. Unit tests: provide nothing
108
150
 
109
151
  Effect's default `Tracer` is already an in-memory native tracer, so handler tests
110
152
  need no layer at all — `Effect.withSpan` runs and exports nowhere:
@@ -118,12 +160,16 @@ that assert on exported spans.
118
160
 
119
161
  ## Trace shape
120
162
 
163
+ With `Effect.runPromise(withAutotel(program))`:
164
+
121
165
  ```text
122
166
  HTTP GET /api/todos (autotel node:http)
123
167
  └── todo.list (Effect.withSpan)
124
168
  └── db.query (another withSpan or instrumented client)
125
169
  ```
126
170
 
171
+ Without `withAutotel`, `todo.list` is the root of a second, unrelated trace.
172
+
127
173
  ## Example
128
174
 
129
175
  See [`apps/example-effect`](../../apps/example-effect) in the autotel monorepo.
package/dist/index.cjs CHANGED
@@ -21,14 +21,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
+ let _effect_opentelemetry_OtelLogger = require("@effect/opentelemetry/OtelLogger");
24
25
  let _effect_opentelemetry_OtelTracer = require("@effect/opentelemetry/OtelTracer");
25
26
  _effect_opentelemetry_OtelTracer = __toESM(_effect_opentelemetry_OtelTracer, 1);
26
27
  let _effect_opentelemetry_Resource = require("@effect/opentelemetry/Resource");
27
28
  _effect_opentelemetry_Resource = __toESM(_effect_opentelemetry_Resource, 1);
28
29
  let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
30
+ let autotel = require("autotel");
29
31
  let autotel_logger = require("autotel/logger");
30
32
  let effect_Cause = require("effect/Cause");
31
33
  effect_Cause = __toESM(effect_Cause, 1);
34
+ let effect_Effect = require("effect/Effect");
35
+ effect_Effect = __toESM(effect_Effect, 1);
32
36
  let effect_Layer = require("effect/Layer");
33
37
  effect_Layer = __toESM(effect_Layer, 1);
34
38
  let effect_Logger = require("effect/Logger");
@@ -56,6 +60,23 @@ function layer(options) {
56
60
  }));
57
61
  }
58
62
  /**
63
+ * Runs `self` as a child of the autotel span active around it, so an
64
+ * `Effect.withSpan` inside an instrumented handler lands in that request's
65
+ * trace: `Effect.runPromise(withAutotel(program))`.
66
+ *
67
+ * Effect takes a span's parent from its own `Tracer.ParentSpan`, which the
68
+ * ambient OpenTelemetry context does not supply. The active span is read when
69
+ * the effect runs rather than when it is built, so a program assembled once at
70
+ * startup still joins the request it runs inside - and why this is not part of
71
+ * `layer()`, which is built once for the whole application.
72
+ */
73
+ function withAutotel(self) {
74
+ return effect_Effect.suspend(() => {
75
+ const spanContext = (0, autotel.getActiveSpan)()?.spanContext();
76
+ return spanContext && autotel.otelTrace.isSpanContextValid(spanContext) ? _effect_opentelemetry_OtelTracer.withSpanContext(self, spanContext) : self;
77
+ });
78
+ }
79
+ /**
59
80
  * Routes Effect's `Effect.log*` through autotel instead of Effect's console
60
81
  * logger, emitting each log as an OpenTelemetry log record (so it reaches any
61
82
  * OTLP log backend, autotel-devtools included) and, unless disabled, a
@@ -82,12 +103,12 @@ function loggerLayer(options) {
82
103
  if (options.console ?? true) log[method](metadata, body);
83
104
  otelLogger().emit({
84
105
  body,
85
- severityNumber: LEVEL_TO_SEVERITY[logLevel],
106
+ severityNumber: (0, _effect_opentelemetry_OtelLogger.logLevelToSeverityNumber)(logLevel),
86
107
  severityText: logLevel,
87
- attributes: {
108
+ attributes: (0, autotel.flattenToAttributes)({
88
109
  ...metadata,
89
110
  ...Array.isArray(metadata.err) ? { err: metadata.err.join("\n") } : {}
90
- }
111
+ })
91
112
  });
92
113
  })], { mergeWithExisting: options.mergeWithExisting ?? false });
93
114
  }
@@ -99,16 +120,6 @@ const LEVEL_TO_METHOD = {
99
120
  Error: "error",
100
121
  Fatal: "error"
101
122
  };
102
- const LEVEL_TO_SEVERITY = {
103
- All: _opentelemetry_api_logs.SeverityNumber.UNSPECIFIED,
104
- Trace: _opentelemetry_api_logs.SeverityNumber.TRACE,
105
- Debug: _opentelemetry_api_logs.SeverityNumber.DEBUG,
106
- Info: _opentelemetry_api_logs.SeverityNumber.INFO,
107
- Warn: _opentelemetry_api_logs.SeverityNumber.WARN,
108
- Error: _opentelemetry_api_logs.SeverityNumber.ERROR,
109
- Fatal: _opentelemetry_api_logs.SeverityNumber.FATAL,
110
- None: _opentelemetry_api_logs.SeverityNumber.UNSPECIFIED
111
- };
112
123
  function formatPart(part) {
113
124
  if (typeof part === "string") return part;
114
125
  if (part instanceof Error) return String(part);
@@ -121,3 +132,4 @@ function formatPart(part) {
121
132
  //#endregion
122
133
  exports.layer = layer;
123
134
  exports.loggerLayer = loggerLayer;
135
+ exports.withAutotel = withAutotel;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BuiltinLoggerOptions } from "autotel/logger";
2
+ import * as Effect from "effect/Effect";
2
3
  import * as Layer from "effect/Layer";
3
4
  //#region src/index.d.ts
4
5
  interface AutotelEffectLoggerOptions {
@@ -36,6 +37,18 @@ interface AutotelEffectLayerOptions {
36
37
  * instrumentation module with `node --import` or `tsx --import`.
37
38
  */
38
39
  declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<never>;
40
+ /**
41
+ * Runs `self` as a child of the autotel span active around it, so an
42
+ * `Effect.withSpan` inside an instrumented handler lands in that request's
43
+ * trace: `Effect.runPromise(withAutotel(program))`.
44
+ *
45
+ * Effect takes a span's parent from its own `Tracer.ParentSpan`, which the
46
+ * ambient OpenTelemetry context does not supply. The active span is read when
47
+ * the effect runs rather than when it is built, so a program assembled once at
48
+ * startup still joins the request it runs inside - and why this is not part of
49
+ * `layer()`, which is built once for the whole application.
50
+ */
51
+ declare function withAutotel<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
39
52
  /**
40
53
  * Routes Effect's `Effect.log*` through autotel instead of Effect's console
41
54
  * logger, emitting each log as an OpenTelemetry log record (so it reaches any
@@ -47,4 +60,4 @@ declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<never>;
47
60
  */
48
61
  declare function loggerLayer(options: Omit<AutotelEffectLayerOptions, 'logs'> & AutotelEffectLoggerOptions): Layer.Layer<never, never, never>;
49
62
  //#endregion
50
- export { AutotelEffectLayerOptions, AutotelEffectLoggerOptions, layer, loggerLayer };
63
+ export { AutotelEffectLayerOptions, AutotelEffectLoggerOptions, layer, loggerLayer, withAutotel };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BuiltinLoggerOptions } from "autotel/logger";
2
+ import * as Effect from "effect/Effect";
2
3
  import * as Layer from "effect/Layer";
3
4
  //#region src/index.d.ts
4
5
  interface AutotelEffectLoggerOptions {
@@ -36,6 +37,18 @@ interface AutotelEffectLayerOptions {
36
37
  * instrumentation module with `node --import` or `tsx --import`.
37
38
  */
38
39
  declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<never>;
40
+ /**
41
+ * Runs `self` as a child of the autotel span active around it, so an
42
+ * `Effect.withSpan` inside an instrumented handler lands in that request's
43
+ * trace: `Effect.runPromise(withAutotel(program))`.
44
+ *
45
+ * Effect takes a span's parent from its own `Tracer.ParentSpan`, which the
46
+ * ambient OpenTelemetry context does not supply. The active span is read when
47
+ * the effect runs rather than when it is built, so a program assembled once at
48
+ * startup still joins the request it runs inside - and why this is not part of
49
+ * `layer()`, which is built once for the whole application.
50
+ */
51
+ declare function withAutotel<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
39
52
  /**
40
53
  * Routes Effect's `Effect.log*` through autotel instead of Effect's console
41
54
  * logger, emitting each log as an OpenTelemetry log record (so it reaches any
@@ -47,4 +60,4 @@ declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<never>;
47
60
  */
48
61
  declare function loggerLayer(options: Omit<AutotelEffectLayerOptions, 'logs'> & AutotelEffectLoggerOptions): Layer.Layer<never, never, never>;
49
62
  //#endregion
50
- export { AutotelEffectLayerOptions, AutotelEffectLoggerOptions, layer, loggerLayer };
63
+ export { AutotelEffectLayerOptions, AutotelEffectLoggerOptions, layer, loggerLayer, withAutotel };
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
+ import { logLevelToSeverityNumber } from "@effect/opentelemetry/OtelLogger";
1
2
  import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
2
3
  import * as Resource from "@effect/opentelemetry/Resource";
3
- import { SeverityNumber, logs } from "@opentelemetry/api-logs";
4
+ import { logs } from "@opentelemetry/api-logs";
5
+ import { flattenToAttributes, getActiveSpan, otelTrace } from "autotel";
4
6
  import { createBuiltinLogger } from "autotel/logger";
5
7
  import * as Cause from "effect/Cause";
8
+ import * as Effect from "effect/Effect";
6
9
  import * as Layer from "effect/Layer";
7
10
  import * as Logger from "effect/Logger";
8
11
  import * as References from "effect/References";
@@ -27,6 +30,23 @@ function layer(options) {
27
30
  }));
28
31
  }
29
32
  /**
33
+ * Runs `self` as a child of the autotel span active around it, so an
34
+ * `Effect.withSpan` inside an instrumented handler lands in that request's
35
+ * trace: `Effect.runPromise(withAutotel(program))`.
36
+ *
37
+ * Effect takes a span's parent from its own `Tracer.ParentSpan`, which the
38
+ * ambient OpenTelemetry context does not supply. The active span is read when
39
+ * the effect runs rather than when it is built, so a program assembled once at
40
+ * startup still joins the request it runs inside - and why this is not part of
41
+ * `layer()`, which is built once for the whole application.
42
+ */
43
+ function withAutotel(self) {
44
+ return Effect.suspend(() => {
45
+ const spanContext = getActiveSpan()?.spanContext();
46
+ return spanContext && otelTrace.isSpanContextValid(spanContext) ? OtelTracer.withSpanContext(self, spanContext) : self;
47
+ });
48
+ }
49
+ /**
30
50
  * Routes Effect's `Effect.log*` through autotel instead of Effect's console
31
51
  * logger, emitting each log as an OpenTelemetry log record (so it reaches any
32
52
  * OTLP log backend, autotel-devtools included) and, unless disabled, a
@@ -53,12 +73,12 @@ function loggerLayer(options) {
53
73
  if (options.console ?? true) log[method](metadata, body);
54
74
  otelLogger().emit({
55
75
  body,
56
- severityNumber: LEVEL_TO_SEVERITY[logLevel],
76
+ severityNumber: logLevelToSeverityNumber(logLevel),
57
77
  severityText: logLevel,
58
- attributes: {
78
+ attributes: flattenToAttributes({
59
79
  ...metadata,
60
80
  ...Array.isArray(metadata.err) ? { err: metadata.err.join("\n") } : {}
61
- }
81
+ })
62
82
  });
63
83
  })], { mergeWithExisting: options.mergeWithExisting ?? false });
64
84
  }
@@ -70,16 +90,6 @@ const LEVEL_TO_METHOD = {
70
90
  Error: "error",
71
91
  Fatal: "error"
72
92
  };
73
- const LEVEL_TO_SEVERITY = {
74
- All: SeverityNumber.UNSPECIFIED,
75
- Trace: SeverityNumber.TRACE,
76
- Debug: SeverityNumber.DEBUG,
77
- Info: SeverityNumber.INFO,
78
- Warn: SeverityNumber.WARN,
79
- Error: SeverityNumber.ERROR,
80
- Fatal: SeverityNumber.FATAL,
81
- None: SeverityNumber.UNSPECIFIED
82
- };
83
93
  function formatPart(part) {
84
94
  if (typeof part === "string") return part;
85
95
  if (part instanceof Error) return String(part);
@@ -90,4 +100,4 @@ function formatPart(part) {
90
100
  }
91
101
  }
92
102
  //#endregion
93
- export { layer, loggerLayer };
103
+ export { layer, loggerLayer, withAutotel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-effect",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Bridge autotel and Effect v4: Effect.withSpan spans export through autotel's global TracerProvider",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,9 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "README.md"
18
+ "README.md",
19
+ "LICENSE",
20
+ "NOTICE"
19
21
  ],
20
22
  "keywords": [
21
23
  "autotel",
@@ -53,7 +55,7 @@
53
55
  "@opentelemetry/api-logs": "^0.222.0",
54
56
  "@opentelemetry/sdk-logs": "^0.222.0",
55
57
  "@types/node": "^26.1.2",
56
- "autotel": "7.6.1",
58
+ "autotel": "7.6.2",
57
59
  "effect": "4.0.0-rc.112",
58
60
  "rimraf": "^6.1.3",
59
61
  "tsdown": "^0.22.14",