autotel-effect 4.0.1 → 5.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/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)
@@ -113,7 +146,7 @@ allows it); `pretty: true` swaps JSON for human-readable stdout; `console: false
113
146
  drops the stdout line and emits only the log record — use it when
114
147
  `captureConsole()` is on, or every Effect log is reported twice.
115
148
 
116
- ### 4. Unit tests: provide nothing
149
+ ### 5. Unit tests: provide nothing
117
150
 
118
151
  Effect's default `Tracer` is already an in-memory native tracer, so handler tests
119
152
  need no layer at all — `Effect.withSpan` runs and exports nowhere:
@@ -127,12 +160,16 @@ that assert on exported spans.
127
160
 
128
161
  ## Trace shape
129
162
 
163
+ With `Effect.runPromise(withAutotel(program))`:
164
+
130
165
  ```text
131
166
  HTTP GET /api/todos (autotel node:http)
132
167
  └── todo.list (Effect.withSpan)
133
168
  └── db.query (another withSpan or instrumented client)
134
169
  ```
135
170
 
171
+ Without `withAutotel`, `todo.list` is the root of a second, unrelated trace.
172
+
136
173
  ## Example
137
174
 
138
175
  See [`apps/example-effect`](../../apps/example-effect) in the autotel monorepo.
package/dist/index.cjs CHANGED
@@ -31,6 +31,8 @@ let autotel = require("autotel");
31
31
  let autotel_logger = require("autotel/logger");
32
32
  let effect_Cause = require("effect/Cause");
33
33
  effect_Cause = __toESM(effect_Cause, 1);
34
+ let effect_Effect = require("effect/Effect");
35
+ effect_Effect = __toESM(effect_Effect, 1);
34
36
  let effect_Layer = require("effect/Layer");
35
37
  effect_Layer = __toESM(effect_Layer, 1);
36
38
  let effect_Logger = require("effect/Logger");
@@ -58,6 +60,23 @@ function layer(options) {
58
60
  }));
59
61
  }
60
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
+ /**
61
80
  * Routes Effect's `Effect.log*` through autotel instead of Effect's console
62
81
  * logger, emitting each log as an OpenTelemetry log record (so it reaches any
63
82
  * OTLP log backend, autotel-devtools included) and, unless disabled, a
@@ -113,3 +132,4 @@ function formatPart(part) {
113
132
  //#endregion
114
133
  exports.layer = layer;
115
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
@@ -2,9 +2,10 @@ import { logLevelToSeverityNumber } from "@effect/opentelemetry/OtelLogger";
2
2
  import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
3
3
  import * as Resource from "@effect/opentelemetry/Resource";
4
4
  import { logs } from "@opentelemetry/api-logs";
5
- import { flattenToAttributes } from "autotel";
5
+ import { flattenToAttributes, getActiveSpan, otelTrace } from "autotel";
6
6
  import { createBuiltinLogger } from "autotel/logger";
7
7
  import * as Cause from "effect/Cause";
8
+ import * as Effect from "effect/Effect";
8
9
  import * as Layer from "effect/Layer";
9
10
  import * as Logger from "effect/Logger";
10
11
  import * as References from "effect/References";
@@ -29,6 +30,23 @@ function layer(options) {
29
30
  }));
30
31
  }
31
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
+ /**
32
50
  * Routes Effect's `Effect.log*` through autotel instead of Effect's console
33
51
  * logger, emitting each log as an OpenTelemetry log record (so it reaches any
34
52
  * OTLP log backend, autotel-devtools included) and, unless disabled, a
@@ -82,4 +100,4 @@ function formatPart(part) {
82
100
  }
83
101
  }
84
102
  //#endregion
85
- 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.1",
3
+ "version": "5.0.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",
@@ -55,7 +55,7 @@
55
55
  "@opentelemetry/api-logs": "^0.222.0",
56
56
  "@opentelemetry/sdk-logs": "^0.222.0",
57
57
  "@types/node": "^26.1.2",
58
- "autotel": "7.6.2",
58
+ "autotel": "7.7.0",
59
59
  "effect": "4.0.0-rc.112",
60
60
  "rimraf": "^6.1.3",
61
61
  "tsdown": "^0.22.14",