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 +9 -0
- package/README.md +49 -3
- package/dist/index.cjs +25 -13
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +25 -15
- package/package.json +5 -3
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
|
-
|
|
76
|
+
### 3. Join the surrounding trace with `withAutotel`
|
|
77
77
|
|
|
78
|
-
|
|
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
|
-
###
|
|
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:
|
|
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 {
|
|
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:
|
|
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.
|
|
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.
|
|
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",
|