1/**
2 * Defines the low-level tracing model used by Effect.
3 *
4 * A span records the lifetime of an operation, including its name, parent,
5 * attributes, links, annotations, sampling decision, kind, and completion
6 * status. The module also defines the tracer service, parent-span context,
7 * external span support, trace propagation settings, and the default in-memory
8 * span implementation.
9 *
10 * @since 2.0.0
11 */
12import * as Context from "./Context.ts"
13import * as Hex from "./encoding/Hex.ts"
14import type * as Exit from "./Exit.ts"
15import type { Fiber } from "./Fiber.ts"
16import { constFalse, type LazyArg } from "./Function.ts"
17import type * as core from "./internal/core.ts"
18import type { LogLevel } from "./LogLevel.ts"
19import * as Option from "./Option.ts"
20
21/**
22 * A tracing backend used by Effect to create spans. Custom tracers implement
23 * `span` to allocate a span from the supplied name, parent, annotations,
24 * links, start time, kind, root flag, and sampling decision.
25 *
26 * @category services
27 * @since 2.0.0
28 */
29export interface Tracer {
30 span(this: Tracer, options: {
31 readonly name: string
32 readonly parent: Option.Option<AnySpan>
33 readonly annotations: Context.Context<never>
34 readonly links: Array<SpanLink>
35 readonly startTime: bigint
36 readonly kind: SpanKind
37 readonly root: boolean
38 readonly sampled: boolean
39 }): Span
40 readonly context?:
41 | (<X>(primitive: EffectPrimitive<X>, fiber: Fiber<any, any>) => X)
42 | undefined
43}
44
45const evaluate = "~effect/Effect/evaluate" satisfies core.evaluate
46
47/**
48 * A low-level Effect primitive that can be evaluated by a tracer-specific
49 * context for the current fiber.
50 *
51 * @category models
52 * @since 4.0.0
53 */
54export interface EffectPrimitive<X> {
55 [evaluate](this: EffectPrimitive<X>, fiber: Fiber<any, any>): X
56}
57
58/**
59 * Lifecycle state of a span, where `Started` records the start time and
60 * `Ended` records the start time, end time, and exit value with which the span
61 * completed.
62 *
63 * **Example** (Creating span statuses)
64 *
65 * ```ts import.meta.vitest
66 * import { Exit } from "effect"
67 * import type { Tracer } from "effect"
68 *
69 * const startTime = 1_000_000_000n
70 * const endTime = 1_500_000_000n
71 *
72 * const startedStatus: Tracer.SpanStatus = {
73 * _tag: "Started",
74 * startTime
75 * }
76 *
77 * const endedStatus: Tracer.SpanStatus = {
78 * _tag: "Ended",
79 * startTime,
80 * endTime,
81 * exit: Exit.succeed("result")
82 * }
83 *
84 * startedStatus._tag // => "Started"
85 * endedStatus.endTime - endedStatus.startTime // => 500_000_000n
86 * ```
87 *
88 * @category models
89 * @since 2.0.0
90 */
91export type SpanStatus = {
92 _tag: "Started"
93 startTime: bigint
94} | {
95 _tag: "Ended"
96 startTime: bigint
97 endTime: bigint
98 exit: Exit.Exit<unknown, unknown>
99}
100
101/**
102 * A span value that can participate in tracing, either an Effect-managed
103 * `Span` or an `ExternalSpan` propagated from another tracing system.
104 *
105 * **Example** (Accepting any span)
106 *
107 * ```ts import.meta.vitest
108 * import { Effect, Tracer } from "effect"
109 *
110 * // Function that accepts any span type
111 * const getSpanIds = (span: Tracer.AnySpan) => Effect.succeed([span.spanId, span.traceId])
112 *
113 * // Works with both Span and ExternalSpan
114 * const externalSpan = Tracer.externalSpan({
115 * spanId: "span-123",
116 * traceId: "trace-456"
117 * })
118 *
119 * await Effect.runPromise(getSpanIds(externalSpan)) // => ["span-123", "trace-456"]
120 * ```
121 *
122 * @category models
123 * @since 2.0.0
124 */
125export type AnySpan = Span | ExternalSpan
126
127/**
128 * Defines the string key for the parent-span context service.
129 *
130 * **When to use**
131 *
132 * Use when you need the raw context key for parent span lookup in lower-level
133 * tracing code.
134 *
135 * **Example** (Reading the parent span key)
136 *
137 * ```ts import.meta.vitest
138 * import { Tracer } from "effect"
139 *
140 * // The key used to identify parent spans in the context
141 * Tracer.ParentSpanKey // => "effect/Tracer/ParentSpan"
142 * ```
143 *
144 * @category constants
145 * @since 4.0.0
146 */
147export const ParentSpanKey = "effect/Tracer/ParentSpan"
148
149/**
150 * Context service containing the `Span` or `ExternalSpan` to use as the parent
151 * of newly-created child spans.
152 *
153 * **Example** (Accessing the parent span)
154 *
155 * ```ts import.meta.vitest
156 * import { Effect, Tracer } from "effect"
157 *
158 * // Access the parent span from the context
159 * const program = Effect.gen(function*() {
160 * const parentSpan = yield* Effect.service(Tracer.ParentSpan)
161 * return parentSpan.spanId
162 * })
163 *
164 * const parent = Tracer.externalSpan({ spanId: "span-123", traceId: "trace-456" })
165 * await Effect.runPromise(Effect.provideService(program, Tracer.ParentSpan, parent)) // => "span-123"
166 * ```
167 *
168 * @category services
169 * @since 2.0.0
170 */
171export class ParentSpan extends Context.Service<ParentSpan, AnySpan>()(ParentSpanKey, { fiberCached: true }) {}
172
173/**
174 * Represents a span created outside Effect's tracer, carrying trace and span
175 * identifiers, sampling state, and annotations so it can be used as a parent or
176 * link in Effect tracing.
177 *
178 * **Example** (Creating an external span value)
179 *
180 * ```ts import.meta.vitest
181 * import { Context } from "effect"
182 * import type { Tracer } from "effect"
183 *
184 * // Create an external span from another tracing system
185 * const externalSpan: Tracer.ExternalSpan = {
186 * _tag: "ExternalSpan",
187 * spanId: "span-abc-123",
188 * traceId: "trace-xyz-789",
189 * sampled: true,
190 * annotations: Context.empty()
191 * }
192 *
193 * externalSpan.spanId // => "span-abc-123"
194 * ```
195 *
196 * @category models
197 * @since 2.0.0
198 */
199export interface ExternalSpan {
200 readonly _tag: "ExternalSpan"
201 readonly spanId: string
202 readonly traceId: string
203 readonly sampled: boolean
204 readonly annotations: Context.Context<never>
205}
206
207/**
208 * Options accepted by span-creating APIs, combining span metadata such as
209 * attributes, links, parent/root selection, kind, sampling, and trace level
210 * with stack trace capture settings.
211 *
212 * **Example** (Configuring span options)
213 *
214 * ```ts import.meta.vitest
215 * import { Effect, Tracer } from "effect"
216 *
217 * // Create an effect with span options
218 * const options: Tracer.SpanOptions = {
219 * attributes: { "user.id": "123", "operation": "data-processing" },
220 * kind: "internal",
221 * root: false,
222 * captureStackTrace: true
223 * }
224 *
225 * const program = Effect.succeed("Hello World").pipe(
226 * Effect.withSpan("my-operation", options)
227 * )
228 *
229 * const spans: Array<Tracer.NativeSpan> = []
230 * const tracer = Tracer.make({
231 * span(options) {
232 * const span = new Tracer.NativeSpan(options)
233 * spans.push(span)
234 * return span
235 * }
236 * })
237 * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer)) // => "Hello World"
238 *
239 * spans[0]?.attributes.get("user.id") // => "123"
240 * spans[0]?.status._tag // => "Ended"
241 * ```
242 *
243 * @category options
244 * @since 3.1.0
245 */
246export interface SpanOptions extends SpanOptionsNoTrace, TraceOptions {}
247
248/**
249 * Span creation options that do not control stack trace capture, including
250 * attributes, links, parent or root selection, annotations, span kind,
251 * sampling, and the trace level used for filtering.
252 *
253 * @category options
254 * @since 4.0.0
255 */
256export interface SpanOptionsNoTrace {
257 readonly attributes?: Record<string, unknown> | undefined
258 readonly links?: ReadonlyArray<SpanLink> | undefined
259 readonly parent?: AnySpan | undefined
260 readonly root?: boolean | undefined
261 readonly annotations?: Context.Context<never> | undefined
262 readonly kind?: SpanKind | undefined
263 readonly sampled?: boolean | undefined
264 readonly level?: LogLevel | undefined
265}
266
267/**
268 * Options that control stack trace capture for tracing wrappers.
269 * `captureStackTrace` can disable capture or provide a lazy stack string.
270 *
271 * @category options
272 * @since 4.0.0
273 */
274export interface TraceOptions {
275 readonly captureStackTrace?: boolean | LazyArg<string | undefined> | undefined
276}
277
278/**
279 * OpenTelemetry-style role describing the kind of operation represented by a
280 * span: internal work, server handling, client calls, producing, or consuming.
281 *
282 * **Example** (Configuring span kinds)
283 *
284 * ```ts import.meta.vitest
285 * import { Effect, Tracer } from "effect"
286 *
287 * // Different span kinds for different operations
288 * const program = Effect.succeed("handled").pipe(
289 * Effect.withSpan("handle-request", {
290 * kind: "server" as Tracer.SpanKind
291 * })
292 * )
293 *
294 * const spans: Array<Tracer.NativeSpan> = []
295 * const tracer = Tracer.make({
296 * span(options) {
297 * const span = new Tracer.NativeSpan(options)
298 * spans.push(span)
299 * return span
300 * }
301 * })
302 * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer)) // => "handled"
303 *
304 * spans[0]?.kind // => "server"
305 * ```
306 *
307 * @category models
308 * @since 3.1.0
309 */
310export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer"
311
312/**
313 * A span created by an Effect tracer. It carries trace identity, parent,
314 * annotations, attributes, links, sampling and kind information, lifecycle
315 * status, and methods to end the span or add attributes, events, and links.
316 *
317 * **Example** (Working with spans)
318 *
319 * ```ts import.meta.vitest
320 * import { Context, Exit, Option } from "effect"
321 * import type { Tracer } from "effect"
322 *
323 * const attributes = new Map<string, unknown>()
324 * const links: Array<Tracer.SpanLink> = []
325 * const events: Array<[name: string, startTime: bigint, attributes: Record<string, unknown>]> = []
326 * let status: Tracer.SpanStatus = {
327 * _tag: "Started",
328 * startTime: 1_000_000_000n
329 * }
330 *
331 * const span: Tracer.Span = {
332 * _tag: "Span",
333 * name: "load-user",
334 * spanId: "span-1",
335 * traceId: "trace-1",
336 * parent: Option.none(),
337 * annotations: Context.empty(),
338 * get status() {
339 * return status
340 * },
341 * attributes,
342 * links,
343 * sampled: true,
344 * kind: "internal",
345 * end(endTime, exit) {
346 * status = { _tag: "Ended", startTime: status.startTime, endTime, exit }
347 * },
348 * attribute(key, value) {
349 * attributes.set(key, value)
350 * },
351 * event(name, startTime, eventAttributes = {}) {
352 * events.push([name, startTime, eventAttributes])
353 * },
354 * addLinks(newLinks) {
355 * links.push(...newLinks)
356 * }
357 * }
358 *
359 * span.attribute("user.id", "123")
360 * span.event("loaded", 1_250_000_000n, { "cache.hit": true })
361 * span.end(1_500_000_000n, Exit.succeed("user"))
362 *
363 * span.name // => "load-user"
364 * span.attributes.get("user.id") // => "123"
365 * span.status._tag // => "Ended"
366 * events // => [["loaded", 1_250_000_000n, { "cache.hit": true }]]
367 * ```
368 *
369 * @category models
370 * @since 2.0.0
371 */
372export interface Span {
373 readonly _tag: "Span"
374 readonly name: string
375 readonly spanId: string
376 readonly traceId: string
377 readonly parent: Option.Option<AnySpan>
378 readonly annotations: Context.Context<never>
379 readonly status: SpanStatus
380 readonly attributes: ReadonlyMap<string, unknown>
381 readonly links: ReadonlyArray<SpanLink>
382 readonly sampled: boolean
383 readonly kind: SpanKind
384 end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void
385 attribute(key: string, value: unknown): void
386 event(name: string, startTime: bigint, attributes?: Record<string, unknown>): void
387 addLinks(links: ReadonlyArray<SpanLink>): void
388}
389
390/**
391 * A relationship from one span to another span, with attributes describing the
392 * relationship.
393 *
394 * **Example** (Linking spans)
395 *
396 * ```ts import.meta.vitest
397 * import { Effect, Tracer } from "effect"
398 *
399 * // Create a span link to connect spans
400 * const externalSpan = Tracer.externalSpan({
401 * spanId: "external-span-123",
402 * traceId: "trace-456"
403 * })
404 *
405 * const link: Tracer.SpanLink = {
406 * span: externalSpan,
407 * attributes: { "link.type": "follows-from", "service": "external-api" }
408 * }
409 *
410 * const program = Effect.succeed("result").pipe(
411 * Effect.withSpan("linked-operation", { links: [link] })
412 * )
413 *
414 * const spans: Array<Tracer.NativeSpan> = []
415 * const tracer = Tracer.make({
416 * span(options) {
417 * const span = new Tracer.NativeSpan(options)
418 * spans.push(span)
419 * return span
420 * }
421 * })
422 * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer)) // => "result"
423 *
424 * spans[0]?.links[0]?.span.spanId // => "external-span-123"
425 * spans[0]?.links[0]?.attributes["link.type"] // => "follows-from"
426 * ```
427 *
428 * @category models
429 * @since 2.0.0
430 */
431export interface SpanLink {
432 readonly span: AnySpan
433 readonly attributes: Readonly<Record<string, unknown>>
434}
435
436/**
437 * Creates a `Tracer` value from a tracer implementation object.
438 *
439 * **When to use**
440 *
441 * Use to create a custom tracing backend value that Effect can use when
442 * creating spans.
443 *
444 * **Details**
445 *
446 * `make` returns the supplied implementation object unchanged. The object must
447 * satisfy the `Tracer` contract, including a `span` method that returns a
448 * `Span`.
449 *
450 * @see {@link Span} for the span values returned by tracer implementations
451 *
452 * @category constructors
453 * @since 2.0.0
454 */
455export const make = (options: Tracer): Tracer => options
456
457/**
458 * Creates an `ExternalSpan` from trace and span identifiers, defaulting
459 * `sampled` to `true` and annotations to an empty context when they are not
460 * provided.
461 *
462 * **Example** (Creating an external span)
463 *
464 * ```ts import.meta.vitest
465 * import { Effect, Option, Tracer } from "effect"
466 *
467 * // Create an external span from another tracing system
468 * const span = Tracer.externalSpan({
469 * spanId: "span-abc-123",
470 * traceId: "trace-xyz-789",
471 * sampled: true
472 * })
473 *
474 * // Use the external span as a parent
475 * const program = Effect.succeed("Hello").pipe(
476 * Effect.withSpan("child-operation", { parent: span })
477 * )
478 *
479 * const spans: Array<Tracer.NativeSpan> = []
480 * const tracer = Tracer.make({
481 * span(options) {
482 * const span = new Tracer.NativeSpan(options)
483 * spans.push(span)
484 * return span
485 * }
486 * })
487 * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer))
488 *
489 * value // => "Hello"
490 * spans.map((span) => Option.getOrUndefined(span.parent)?.spanId) // => ["span-abc-123"]
491 * ```
492 *
493 * @category constructors
494 * @since 2.0.0
495 */
496export const externalSpan = (
497 options: {
498 readonly spanId: string
499 readonly traceId: string
500 readonly sampled?: boolean | undefined
501 readonly annotations?: Context.Context<never> | undefined
502 }
503): ExternalSpan => ({
504 _tag: "ExternalSpan",
505 spanId: options.spanId,
506 traceId: options.traceId,
507 sampled: options.sampled ?? true,
508 annotations: options.annotations ?? Context.empty()
509})
510
511/**
512 * Context reference for disabling trace propagation.
513 *
514 * **When to use**
515 *
516 * Use to prevent spans in a scope from propagating tracing context.
517 *
518 * **Details**
519 *
520 * When enabled on fiber or span annotations, new spans are created as
521 * non-propagating no-op spans and disabled spans are skipped when deriving a
522 * parent span.
523 *
524 * **Example** (Disabling span propagation)
525 *
526 * ```ts import.meta.vitest
527 * import { Effect, Tracer } from "effect"
528 *
529 * // Disable span propagation for a specific effect
530 * const program = Tracer.DisablePropagation.pipe(
531 * Effect.provideService(Tracer.DisablePropagation, true)
532 * )
533 *
534 * await Effect.runPromise(program) // => true
535 * ```
536 *
537 * @category services
538 * @since 3.12.0
539 */
540export const DisablePropagation = Context.Reference<boolean>(
541 "effect/Tracer/DisablePropagation",
542 { defaultValue: constFalse }
543)
544
545/**
546 * Context reference for controlling the current trace level for dynamic filtering.
547 *
548 * **When to use**
549 *
550 * Use to set the default trace level for spans in a scope when span options do
551 * not provide `level`.
552 *
553 * **Details**
554 *
555 * The default value is `"Info"`. Span creation uses `options.level ??
556 * CurrentTraceLevel` before applying `MinimumTraceLevel`.
557 *
558 * @see {@link MinimumTraceLevel} for the threshold that decides whether spans at that level are sampled
559 *
560 * @category services
561 * @since 4.0.0
562 */
563export const CurrentTraceLevel: Context.Reference<LogLevel> = Context.Reference<LogLevel>(
564 "effect/Tracer/CurrentTraceLevel",
565 { defaultValue: () => "Info" }
566)
567
568/**
569 * Context reference for setting the minimum trace level threshold. Spans and their
570 * descendants below this level will have their sampling decision forced to
571 * false, preventing them from being exported.
572 *
573 * **When to use**
574 *
575 * Use to set the trace-level threshold that controls whether spans are sampled
576 * by default.
577 *
578 * **Details**
579 *
580 * The default value is `"All"`. Span creation compares the span level from
581 * `options.level ?? CurrentTraceLevel` against this threshold.
582 *
583 * **Gotchas**
584 *
585 * Explicit `options.sampled` bypasses threshold computation.
586 *
587 * @see {@link CurrentTraceLevel} for the default span level used when options do not specify one
588 *
589 * @category services
590 * @since 4.0.0
591 */
592export const MinimumTraceLevel = Context.Reference<
593 LogLevel
594>("effect/Tracer/MinimumTraceLevel", { defaultValue: () => "All" })
595
596/**
597 * Defines the string key for the active tracer context reference.
598 *
599 * **When to use**
600 *
601 * Use when you need the raw context key for active tracer lookup in lower-level
602 * tracing code.
603 *
604 * @category constants
605 * @since 4.0.0
606 */
607export const TracerKey = "effect/Tracer"
608
609/**
610 * Context reference for the active tracer service. By default it uses the
611 * native tracer, which creates `NativeSpan` instances.
612 *
613 * **Example** (Accessing the current tracer)
614 *
615 * ```ts import.meta.vitest
616 * import { Effect, Tracer } from "effect"
617 *
618 * // Access the current tracer from the context
619 * const program = Effect.gen(function*() {
620 * const tracer = yield* Effect.service(Tracer.Tracer)
621 * // Or use the built-in tracer effect
622 * const tracerFromAccessor = yield* Effect.tracer
623 * return tracer === tracerFromAccessor
624 * })
625 *
626 * await Effect.runPromise(program) // => true
627 * ```
628 *
629 * @category services
630 * @since 2.0.0
631 */
632export const Tracer: Context.Reference<Tracer> = Context.Reference<Tracer>(TracerKey, {
633 fiberCached: true,
634 defaultValue: () => nativeTracer
635})
636
637/**
638 * The default `Tracer` implementation backing the `Tracer` reference. It
639 * creates in-memory `NativeSpan` instances and does not export them anywhere.
640 *
641 * **Details**
642 *
643 * Runtime code can compare the active tracer against `nativeTracer` to detect
644 * that no tracing backend is installed and skip work that only a backend could
645 * observe, such as recording span attributes.
646 *
647 * @category references
648 * @since 4.0.0
649 */
650export const nativeTracer: Tracer = make({
651 span: (options) => new NativeSpan(options)
652})
653
654/**
655 * Default in-memory `Span` implementation used by the native tracer. It
656 * generates span and trace identifiers, stores attributes, events, and links,
657 * and records `Started` or `Ended` status.
658 *
659 * **Details**
660 *
661 * The constructor initializes the span with `Started` status. Trace and span
662 * identifiers, the attribute map, and the event list are created lazily on
663 * first access, so spans that are never inspected allocate as little as
664 * possible. Attributes, events, links, and status are mutated through `Span`
665 * methods.
666 *
667 * @see {@link Span} for the interface implemented by native spans
668 *
669 * @category models
670 * @since 4.0.0
671 */
672export class NativeSpan implements Span {
673 readonly _tag = "Span"
674 readonly sampled: boolean
675
676 readonly name: string
677 readonly parent: Option.Option<AnySpan>
678 readonly annotations: Context.Context<never>
679 readonly links: Array<SpanLink>
680 readonly startTime: bigint
681 readonly kind: SpanKind
682
683 status: SpanStatus
684 _traceId: string | undefined = undefined
685 _spanId: string | undefined = undefined
686 _attributes: Map<string, unknown> | undefined = undefined
687 _events: Array<[name: string, startTime: bigint, attributes: Record<string, unknown>]> | undefined = undefined
688
689 constructor(options: {
690 readonly name: string
691 readonly parent: Option.Option<AnySpan>
692 readonly annotations: Context.Context<never>
693 readonly links: Array<SpanLink>
694 readonly startTime: bigint
695 readonly kind: SpanKind
696 readonly sampled: boolean
697 }) {
698 this.name = options.name
699 this.parent = options.parent
700 this.annotations = options.annotations
701 this.links = options.links
702 this.startTime = options.startTime
703 this.kind = options.kind
704 this.sampled = options.sampled
705 this.status = {
706 _tag: "Started",
707 startTime: options.startTime
708 }
709 }
710
711 get traceId(): string {
712 return this._traceId ??= Option.getOrUndefined(this.parent)?.traceId ?? Hex.random(32)
713 }
714
715 get spanId(): string {
716 return this._spanId ??= Hex.random(16)
717 }
718
719 get attributes(): Map<string, unknown> {
720 return this._attributes ??= new Map()
721 }
722
723 get events(): Array<[name: string, startTime: bigint, attributes: Record<string, unknown>]> {
724 return this._events ??= []
725 }
726
727 end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
728 this.status = {
729 _tag: "Ended",
730 endTime,
731 exit,
732 startTime: this.status.startTime
733 }
734 }
735
736 attribute(key: string, value: unknown): void {
737 this.attributes.set(key, value)
738 }
739
740 event(name: string, startTime: bigint, attributes?: Record<string, unknown>): void {
741 this.events.push([name, startTime, attributes ?? {}])
742 }
743
744 addLinks(links: ReadonlyArray<SpanLink>): void {
745 // oxlint-disable-next-line no-restricted-syntax
746 this.links.push(...links)
747 }
748}