@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.
Files changed (164) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -583
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/index.md +9 -89
  37. package/reference/endpoint/path-codec.md +12 -24
  38. package/reference/endpoint/route-pattern.md +4 -6
  39. package/reference/endpoint/segment-codec.md +19 -32
  40. package/reference/html.md +313 -9
  41. package/reference/htmx/index.md +4 -52
  42. package/reference/htmx/response-headers.md +240 -0
  43. package/reference/http-model/headers.md +735 -0
  44. package/reference/http-model/index.md +3 -1
  45. package/reference/http-model/model.md +107 -71
  46. package/reference/http-model/schema-codecs.md +522 -0
  47. package/reference/http-model/schema.md +6 -3
  48. package/reference/http-model/server-sent-event.md +341 -0
  49. package/reference/jwt.md +195 -0
  50. package/reference/maybe.md +128 -11
  51. package/reference/media-type.md +2 -2
  52. package/reference/mux.md +254 -0
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/resource.md +2 -98
  57. package/reference/resource-management/scope.md +1 -209
  58. package/reference/resource-management/wire.md +4 -50
  59. package/reference/ringbuffer/advanced.mdx +1 -1
  60. package/reference/ringbuffer/index.mdx +3 -3
  61. package/reference/ringbuffer/mpmc.mdx +38 -4
  62. package/reference/ringbuffer/mpsc.mdx +36 -4
  63. package/reference/ringbuffer/spmc.mdx +1 -1
  64. package/reference/ringbuffer/spsc.mdx +87 -15
  65. package/reference/schema/allows.md +0 -96
  66. package/reference/schema/binding.md +2 -2
  67. package/reference/schema/built-in-codecs/avro.md +2 -2
  68. package/reference/schema/built-in-codecs/bson.md +50 -20
  69. package/reference/schema/built-in-codecs/csv.md +2 -2
  70. package/reference/schema/built-in-codecs/index.md +3 -3
  71. package/reference/schema/built-in-codecs/json/index.md +2 -2
  72. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  73. package/reference/schema/built-in-codecs/thrift.md +2 -2
  74. package/reference/schema/built-in-codecs/toon.md +3 -3
  75. package/reference/schema/built-in-codecs/yaml.md +2 -2
  76. package/reference/schema/codec.md +11 -11
  77. package/reference/schema/dynamic-optic.md +48 -3
  78. package/reference/schema/dynamic-schema.md +3 -3
  79. package/reference/schema/index.md +2 -0
  80. package/reference/schema/path-interpolator.md +2 -0
  81. package/reference/schema/reflect-transformer.md +140 -0
  82. package/reference/schema/schema-evolution/as.md +4 -4
  83. package/reference/schema/schema-evolution/into.md +2 -2
  84. package/reference/schema/schema-expr.md +2 -2
  85. package/reference/schema/schema-search.md +263 -0
  86. package/reference/schema/schema.md +10 -2
  87. package/reference/schema/type-class-derivation.md +1 -1
  88. package/reference/smithy.md +502 -3
  89. package/reference/sql/db-codec-deriver.md +3 -3
  90. package/reference/sql/db-codec.md +22 -22
  91. package/reference/sql/db-con.md +4 -4
  92. package/reference/sql/db-connection.md +1 -1
  93. package/reference/sql/db-param.md +1 -1
  94. package/reference/sql/db-result-reader.md +4 -2
  95. package/reference/sql/db-tx.md +46 -14
  96. package/reference/sql/ddl.md +1 -1
  97. package/reference/sql/frag.md +44 -10
  98. package/reference/sql/index.md +7 -7
  99. package/reference/sql/repo.md +15 -15
  100. package/reference/sql/sql-dialect.md +1 -1
  101. package/reference/sql/sql-logger.md +1 -1
  102. package/reference/sql/sql-name-mapper.md +3 -3
  103. package/reference/sql/table-metadata.md +3 -3
  104. package/reference/sql/table.md +10 -10
  105. package/reference/sql/transactor-zio.md +1 -1
  106. package/reference/sql/transactor.md +21 -11
  107. package/reference/sql-zio.md +1 -1
  108. package/reference/streams/core/index.md +32 -0
  109. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  110. package/reference/streams/{sink.md → core/sink.md} +331 -353
  111. package/reference/streams/{stream.md → core/stream.md} +919 -209
  112. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  113. package/reference/streams/execution-and-compatibility/index.md +35 -0
  114. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  115. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  116. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  117. package/reference/streams/index.md +140 -67
  118. package/reference/streams/primitives/index.md +30 -0
  119. package/reference/streams/primitives/reader.md +1992 -0
  120. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  121. package/reference/telemetry/common/any-value.md +90 -0
  122. package/reference/telemetry/common/attribute-key.md +87 -0
  123. package/reference/telemetry/common/attributes.md +118 -0
  124. package/reference/telemetry/common/index.md +39 -0
  125. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  126. package/reference/telemetry/common/resource.md +34 -0
  127. package/reference/telemetry/index.md +311 -0
  128. package/reference/telemetry/logging/index.md +197 -0
  129. package/reference/telemetry/logging/log-enrichment.md +72 -0
  130. package/reference/telemetry/logging/log-formatter.md +100 -0
  131. package/reference/telemetry/logging/log-record-processor.md +56 -0
  132. package/reference/telemetry/logging/log-record.md +44 -0
  133. package/reference/telemetry/logging/log-writer.md +64 -0
  134. package/reference/telemetry/logging/logger-provider.md +142 -0
  135. package/reference/telemetry/logging/logger.md +83 -0
  136. package/reference/telemetry/logging/severity.md +62 -0
  137. package/reference/telemetry/metrics/index.md +150 -0
  138. package/reference/telemetry/metrics/instruments.md +183 -0
  139. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  140. package/reference/telemetry/metrics/meter-provider.md +76 -0
  141. package/reference/telemetry/metrics/meter.md +98 -0
  142. package/reference/telemetry/metrics/metric-data.md +57 -0
  143. package/reference/telemetry/otel/custom-exporter.md +216 -0
  144. package/reference/telemetry/otel/index.md +212 -0
  145. package/reference/telemetry/tracing/index.md +155 -0
  146. package/reference/telemetry/tracing/sampler.md +89 -0
  147. package/reference/telemetry/tracing/span-builder.md +57 -0
  148. package/reference/telemetry/tracing/span-context.md +39 -0
  149. package/reference/telemetry/tracing/span-data.md +32 -0
  150. package/reference/telemetry/tracing/span-kind.md +55 -0
  151. package/reference/telemetry/tracing/span-processor.md +53 -0
  152. package/reference/telemetry/tracing/span-status.md +47 -0
  153. package/reference/telemetry/tracing/span.md +117 -0
  154. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  155. package/reference/telemetry/tracing/tracer.md +52 -0
  156. package/reference/typeid.md +0 -64
  157. package/sidebars.js +150 -12
  158. package/undocumented-report.md +528 -270
  159. package/reference/config.md +0 -158
  160. package/reference/streams/concurrent-operators.md +0 -106
  161. package/reference/streams/reader.md +0 -1284
  162. package/reference/streams/scala-2-compatibility.md +0 -55
  163. package/reference/streams/zero-boxing.md +0 -275
  164. 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.