Skip to content

Effect

Effect v4 ships official OpenTelemetry support via @effect/opentelemetry. Autotel registers the global TracerProvider and handles export. autotel-effect connects the two so you do not copy the same layer wiring into every app.

Terminal window
npm install autotel autotel-effect effect @effect/opentelemetry

Effect v4 only (^4.0.0-rc.112).

instrumentation.ts
import { init } from 'autotel';
init({
service: 'my-api',
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
});
Terminal window
tsx --import ./instrumentation.ts src/index.ts
import * as Effect from 'effect/Effect';
import { layer } from 'autotel-effect';
const AutotelEffect = layer({ serviceName: 'my-api' });
await Effect.runPromise(
Effect.withSpan('my.operation')(Effect.log('hello')).pipe(
Effect.provide(AutotelEffect),
),
);

HTTP spans from autotel’s node:http instrumentation become parents of domain spans from Effect.withSpan.

That one layer() call bridges logs as well as spans. Effect.log* becomes an OpenTelemetry log record — reaching any OTLP log backend, autotel-devtools included — plus a trace-correlated structured line on stdout.

A log written inside Effect.withSpan carries that span’s trace and span ids, so logs and traces line up in the same backend. Effect.annotateLogs values become log-record attributes, and an Error or a Cause passed to Effect.logError is reported as an err attribute with its stack:

Effect.log('charged').pipe(Effect.annotateLogs({ 'order.id': 'o-1' }));
// {"level":"info","msg":"charged","order.id":"o-1","traceId":"...","spanId":"..."}

Rich annotation values are flattened to the attribute shape the rest of autotel produces: a nested object becomes dot-notation keys, a Date becomes an ISO string, an Error becomes its message.

Effect.annotateLogs({ order: { id: 7 }, at: new Date(0) });
// attributes: { 'order.id': 7, at: '1970-01-01T00:00:00.000Z' }

Pass logs: false for spans only, or an options object to configure the logger:

layer({ serviceName: 'my-api', logs: { level: 'debug', pretty: true } });
layer({ serviceName: 'my-api', logs: false });

mergeWithExisting: true keeps Effect’s console logger alongside this one, level sets the stdout minimum level (default 'info', so pass 'debug' for Effect.logDebug to reach stdout), pretty: true swaps JSON for human-readable stdout, and console: false emits only the log record — use it when captureConsole() is on. loggerLayer() is exported for the case where something else owns the tracer.

Effect’s default Tracer is an in-memory native tracer, so handler tests need no layer at all. Provide layer(...) with autotel’s createMemoryExporter() only where you assert on exported spans.

example-effect

autotel-effect