Skip to content

Commit

Permalink
expand guides/observability/logging (Effect-TS#702)
Browse files Browse the repository at this point in the history
- fix broken link to concurrency/fibers
- add multiple messages example
- document Effect.annotateLogs
- document Effect.annotateLogsScoped
- remove `{:ts}` everywhere
  • Loading branch information
gcanti authored Jun 8, 2024
1 parent b1258dd commit df6d66b
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const program = Effect.all([flakyFoo, flakyBar]).pipe(
```

In the above program, we have two operations: `flakyFoo` and `flakyBar`, each representing a potential source of error.
These operations are combined using the `Effect.all(effects){:ts}` function from the Effect library, which allows us to sequence them together.
These operations are combined using the `Effect.all(effects)` function from the Effect library, which allows us to sequence them together.

</Tab>
</Tabs>
Expand Down
4 changes: 2 additions & 2 deletions content/docs/400-guides/250-resource-management/100-scope.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ So, finalizers run when the scope is closed, not necessarily when the effect fin

## Defining Resources

We can define a resource using operators like `Effect.acquireRelease(acquire, release){:ts}`, which allows us to create a scoped value from an `acquire` and `release` workflow.
We can define a resource using operators like `Effect.acquireRelease(acquire, release)`, which allows us to create a scoped value from an `acquire` and `release` workflow.

Every acquire release requires three actions:

Expand Down Expand Up @@ -594,7 +594,7 @@ Resource released

## acquireUseRelease

The `Effect.acquireUseRelease(acquire, use, release){:ts}` function is a specialized version of the `Effect.acquireRelease` function that simplifies resource management by automatically handling the scoping of resources.
The `Effect.acquireUseRelease(acquire, use, release)` function is a specialized version of the `Effect.acquireRelease` function that simplifies resource management by automatically handling the scoping of resources.

The main difference is that `acquireUseRelease` eliminates the need to manually call `Effect.scoped` to manage the resource's scope. It has additional knowledge about when you are done using the resource created with the `acquire` step. This is achieved by providing a `use` argument, which represents the function that operates on the acquired resource. As a result, `acquireUseRelease` can automatically determine when it should execute the release step.

Expand Down
142 changes: 112 additions & 30 deletions content/docs/400-guides/400-observability/100-logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Now, let's dive into the specific logging utilities provided by Effect.

## log

The `log` function is used to print a message at the current log level, which is `INFO` by default.
The `Effect.log` function outputs a log message at the default `INFO` level.

```ts twoslash
import { Effect } from "effect"
Expand All @@ -38,29 +38,45 @@ timestamp=... level=INFO fiber=#0 message="Application started"
*/
```

The log message contains the following information:
Details included in a log message:

- `timestamp`: The timestamp when the log message was generated.
- `level`: The log level at which the message is logged.
- `fiber`: The identifier of the [fiber](../concurrency/fibers.mdx#what-is-a-fiber) executing the program.
- `message`: The content of the log message.
- `span`: (Optional) The duration of the span in milliseconds.
- `fiber`: The identifier of the [fiber](../concurrency/fibers) executing the program.
- `message`: The log content, which can include multiple items.
- `span`: (Optional) The duration of the [span](#spans) in milliseconds.

Additionally, you can pass one or more [Cause](../../other/data-types/cause) instances after the main message. These will provide further context under an additional `cause` information:
You can log multiple messages simultaneously:

```ts twoslash
import { Effect } from "effect"

const program = Effect.log("message1", "message2", "message3")

Effect.runSync(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message1 message=message2 message=message3
*/
```

For added context, you can also include one or more [Cause](../../other/data-types/cause) instances in your logs,
which provide detailed error information under an additional `cause` annotation:

```ts twoslash
import { Effect, Cause } from "effect"

const program = Effect.log(
"message...",
"message1",
"message2",
Cause.die("Oh no!"),
Cause.die("Oh uh!")
)

Effect.runSync(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message... cause="Error: Oh no!
timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no!
Error: Oh uh!"
*/
```
Expand Down Expand Up @@ -101,9 +117,9 @@ const program = Effect.gen(function* () {
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=DEBUG message="task1 done" <-- 2 seconds later
... level=INFO message=done <-- 1 second later
timestamp=... level=INFO message=start
timestamp=... level=DEBUG message="task1 done" <-- 2 seconds later
timestamp=... level=INFO message=done <-- 1 second later
*/
```

Expand Down Expand Up @@ -131,9 +147,9 @@ const program = Effect.log("start").pipe(
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=DEBUG message="task1 done" <-- 2 seconds later
... level=INFO message=done <-- 1 second later
timestamp=... level=INFO message=start
timestamp=... level=DEBUG message="task1 done" <-- 2 seconds later
timestamp=... level=INFO message=done <-- 1 second later
*/
```

Expand All @@ -142,7 +158,7 @@ Output:

In the above example, we enable `DEBUG` messages specifically for `task1` by using the `Logger.withMinimumLogLevel` function.

By using `Logger.withMinimumLogLevel(effect, level){:ts}`, you have the flexibility to selectively enable different log levels for specific effects in your program. This allows you to control the level of detail in your logs and focus on the information that is most relevant to your debugging and troubleshooting needs.
By using `Logger.withMinimumLogLevel(effect, level)`, you have the flexibility to selectively enable different log levels for specific effects in your program. This allows you to control the level of detail in your logs and focus on the information that is most relevant to your debugging and troubleshooting needs.

### logInfo

Expand All @@ -164,8 +180,8 @@ const program = Effect.gen(function* () {
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=INFO message=done <-- 3 seconds later
timestamp=... level=INFO message=start
timestamp=... level=INFO message=done <-- 3 seconds later
*/
```

Expand All @@ -184,8 +200,8 @@ const program = Effect.logInfo("start").pipe(
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=INFO message=done <-- 3 seconds later
timestamp=... level=INFO message=start
timestamp=... level=INFO message=done <-- 3 seconds later
*/
```

Expand Down Expand Up @@ -219,7 +235,7 @@ const program = Effect.gen(function* () {
Effect.runPromise(program)
/*
Output:
... level=WARN fiber=#0 message="Oh uh!"
timestamp=... level=WARN fiber=#0 message="Oh uh!"
*/
```

Expand All @@ -238,7 +254,7 @@ const program = task.pipe(
Effect.runPromise(program)
/*
Output:
... level=WARN fiber=#0 message="Oh uh!"
timestamp=... level=WARN fiber=#0 message="Oh uh!"
*/
```

Expand Down Expand Up @@ -270,7 +286,7 @@ const program = Effect.gen(function* () {
Effect.runPromise(program)
/*
Output:
... level=ERROR fiber=#0 message="Oh uh!"
timestamp=... level=ERROR fiber=#0 message="Oh uh!"
*/
```

Expand All @@ -289,7 +305,7 @@ const program = task.pipe(
Effect.runPromise(program)
/*
Output:
... level=ERROR fiber=#0 message="Oh uh!"
timestamp=... level=ERROR fiber=#0 message="Oh uh!"
*/
```

Expand Down Expand Up @@ -321,7 +337,7 @@ const program = Effect.gen(function* () {
Effect.runPromise(program)
/*
Output:
... level=FATAL fiber=#0 message="Oh uh!"
timestamp=... level=FATAL fiber=#0 message="Oh uh!"
*/
```

Expand All @@ -340,16 +356,80 @@ const program = task.pipe(
Effect.runPromise(program)
/*
Output:
... level=FATAL fiber=#0 message="Oh uh!"
timestamp=... level=FATAL fiber=#0 message="Oh uh!"
*/
```

</Tab>
</Tabs>

## Spans
## Custom Annotations

Enhance your log outputs by incorporating custom annotations with the `Effect.annotateLogs` function.
This function allows you to append additional metadata to each log entry of an effect, enhancing traceability and context.

Here's how to apply a single annotation as a key/value pair:

```ts twoslash
import { Effect } from "effect"

const program = Effect.gen(function* () {
yield* Effect.log("message1")
yield* Effect.log("message2")
}).pipe(Effect.annotateLogs("key", "value")) // Annotation as key/value pair

Effect.runSync(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message1 key=value
timestamp=... level=INFO fiber=#0 message=message2 key=value
*/
```

To apply multiple annotations at once, you can pass an object containing several key/value pairs:

```ts twoslash
import { Effect } from "effect"

const program = Effect.gen(function* () {
yield* Effect.log("message1")
yield* Effect.log("message2")
}).pipe(Effect.annotateLogs({ key1: "value1", key2: "value2" }))

Effect.runSync(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message1 key2=value2 key1=value1
timestamp=... level=INFO fiber=#0 message=message2 key2=value2 key1=value1
*/
```

Annotations can also be applied with a scoped lifetime using `Effect.annotateLogsScoped`.
This method confines the application of annotations to logs within a specific [Scope](../resource-management/scope) of your effect computation:

```ts twoslash
import { Effect } from "effect"

const program = Effect.gen(function* () {
yield* Effect.log("no annotations")
yield* Effect.annotateLogsScoped({ key: "value" })
yield* Effect.log("message1") // Annotation is applied to this log
yield* Effect.log("message2") // Annotation is applied to this log
}).pipe(Effect.scoped, Effect.andThen(Effect.log("no annotations again")))

Effect.runSync(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="no annotations"
timestamp=... level=INFO fiber=#0 message=message1 key=value
timestamp=... level=INFO fiber=#0 message=message2 key=value
timestamp=... level=INFO fiber=#0 message="no annotations again"
*/
```

## Log Spans

Effect also provides support for spans, allowing you to measure the duration of specific operations or tasks within your program.
Effect also provides support for log spans, allowing you to measure the duration of specific operations or tasks within your program.

<Tabs items={["Using Effect.gen", "Using pipe"]}>
<Tab>
Expand All @@ -365,7 +445,7 @@ const program = Effect.gen(function* () {
Effect.runPromise(program)
/*
Output:
... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms
timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms
*/
```

Expand All @@ -383,14 +463,16 @@ const program = Effect.sleep("1 second").pipe(
Effect.runPromise(program)
/*
Output:
... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms
timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms
*/
```

</Tab>
</Tabs>

In the above example, a span is created using the `Effect.withLogSpan` function. It measures the duration of the code block within the span and logs an `INFO` message with the content "The job is finished!" along with the span duration of 1011ms (`myspan=1011ms`).
In the above example, a log span is created using the `Effect.withLogSpan(label)` function.
It measures the duration of the code block within the span.
The resulting duration is then automatically recorded as an annotation within the log message.

## Disabling Default Logging

Expand Down
4 changes: 2 additions & 2 deletions content/docs/400-guides/450-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export const config = Config.map(
)
```

In the above example, we use the `Config.all(configs){:ts}` operator to combine two primitive configs `Config<string>` and `Config<number>` into a `Config<[string, number]>`.
In the above example, we use the `Config.all(configs)` operator to combine two primitive configs `Config<string>` and `Config<number>` into a `Config<[string, number]>`.

If we use this customized configuration in our application:

Expand Down Expand Up @@ -392,7 +392,7 @@ export const program = HostPort.config.pipe(
</Tab>
</Tabs>

when you run the program using `Effect.runSync(program){:ts}`, it will attempt to read the corresponding values from environment variables (`HOST` and `PORT`):
when you run the program using `Effect.runSync(program)`, it will attempt to read the corresponding values from environment variables (`HOST` and `PORT`):

```bash filename="Terminal"
HOST=localhost PORT=8080 npx ts-node HostPort.ts
Expand Down
2 changes: 1 addition & 1 deletion content/docs/400-guides/640-concurrency/130-deferred.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ A `Deferred` in Effect is conceptually similar to JavaScript's `Promise`. The ke

### Creating

You can create a `Deferred` using `Deferred.make<A, E>(){:ts}`. This returns an `Effect<Deferred<A, E>>`, which describes the creation of a `Deferred`. Note that `Deferred`s can only be created within an `Effect` because creating them involves effectful memory allocation, which must be managed safely within an `Effect`.
You can create a `Deferred` using `Deferred.make<A, E>()`. This returns an `Effect<Deferred<A, E>>`, which describes the creation of a `Deferred`. Note that `Deferred`s can only be created within an `Effect` because creating them involves effectful memory allocation, which must be managed safely within an `Effect`.

### Awaiting

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ Output:
*/
```

In this example, we've used the `Schedule.spaced("1 second"){:ts}` schedule to introduce a one-second gap between each emission in the stream.
In this example, we've used the `Schedule.spaced("1 second")` schedule to introduce a one-second gap between each emission in the stream.
8 changes: 4 additions & 4 deletions content/docs/400-guides/720-style/300-pattern-matching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ console.log(match({ b: "hello" })) // Output: "hello"

Let's dissect what's happening:

- `Match.type<{ a: number } | { b: string }>(){:ts}`: This creates a `Matcher` for objects that are either of type `{ a: number }` or `{ b: string }`.
- `Match.when({ a: Match.number }, (_) => _.a){:ts}`: This sets up a condition to match an object with a property `a` containing a number. If matched, it returns the value of property `a`.
- `Match.when({ b: Match.string }, (_) => _.b){:ts}`: This condition matches an object with a property `b` containing a string. If found, it returns the value of property `b`.
- `Match.exhaustive{:ts}`: This function ensures that all possible cases are considered and matched, making sure that no other unaccounted cases exist. It helps to prevent overlooking any potential scenario.
- `Match.type<{ a: number } | { b: string }>()`: This creates a `Matcher` for objects that are either of type `{ a: number }` or `{ b: string }`.
- `Match.when({ a: Match.number }, (_) => _.a)`: This sets up a condition to match an object with a property `a` containing a number. If matched, it returns the value of property `a`.
- `Match.when({ b: Match.string }, (_) => _.b)`: This condition matches an object with a property `b` containing a string. If found, it returns the value of property `b`.
- `Match.exhaustive`: This function ensures that all possible cases are considered and matched, making sure that no other unaccounted cases exist. It helps to prevent overlooking any potential scenario.

Finally, the `match` function is applied to test two different objects, `{ a: 0 }` and `{ b: "hello" }`. As per the defined conditions within the `Matcher`, it correctly matches the objects and provides the expected output based on the defined conditions.

Expand Down
7 changes: 5 additions & 2 deletions content/docs/700-other/300-data-types/chunk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ A `Chunk<A>` represents a chunk of values of type `A`.
Chunks are usually backed by arrays, but expose a purely functional, safe interface to the underlying elements, and they become lazy on operations that would be costly with arrays, such as repeated concatenation. Like lists and arrays, `Chunk` is an ordered collection.

<Warning>
`Chunk` is purpose-built to amoritize the cost of repeated concatenation of arrays. Therefore, for use-cases that **do not** involve repeated concatenation of arrays, the overhead of `Chunk` will result in reduced performance.
`Chunk` is purpose-built to amoritize the cost of repeated concatenation of
arrays. Therefore, for use-cases that **do not** involve repeated
concatenation of arrays, the overhead of `Chunk` will result in reduced
performance.
</Warning>

## Why Chunk?
Expand All @@ -31,7 +34,7 @@ import { Chunk } from "effect"
const emptyChunk = Chunk.empty()
```

If you want to create a `Chunk` with specific values, you can use the `Chunk.make(...values){:ts}` function:
If you want to create a `Chunk` with specific values, you can use the `Chunk.make(...values)` function:

```ts twoslash
import { Chunk } from "effect"
Expand Down

0 comments on commit df6d66b

Please sign in to comment.