Skip to content

Tracing in Effect

Although logs and metrics are useful to understand the behavior of individual services, they are not enough to provide a complete overview of the lifetime of a request in a distributed system.

In a distributed system, a request can span multiple services and each service can make multiple requests to other services to fulfill the request. In such a scenario, we need to have a way to track the lifetime of a request across multiple services to diagnose what services are the bottlenecks and where the request is spending most of its time.

A span represents a unit of work or operation. It tracks specific operations that a request makes, painting a picture of what happened during the time in which that operation was executed.

A typical Span contains the following information:

  • Name: Describes the operation being tracked.

  • Time-Related Data: Timestamps to measure when the operation started and how long it took.

  • Structured Log Messages: Records essential information during the operation.

  • Metadata (Attributes): Additional data that provides context about the operation.

A trace records the paths taken by requests (made by an application or end-user) as they propagate through multi-service architectures, like microservice and serverless applications.

Without tracing, it is challenging to pinpoint the cause of performance problems in a distributed system.

A trace is made of one or more spans. The first span represents the root span. Each root span represents a request from start to finish. The spans underneath the parent provide a more in-depth context of what occurs during a request (or what steps make up a request).

Many Observability back-ends visualize traces as waterfall diagrams that may look something like this:

Trace Waterfall Diagram

Waterfall diagrams show the parent-child relationship between a root span and its child spans. When a span encapsulates another span, this also represents a nested relationship.

You can instrument an effect with a Span using the Effect.withSpan API. Here’s how you can do it:

1
import {
import Effect
Effect
} from "effect"
2
3
const
const program: Effect.Effect<void, never, never>
program
=
import Effect
Effect
.
(alias) const void: Effect.Effect<void, never, never> export void
void
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const delay: (duration: DurationInput) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Returns an effect that is delayed from this effect by the specified `Duration`.

delay
("100 millis"))
4
5
const
const instrumented: Effect.Effect<void, never, never>
instrumented
=
const program: Effect.Effect<void, never, never>
program
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

Wraps the effect with a new span for tracing.

withSpan
("myspan"))

It’s important to note that instrumenting an effect doesn’t change its type. You start with an Effect<void>, and you still get an Effect<void>.

Now, let’s print our Span to the console. To achieve this, we need to install some specific tools, including

Terminal window
npm install @effect/opentelemetry
npm install @opentelemetry/exporter-trace-otlp-http
npm install @opentelemetry/sdk-trace-base
# If your application is running on NodeJS
npm install @opentelemetry/sdk-trace-node
# If your application is running on the browser
npm install @opentelemetry/sdk-trace-web
# If your application will also be exporting metrics
npm install @opentelemetry/sdk-metrics

With these in place, we can visualize and understand the Spans in our application.

Here’s a code snippet demonstrating how to set up the necessary environment and print the Span to the console:

1
import {
import Effect
Effect
} from "effect"
2
import {
import NodeSdk
NodeSdk
} from "@effect/opentelemetry"
3
import {
4
(alias) class ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
,
5
(alias) class BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
6
} from "@opentelemetry/sdk-trace-base"
7
8
const
const program: Effect.Effect<void, never, never>
program
=
import Effect
Effect
.
(alias) const void: Effect.Effect<void, never, never> export void
void
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const delay: (duration: DurationInput) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Returns an effect that is delayed from this effect by the specified `Duration`.

delay
("100 millis"))
9
10
const
const instrumented: Effect.Effect<void, never, never>
instrumented
=
const program: Effect.Effect<void, never, never>
program
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

Wraps the effect with a new span for tracing.

withSpan
("myspan"))
11
12
const
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
=
import NodeSdk
NodeSdk
.
const layer: (evaluate: LazyArg<NodeSdk.Configuration>) => Layer<Resource> (+1 overload)
layer
(() => ({
13
(property) Configuration.resource?: { readonly serviceName: string; readonly serviceVersion?: string; readonly attributes?: ResourceAttributes; } | undefined
resource
: {
(property) serviceName: string
serviceName
: "example" },
14
(property) Configuration.spanProcessor?: SpanProcessor | readonly SpanProcessor[] | undefined
spanProcessor
: new
(alias) new BatchSpanProcessor<BufferConfig>(_exporter: SpanExporter, config?: BufferConfig | undefined): BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
(new
(alias) new ConsoleSpanExporter(): ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
())
15
}))
16
17
import Effect
Effect
.
const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<void>

Runs an `Effect` workflow, returning a `Promise` which resolves with the result of the workflow or rejects with an error.

runPromise
(
const instrumented: Effect.Effect<void, never, never>
instrumented
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <Resource, never, never>(layer: Layer<Resource, never, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Resource>> (+9 overloads)

Splits the context into two parts, providing one part using the specified layer/context/runtime and leaving the remainder `R0`

provide
(
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
)))
18
/*
19
Example Output:
20
{
21
traceId: 'd0f730abfc366205806469596092b239',
22
parentId: undefined,
23
traceState: undefined,
24
name: 'myspan',
25
id: 'ab4e42592e7f1f7c',
26
kind: 0,
27
timestamp: 1697040012664380.5,
28
duration: 2895.769,
29
attributes: {},
30
status: { code: 1 },
31
events: [],
32
links: []
33
}
34
*/

Here’s a breakdown of the output:

FieldDescription
traceIdA unique identifier for the entire trace, helping trace requests or operations as they move through an application.
parentIdIdentifies the parent span of the current span, marked as undefined in the output when there is no parent span, making it a root span.
nameDescribes the name of the span, indicating the operation being tracked (e.g., “myspan”).
idA unique identifier for the current span, distinguishing it from other spans within a trace.
timestampA timestamp representing when the span started, measured in microseconds since the Unix epoch.
durationSpecifies the duration of the span, representing the time taken to complete the operation (e.g., 2895.769 microseconds).
attributesSpans may contain attributes, which are key-value pairs providing additional context or information about the operation. In this output, it’s an empty object, indicating no specific attributes in this span.
statusThe status field provides information about the span’s status. In this case, it has a code of 1, which typically indicates an OK status (whereas a code of 2 signifies an ERROR status)
eventsSpans can include events, which are records of specific moments during the span’s lifecycle. In this output, it’s an empty array, suggesting no specific events recorded.
linksLinks can be used to associate this span with other spans in different traces. In the output, it’s an empty array, indicating no specific links for this span.

Let’s examine the output of an effect that encountered an error:

1
import {
import Effect
Effect
} from "effect"
2
import {
import NodeSdk
NodeSdk
} from "@effect/opentelemetry"
3
import {
4
(alias) class ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
,
5
(alias) class BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
6
} from "@opentelemetry/sdk-trace-base"
7
8
const
const program: Effect.Effect<never, string, never>
program
=
import Effect
Effect
.
const fail: <string>(error: string) => Effect.Effect<never, string, never>
fail
("Oh no!").
(method) Pipeable.pipe<Effect.Effect<never, string, never>, Effect.Effect<never, string, never>, Effect.Effect<never, string, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<never, string, never>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
9
import Effect
Effect
.
const delay: (duration: DurationInput) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Returns an effect that is delayed from this effect by the specified `Duration`.

delay
("100 millis"),
10
import Effect
Effect
.
const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

Wraps the effect with a new span for tracing.

withSpan
("myspan")
11
)
12
13
const
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
=
import NodeSdk
NodeSdk
.
const layer: (evaluate: LazyArg<NodeSdk.Configuration>) => Layer<Resource> (+1 overload)
layer
(() => ({
14
(property) Configuration.resource?: { readonly serviceName: string; readonly serviceVersion?: string; readonly attributes?: ResourceAttributes; } | undefined
resource
: {
(property) serviceName: string
serviceName
: "example" },
15
(property) Configuration.spanProcessor?: SpanProcessor | readonly SpanProcessor[] | undefined
spanProcessor
: new
(alias) new BatchSpanProcessor<BufferConfig>(_exporter: SpanExporter, config?: BufferConfig | undefined): BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
(new
(alias) new ConsoleSpanExporter(): ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
())
16
}))
17
18
import Effect
Effect
.
const runPromiseExit: <never, string>(effect: Effect.Effect<never, string, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<Exit<never, string>>

Runs an `Effect` workflow, returning a `Promise` which resolves with the `Exit` value of the workflow.

runPromiseExit
(
const program: Effect.Effect<never, string, never>
program
.
(method) Pipeable.pipe<Effect.Effect<never, string, never>, Effect.Effect<never, string, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<never, string, never>) => Effect.Effect<never, string, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <Resource, never, never>(layer: Layer<Resource, never, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Resource>> (+9 overloads)

Splits the context into two parts, providing one part using the specified layer/context/runtime and leaving the remainder `R0`

provide
(
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
))).
(method) Promise<Exit<never, string>>.then<void, never>(onfulfilled?: ((value: Exit<never, string>) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>

Attaches callbacks for the resolution and/or rejection of the Promise.

then
(
19
namespace console var console: Console

The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```

console
.
(method) Console.log(message?: any, ...optionalParams: any[]): void

Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.

log
20
)
21
/*
22
Example Output:
23
{
24
traceId: '760510a3f9a0881a09de990c87ec1cef',
25
parentId: undefined,
26
traceState: undefined,
27
name: 'myspan',
28
id: 'a528e38e82e848a5',
29
kind: 0,
30
timestamp: 1697091363002970.5,
31
duration: 110371.664,
32
attributes: {},
33
status: { code: 2, message: 'Error: Oh no!' },
34
events: [],
35
links: []
36
}
37
{
38
_id: 'Exit',
39
_tag: 'Failure',
40
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }
41
}
42
*/

You can provide extra information to a span by utilizing the Effect.annotateCurrentSpan function. This tool allows you to associate key-value pairs, offering more context about the execution of the span.

1
import {
import Effect
Effect
} from "effect"
2
import {
import NodeSdk
NodeSdk
} from "@effect/opentelemetry"
3
import {
4
(alias) class ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
,
5
(alias) class BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
6
} from "@opentelemetry/sdk-trace-base"
7
8
const
const program: Effect.Effect<void, never, never>
program
=
import Effect
Effect
.
(alias) const void: Effect.Effect<void, never, never> export void
void
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>, Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>, cd: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
9
import Effect
Effect
.
const delay: (duration: DurationInput) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Returns an effect that is delayed from this effect by the specified `Duration`.

delay
("100 millis"),
10
import Effect
Effect
.
const tap: <void, Effect.Effect<void, never, never>>(f: (a: void) => Effect.Effect<void, never, never>) => <E, R>(self: Effect.Effect<void, E, R>) => Effect.Effect<...> (+7 overloads)
tap
(() =>
import Effect
Effect
.
const annotateCurrentSpan: (key: string, value: unknown) => Effect.Effect<void> (+1 overload)

Adds an annotation to the current span if available

annotateCurrentSpan
("key", "value")),
11
import Effect
Effect
.
const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

Wraps the effect with a new span for tracing.

withSpan
("myspan")
12
)
13
14
const
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
=
import NodeSdk
NodeSdk
.
const layer: (evaluate: LazyArg<NodeSdk.Configuration>) => Layer<Resource> (+1 overload)
layer
(() => ({
15
(property) Configuration.resource?: { readonly serviceName: string; readonly serviceVersion?: string; readonly attributes?: ResourceAttributes; } | undefined
resource
: {
(property) serviceName: string
serviceName
: "example" },
16
(property) Configuration.spanProcessor?: SpanProcessor | readonly SpanProcessor[] | undefined
spanProcessor
: new
(alias) new BatchSpanProcessor<BufferConfig>(_exporter: SpanExporter, config?: BufferConfig | undefined): BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
(new
(alias) new ConsoleSpanExporter(): ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
())
17
}))
18
19
import Effect
Effect
.
const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<void>

Runs an `Effect` workflow, returning a `Promise` which resolves with the result of the workflow or rejects with an error.

runPromise
(
const program: Effect.Effect<void, never, never>
program
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <Resource, never, never>(layer: Layer<Resource, never, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Resource>> (+9 overloads)

Splits the context into two parts, providing one part using the specified layer/context/runtime and leaving the remainder `R0`

provide
(
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
)))
20
/*
21
Example Output:
22
{
23
traceId: '869c9d74d9db14a4ba4393ca8e0f61db',
24
parentId: undefined,
25
traceState: undefined,
26
name: 'myspan',
27
id: '31eb49570d197f8d',
28
kind: 0,
29
timestamp: 1697045981663321.5,
30
duration: 109563.353,
31
attributes: { key: 'value' },
32
status: { code: 1 },
33
events: [],
34
links: []
35
}
36
*/

Logs are transformed into what are known as “Span Events.” These events provide structured information and a timeline of occurrences within your application.

1
import {
import Effect
Effect
} from "effect"
2
import {
import NodeSdk
NodeSdk
} from "@effect/opentelemetry"
3
import {
4
(alias) class ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
,
5
(alias) class BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
6
} from "@opentelemetry/sdk-trace-base"
7
8
const
const program: Effect.Effect<void, never, never>
program
=
import Effect
Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs one or more messages or error causes at the current log level, which is INFO by default. This function allows logging multiple items at once and can include detailed error information using `Cause` instances. To adjust the log level, use the `Logger.withMinimumLogLevel` function.

log
("Hello").
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
9
import Effect
Effect
.
const delay: (duration: DurationInput) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Returns an effect that is delayed from this effect by the specified `Duration`.

delay
("100 millis"),
10
import Effect
Effect
.
const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

Wraps the effect with a new span for tracing.

withSpan
("myspan")
11
)
12
13
const
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
=
import NodeSdk
NodeSdk
.
const layer: (evaluate: LazyArg<NodeSdk.Configuration>) => Layer<Resource> (+1 overload)
layer
(() => ({
14
(property) Configuration.resource?: { readonly serviceName: string; readonly serviceVersion?: string; readonly attributes?: ResourceAttributes; } | undefined
resource
: {
(property) serviceName: string
serviceName
: "example" },
15
(property) Configuration.spanProcessor?: SpanProcessor | readonly SpanProcessor[] | undefined
spanProcessor
: new
(alias) new BatchSpanProcessor<BufferConfig>(_exporter: SpanExporter, config?: BufferConfig | undefined): BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
(new
(alias) new ConsoleSpanExporter(): ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
())
16
}))
17
18
import Effect
Effect
.
const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<void>

Runs an `Effect` workflow, returning a `Promise` which resolves with the result of the workflow or rejects with an error.

runPromise
(
const program: Effect.Effect<void, never, never>
program
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <Resource, never, never>(layer: Layer<Resource, never, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Resource>> (+9 overloads)

Splits the context into two parts, providing one part using the specified layer/context/runtime and leaving the remainder `R0`

provide
(
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
)))
19
/*
20
Example Output:
21
{
22
traceId: 'ad708d58c15f9e5c7b5cca2eeb6838a2',
23
parentId: undefined,
24
traceState: undefined,
25
name: 'myspan',
26
id: '4353fd47423e786a',
27
kind: 0,
28
timestamp: 1697043230170724.2,
29
duration: 112052.514,
30
attributes: {},
31
status: { code: 1 },
32
events: [
33
{
34
name: 'Hello',
35
attributes: { 'effect.fiberId': '#0', 'effect.logLevel': 'INFO' },
36
time: [ 1697043230, 280923805 ],
37
droppedAttributesCount: 0
38
}
39
],
40
links: []
41
}
42
*/

Spans can contain events, which are records of specific moments during the span’s lifecycle. In this output, there is one event named 'Hello'. It includes associated attributes, such as 'effect.fiberId' and 'effect.logLevel', providing information about the logged event. The time field represents the timestamp when the event occurred.

Spans can be nested, creating a hierarchy of operations. This concept is illustrated in the following code:

1
import {
import Effect
Effect
} from "effect"
2
import {
import NodeSdk
NodeSdk
} from "@effect/opentelemetry"
3
import {
4
(alias) class ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
,
5
(alias) class BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
6
} from "@opentelemetry/sdk-trace-base"
7
8
const
const child: Effect.Effect<void, never, never>
child
=
import Effect
Effect
.
(alias) const void: Effect.Effect<void, never, never> export void
void
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
9
import Effect
Effect
.
const delay: (duration: DurationInput) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Returns an effect that is delayed from this effect by the specified `Duration`.

delay
("100 millis"),
10
import Effect
Effect
.
const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

Wraps the effect with a new span for tracing.

withSpan
("child")
11
)
12
13
const
const parent: Effect.Effect<void, never, never>
parent
=
import Effect
Effect
.
const gen: <YieldWrap<Effect.Effect<void, never, never>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Effect.Effect<void, never, never>>, void, never>) => Effect.Effect<...> (+1 overload)
gen
(function* () {
14
yield*
import Effect
Effect
.
const sleep: (duration: DurationInput) => Effect.Effect<void>

Returns an effect that suspends for the specified duration. This method is asynchronous, and does not actually block the fiber executing the effect.

sleep
("20 millis")
15
yield*
const child: Effect.Effect<void, never, never>
child
16
yield*
import Effect
Effect
.
const sleep: (duration: DurationInput) => Effect.Effect<void>

Returns an effect that suspends for the specified duration. This method is asynchronous, and does not actually block the fiber executing the effect.

sleep
("10 millis")
17
}).
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

Wraps the effect with a new span for tracing.

withSpan
("parent"))
18
19
const
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
=
import NodeSdk
NodeSdk
.
const layer: (evaluate: LazyArg<NodeSdk.Configuration>) => Layer<Resource> (+1 overload)
layer
(() => ({
20
(property) Configuration.resource?: { readonly serviceName: string; readonly serviceVersion?: string; readonly attributes?: ResourceAttributes; } | undefined
resource
: {
(property) serviceName: string
serviceName
: "example" },
21
(property) Configuration.spanProcessor?: SpanProcessor | readonly SpanProcessor[] | undefined
spanProcessor
: new
(alias) new BatchSpanProcessor<BufferConfig>(_exporter: SpanExporter, config?: BufferConfig | undefined): BatchSpanProcessor import BatchSpanProcessor
BatchSpanProcessor
(new
(alias) new ConsoleSpanExporter(): ConsoleSpanExporter import ConsoleSpanExporter

This is implementation of {@link SpanExporter } that prints spans to the console. This class can be used for diagnostic purposes. NOTE: This {@link SpanExporter } is intended for diagnostics use only, output rendered to the console may change at any time.

ConsoleSpanExporter
())
22
}))
23
24
import Effect
Effect
.
const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<void>

Runs an `Effect` workflow, returning a `Promise` which resolves with the result of the workflow or rejects with an error.

runPromise
(
const parent: Effect.Effect<void, never, never>
parent
.
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <Resource, never, never>(layer: Layer<Resource, never, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Resource>> (+9 overloads)

Splits the context into two parts, providing one part using the specified layer/context/runtime and leaving the remainder `R0`

provide
(
const NodeSdkLive: Layer<Resource, never, never>
NodeSdkLive
)))
25
/*
26
Example Output:
27
{
28
traceId: '92fe81f1454d9c099198568cf867dc59',
29
parentId: 'b953d6c7d37ad93d',
30
traceState: undefined,
31
name: 'child',
32
id: '2fd19c8c23ebc7e8',
33
kind: 0,
34
timestamp: 1697043815321888.2,
35
duration: 106536.264,
36
attributes: {},
37
status: { code: 1 },
38
events: [],
39
links: []
40
}
41
{
42
traceId: '92fe81f1454d9c099198568cf867dc59',
43
parentId: undefined,
44
traceState: undefined,
45
name: 'parent',
46
id: 'b953d6c7d37ad93d',
47
kind: 0,
48
timestamp: 1697043815292133.2,
49
duration: 149724.295,
50
attributes: {},
51
status: { code: 1 },
52
events: [],
53
links: []
54
}
55
*/

As you can observe, the b953d6c7d37ad93d value plays a crucial role in maintaining the parent-child relationship between these spans. It provides a clear indication of how spans can be nested, creating a trace that helps developers understand the flow and hierarchy of operations in their applications.

In this tutorial, we’ll guide you through simulating and visualizing traces using a sample instrumented Node.js application. We will use Docker, Prometheus, Grafana, and Tempo to create, collect, and visualize traces.

Let’s understand the tools we’ll be using in simple terms:

  • Docker: Docker allows us to run applications in containers. Think of a container as a lightweight and isolated environment where your application can run consistently, regardless of the host system. It’s a bit like a virtual machine but more efficient.

  • Prometheus: Prometheus is a monitoring and alerting toolkit. It collects metrics and data about your applications and stores them for further analysis. This helps in identifying performance issues and understanding the behavior of your applications.

  • Grafana: Grafana is a visualization and analytics platform. It helps in creating beautiful and interactive dashboards to visualize your application’s data. You can use it to graphically represent metrics collected by Prometheus.

  • Tempo: Tempo is a distributed tracing system that allows you to trace the journey of a request as it flows through your application. It provides insights into how requests are processed and helps in debugging and optimizing your applications.

To get Docker, follow these steps:

  1. Visit the Docker website at https://www.docker.com/.

  2. Download Docker Desktop for your operating system (Windows or macOS) and install it.

  3. After installation, open Docker Desktop, and it will run in the background.

Now, let’s simulate traces using a sample Node.js application. We’ll provide you with the code and guide you on setting up the necessary components.

  1. Download Docker Files. Download the required Docker files: docker.zip

  2. Set Up docker. Unzip the downloaded file, navigate to the /docker/local directory in your terminal or command prompt and run the following command to start the necessary services:

    Terminal window
    docker-compose up
  3. Simulate Traces. Run the following example code in your Node.js environment. This code simulates a set of tasks and generates traces.

    Before proceeding, you’ll need to install additional libraries in addition to the latest version of effect. Here are the required libraries:

    Terminal window
    npm install @effect/opentelemetry
    npm install @opentelemetry/exporter-trace-otlp-http
    npm install @opentelemetry/sdk-trace-base
    # If your application is running on NodeJS
    npm install @opentelemetry/sdk-trace-node
    # If your application is running on the browser
    npm install @opentelemetry/sdk-trace-web
    # If your application will also be exporting metrics
    npm install @opentelemetry/sdk-metrics
    1
    import {
    import Effect
    Effect
    } from "effect"
    2
    import {
    import NodeSdk
    NodeSdk
    } from "@effect/opentelemetry"
    3
    import {
    (alias) class BatchSpanProcessor import BatchSpanProcessor
    BatchSpanProcessor
    } from "@opentelemetry/sdk-trace-base"
    4
    import {
    (alias) class OTLPTraceExporter import OTLPTraceExporter

    Collector Trace Exporter for Node

    OTLPTraceExporter
    } from "@opentelemetry/exporter-trace-otlp-http"
    5
    6
    // Function to simulate a task with possible subtasks
    7
    const
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    = (
    8
    (parameter) name: string
    name
    : string,
    9
    (parameter) delay: number
    delay
    : number,
    10
    (parameter) children: readonly Effect.Effect<void, never, never>[]
    children
    :
    interface ReadonlyArray<T>
    ReadonlyArray
    <
    import Effect
    Effect
    .
    interface Effect<out A, out E = never, out R = never> namespace Effect

    The `Effect` interface defines a value that lazily describes a workflow or job. The workflow requires some context `R`, and may fail with an error of type `E`, or succeed with a value of type `A`. `Effect` values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability. To run an `Effect` value, you need a `Runtime`, which is a type that is capable of executing `Effect` values.

    Effect
    <void>> = []
    11
    ) =>
    12
    import Effect
    Effect
    .
    const gen: <YieldWrap<Effect.Effect<void, never, never>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Effect.Effect<void, never, never>>, void, never>) => Effect.Effect<...> (+1 overload)
    gen
    (function* () {
    13
    yield*
    import Effect
    Effect
    .
    const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

    Logs one or more messages or error causes at the current log level, which is INFO by default. This function allows logging multiple items at once and can include detailed error information using `Cause` instances. To adjust the log level, use the `Logger.withMinimumLogLevel` function.

    log
    (
    (parameter) name: string
    name
    )
    14
    yield*
    import Effect
    Effect
    .
    const sleep: (duration: DurationInput) => Effect.Effect<void>

    Returns an effect that suspends for the specified duration. This method is asynchronous, and does not actually block the fiber executing the effect.

    sleep
    (`${
    (parameter) delay: number
    delay
    } millis`)
    15
    for (const
    const child: Effect.Effect<void, never, never>
    child
    of
    (parameter) children: readonly Effect.Effect<void, never, never>[]
    children
    ) {
    16
    yield*
    const child: Effect.Effect<void, never, never>
    child
    17
    }
    18
    yield*
    import Effect
    Effect
    .
    const sleep: (duration: DurationInput) => Effect.Effect<void>

    Returns an effect that suspends for the specified duration. This method is asynchronous, and does not actually block the fiber executing the effect.

    sleep
    (`${
    (parameter) delay: number
    delay
    } millis`)
    19
    }).
    (method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
    pipe
    (
    import Effect
    Effect
    .
    const withSpan: (name: string, options?: SpanOptions | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ParentSpan>> (+1 overload)

    Wraps the effect with a new span for tracing.

    withSpan
    (
    (parameter) name: string
    name
    ))
    20
    21
    const
    const poll: Effect.Effect<void, never, never>
    poll
    =
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/poll", 1)
    22
    23
    // Create a program with tasks and subtasks
    24
    const
    const program: Effect.Effect<void, never, never>
    program
    =
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("client", 2, [
    25
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/api", 3, [
    26
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/authN", 4, [
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/authZ", 5)]),
    27
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/payment Gateway", 6, [
    28
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("DB", 7),
    29
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("Ext. Merchant", 8)
    30
    ]),
    31
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/dispatch", 9, [
    32
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/dispatch/search", 10),
    33
    import Effect
    Effect
    .
    const all: <readonly [Effect.Effect<void, never, never>, Effect.Effect<void, never, never>, Effect.Effect<void, never, never>], { concurrency: "inherit"; }>(arg: readonly [Effect.Effect<void, never, never>, Effect.Effect<...>, Effect.Effect<...>], options?: { ...; } | undefined) => Effect.Effect<...>

    Runs all the provided effects in sequence respecting the structure provided in input. Supports multiple arguments, a single argument tuple / array or record / struct.

    all
    ([
    const poll: Effect.Effect<void, never, never>
    poll
    ,
    const poll: Effect.Effect<void, never, never>
    poll
    ,
    const poll: Effect.Effect<void, never, never>
    poll
    ], {
    (property) concurrency: "inherit"
    concurrency
    : "inherit" }),
    34
    const task: (name: string, delay: number, children?: ReadonlyArray<Effect.Effect<void>>) => Effect.Effect<void, never, never>
    task
    ("/pollDriver/{id}", 11)
    35
    ])
    36
    ])
    37
    ])
    38
    39
    const
    const NodeSdkLive: Layer<Resource, never, never>
    NodeSdkLive
    =
    import NodeSdk
    NodeSdk
    .
    const layer: (evaluate: LazyArg<NodeSdk.Configuration>) => Layer<Resource> (+1 overload)
    layer
    (() => ({
    40
    (property) Configuration.resource?: { readonly serviceName: string; readonly serviceVersion?: string; readonly attributes?: ResourceAttributes; } | undefined
    resource
    : {
    (property) serviceName: string
    serviceName
    : "example" },
    41
    (property) Configuration.spanProcessor?: SpanProcessor | readonly SpanProcessor[] | undefined
    spanProcessor
    : new
    (alias) new BatchSpanProcessor<BufferConfig>(_exporter: SpanExporter, config?: BufferConfig | undefined): BatchSpanProcessor import BatchSpanProcessor
    BatchSpanProcessor
    (new
    (alias) new OTLPTraceExporter(config?: OTLPExporterNodeConfigBase): OTLPTraceExporter import OTLPTraceExporter

    Collector Trace Exporter for Node

    OTLPTraceExporter
    ())
    42
    }))
    43
    44
    import Effect
    Effect
    .
    const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<void>

    Runs an `Effect` workflow, returning a `Promise` which resolves with the result of the workflow or rejects with an error.

    runPromise
    (
    45
    const program: Effect.Effect<void, never, never>
    program
    .
    (method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
    pipe
    (
    46
    import Effect
    Effect
    .
    const provide: <Resource, never, never>(layer: Layer<Resource, never, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Resource>> (+9 overloads)

    Splits the context into two parts, providing one part using the specified layer/context/runtime and leaving the remainder `R0`

    provide
    (
    const NodeSdkLive: Layer<Resource, never, never>
    NodeSdkLive
    ),
    47
    import Effect
    Effect
    .
    const catchAllCause: <never, void, never, never>(f: (cause: Cause<never>) => Effect.Effect<void, never, never>) => <A, R>(self: Effect.Effect<A, never, R>) => Effect.Effect<void | A, never, R> (+1 overload)

    Recovers from both recoverable and unrecoverable errors. See `sandbox`, `mapErrorCause` for other functions that can recover from defects.

    catchAllCause
    (
    import Effect
    Effect
    .
    const logError: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

    Logs the specified message or cause at the Error log level.

    logError
    )
    48
    )
    49
    )
    50
    /*
    51
    Output:
    52
    timestamp=... level=INFO fiber=#0 message=client
    53
    timestamp=... level=INFO fiber=#0 message=/api
    54
    timestamp=... level=INFO fiber=#0 message=/authN
    55
    timestamp=... level=INFO fiber=#0 message=/authZ
    56
    timestamp=... level=INFO fiber=#0 message="/payment Gateway"
    57
    timestamp=... level=INFO fiber=#0 message=DB
    58
    timestamp=... level=INFO fiber=#0 message="Ext. Merchant"
    59
    timestamp=... level=INFO fiber=#0 message=/dispatch
    60
    timestamp=... level=INFO fiber=#0 message=/dispatch/search
    61
    timestamp=... level=INFO fiber=#3 message=/poll
    62
    timestamp=... level=INFO fiber=#4 message=/poll
    63
    timestamp=... level=INFO fiber=#5 message=/poll
    64
    timestamp=... level=INFO fiber=#0 message=/pollDriver/{id}
    65
    */
  4. Visualize Traces. Now, open your web browser and go to http://localhost:3000/explore. You will see a generated Trace ID on the web page. Click on it to see the details of the trace.

    Traces in Grafana Tempo

That’s it! You’ve simulated and visualized traces using Docker, Prometheus, Grafana, and Tempo. You can use these tools to monitor, analyze, and gain insights into your applications’ performance and behavior.