autotel-effect 3.0.0 → 4.0.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/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
@@ -75,7 +75,45 @@ appLayer.pipe(Layer.provideMerge(layer({ serviceName: 'my-api' })));
75
75
 
76
76
  HTTP spans from autotel's `node:http` instrumentation become parents of domain spans from `Effect.withSpan`.
77
77
 
78
- ### 3. Unit tests: provide nothing
78
+ ### 3. Logs come with it
79
+
80
+ One `layer()` call bridges both signals. `Effect.log*` becomes an OpenTelemetry
81
+ **log record** (so it reaches any OTLP log backend, including autotel-devtools)
82
+ plus a trace-correlated structured line on stdout — no second layer to remember:
83
+
84
+ ```typescript
85
+ const AutotelEffect = layer({ serviceName: 'my-api' }); // spans + logs
86
+ ```
87
+
88
+ Pass `logs: false` for spans only, or an options object to configure the
89
+ logger. Log annotations become log metadata, and failure causes are logged as
90
+ `err`:
91
+
92
+ ```typescript
93
+ Effect.log('charged').pipe(Effect.annotateLogs({ 'order.id': 'o-1' }));
94
+ // {"level":"info","msg":"charged","order.id":"o-1","traceId":"...","spanId":"..."}
95
+ ```
96
+
97
+ Rich annotation values are flattened to the attribute shape the rest of autotel
98
+ produces: a nested object becomes dot-notation keys, a `Date` becomes an ISO
99
+ string, an `Error` becomes its message.
100
+
101
+ ```typescript
102
+ Effect.annotateLogs({ order: { id: 7 }, at: new Date(0) });
103
+ // attributes: { 'order.id': 7, at: '1970-01-01T00:00:00.000Z' }
104
+ ```
105
+
106
+ Errors keep their stack: a `Cause` passed to `Effect.logError` (or an `Error`
107
+ in the message parts) is logged as `err` rather than stringified into `msg`.
108
+
109
+ Options: `mergeWithExisting: true` keeps Effect's console logger alongside this
110
+ one; `level` sets the stdout minimum level (default `'info'` — leave it there
111
+ and `Effect.logDebug` never reaches stdout even when Effect's own minimum level
112
+ allows it); `pretty: true` swaps JSON for human-readable stdout; `console: false`
113
+ drops the stdout line and emits only the log record — use it when
114
+ `captureConsole()` is on, or every Effect log is reported twice.
115
+
116
+ ### 4. Unit tests: provide nothing
79
117
 
80
118
  Effect's default `Tracer` is already an in-memory native tracer, so handler tests
81
119
  need no layer at all — `Effect.withSpan` runs and exports nowhere:
package/dist/index.cjs CHANGED
@@ -21,24 +21,95 @@ 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);
29
+ let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
30
+ let autotel = require("autotel");
31
+ let autotel_logger = require("autotel/logger");
32
+ let effect_Cause = require("effect/Cause");
33
+ effect_Cause = __toESM(effect_Cause, 1);
28
34
  let effect_Layer = require("effect/Layer");
29
35
  effect_Layer = __toESM(effect_Layer, 1);
36
+ let effect_Logger = require("effect/Logger");
37
+ effect_Logger = __toESM(effect_Logger, 1);
38
+ let effect_References = require("effect/References");
39
+ effect_References = __toESM(effect_References, 1);
30
40
  //#region src/index.ts
31
41
  /**
32
- * Routes `Effect.withSpan` through autotel's global OpenTelemetry provider.
42
+ * Routes `Effect.withSpan` and `Effect.log*` through autotel's global
43
+ * OpenTelemetry provider.
33
44
  *
34
45
  * Call `autotel.init()` before this layer is built — typically by loading an
35
46
  * instrumentation module with `node --import` or `tsx --import`.
36
47
  */
37
48
  function layer(options) {
38
- return _effect_opentelemetry_OtelTracer.layerGlobal.pipe(effect_Layer.provide(_effect_opentelemetry_Resource.layer({
49
+ const tracer = _effect_opentelemetry_OtelTracer.layerGlobal.pipe(effect_Layer.provide(_effect_opentelemetry_Resource.layer({
39
50
  serviceName: options.serviceName,
40
51
  ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {}
41
52
  })));
53
+ const logs = options.logs ?? true;
54
+ return effect_Layer.mergeAll(tracer, logs === false ? effect_Layer.empty : loggerLayer({
55
+ serviceName: options.serviceName,
56
+ ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {},
57
+ ...logs === true ? {} : logs
58
+ }));
59
+ }
60
+ /**
61
+ * Routes Effect's `Effect.log*` through autotel instead of Effect's console
62
+ * logger, emitting each log as an OpenTelemetry log record (so it reaches any
63
+ * OTLP log backend, autotel-devtools included) and, unless disabled, a
64
+ * structured line on stdout via autotel's built-in logger.
65
+ *
66
+ * `layer()` already includes this. Reach for it on its own only when something
67
+ * else owns the tracer and you want autotel to own the logs.
68
+ */
69
+ function loggerLayer(options) {
70
+ const log = (0, autotel_logger.createBuiltinLogger)(options.serviceName, {
71
+ ...options.level ? { level: options.level } : {},
72
+ ...options.pretty ? { pretty: options.pretty } : {}
73
+ });
74
+ const otelLogger = () => _opentelemetry_api_logs.logs.getLogger(options.serviceName, options.serviceVersion);
75
+ return effect_Logger.layer([effect_Logger.make(({ cause, fiber, logLevel, message }) => {
76
+ const method = LEVEL_TO_METHOD[logLevel];
77
+ if (!method) return;
78
+ const parts = Array.isArray(message) ? message : [message];
79
+ const metadata = { ...fiber.getRef(effect_References.CurrentLogAnnotations) };
80
+ const errors = parts.filter((part) => part instanceof Error);
81
+ if (cause.reasons.length > 0) metadata.err = effect_Cause.pretty(cause);
82
+ else if (errors.length > 0) metadata.err = errors.map((error) => error.stack ?? String(error));
83
+ const body = parts.map(formatPart).join(" ");
84
+ if (options.console ?? true) log[method](metadata, body);
85
+ otelLogger().emit({
86
+ body,
87
+ severityNumber: (0, _effect_opentelemetry_OtelLogger.logLevelToSeverityNumber)(logLevel),
88
+ severityText: logLevel,
89
+ attributes: (0, autotel.flattenToAttributes)({
90
+ ...metadata,
91
+ ...Array.isArray(metadata.err) ? { err: metadata.err.join("\n") } : {}
92
+ })
93
+ });
94
+ })], { mergeWithExisting: options.mergeWithExisting ?? false });
95
+ }
96
+ const LEVEL_TO_METHOD = {
97
+ Trace: "debug",
98
+ Debug: "debug",
99
+ Info: "info",
100
+ Warn: "warn",
101
+ Error: "error",
102
+ Fatal: "error"
103
+ };
104
+ function formatPart(part) {
105
+ if (typeof part === "string") return part;
106
+ if (part instanceof Error) return String(part);
107
+ try {
108
+ return JSON.stringify(part) ?? String(part);
109
+ } catch {
110
+ return String(part);
111
+ }
42
112
  }
43
113
  //#endregion
44
114
  exports.layer = layer;
115
+ exports.loggerLayer = loggerLayer;
package/dist/index.d.cts CHANGED
@@ -1,16 +1,50 @@
1
- import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
1
+ import { BuiltinLoggerOptions } from "autotel/logger";
2
2
  import * as Layer from "effect/Layer";
3
3
  //#region src/index.d.ts
4
+ interface AutotelEffectLoggerOptions {
5
+ /** Keep Effect's default console logger alongside this one. Default: false. */
6
+ readonly mergeWithExisting?: boolean;
7
+ /**
8
+ * Minimum level autotel's logger emits to stdout. Default: `'info'` — leave
9
+ * it there and `Effect.logDebug` never reaches stdout even when Effect's own
10
+ * minimum log level allows it.
11
+ */
12
+ readonly level?: BuiltinLoggerOptions['level'];
13
+ /** Pretty-print the stdout line instead of JSON. Default: false. */
14
+ readonly pretty?: boolean;
15
+ /**
16
+ * Also write each log to stdout via autotel's built-in logger.
17
+ * Default: true. Set to false when `captureConsole()` is on, otherwise
18
+ * every Effect log is reported twice.
19
+ */
20
+ readonly console?: boolean;
21
+ }
4
22
  interface AutotelEffectLayerOptions {
5
23
  readonly serviceName: string;
6
24
  readonly serviceVersion?: string;
25
+ /**
26
+ * Bridge `Effect.log*` as well as spans. Default: `true`. Pass `false` for
27
+ * spans only, or an options object to configure the logger.
28
+ */
29
+ readonly logs?: boolean | AutotelEffectLoggerOptions;
7
30
  }
8
31
  /**
9
- * Routes `Effect.withSpan` through autotel's global OpenTelemetry provider.
32
+ * Routes `Effect.withSpan` and `Effect.log*` through autotel's global
33
+ * OpenTelemetry provider.
10
34
  *
11
35
  * Call `autotel.init()` before this layer is built — typically by loading an
12
36
  * instrumentation module with `node --import` or `tsx --import`.
13
37
  */
14
- declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<OtelTracer.OtelTracer, never, never>;
38
+ declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<never>;
39
+ /**
40
+ * Routes Effect's `Effect.log*` through autotel instead of Effect's console
41
+ * logger, emitting each log as an OpenTelemetry log record (so it reaches any
42
+ * OTLP log backend, autotel-devtools included) and, unless disabled, a
43
+ * structured line on stdout via autotel's built-in logger.
44
+ *
45
+ * `layer()` already includes this. Reach for it on its own only when something
46
+ * else owns the tracer and you want autotel to own the logs.
47
+ */
48
+ declare function loggerLayer(options: Omit<AutotelEffectLayerOptions, 'logs'> & AutotelEffectLoggerOptions): Layer.Layer<never, never, never>;
15
49
  //#endregion
16
- export { AutotelEffectLayerOptions, layer };
50
+ export { AutotelEffectLayerOptions, AutotelEffectLoggerOptions, layer, loggerLayer };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,50 @@
1
- import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
1
+ import { BuiltinLoggerOptions } from "autotel/logger";
2
2
  import * as Layer from "effect/Layer";
3
3
  //#region src/index.d.ts
4
+ interface AutotelEffectLoggerOptions {
5
+ /** Keep Effect's default console logger alongside this one. Default: false. */
6
+ readonly mergeWithExisting?: boolean;
7
+ /**
8
+ * Minimum level autotel's logger emits to stdout. Default: `'info'` — leave
9
+ * it there and `Effect.logDebug` never reaches stdout even when Effect's own
10
+ * minimum log level allows it.
11
+ */
12
+ readonly level?: BuiltinLoggerOptions['level'];
13
+ /** Pretty-print the stdout line instead of JSON. Default: false. */
14
+ readonly pretty?: boolean;
15
+ /**
16
+ * Also write each log to stdout via autotel's built-in logger.
17
+ * Default: true. Set to false when `captureConsole()` is on, otherwise
18
+ * every Effect log is reported twice.
19
+ */
20
+ readonly console?: boolean;
21
+ }
4
22
  interface AutotelEffectLayerOptions {
5
23
  readonly serviceName: string;
6
24
  readonly serviceVersion?: string;
25
+ /**
26
+ * Bridge `Effect.log*` as well as spans. Default: `true`. Pass `false` for
27
+ * spans only, or an options object to configure the logger.
28
+ */
29
+ readonly logs?: boolean | AutotelEffectLoggerOptions;
7
30
  }
8
31
  /**
9
- * Routes `Effect.withSpan` through autotel's global OpenTelemetry provider.
32
+ * Routes `Effect.withSpan` and `Effect.log*` through autotel's global
33
+ * OpenTelemetry provider.
10
34
  *
11
35
  * Call `autotel.init()` before this layer is built — typically by loading an
12
36
  * instrumentation module with `node --import` or `tsx --import`.
13
37
  */
14
- declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<OtelTracer.OtelTracer, never, never>;
38
+ declare function layer(options: AutotelEffectLayerOptions): Layer.Layer<never>;
39
+ /**
40
+ * Routes Effect's `Effect.log*` through autotel instead of Effect's console
41
+ * logger, emitting each log as an OpenTelemetry log record (so it reaches any
42
+ * OTLP log backend, autotel-devtools included) and, unless disabled, a
43
+ * structured line on stdout via autotel's built-in logger.
44
+ *
45
+ * `layer()` already includes this. Reach for it on its own only when something
46
+ * else owns the tracer and you want autotel to own the logs.
47
+ */
48
+ declare function loggerLayer(options: Omit<AutotelEffectLayerOptions, 'logs'> & AutotelEffectLoggerOptions): Layer.Layer<never, never, never>;
15
49
  //#endregion
16
- export { AutotelEffectLayerOptions, layer };
50
+ export { AutotelEffectLayerOptions, AutotelEffectLoggerOptions, layer, loggerLayer };
package/dist/index.js CHANGED
@@ -1,18 +1,85 @@
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";
4
+ import { logs } from "@opentelemetry/api-logs";
5
+ import { flattenToAttributes } from "autotel";
6
+ import { createBuiltinLogger } from "autotel/logger";
7
+ import * as Cause from "effect/Cause";
3
8
  import * as Layer from "effect/Layer";
9
+ import * as Logger from "effect/Logger";
10
+ import * as References from "effect/References";
4
11
  //#region src/index.ts
5
12
  /**
6
- * Routes `Effect.withSpan` through autotel's global OpenTelemetry provider.
13
+ * Routes `Effect.withSpan` and `Effect.log*` through autotel's global
14
+ * OpenTelemetry provider.
7
15
  *
8
16
  * Call `autotel.init()` before this layer is built — typically by loading an
9
17
  * instrumentation module with `node --import` or `tsx --import`.
10
18
  */
11
19
  function layer(options) {
12
- return OtelTracer.layerGlobal.pipe(Layer.provide(Resource.layer({
20
+ const tracer = OtelTracer.layerGlobal.pipe(Layer.provide(Resource.layer({
13
21
  serviceName: options.serviceName,
14
22
  ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {}
15
23
  })));
24
+ const logs = options.logs ?? true;
25
+ return Layer.mergeAll(tracer, logs === false ? Layer.empty : loggerLayer({
26
+ serviceName: options.serviceName,
27
+ ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {},
28
+ ...logs === true ? {} : logs
29
+ }));
30
+ }
31
+ /**
32
+ * Routes Effect's `Effect.log*` through autotel instead of Effect's console
33
+ * logger, emitting each log as an OpenTelemetry log record (so it reaches any
34
+ * OTLP log backend, autotel-devtools included) and, unless disabled, a
35
+ * structured line on stdout via autotel's built-in logger.
36
+ *
37
+ * `layer()` already includes this. Reach for it on its own only when something
38
+ * else owns the tracer and you want autotel to own the logs.
39
+ */
40
+ function loggerLayer(options) {
41
+ const log = createBuiltinLogger(options.serviceName, {
42
+ ...options.level ? { level: options.level } : {},
43
+ ...options.pretty ? { pretty: options.pretty } : {}
44
+ });
45
+ const otelLogger = () => logs.getLogger(options.serviceName, options.serviceVersion);
46
+ return Logger.layer([Logger.make(({ cause, fiber, logLevel, message }) => {
47
+ const method = LEVEL_TO_METHOD[logLevel];
48
+ if (!method) return;
49
+ const parts = Array.isArray(message) ? message : [message];
50
+ const metadata = { ...fiber.getRef(References.CurrentLogAnnotations) };
51
+ const errors = parts.filter((part) => part instanceof Error);
52
+ if (cause.reasons.length > 0) metadata.err = Cause.pretty(cause);
53
+ else if (errors.length > 0) metadata.err = errors.map((error) => error.stack ?? String(error));
54
+ const body = parts.map(formatPart).join(" ");
55
+ if (options.console ?? true) log[method](metadata, body);
56
+ otelLogger().emit({
57
+ body,
58
+ severityNumber: logLevelToSeverityNumber(logLevel),
59
+ severityText: logLevel,
60
+ attributes: flattenToAttributes({
61
+ ...metadata,
62
+ ...Array.isArray(metadata.err) ? { err: metadata.err.join("\n") } : {}
63
+ })
64
+ });
65
+ })], { mergeWithExisting: options.mergeWithExisting ?? false });
66
+ }
67
+ const LEVEL_TO_METHOD = {
68
+ Trace: "debug",
69
+ Debug: "debug",
70
+ Info: "info",
71
+ Warn: "warn",
72
+ Error: "error",
73
+ Fatal: "error"
74
+ };
75
+ function formatPart(part) {
76
+ if (typeof part === "string") return part;
77
+ if (part instanceof Error) return String(part);
78
+ try {
79
+ return JSON.stringify(part) ?? String(part);
80
+ } catch {
81
+ return String(part);
82
+ }
16
83
  }
17
84
  //#endregion
18
- export { layer };
85
+ export { layer, loggerLayer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-effect",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
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",
@@ -50,14 +52,19 @@
50
52
  },
51
53
  "devDependencies": {
52
54
  "@effect/opentelemetry": "4.0.0-rc.112",
55
+ "@opentelemetry/api-logs": "^0.222.0",
56
+ "@opentelemetry/sdk-logs": "^0.222.0",
53
57
  "@types/node": "^26.1.2",
54
- "autotel": "7.6.0",
58
+ "autotel": "7.6.2",
55
59
  "effect": "4.0.0-rc.112",
56
60
  "rimraf": "^6.1.3",
57
61
  "tsdown": "^0.22.14",
58
62
  "typescript": "^6.0.3",
59
63
  "vitest": "^4.1.10"
60
64
  },
65
+ "dependencies": {
66
+ "@opentelemetry/api-logs": "^0.222.0"
67
+ },
61
68
  "scripts": {
62
69
  "build": "tsdown",
63
70
  "dev": "tsdown --watch",