autotel-effect 3.0.0 → 4.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
@@ -75,7 +75,36 @@ 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
+ Errors keep their stack: a `Cause` passed to `Effect.logError` (or an `Error`
98
+ in the message parts) is logged as `err` rather than stringified into `msg`.
99
+
100
+ Options: `mergeWithExisting: true` keeps Effect's console logger alongside this
101
+ one; `level` sets the stdout minimum level (default `'info'` — leave it there
102
+ and `Effect.logDebug` never reaches stdout even when Effect's own minimum level
103
+ allows it); `pretty: true` swaps JSON for human-readable stdout; `console: false`
104
+ drops the stdout line and emits only the log record — use it when
105
+ `captureConsole()` is on, or every Effect log is reported twice.
106
+
107
+ ### 4. Unit tests: provide nothing
79
108
 
80
109
  Effect's default `Tracer` is already an in-memory native tracer, so handler tests
81
110
  need no layer at all — `Effect.withSpan` runs and exports nowhere:
package/dist/index.cjs CHANGED
@@ -25,20 +25,99 @@ let _effect_opentelemetry_OtelTracer = require("@effect/opentelemetry/OtelTracer
25
25
  _effect_opentelemetry_OtelTracer = __toESM(_effect_opentelemetry_OtelTracer, 1);
26
26
  let _effect_opentelemetry_Resource = require("@effect/opentelemetry/Resource");
27
27
  _effect_opentelemetry_Resource = __toESM(_effect_opentelemetry_Resource, 1);
28
+ let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
29
+ let autotel_logger = require("autotel/logger");
30
+ let effect_Cause = require("effect/Cause");
31
+ effect_Cause = __toESM(effect_Cause, 1);
28
32
  let effect_Layer = require("effect/Layer");
29
33
  effect_Layer = __toESM(effect_Layer, 1);
34
+ let effect_Logger = require("effect/Logger");
35
+ effect_Logger = __toESM(effect_Logger, 1);
36
+ let effect_References = require("effect/References");
37
+ effect_References = __toESM(effect_References, 1);
30
38
  //#region src/index.ts
31
39
  /**
32
- * Routes `Effect.withSpan` through autotel's global OpenTelemetry provider.
40
+ * Routes `Effect.withSpan` and `Effect.log*` through autotel's global
41
+ * OpenTelemetry provider.
33
42
  *
34
43
  * Call `autotel.init()` before this layer is built — typically by loading an
35
44
  * instrumentation module with `node --import` or `tsx --import`.
36
45
  */
37
46
  function layer(options) {
38
- return _effect_opentelemetry_OtelTracer.layerGlobal.pipe(effect_Layer.provide(_effect_opentelemetry_Resource.layer({
47
+ const tracer = _effect_opentelemetry_OtelTracer.layerGlobal.pipe(effect_Layer.provide(_effect_opentelemetry_Resource.layer({
39
48
  serviceName: options.serviceName,
40
49
  ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {}
41
50
  })));
51
+ const logs = options.logs ?? true;
52
+ return effect_Layer.mergeAll(tracer, logs === false ? effect_Layer.empty : loggerLayer({
53
+ serviceName: options.serviceName,
54
+ ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {},
55
+ ...logs === true ? {} : logs
56
+ }));
57
+ }
58
+ /**
59
+ * Routes Effect's `Effect.log*` through autotel instead of Effect's console
60
+ * logger, emitting each log as an OpenTelemetry log record (so it reaches any
61
+ * OTLP log backend, autotel-devtools included) and, unless disabled, a
62
+ * structured line on stdout via autotel's built-in logger.
63
+ *
64
+ * `layer()` already includes this. Reach for it on its own only when something
65
+ * else owns the tracer and you want autotel to own the logs.
66
+ */
67
+ function loggerLayer(options) {
68
+ const log = (0, autotel_logger.createBuiltinLogger)(options.serviceName, {
69
+ ...options.level ? { level: options.level } : {},
70
+ ...options.pretty ? { pretty: options.pretty } : {}
71
+ });
72
+ const otelLogger = () => _opentelemetry_api_logs.logs.getLogger(options.serviceName, options.serviceVersion);
73
+ return effect_Logger.layer([effect_Logger.make(({ cause, fiber, logLevel, message }) => {
74
+ const method = LEVEL_TO_METHOD[logLevel];
75
+ if (!method) return;
76
+ const parts = Array.isArray(message) ? message : [message];
77
+ const metadata = { ...fiber.getRef(effect_References.CurrentLogAnnotations) };
78
+ const errors = parts.filter((part) => part instanceof Error);
79
+ if (cause.reasons.length > 0) metadata.err = effect_Cause.pretty(cause);
80
+ else if (errors.length > 0) metadata.err = errors.map((error) => error.stack ?? String(error));
81
+ const body = parts.map(formatPart).join(" ");
82
+ if (options.console ?? true) log[method](metadata, body);
83
+ otelLogger().emit({
84
+ body,
85
+ severityNumber: LEVEL_TO_SEVERITY[logLevel],
86
+ severityText: logLevel,
87
+ attributes: {
88
+ ...metadata,
89
+ ...Array.isArray(metadata.err) ? { err: metadata.err.join("\n") } : {}
90
+ }
91
+ });
92
+ })], { mergeWithExisting: options.mergeWithExisting ?? false });
93
+ }
94
+ const LEVEL_TO_METHOD = {
95
+ Trace: "debug",
96
+ Debug: "debug",
97
+ Info: "info",
98
+ Warn: "warn",
99
+ Error: "error",
100
+ Fatal: "error"
101
+ };
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
+ function formatPart(part) {
113
+ if (typeof part === "string") return part;
114
+ if (part instanceof Error) return String(part);
115
+ try {
116
+ return JSON.stringify(part) ?? String(part);
117
+ } catch {
118
+ return String(part);
119
+ }
42
120
  }
43
121
  //#endregion
44
122
  exports.layer = layer;
123
+ 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,93 @@
1
1
  import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
2
2
  import * as Resource from "@effect/opentelemetry/Resource";
3
+ import { SeverityNumber, logs } from "@opentelemetry/api-logs";
4
+ import { createBuiltinLogger } from "autotel/logger";
5
+ import * as Cause from "effect/Cause";
3
6
  import * as Layer from "effect/Layer";
7
+ import * as Logger from "effect/Logger";
8
+ import * as References from "effect/References";
4
9
  //#region src/index.ts
5
10
  /**
6
- * Routes `Effect.withSpan` through autotel's global OpenTelemetry provider.
11
+ * Routes `Effect.withSpan` and `Effect.log*` through autotel's global
12
+ * OpenTelemetry provider.
7
13
  *
8
14
  * Call `autotel.init()` before this layer is built — typically by loading an
9
15
  * instrumentation module with `node --import` or `tsx --import`.
10
16
  */
11
17
  function layer(options) {
12
- return OtelTracer.layerGlobal.pipe(Layer.provide(Resource.layer({
18
+ const tracer = OtelTracer.layerGlobal.pipe(Layer.provide(Resource.layer({
13
19
  serviceName: options.serviceName,
14
20
  ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {}
15
21
  })));
22
+ const logs = options.logs ?? true;
23
+ return Layer.mergeAll(tracer, logs === false ? Layer.empty : loggerLayer({
24
+ serviceName: options.serviceName,
25
+ ...options.serviceVersion ? { serviceVersion: options.serviceVersion } : {},
26
+ ...logs === true ? {} : logs
27
+ }));
28
+ }
29
+ /**
30
+ * Routes Effect's `Effect.log*` through autotel instead of Effect's console
31
+ * logger, emitting each log as an OpenTelemetry log record (so it reaches any
32
+ * OTLP log backend, autotel-devtools included) and, unless disabled, a
33
+ * structured line on stdout via autotel's built-in logger.
34
+ *
35
+ * `layer()` already includes this. Reach for it on its own only when something
36
+ * else owns the tracer and you want autotel to own the logs.
37
+ */
38
+ function loggerLayer(options) {
39
+ const log = createBuiltinLogger(options.serviceName, {
40
+ ...options.level ? { level: options.level } : {},
41
+ ...options.pretty ? { pretty: options.pretty } : {}
42
+ });
43
+ const otelLogger = () => logs.getLogger(options.serviceName, options.serviceVersion);
44
+ return Logger.layer([Logger.make(({ cause, fiber, logLevel, message }) => {
45
+ const method = LEVEL_TO_METHOD[logLevel];
46
+ if (!method) return;
47
+ const parts = Array.isArray(message) ? message : [message];
48
+ const metadata = { ...fiber.getRef(References.CurrentLogAnnotations) };
49
+ const errors = parts.filter((part) => part instanceof Error);
50
+ if (cause.reasons.length > 0) metadata.err = Cause.pretty(cause);
51
+ else if (errors.length > 0) metadata.err = errors.map((error) => error.stack ?? String(error));
52
+ const body = parts.map(formatPart).join(" ");
53
+ if (options.console ?? true) log[method](metadata, body);
54
+ otelLogger().emit({
55
+ body,
56
+ severityNumber: LEVEL_TO_SEVERITY[logLevel],
57
+ severityText: logLevel,
58
+ attributes: {
59
+ ...metadata,
60
+ ...Array.isArray(metadata.err) ? { err: metadata.err.join("\n") } : {}
61
+ }
62
+ });
63
+ })], { mergeWithExisting: options.mergeWithExisting ?? false });
64
+ }
65
+ const LEVEL_TO_METHOD = {
66
+ Trace: "debug",
67
+ Debug: "debug",
68
+ Info: "info",
69
+ Warn: "warn",
70
+ Error: "error",
71
+ Fatal: "error"
72
+ };
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
+ function formatPart(part) {
84
+ if (typeof part === "string") return part;
85
+ if (part instanceof Error) return String(part);
86
+ try {
87
+ return JSON.stringify(part) ?? String(part);
88
+ } catch {
89
+ return String(part);
90
+ }
16
91
  }
17
92
  //#endregion
18
- export { layer };
93
+ 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.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",
@@ -50,14 +50,19 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@effect/opentelemetry": "4.0.0-rc.112",
53
+ "@opentelemetry/api-logs": "^0.222.0",
54
+ "@opentelemetry/sdk-logs": "^0.222.0",
53
55
  "@types/node": "^26.1.2",
54
- "autotel": "7.6.0",
56
+ "autotel": "7.6.1",
55
57
  "effect": "4.0.0-rc.112",
56
58
  "rimraf": "^6.1.3",
57
59
  "tsdown": "^0.22.14",
58
60
  "typescript": "^6.0.3",
59
61
  "vitest": "^4.1.10"
60
62
  },
63
+ "dependencies": {
64
+ "@opentelemetry/api-logs": "^0.222.0"
65
+ },
61
66
  "scripts": {
62
67
  "build": "tsdown",
63
68
  "dev": "tsdown --watch",