@zio.dev/zio-blocks 0.0.51 → 0.0.55
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/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +6 -0
- package/guides/getting-started-with-mux.md +0 -112
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +395 -1
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +131 -70
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +200 -583
- package/package.json +1 -1
- package/reference/async.md +1379 -531
- package/reference/chunk.md +3 -3
- package/reference/codegen/index.md +1 -1
- package/reference/combinators.md +4 -4
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +6 -49
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +2 -2
- package/reference/docs.md +2 -2
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/index.md +9 -89
- package/reference/endpoint/path-codec.md +12 -24
- package/reference/endpoint/route-pattern.md +4 -6
- package/reference/endpoint/segment-codec.md +19 -32
- package/reference/html.md +313 -9
- package/reference/htmx/index.md +4 -52
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +3 -1
- package/reference/http-model/model.md +107 -71
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +6 -3
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +128 -11
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +7 -2
- package/reference/openapi.md +3 -3
- package/reference/projection.md +654 -0
- package/reference/resource-management/resource.md +2 -98
- package/reference/resource-management/scope.md +1 -209
- package/reference/resource-management/wire.md +4 -50
- package/reference/ringbuffer/advanced.mdx +1 -1
- package/reference/ringbuffer/index.mdx +3 -3
- package/reference/ringbuffer/mpmc.mdx +38 -4
- package/reference/ringbuffer/mpsc.mdx +36 -4
- package/reference/ringbuffer/spmc.mdx +1 -1
- package/reference/ringbuffer/spsc.mdx +87 -15
- package/reference/schema/allows.md +0 -96
- package/reference/schema/binding.md +2 -2
- package/reference/schema/built-in-codecs/avro.md +2 -2
- package/reference/schema/built-in-codecs/bson.md +50 -20
- package/reference/schema/built-in-codecs/csv.md +2 -2
- package/reference/schema/built-in-codecs/index.md +3 -3
- package/reference/schema/built-in-codecs/json/index.md +2 -2
- package/reference/schema/built-in-codecs/messagepack.md +3 -3
- package/reference/schema/built-in-codecs/thrift.md +2 -2
- package/reference/schema/built-in-codecs/toon.md +3 -3
- package/reference/schema/built-in-codecs/yaml.md +2 -2
- package/reference/schema/codec.md +11 -11
- package/reference/schema/dynamic-optic.md +48 -3
- package/reference/schema/dynamic-schema.md +3 -3
- package/reference/schema/index.md +2 -0
- package/reference/schema/path-interpolator.md +2 -0
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/schema/schema-evolution/as.md +4 -4
- package/reference/schema/schema-evolution/into.md +2 -2
- package/reference/schema/schema-expr.md +2 -2
- package/reference/schema/schema-search.md +263 -0
- package/reference/schema/schema.md +10 -2
- package/reference/schema/type-class-derivation.md +1 -1
- package/reference/smithy.md +502 -3
- package/reference/sql/db-codec-deriver.md +3 -3
- package/reference/sql/db-codec.md +22 -22
- package/reference/sql/db-con.md +4 -4
- package/reference/sql/db-connection.md +1 -1
- package/reference/sql/db-param.md +1 -1
- package/reference/sql/db-result-reader.md +4 -2
- package/reference/sql/db-tx.md +46 -14
- package/reference/sql/ddl.md +1 -1
- package/reference/sql/frag.md +44 -10
- package/reference/sql/index.md +7 -7
- package/reference/sql/repo.md +15 -15
- package/reference/sql/sql-dialect.md +1 -1
- package/reference/sql/sql-logger.md +1 -1
- package/reference/sql/sql-name-mapper.md +3 -3
- package/reference/sql/table-metadata.md +3 -3
- package/reference/sql/table.md +10 -10
- package/reference/sql/transactor-zio.md +1 -1
- package/reference/sql/transactor.md +21 -11
- package/reference/sql-zio.md +1 -1
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
- package/reference/streams/{sink.md → core/sink.md} +331 -353
- package/reference/streams/{stream.md → core/stream.md} +919 -209
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +140 -67
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/{writer.md → primitives/writer.md} +254 -98
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +0 -64
- package/sidebars.js +150 -12
- package/undocumented-report.md +528 -270
- package/reference/config.md +0 -158
- package/reference/streams/concurrent-operators.md +0 -106
- package/reference/streams/reader.md +0 -1284
- package/reference/streams/scala-2-compatibility.md +0 -55
- package/reference/streams/zero-boxing.md +0 -275
- package/reference/telemetry.md +0 -693
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: log-formatter
|
|
3
|
+
title: "LogFormatter"
|
|
4
|
+
description: "How a LogRecord is rendered to text: the built-in human-readable and OTLP-JSON formatters behind log.writer."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Structured Logging"
|
|
7
|
+
- "Log Formatting"
|
|
8
|
+
- "Text and JSON Output"
|
|
9
|
+
- "LogFormatter"
|
|
10
|
+
sidebar_label: "LogFormatter"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`LogFormatter` renders a [`LogRecord`](./log-record.md) into text; its partner, the [`LogWriter`](./log-writer.md), routes that text to a destination. You rarely name a formatter directly — you hand a formatter/writer pair to [`log.writer`](./index.md), and the two built-ins below cover the common cases. Implement the trait yourself when your pipeline expects a shape neither produces.
|
|
14
|
+
|
|
15
|
+
## Built-in Formatters
|
|
16
|
+
|
|
17
|
+
Two singletons cover the usual output formats — both safe to share across threads, with `TextLogFormatter` caching its timestamp prefix for the current second:
|
|
18
|
+
|
|
19
|
+
| Formatter | Output |
|
|
20
|
+
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
|
|
21
|
+
| `TextLogFormatter` | Human-readable — `2026-07-29T10:00:00.000Z INFO [Svc.doWork:42] message {key=val}` |
|
|
22
|
+
| `JsonLogFormatter` | OTLP-compatible JSON — `{"timeUnixNano":"...","severityNumber":9,"severityText":"INFO","body":{"stringValue":"message"},"attributes":[...]}` |
|
|
23
|
+
|
|
24
|
+
Both render the record's timestamp, severity, source location (from the `code.*` attributes), message body, and an attached throwable's stack trace. They differ in what else they carry: `TextLogFormatter` prints the remaining attributes as a compact `{key=value}` list and drops the trace and span IDs, while `JsonLogFormatter` emits every attribute — including the four `code.*` entries — plus `traceId` and `spanId` when the record was written inside a span.
|
|
25
|
+
|
|
26
|
+
## Example Usage
|
|
27
|
+
|
|
28
|
+
Choosing a formatter is choosing who reads your logs. In development that's you, so pick `TextLogFormatter` — one aligned line per record, easy to scan in a terminal. In production it's a log collector, so pick `JsonLogFormatter` — every attribute stays a separate field the collector can index and query. Select it once at startup, alongside a [`LogWriter`](./log-writer.md) for the destination:
|
|
29
|
+
|
|
30
|
+
```scala
|
|
31
|
+
import zio.blocks.telemetry._
|
|
32
|
+
|
|
33
|
+
val isDevelopment = sys.env.get("ENV").forall(_ != "production")
|
|
34
|
+
|
|
35
|
+
if (isDevelopment) log.writer(TextLogFormatter, StdoutWriter) // readable in a terminal
|
|
36
|
+
else log.writer(JsonLogFormatter, StdoutWriter) // queryable by a collector
|
|
37
|
+
|
|
38
|
+
log.info("server ready", "port" -> 8080L)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You can also register both — `log.writer` is additive, so each call adds another channel and every record goes to all of them. That's how you keep readable console output while also emitting machine-readable JSON:
|
|
42
|
+
|
|
43
|
+
```scala
|
|
44
|
+
import zio.blocks.telemetry._
|
|
45
|
+
|
|
46
|
+
log.writer(TextLogFormatter, StdoutWriter) // for the developer watching the terminal
|
|
47
|
+
log.writer(JsonLogFormatter, StderrWriter) // for the collector tailing stderr
|
|
48
|
+
|
|
49
|
+
log.info("server ready", "port" -> 8080L) // written to both channels
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Custom Formatter
|
|
53
|
+
|
|
54
|
+
Sometimes neither built-in shape fits — an existing ingest pipeline expects logfmt, or a report wants CSV. Implement the trait yourself and pass your formatter to `log.writer` like any built-in. Two methods are abstract, but only one of them is yours to write:
|
|
55
|
+
|
|
56
|
+
```scala
|
|
57
|
+
trait LogFormatter {
|
|
58
|
+
def format(
|
|
59
|
+
sb: StringBuilder, timestampNanos: Long, severity: Severity, severityText: String,
|
|
60
|
+
body: String, builder: Attributes.AttributesBuilder,
|
|
61
|
+
traceIdHi: Long, traceIdLo: Long, spanId: Long, traceFlags: Byte,
|
|
62
|
+
throwable: Option[Throwable]
|
|
63
|
+
): Unit
|
|
64
|
+
|
|
65
|
+
def formatRecord(sb: StringBuilder, record: LogRecord): Unit
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`formatRecord` is the one that runs: a channel registered with `log.writer` renders each finished [`LogRecord`](./log-record.md) through it. Put your format there.
|
|
70
|
+
|
|
71
|
+
`format` serves a lower-allocation path that skips building a `LogRecord`, and the library uses it only for its own default console output — no formatter you register ever reaches it. You still have to define it to satisfy the trait, so mirror the scalar fields and move on; it cannot render attributes anyway, since the accessors for reading them off the builder are internal.
|
|
72
|
+
|
|
73
|
+
Both methods receive a fresh, empty `StringBuilder` from the caller, which writes it out and discards it. Append your output and return — the caller handles writing.
|
|
74
|
+
|
|
75
|
+
A CSV formatter — three comma-separated fields in `formatRecord`, with `format` mirroring them:
|
|
76
|
+
|
|
77
|
+
```scala
|
|
78
|
+
import zio.blocks.telemetry._
|
|
79
|
+
|
|
80
|
+
object CsvFormatter extends LogFormatter {
|
|
81
|
+
def formatRecord(sb: StringBuilder, record: LogRecord): Unit =
|
|
82
|
+
sb.append(record.timestampNanos).append(',').append(record.severityText).append(',').append(record.body.value)
|
|
83
|
+
|
|
84
|
+
def format(
|
|
85
|
+
sb: StringBuilder, timestampNanos: Long, severity: Severity, severityText: String,
|
|
86
|
+
body: String, builder: Attributes.AttributesBuilder,
|
|
87
|
+
traceIdHi: Long, traceIdLo: Long, spanId: Long, traceFlags: Byte,
|
|
88
|
+
throwable: Option[Throwable]
|
|
89
|
+
): Unit = sb.append(timestampNanos).append(',').append(severityText).append(',').append(body)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log.writer(CsvFormatter, StdoutWriter)
|
|
93
|
+
log.info("order placed")
|
|
94
|
+
// 2026-07-31T13:23:25.137Z INFO [Main$.main:20] order placed <- default console output
|
|
95
|
+
// 1785504205137002324,INFO,order placed <- your channel
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Two lines, because `log.writer` adds a channel rather than replacing one, and the default console output is still registered.
|
|
99
|
+
|
|
100
|
+
To render attributes too, pass an `AttributeVisitor` to `record.attributes.accept` — it fires once per attribute with the raw unboxed value, so nothing is boxed on the way out. Only the four scalar methods are abstract; the seq variants default to no-ops. Source location arrives among those attributes as `code.filepath`, `code.namespace`, `code.function`, and `code.lineno`. Decide which convention you want: `TextLogFormatter` lifts them into its `[Svc.doWork:42]` prefix and then skips those keys, while `JsonLogFormatter` emits them as ordinary attributes. Skip any key starting with `code.` if you render the location separately.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: log-record-processor
|
|
3
|
+
title: "LogRecordProcessor"
|
|
4
|
+
description: "A destination for log records — each registered processor receives every log your application emits, to print, export, or discard."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Structured Logging"
|
|
7
|
+
- "Log Export"
|
|
8
|
+
- "Log Destinations"
|
|
9
|
+
- "LogRecordProcessor"
|
|
10
|
+
sidebar_label: "LogRecordProcessor"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A `LogRecordProcessor` is a destination for your logs. Every time you call `log.info(...)`, the finished [`LogRecord`](./log-record.md) is handed to each registered processor, and what happens next is entirely up to that processor: print it, ship it to a backend, forward it into another logging library, or drop it.
|
|
14
|
+
|
|
15
|
+
The type exists because where logs belong depends on your deployment, which the library can't know. Registering destinations is how you tell it — and you can register several, each receiving the same record, which is how logs reach your console and a remote backend at once.
|
|
16
|
+
|
|
17
|
+
Most applications never implement one, because the module ships the common destinations. `ConsoleLogRecordProcessor` prints readable text to stdout — the default, and also the fastest path: when it's the only processor registered, the `Logger` skips building a `LogRecord` and formats straight from the raw values. `LogRecordProcessor.noop` accepts every record and discards it, for tests or as a placeholder. And for formatted output — JSON, a file, your own shape — [`log.writer(formatter, writer)`](./index.md) builds a processor for you out of a [`LogFormatter`](./log-formatter.md) and a [`LogWriter`](./log-writer.md), which is less code than implementing this trait.
|
|
18
|
+
|
|
19
|
+
Write your own to reach somewhere the module doesn't cover — an OTLP collector, a vendor's API, or an existing logging framework you're migrating from.
|
|
20
|
+
|
|
21
|
+
```scala
|
|
22
|
+
trait LogRecordProcessor extends AutoCloseable {
|
|
23
|
+
def onEmit(logRecord: LogRecord): Unit // fires for every record the Logger lets through
|
|
24
|
+
def shutdown(): Unit // flush and release; called by LoggerProvider.shutdown()
|
|
25
|
+
def forceFlush(): Unit // flush buffered records now
|
|
26
|
+
override def close(): Unit = shutdown()
|
|
27
|
+
|
|
28
|
+
def minimumLevel: Int = 1 // lowest Severity.number accepted; default accepts all
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
object LogRecordProcessor {
|
|
32
|
+
val noop: LogRecordProcessor // ignores every record
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`onEmit` is the method that matters — it runs once per log, **on the thread that logged**, before your code continues. So keep it quick. If you send each record over the network from inside `onEmit`, every `log.info` in your application waits for that round trip; buffer records instead and ship them in batches from a background thread, then flush what's left when `shutdown` or `forceFlush` is called.
|
|
37
|
+
|
|
38
|
+
`minimumLevel` lets a processor say "only send me warnings and worse" as a `Severity` number. It's a volume hint, not a filter applied per processor: the `Logger` takes the lowest `minimumLevel` across every registered processor and, for anything below it, doesn't even build a `LogRecord` — so a debug line no destination wants costs almost nothing. Anything above that shared floor is delivered to every processor, including ones whose own `minimumLevel` is higher, so a processor that must not see debug records has to re-check `record.severity` in `onEmit`.
|
|
39
|
+
|
|
40
|
+
Your own processor implements those three methods — with nothing buffered, `shutdown` and `forceFlush` can stay empty — and registers on the global [`log`](./index.md):
|
|
41
|
+
|
|
42
|
+
```scala
|
|
43
|
+
import zio.blocks.telemetry._
|
|
44
|
+
|
|
45
|
+
val exporter = new LogRecordProcessor {
|
|
46
|
+
def onEmit(r: LogRecord): Unit = println(s"[EXPORT] ${r.severity.text} ${r.body.value}")
|
|
47
|
+
def shutdown(): Unit = ()
|
|
48
|
+
def forceFlush(): Unit = ()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
log.addProcessor(exporter)
|
|
52
|
+
log.info("checkout complete")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`log.addProcessor` is the easiest route: your processor joins the destinations already registered, with nothing to rebuild — when you build a provider yourself, pass processors to `LoggerProvider.builder.addLogRecordProcessor(...)` instead. Either way, an exception your processor throws is caught and printed to stderr, and the remaining processors still receive the record, so one broken destination can't take down your logging.
|
|
56
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: log-record
|
|
3
|
+
title: "LogRecord"
|
|
4
|
+
description: "Immutable snapshot of a single log emission — the read-only value a LogRecordProcessor receives and a LogFormatter renders."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Structured Logging"
|
|
7
|
+
- "Trace Correlation"
|
|
8
|
+
- "Immutable Log Snapshot"
|
|
9
|
+
- "LogRecord"
|
|
10
|
+
sidebar_label: "LogRecord"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A `LogRecord` is one log entry as a value — everything a single `log.info(...)` call captured, bundled together: the message, its severity, when it happened, the key/value fields attached to it, and which request it belongs to.
|
|
14
|
+
|
|
15
|
+
The type exists because a log entry has to travel. From the call site it may go to your console, to a file, and to a backend that indexes it, and each of those destinations needs the whole story in one self-contained package — a bare string wouldn't carry the severity or the trace it came from. Because a record is immutable, every destination can read the same one safely.
|
|
16
|
+
|
|
17
|
+
You never build a `LogRecord` in normal use; the global [`log`](./index.md) (or a [`Logger`](./logger.md)) builds one for you on every call. You meet it when you extend the pipeline: it's the value handed to a [`LogRecordProcessor`](./log-record-processor.md)'s `onEmit` and to a [`LogFormatter`](./log-formatter.md), so writing either means reading fields off a record.
|
|
18
|
+
|
|
19
|
+
```scala
|
|
20
|
+
final case class LogRecord(
|
|
21
|
+
timestampNanos: Long,
|
|
22
|
+
observedTimestampNanos: Long,
|
|
23
|
+
severity: Severity,
|
|
24
|
+
severityText: String,
|
|
25
|
+
body: LogMessage, // LogMessage.Simple(text), or a lazy template
|
|
26
|
+
attributes: Attributes, // code.* source location + caller-supplied attrs
|
|
27
|
+
traceIdHi: Long, // 0L when no span is active
|
|
28
|
+
traceIdLo: Long, // 0L when no span is active
|
|
29
|
+
spanId: Long, // 0L when no span is active
|
|
30
|
+
traceFlags: Byte, // 0x00 when not sampled
|
|
31
|
+
resource: Resource,
|
|
32
|
+
instrumentationScope: InstrumentationScope,
|
|
33
|
+
throwable: Option[Throwable] = None
|
|
34
|
+
) {
|
|
35
|
+
def hasTraceId: Boolean // traceIdHi != 0L || traceIdLo != 0L
|
|
36
|
+
def hasSpanId: Boolean // spanId != 0L
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
object LogRecord {
|
|
40
|
+
def builder: LogRecordBuilder
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Each field mirrors what the emission recorded. The source location the macro captured arrives inside `attributes`, as the `code.filepath`, `code.namespace`, `code.function`, and `code.lineno` keys. The trace and span IDs are what tie this entry back to the request that produced it: they're filled in when the call ran inside an active span, and left as `0` when it didn't — so check `hasTraceId`/`hasSpanId` rather than comparing to zero yourself.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: log-writer
|
|
3
|
+
title: "LogWriter"
|
|
4
|
+
description: "The sink that routes formatted log text to a destination: the built-in stdout, stderr, and no-op writers behind log.writer."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Structured Logging"
|
|
7
|
+
- "Log Output"
|
|
8
|
+
- "Output Sink"
|
|
9
|
+
- "LogWriter"
|
|
10
|
+
sidebar_label: "LogWriter"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`LogWriter` is the sink half of console log output: it takes text that has already been formatted and pushes it to a destination — stdout, stderr, a file. Its partner is the [`LogFormatter`](./log-formatter.md), which turns a [`LogRecord`](./log-record.md) into that text. You rarely name a writer directly — you hand a formatter/writer pair to [`log.writer`](./index.md), and the built-ins below cover the usual destinations. Reach for a custom one only to send output somewhere the built-ins don't.
|
|
14
|
+
|
|
15
|
+
```scala
|
|
16
|
+
trait LogWriter {
|
|
17
|
+
def write(content: CharSequence): Unit
|
|
18
|
+
def flush(): Unit = ()
|
|
19
|
+
def close(): Unit = ()
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`write` receives one fully-formatted line; `flush` and `close` default to no-ops, so a simple writer overrides only `write`.
|
|
24
|
+
|
|
25
|
+
## Built-in Writers
|
|
26
|
+
|
|
27
|
+
Three singletons cover the common destinations:
|
|
28
|
+
|
|
29
|
+
| Writer | Destination |
|
|
30
|
+
|--------|-------------|
|
|
31
|
+
| `StdoutWriter` | `System.out` |
|
|
32
|
+
| `StderrWriter` | `System.err` |
|
|
33
|
+
| `NoopWriter` | Discards output — for benchmarking the formatting path |
|
|
34
|
+
|
|
35
|
+
## Example Usage
|
|
36
|
+
|
|
37
|
+
Pair a writer with a formatter and hand both to `log.writer`. It is additive — each call adds another channel, and every record is sent to all of them:
|
|
38
|
+
|
|
39
|
+
```scala
|
|
40
|
+
import zio.blocks.telemetry._
|
|
41
|
+
|
|
42
|
+
log.writer(TextLogFormatter, StdoutWriter) // human-readable text to stdout
|
|
43
|
+
log.writer(JsonLogFormatter, StderrWriter) // OTLP JSON to stderr
|
|
44
|
+
|
|
45
|
+
log.info("server ready", "port" -> 8080L) // written to both channels
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Custom Writer
|
|
49
|
+
|
|
50
|
+
Implement the trait to send formatted output somewhere the built-ins don't — a file, say. Override `write`, plus `flush`/`close` if the destination holds resources:
|
|
51
|
+
|
|
52
|
+
```scala
|
|
53
|
+
import zio.blocks.telemetry._
|
|
54
|
+
import java.io.PrintWriter
|
|
55
|
+
|
|
56
|
+
val fileWriter = new LogWriter {
|
|
57
|
+
private val out = new PrintWriter("app.log")
|
|
58
|
+
def write(content: CharSequence): Unit = out.println(content)
|
|
59
|
+
override def flush(): Unit = out.flush()
|
|
60
|
+
override def close(): Unit = out.close()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log.writer(TextLogFormatter, fileWriter)
|
|
64
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: logger-provider
|
|
3
|
+
title: "LoggerProvider"
|
|
4
|
+
description: "Root factory for structured logging: produces Logger instances sharing a Resource, LogRecordProcessor pipeline, and ContextStorage."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Structured Logging"
|
|
7
|
+
- "Logging Configuration"
|
|
8
|
+
- "Logger Factory"
|
|
9
|
+
- "LoggerProvider"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A `LoggerProvider` is what makes [`Logger`](./logger.md)s. You build one at startup, and it hands out every logger your application uses.
|
|
13
|
+
|
|
14
|
+
It exists so that two questions get answered once instead of at every call site: *who is logging* — your service's identity, a [`Resource`](../common/resource.md) — and *where the logs go* — the [`LogRecordProcessor`](./log-record-processor.md)s that render or ship each record. Every logger the provider creates inherits both, so pointing your whole application at a different destination is a one-line change at startup rather than an edit everywhere you log.
|
|
15
|
+
|
|
16
|
+
Most applications never hold a provider directly: build one, hand a logger from it to `log.install(...)`, and use the global [`log`](./index.md) from then on. Reach for the provider itself when you need that startup configuration under your own control.
|
|
17
|
+
|
|
18
|
+
```scala
|
|
19
|
+
final class LoggerProvider private[telemetry] (...) extends AutoCloseable {
|
|
20
|
+
def get(name: String, version: String = ""): Logger // a Logger for a named scope
|
|
21
|
+
|
|
22
|
+
def shutdown(): Unit // shut down every processor; call once at exit
|
|
23
|
+
override def close(): Unit // alias for shutdown() — satisfies AutoCloseable
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
object LoggerProvider {
|
|
27
|
+
def builder: LoggerProviderBuilder
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
final class LoggerProviderBuilder private[telemetry] (...) {
|
|
31
|
+
def setResource(resource: Resource): LoggerProviderBuilder
|
|
32
|
+
def addLogRecordProcessor(processor: LogRecordProcessor): LoggerProviderBuilder
|
|
33
|
+
def setContextStorage(contextStorage: ContextStorage[Option[SpanContext]]): LoggerProviderBuilder
|
|
34
|
+
def build(): LoggerProvider
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Build a provider once at startup, set only what you need — a `Resource` for your service identity and the `LogRecordProcessor`s that handle your records — then `get` a `Logger` from it. Anything left unset takes a sensible default (`Resource.default`, and an empty pipeline until you add a processor):
|
|
41
|
+
|
|
42
|
+
```scala
|
|
43
|
+
import zio.blocks.telemetry._
|
|
44
|
+
|
|
45
|
+
val provider = LoggerProvider.builder
|
|
46
|
+
.setResource(Resource.create(Attributes.of(Attributes.ServiceName, "payments")))
|
|
47
|
+
.addLogRecordProcessor(LogRecordProcessor.noop) // replace with a console writer or OTLP exporter
|
|
48
|
+
.build()
|
|
49
|
+
|
|
50
|
+
val logger = provider.get("com.example.payments")
|
|
51
|
+
logger.info("order placed", "orderId" -> AttributeValue.StringValue("ord-001"))
|
|
52
|
+
|
|
53
|
+
provider.shutdown()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Note that `get` builds a logger rather than looking one up: call it twice with the same name and you get two separate `Logger` objects that behave identically. So call it once where a component is created and keep the result in a `val`, not inside a request handler that runs thousands of times a second — per-request calls produce the same log output, they just make throwaway objects for the garbage collector:
|
|
57
|
+
|
|
58
|
+
```scala
|
|
59
|
+
import zio.blocks.telemetry._
|
|
60
|
+
|
|
61
|
+
class PaymentService(provider: LoggerProvider) {
|
|
62
|
+
private val logger = provider.get("com.example.payments") // once, at construction
|
|
63
|
+
|
|
64
|
+
def handle(orderId: String): Unit =
|
|
65
|
+
logger.info("handling payment", "orderId" -> AttributeValue.StringValue(orderId))
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The name you pass is what identifies where a log came from, so one logger per component — a service, a module — is the usual granularity.
|
|
70
|
+
|
|
71
|
+
Passing a `Logger` down through every constructor gets tedious, though. Register one on the global [`log`](./index.md) at startup instead, and the rest of your code can log without knowing a provider exists:
|
|
72
|
+
|
|
73
|
+
```scala
|
|
74
|
+
import zio.blocks.telemetry._
|
|
75
|
+
|
|
76
|
+
val provider = LoggerProvider.builder
|
|
77
|
+
.setResource(Resource.create(Attributes.of(Attributes.ServiceName, "catalog")))
|
|
78
|
+
.addLogRecordProcessor(LogRecordProcessor.noop)
|
|
79
|
+
.build()
|
|
80
|
+
|
|
81
|
+
log.install(provider.get("com.example.catalog"))
|
|
82
|
+
|
|
83
|
+
log.info("server started")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Lifecycle
|
|
87
|
+
|
|
88
|
+
Logs don't always leave your process the moment you write them. A processor that ships records to a backend usually batches them, so at any instant the most recent logs may still be sitting in a buffer — and if the process exits right then, they're gone. Those are exactly the logs you want when something went wrong at shutdown.
|
|
89
|
+
|
|
90
|
+
`provider.shutdown()` prevents that. It walks the registered processors in the order you added them and shuts each one down, which is a processor's cue to flush whatever it's holding and release what it owns — a file handle, a network connection. Call it once, on the way out:
|
|
91
|
+
|
|
92
|
+
```scala
|
|
93
|
+
import zio.blocks.telemetry._
|
|
94
|
+
|
|
95
|
+
val provider = LoggerProvider.builder
|
|
96
|
+
.addLogRecordProcessor(LogRecordProcessor.noop)
|
|
97
|
+
.build()
|
|
98
|
+
|
|
99
|
+
try log.install(provider.get("com.example"))
|
|
100
|
+
finally provider.shutdown() // last logs flushed, resources released
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
That `finally` is the easy part to forget. So a provider implements `AutoCloseable` — the standard interface for "something that must be closed when you're done with it," the same one files and sockets use — with `close()` simply calling `shutdown()`. That lets you hand the cleanup to the language rather than writing it yourself. In Scala, `scala.util.Using.resource` does the calling:
|
|
104
|
+
|
|
105
|
+
```scala
|
|
106
|
+
import zio.blocks.telemetry._
|
|
107
|
+
import scala.util.Using
|
|
108
|
+
|
|
109
|
+
Using.resource(LoggerProvider.builder.addLogRecordProcessor(LogRecordProcessor.noop).build()) { provider =>
|
|
110
|
+
log.install(provider.get("com.example"))
|
|
111
|
+
log.info("server started")
|
|
112
|
+
} // close() — and so shutdown() — runs here, even if the block throws
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The block marks how long the provider lives, and cleanup happens on the way out whether the code finishes normally or raises an exception. From Java the same interface makes a provider usable in try-with-resources.
|
|
116
|
+
|
|
117
|
+
## Trace–Log Correlation
|
|
118
|
+
|
|
119
|
+
This is the part you get for free. Write a log inside a span and it carries that span's trace and span IDs — no request id to thread through your functions, no logger to pass around, nothing to configure. A `LoggerProvider` and a [`TracerProvider`](../tracing/tracer-provider.md) read the same ambient context by default, so each is aware of the span the other opened:
|
|
120
|
+
|
|
121
|
+
```scala
|
|
122
|
+
import zio.blocks.telemetry._
|
|
123
|
+
|
|
124
|
+
trace.install(TracerProvider.builder.build())
|
|
125
|
+
log.install(
|
|
126
|
+
LoggerProvider.builder
|
|
127
|
+
.addLogRecordProcessor(new ConsoleLogRecordProcessor) // a provider with no processor emits nothing
|
|
128
|
+
.build()
|
|
129
|
+
.get("com.example")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
trace.span("checkout") { _ =>
|
|
133
|
+
log.info("order validated", "orderId" -> "ord-123") // stamped with the checkout span's IDs
|
|
134
|
+
log.warn("inventory low", "sku" -> "sku-42") // same trace, same span
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
That stamping is what makes a log backend useful: filter on one trace id and you have every log line from that single request, in order, across every component that touched it — and from any one of those lines you can open the trace it belongs to.
|
|
139
|
+
|
|
140
|
+
If you ever need to control that shared context yourself — isolating correlation between test cases, or fitting a custom runtime — pass one `ContextStorage` to `setContextStorage` on both providers.
|
|
141
|
+
|
|
142
|
+
For the full picture of how `LoggerProvider`, `TracerProvider`, and `MeterProvider` interconnect, see the [Telemetry module index](../index.md).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: logger
|
|
3
|
+
title: "Logger"
|
|
4
|
+
description: "Instance-level structured logger bound to one instrumentation scope; library code takes one, application code prefers the global log."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Structured Logging"
|
|
7
|
+
- "Trace Correlation"
|
|
8
|
+
- "Log Emission"
|
|
9
|
+
- "Logger"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A `Logger` writes logs. It's tied to one name — the component or library it belongs to, like `"com.example.OrderService"` — and a [`LoggerProvider`](./logger-provider.md) hands you one; you never construct it yourself.
|
|
13
|
+
|
|
14
|
+
That name travels with every record, marking which part of the system produced it. It's what lets you tell your own logs from a library's and turn one noisy component down without touching the rest. Records also arrive tied to the trace they happened in, so any line leads back to the request that caused it.
|
|
15
|
+
|
|
16
|
+
Most application code shouldn't use a `Logger` directly — the global [`log`](./index.md) is easier and does more. A `Logger` earns its place when a library accepts one as a parameter: then it's an ordinary dependency, and a test can pass in one whose records it inspects.
|
|
17
|
+
|
|
18
|
+
```scala
|
|
19
|
+
final class Logger {
|
|
20
|
+
// One method per severity: trace, debug, info, warn, error, fatal
|
|
21
|
+
def info(body: String, attrs: (String, AttributeValue)*): Unit
|
|
22
|
+
// def trace/debug/warn/error/fatal are identical, just with different severity
|
|
23
|
+
|
|
24
|
+
def emit(logRecord: LogRecord): Unit // send a pre-built record
|
|
25
|
+
def currentSpanContext(): Option[SpanContext] // active span, or None
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Emitting a Log
|
|
30
|
+
|
|
31
|
+
This is what a `Logger` is for. Pick the method matching the severity, pass a message, and add any attributes worth searching on later — the scope name and the active span's identity are attached for you:
|
|
32
|
+
|
|
33
|
+
```scala
|
|
34
|
+
import zio.blocks.telemetry._
|
|
35
|
+
|
|
36
|
+
val provider = LoggerProvider.builder
|
|
37
|
+
.addLogRecordProcessor(LogRecordProcessor.noop)
|
|
38
|
+
.build()
|
|
39
|
+
|
|
40
|
+
val logger = provider.get("com.example.OrderService", "1.0.0")
|
|
41
|
+
|
|
42
|
+
logger.info("order placed", "orderId" -> AttributeValue.StringValue("ord-001"))
|
|
43
|
+
logger.warn("payment delayed", "durationMs" -> AttributeValue.LongValue(250L))
|
|
44
|
+
logger.error("checkout failed", "code" -> AttributeValue.LongValue(503L))
|
|
45
|
+
|
|
46
|
+
provider.shutdown()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Attributes must be wrapped: `AttributeValue.StringValue`, `LongValue`, `DoubleValue`, `BooleanValue`, or one of their `*SeqValue` forms for arrays. That verbosity is the cost of a bare `Logger` — the global [`log`](./index.md) accepts the same values as plain literals.
|
|
50
|
+
|
|
51
|
+
## Forwarding a Record You Already Have
|
|
52
|
+
|
|
53
|
+
`emit(record)` sends a [`LogRecord`](./log-record.md) you built yourself, as-is. Where `info(...)` fills in the timestamp, scope name, and span identity, `emit` adds nothing, so a hand-built record without span identity won't correlate with any trace. Use it to forward another framework's logs into the same destinations:
|
|
54
|
+
|
|
55
|
+
```scala
|
|
56
|
+
import zio.blocks.telemetry._
|
|
57
|
+
|
|
58
|
+
val logger = LoggerProvider.builder
|
|
59
|
+
.addLogRecordProcessor(new ConsoleLogRecordProcessor) // a provider with no processor emits nothing
|
|
60
|
+
.build()
|
|
61
|
+
.get("com.example.Bridge")
|
|
62
|
+
|
|
63
|
+
logger.emit(
|
|
64
|
+
LogRecord.builder
|
|
65
|
+
.setSeverity(Severity.Warn)
|
|
66
|
+
.setBody("from the legacy logger")
|
|
67
|
+
.build
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Reading the Active Span
|
|
72
|
+
|
|
73
|
+
`currentSpanContext()` returns the span you're inside, or `None` if there isn't one. Correlation is automatic, so you only need this to carry trace IDs across a boundary yourself — an outgoing HTTP header, a queue message, an error response:
|
|
74
|
+
|
|
75
|
+
```scala
|
|
76
|
+
import zio.blocks.telemetry._
|
|
77
|
+
|
|
78
|
+
val logger = LoggerProvider.builder.build().get("com.example.Client")
|
|
79
|
+
|
|
80
|
+
val traceHeader: Option[String] = logger.currentSpanContext().map(_.traceIdHex)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
To configure where these logs go, see [`LoggerProvider`](./logger-provider.md).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: severity
|
|
3
|
+
title: "Severity"
|
|
4
|
+
description: "The 24-level log severity scale following the OpenTelemetry log data model: Trace, Debug, Info, Warn, Error, Fatal, four levels each."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Structured Logging"
|
|
7
|
+
- "Severity Levels"
|
|
8
|
+
- "Log Filtering"
|
|
9
|
+
- "Severity"
|
|
10
|
+
sidebar_label: "Severity"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`Severity` is the level attached to every [`LogRecord`](./log-record.md) — it says how important the message is and drives which records a severity filter keeps. It follows the OpenTelemetry log data model: 24 numeric levels grouped into six named categories with four gradations each, fine enough for detailed instrumentation yet compatible with the familiar five-level `TRACE`/`DEBUG`/`INFO`/`WARN`/`ERROR` scale. Every level shares its category's `text` (all four `Trace` levels report `"TRACE"`), so backends that expect the coarse names still work.
|
|
14
|
+
|
|
15
|
+
```scala
|
|
16
|
+
sealed trait Severity {
|
|
17
|
+
def number: Int // 1 to 24
|
|
18
|
+
def text: String // the category name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
object Severity {
|
|
22
|
+
case object Trace extends Severity // number = 1; then Trace2, Trace3, Trace4 (2-4)
|
|
23
|
+
case object Debug extends Severity // number = 5; then Debug2, Debug3, Debug4 (6-8)
|
|
24
|
+
case object Info extends Severity // number = 9; then Info2, Info3, Info4 (10-12)
|
|
25
|
+
case object Warn extends Severity // number = 13; then Warn2, Warn3, Warn4 (14-16)
|
|
26
|
+
case object Error extends Severity // number = 17; then Error2, Error3, Error4 (18-20)
|
|
27
|
+
case object Fatal extends Severity // number = 21; then Fatal2, Fatal3, Fatal4 (22-24)
|
|
28
|
+
|
|
29
|
+
def fromNumber(n: Int): Option[Severity] // Some when 1 <= n <= 24
|
|
30
|
+
def fromText(text: String): Option[Severity] // case-insensitive; the six category names
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The six category objects (`Trace`, `Debug`, `Info`, `Warn`, `Error`, `Fatal`) are what you name day to day; the numbered gradations exist for instrumentation that needs finer distinctions.
|
|
35
|
+
|
|
36
|
+
## Filtering by Severity
|
|
37
|
+
|
|
38
|
+
The main use of `Severity` is to set a floor below which records are dropped before any `LogRecord` is built. Set a global floor with `log.setMinSeverity`, or a per-namespace override that takes precedence for matching call sites:
|
|
39
|
+
|
|
40
|
+
```scala
|
|
41
|
+
import zio.blocks.telemetry._
|
|
42
|
+
|
|
43
|
+
log.setMinSeverity(Severity.Warn) // globally drop Trace, Debug, Info
|
|
44
|
+
log.setMinSeverity("com.example.db", Severity.Debug) // but keep Debug for the query layer
|
|
45
|
+
|
|
46
|
+
log.debug("suppressed by the global floor")
|
|
47
|
+
log.warn("emitted")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`fromText` and `fromNumber` parse a level from an external source — an environment variable or an OTLP protobuf field — returning `None` for anything out of range:
|
|
51
|
+
|
|
52
|
+
```scala
|
|
53
|
+
import zio.blocks.telemetry._
|
|
54
|
+
|
|
55
|
+
val level = Severity.fromText("ERROR").getOrElse(Severity.Info) // Severity.Error, number 17
|
|
56
|
+
log.setMinSeverity(level)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## See Also
|
|
60
|
+
|
|
61
|
+
- [LogRecord](./log-record.md) — the record whose level `Severity` sets.
|
|
62
|
+
- [Logging](./index.md) — the `log.*` emit methods and the full filtering API.
|