@zio.dev/zio-blocks 0.0.51 → 0.0.56

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 (166) 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 -559
  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/endpoint.md +1 -0
  37. package/reference/endpoint/index.md +9 -89
  38. package/reference/endpoint/path-codec.md +12 -24
  39. package/reference/endpoint/route-pattern.md +4 -6
  40. package/reference/endpoint/segment-codec.md +19 -32
  41. package/reference/html.md +313 -9
  42. package/reference/htmx/index.md +4 -52
  43. package/reference/htmx/response-headers.md +240 -0
  44. package/reference/http-model/headers.md +735 -0
  45. package/reference/http-model/index.md +3 -1
  46. package/reference/http-model/model.md +107 -71
  47. package/reference/http-model/schema-codecs.md +522 -0
  48. package/reference/http-model/schema.md +6 -3
  49. package/reference/http-model/server-sent-event.md +341 -0
  50. package/reference/jwt.md +195 -0
  51. package/reference/maybe.md +128 -11
  52. package/reference/media-type.md +2 -2
  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/index.md +1 -1
  57. package/reference/resource-management/resource.md +2 -98
  58. package/reference/resource-management/scope.md +1 -209
  59. package/reference/resource-management/wire.md +4 -50
  60. package/reference/ringbuffer/advanced.mdx +1 -1
  61. package/reference/ringbuffer/index.mdx +3 -3
  62. package/reference/ringbuffer/mpmc.mdx +38 -4
  63. package/reference/ringbuffer/mpsc.mdx +36 -4
  64. package/reference/ringbuffer/spmc.mdx +1 -1
  65. package/reference/ringbuffer/spsc.mdx +87 -15
  66. package/reference/schema/allows.md +0 -96
  67. package/reference/schema/binding.md +2 -2
  68. package/reference/schema/built-in-codecs/avro.md +2 -2
  69. package/reference/schema/built-in-codecs/bson.md +50 -20
  70. package/reference/schema/built-in-codecs/csv.md +2 -2
  71. package/reference/schema/built-in-codecs/index.md +3 -3
  72. package/reference/schema/built-in-codecs/json/index.md +2 -2
  73. package/reference/schema/built-in-codecs/json/json.md +1 -0
  74. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  75. package/reference/schema/built-in-codecs/thrift.md +2 -2
  76. package/reference/schema/built-in-codecs/toon.md +3 -3
  77. package/reference/schema/built-in-codecs/yaml.md +2 -2
  78. package/reference/schema/codec.md +11 -11
  79. package/reference/schema/dynamic-optic.md +48 -3
  80. package/reference/schema/dynamic-schema.md +3 -3
  81. package/reference/schema/index.md +2 -0
  82. package/reference/schema/path-interpolator.md +2 -0
  83. package/reference/schema/reflect-transformer.md +140 -0
  84. package/reference/schema/schema-evolution/as.md +4 -4
  85. package/reference/schema/schema-evolution/into.md +2 -2
  86. package/reference/schema/schema-expr.md +2 -2
  87. package/reference/schema/schema-search.md +263 -0
  88. package/reference/schema/schema.md +10 -2
  89. package/reference/schema/type-class-derivation.md +1 -1
  90. package/reference/smithy.md +502 -3
  91. package/reference/sql/db-codec-deriver.md +3 -3
  92. package/reference/sql/db-codec.md +22 -22
  93. package/reference/sql/db-con.md +4 -4
  94. package/reference/sql/db-connection.md +1 -1
  95. package/reference/sql/db-param.md +1 -1
  96. package/reference/sql/db-result-reader.md +4 -2
  97. package/reference/sql/db-tx.md +46 -14
  98. package/reference/sql/ddl.md +1 -1
  99. package/reference/sql/frag.md +44 -10
  100. package/reference/sql/index.md +7 -7
  101. package/reference/sql/repo.md +15 -15
  102. package/reference/sql/sql-dialect.md +1 -1
  103. package/reference/sql/sql-logger.md +1 -1
  104. package/reference/sql/sql-name-mapper.md +3 -3
  105. package/reference/sql/table-metadata.md +3 -3
  106. package/reference/sql/table.md +10 -10
  107. package/reference/sql/transactor-zio.md +1 -1
  108. package/reference/sql/transactor.md +21 -11
  109. package/reference/sql-zio.md +2 -2
  110. package/reference/streams/core/index.md +32 -0
  111. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  112. package/reference/streams/{sink.md → core/sink.md} +331 -353
  113. package/reference/streams/{stream.md → core/stream.md} +919 -209
  114. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  115. package/reference/streams/execution-and-compatibility/index.md +35 -0
  116. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  117. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  118. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  119. package/reference/streams/index.md +140 -67
  120. package/reference/streams/primitives/index.md +30 -0
  121. package/reference/streams/primitives/reader.md +1992 -0
  122. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  123. package/reference/telemetry/common/any-value.md +90 -0
  124. package/reference/telemetry/common/attribute-key.md +87 -0
  125. package/reference/telemetry/common/attributes.md +118 -0
  126. package/reference/telemetry/common/index.md +39 -0
  127. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  128. package/reference/telemetry/common/resource.md +34 -0
  129. package/reference/telemetry/index.md +311 -0
  130. package/reference/telemetry/logging/index.md +197 -0
  131. package/reference/telemetry/logging/log-enrichment.md +72 -0
  132. package/reference/telemetry/logging/log-formatter.md +100 -0
  133. package/reference/telemetry/logging/log-record-processor.md +56 -0
  134. package/reference/telemetry/logging/log-record.md +44 -0
  135. package/reference/telemetry/logging/log-writer.md +64 -0
  136. package/reference/telemetry/logging/logger-provider.md +142 -0
  137. package/reference/telemetry/logging/logger.md +83 -0
  138. package/reference/telemetry/logging/severity.md +62 -0
  139. package/reference/telemetry/metrics/index.md +150 -0
  140. package/reference/telemetry/metrics/instruments.md +183 -0
  141. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  142. package/reference/telemetry/metrics/meter-provider.md +76 -0
  143. package/reference/telemetry/metrics/meter.md +98 -0
  144. package/reference/telemetry/metrics/metric-data.md +57 -0
  145. package/reference/telemetry/otel/custom-exporter.md +216 -0
  146. package/reference/telemetry/otel/index.md +212 -0
  147. package/reference/telemetry/tracing/index.md +155 -0
  148. package/reference/telemetry/tracing/sampler.md +89 -0
  149. package/reference/telemetry/tracing/span-builder.md +57 -0
  150. package/reference/telemetry/tracing/span-context.md +39 -0
  151. package/reference/telemetry/tracing/span-data.md +32 -0
  152. package/reference/telemetry/tracing/span-kind.md +55 -0
  153. package/reference/telemetry/tracing/span-processor.md +53 -0
  154. package/reference/telemetry/tracing/span-status.md +47 -0
  155. package/reference/telemetry/tracing/span.md +117 -0
  156. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  157. package/reference/telemetry/tracing/tracer.md +52 -0
  158. package/reference/typeid.md +0 -64
  159. package/sidebars.js +365 -185
  160. package/undocumented-report.md +528 -270
  161. package/reference/config.md +0 -158
  162. package/reference/streams/concurrent-operators.md +0 -106
  163. package/reference/streams/reader.md +0 -1284
  164. package/reference/streams/scala-2-compatibility.md +0 -55
  165. package/reference/streams/zero-boxing.md +0 -275
  166. package/reference/telemetry.md +0 -693
@@ -5,16 +5,22 @@ title: "Telemetry: Architecture, Patterns, and Real-World Usage"
5
5
 
6
6
  `zio-blocks-telemetry` is an effect-free, zero-allocation observability library that gives you structured logging, distributed tracing, and metrics without pulling in the OpenTelemetry SDK or any effect system. This guide explains why it works the way it does, shows what happens inside each operation, and walks through patterns for production systems.
7
7
 
8
- If you're looking for API signatures and quick copy-paste snippets, the [Telemetry Reference](../reference/telemetry.md) has those. This guide focuses on the "how" and "why": architecture, design trade-offs, and the kind of understanding that helps when things go wrong at 3 AM.
8
+ If you're looking for API signatures and quick copy-paste snippets, the [Telemetry Reference](../reference/telemetry/index.md) has those. This guide focuses on the "how" and "why": architecture, design trade-offs, and the kind of understanding that helps when things go wrong at 3 AM.
9
9
 
10
10
  ## Installation
11
11
 
12
12
  ```scala
13
- // Core: logging, tracing, metrics (JVM + JS)
14
- libraryDependencies += "dev.zio" %% "zio-blocks-telemetry" % "0.0.51"
13
+ // Core: logging, tracing, metrics
14
+ libraryDependencies += "dev.zio" %% "zio-blocks-telemetry" % "0.0.56"
15
15
 
16
16
  // OTLP JSON export over HTTP (JVM only)
17
- libraryDependencies += "dev.zio" %% "zio-blocks-telemetry-otel" % "0.0.51"
17
+ libraryDependencies += "dev.zio" %% "zio-blocks-telemetry-otel" % "0.0.56"
18
+ ```
19
+
20
+ The core module also cross-builds for Scala.js:
21
+
22
+ ```scala
23
+ libraryDependencies += "dev.zio" %%% "zio-blocks-telemetry" % "0.0.56"
18
24
  ```
19
25
 
20
26
  ---
@@ -73,10 +79,10 @@ Two separate things must both be cheap: the fast path when a level is disabled,
73
79
  **Disabled level fast path.** The first check in every `log.*` call is:
74
80
 
75
81
  ```scala
76
- if (severity.number >= log.globalMinLevel) { ... }
82
+ if (severity.number >= GlobalLogState.globalMinLevel) { ... }
77
83
  ```
78
84
 
79
- `globalMinLevel` is a `@volatile Int`. Reading it costs roughly one memory fence. If the level is disabled, the compiler has already eliminated all argument evaluation (the arguments are passed by-name or inlined via macro). No string is built, no object is allocated.
85
+ `globalMinLevel` is a `@volatile Int` held inside the library-private `GlobalLogState` (you set it through `log.install`, `log.setMinSeverity`, and `log.withMinSeverity` rather than reading it directly). Reading it costs roughly one memory fence. If the level is disabled, the compiler has already eliminated all argument evaluation (the arguments are passed by-name or inlined via macro). No string is built, no object is allocated.
80
86
 
81
87
  **Enabled level slow path.** When a level is enabled, attribute pairs need to be assembled into a `LogRecord`. The library uses a pooled `AttributeBuilder` (one per thread, reused), backed by parallel primitive arrays. Primitive values (Long, Double, Boolean) are stored as unboxed longs using type discriminator bytes. A `(String, Long)` attribute doesn't box the Long.
82
88
 
@@ -127,29 +133,40 @@ log.info("user signed in", "user_id" -> "u-7890", "method" -> "oauth2")
127
133
  // Numbers are unboxed on the fast path
128
134
  log.info("payment processed", "amount_cents" -> 4999L, "currency" -> "EUR")
129
135
 
130
- // Exceptions get type, message, and stacktrace automatically
131
- log.error("payment failed", new IllegalStateException("card declined"))
136
+ // Exceptions get type, message, and stacktrace automatically.
137
+ // Bind to a Throwable-typed val; an inline `new SomeException(...)` does not resolve.
138
+ val declined: Throwable = new IllegalStateException("card declined")
139
+ log.error("payment failed", declined)
132
140
 
133
141
  // Mix freely
142
+ val timeout: Throwable = new RuntimeException("timeout")
134
143
  log.warn(
135
144
  "upstream degraded",
136
145
  "service" -> "inventory",
137
146
  "latency_ms" -> 850L,
138
- new RuntimeException("timeout")
147
+ timeout
139
148
  )
140
149
  ```
141
150
 
142
- Adding a file output at startup:
151
+ Adding a file output at startup. The module ships stdout, stderr, and no-op writers; a file destination is a few lines of your own:
143
152
 
144
153
  ```scala
145
154
  import zio.blocks.telemetry._
155
+ import java.io.PrintWriter
156
+
157
+ final class AppFileWriter(path: String) extends LogWriter {
158
+ private val out = new PrintWriter(path)
159
+ def write(content: CharSequence): Unit = out.println(content)
160
+ override def flush(): Unit = out.flush()
161
+ override def close(): Unit = out.close()
162
+ }
146
163
 
147
164
  // Human-readable text to one file, OTLP JSON to another
148
- log.writer(TextLogFormatter, FileLogWriter("logs/app.log"))
149
- log.writer(JsonLogFormatter, FileLogWriter("logs/app.jsonl"))
165
+ log.writer(TextLogFormatter, new AppFileWriter("logs/app.log"))
166
+ log.writer(JsonLogFormatter, new AppFileWriter("logs/app.jsonl"))
150
167
  ```
151
168
 
152
- Both outputs are active simultaneously. `writer()` is additive.
169
+ Both outputs are active simultaneously, alongside the default console output — `writer()` is additive.
153
170
 
154
171
  ### Macro mechanics explained
155
172
 
@@ -160,7 +177,7 @@ Conceptual expansion of:
160
177
  log.info("request received", "user_id" -> userId, "attempt" -> 3)
161
178
 
162
179
  Expands to (pseudocode):
163
- if (Severity.Info.number >= log.globalMinLevel) {
180
+ if (Severity.Info.number >= GlobalLogState.globalMinLevel) {
164
181
  val state = log.getState()
165
182
  if (state != null && Severity.Info.number >= state.effectiveLevel(<current class name>)) {
166
183
  val now = EpochClock.epochNanos()
@@ -186,28 +203,28 @@ Expands to (pseudocode):
186
203
  The important things the macro does:
187
204
  - Captures `code.filepath`, `code.namespace`, `code.function`, `code.lineno` at compile time
188
205
  - Resolves each enrichment argument type at compile time and calls the specific builder method (no runtime dispatch on the fast path)
189
- - For rate-limited variants (`infoEvery`, `warnAtMost`), synthesizes a `val counter` or `val lastEmit` at the call site so each call site has independent state
206
+ - For rate-limited variants (`infoEvery`, `warnAtMost`), hashes the source position into a `siteId` constant so each call site gets its own rate-limit slot
190
207
 
191
- For `*Every(N, msg)`, the macro synthesizes:
208
+ For `*Every(N, msg)`, the macro emits a guard against a shared global table:
192
209
 
193
210
  ```scala
194
- // Synthesized at call site by the macro
195
- val _counter_42 = new java.util.concurrent.atomic.AtomicLong(0L)
211
+ // siteId is a compile-time constant: (sourcePath + ":" + line).hashCode
212
+ if (LogRateLimit.shouldLogEvery(siteId, N)) { log.emit(...) }
196
213
 
197
- // At each call:
198
- if (_counter_42.getAndIncrement() % N == 0) { log.emit(...) }
214
+ // LogRateLimit keeps one AtomicLongArray of 4096 counters:
215
+ // counters.incrementAndGet(siteId & 4095) % N == 0
199
216
  ```
200
217
 
201
- For `*AtMost(windowMs, msg)`, it's:
218
+ For `*AtMost(windowMs, msg)`, it's the same shape against a parallel timestamp array:
202
219
 
203
220
  ```scala
204
- val _lastEmit_77 = new java.util.concurrent.atomic.AtomicLong(0L)
221
+ if (LogRateLimit.shouldLogAtMost(siteId, windowMs)) { log.emit(...) }
205
222
 
206
- val now = System.currentTimeMillis()
207
- val last = _lastEmit_77.get()
208
- if (now - last >= windowMs && _lastEmit_77.compareAndSet(last, now)) { log.emit(...) }
223
+ // timestamps.get(idx); if (now - last >= windowMs) timestamps.compareAndSet(idx, last, now)
209
224
  ```
210
225
 
226
+ Two consequences of the shared table: `*Every(N, …)` emits on the *Nth* call rather than the first (the counter is incremented before the modulo), and two call sites whose `siteId` collides modulo 4096 share one slot.
227
+
211
228
  ### Custom enrichment types
212
229
 
213
230
  The `LogEnrichment[A]` typeclass lets the macro delegate argument resolution to your code.
@@ -261,10 +278,18 @@ Every call to `log.writer(formatter, writer)` adds another output. The state is
261
278
 
262
279
  ```scala
263
280
  import zio.blocks.telemetry._
281
+ import java.io.PrintWriter
282
+
283
+ final class AppFileWriter(path: String) extends LogWriter {
284
+ private val out = new PrintWriter(path)
285
+ def write(content: CharSequence): Unit = out.println(content)
286
+ override def flush(): Unit = out.flush()
287
+ override def close(): Unit = out.close()
288
+ }
264
289
 
265
290
  // Setup at startup; all three are active simultaneously
266
- log.writer(TextLogFormatter, FileLogWriter("logs/app.log"))
267
- log.writer(JsonLogFormatter, FileLogWriter("logs/app.jsonl"))
291
+ log.writer(TextLogFormatter, new AppFileWriter("logs/app.log"))
292
+ log.writer(JsonLogFormatter, new AppFileWriter("logs/app.jsonl"))
268
293
  // The OTEL exporter is a LogRecordProcessor, added via LoggerProvider (see Part 5)
269
294
  ```
270
295
 
@@ -273,6 +298,8 @@ Internally, `log.writer(...)` creates a `FormattedLogRecordProcessor` (which pai
273
298
  To remove all file writers:
274
299
 
275
300
  ```scala
301
+ import zio.blocks.telemetry._
302
+
276
303
  log.clearWriters()
277
304
  ```
278
305
 
@@ -283,6 +310,8 @@ This shuts down each `FormattedLogRecordProcessor` cleanly and reverts to proces
283
310
  The same call:
284
311
 
285
312
  ```scala
313
+ import zio.blocks.telemetry._
314
+
286
315
  log.warn("slow query", "table" -> "orders", "duration_ms" -> 450L)
287
316
  ```
288
317
 
@@ -293,7 +322,7 @@ log.warn("slow query", "table" -> "orders", "duration_ms" -> 450L)
293
322
 
294
323
  **JsonLogFormatter output:**
295
324
  ```json
296
- {"timeUnixNano":"1747216200123000000","severityNumber":13,"severityText":"WARN","body":{"stringValue":"slow query"},"attributes":[{"key":"code.filepath","value":{"stringValue":"PaymentService.scala"}},{"key":"code.namespace","value":{"stringValue":"com.example.PaymentService"}},{"key":"code.function","value":{"stringValue":"handleRequest"}},{"key":"code.lineno","value":{"intValue":"87"}},{"key":"table","value":{"stringValue":"orders"}},{"key":"duration_ms","value":{"intValue":"450"}}]}
325
+ {"timeUnixNano":"1747216200123000000","severityNumber":13,"severityText":"WARN","body":{"stringValue":"slow query"},"attributes":[{"key":"code.filepath","value":{"stringValue":"src/main/scala/com/example/PaymentService.scala"}},{"key":"code.namespace","value":{"stringValue":"com.example.PaymentService"}},{"key":"code.function","value":{"stringValue":"handleRequest"}},{"key":"code.lineno","value":{"intValue":"87"}},{"key":"table","value":{"stringValue":"orders"}},{"key":"duration_ms","value":{"intValue":"450"}}]}
297
326
  ```
298
327
 
299
328
  The JSON format follows the OTLP log data model directly, making it compatible with any OTLP-aware log processor (Grafana Loki, OpenTelemetry Collector, etc.).
@@ -339,7 +368,9 @@ assert(warns.exists(_.body.value.contains("payment")))
339
368
  Don't forget to restore state between tests:
340
369
 
341
370
  ```scala
342
- log.uninstall() // reverts to default console logger
371
+ import zio.blocks.telemetry._
372
+
373
+ log.removeAll() // drops every writer and processor; log.* becomes a no-op
343
374
  ```
344
375
 
345
376
  ---
@@ -354,7 +385,7 @@ When you call `tracer.span("name") { span => ... }`, here's the complete sequenc
354
385
 
355
386
  2. **Sampler decision.** `sampler.shouldSample(...)` is called with the parent context, new trace ID bits, span name, and kind. The result is one of `Drop`, `RecordOnly`, or `RecordAndSample`.
356
387
 
357
- 3. **Drop path.** If `Drop`, your block runs with `Span.NoOp`. All `setAttribute`, `addEvent`, `setStatus` calls are no-ops. Zero allocation.
388
+ 3. **Drop path.** If `Drop`, your block runs with `Span.NoOp`. All `setAttribute`, `addEvent`, `setStatus` calls are no-ops, and no processor is notified. A dropped span is still cheap but not free: a fresh `SpanId` and `SpanContext` are allocated and scoped so that nested spans and log records stay on the same trace.
358
389
 
359
390
  4. **Record path.** A `RecordingSpan` is built with a new `SpanContext`. The span gets:
360
391
  - The parent's `traceIdHi`/`traceIdLo` if a valid parent exists, or a fresh random pair
@@ -387,7 +418,7 @@ tracer.span("checkout") { span =>
387
418
  |
388
419
  +-- BatchProcessor.enqueue(spanData)
389
420
  |
390
- (background flush every 5s or at maxBatchSize)
421
+ (background flush every 5s, draining maxBatchSize per request)
391
422
  |
392
423
  +-- HTTP POST /v1/traces to OTLP collector
393
424
  ```
@@ -409,7 +440,7 @@ import zio.blocks.telemetry._
409
440
 
410
441
  val tracerProvider = TracerProvider.builder
411
442
  .setSampler(ParentBasedSampler(root = AlwaysOnSampler))
412
- .addSpanProcessor(yourExporter)
443
+ .addSpanProcessor(SpanProcessor.noop) // your exporter here
413
444
  .build()
414
445
 
415
446
  trace.install(tracerProvider)
@@ -430,9 +461,9 @@ final case class SpanContext(
430
461
 
431
462
  The 128-bit trace ID is stored as two `Long` fields rather than a `UUID` object. A `UUID` would be 32 bytes of heap allocation per span, plus GC pressure. Two `Long` fields on a case class cost nothing beyond the case class itself, and they're JIT-friendly (no dereference).
432
463
 
433
- A fresh trace ID is generated by `TraceId.random()`, which calls `ThreadLocalRandom.current().nextLong()` twice. `ThreadLocalRandom` is faster than `Random` in concurrent scenarios because it doesn't share state between threads.
464
+ A fresh trace ID is generated by `TraceId.random()`, which draws two `Long`s from `scala.util.Random`, retrying in the vanishingly unlikely case that both come out zero (an all-zero trace ID is invalid by the spec).
434
465
 
435
- `SpanId` is an `AnyVal` wrapping a single `Long`, similarly generated by `ThreadLocalRandom.current().nextLong()`. At runtime on the JVM, `AnyVal`s are unboxed wherever possible, so a `SpanId` in a `SpanContext` field costs nothing extra.
466
+ `SpanId` is an `AnyVal` wrapping a single `Long`, generated the same way and likewise retried if it comes out zero. At runtime on the JVM, `AnyVal`s are unboxed wherever possible, so a `SpanId` in a `SpanContext` field costs nothing extra.
436
467
 
437
468
  `traceIdHex` is computed on demand by `TraceId.toHex(traceIdHi, traceIdLo)`, which formats the two longs into 32 hex characters. No caching: hex formatting is only needed when building headers or log output.
438
469
 
@@ -443,9 +474,8 @@ Here's a complete round trip. The server receives an HTTP request with W3C `trac
443
474
  **HTTP server (receiving a trace):**
444
475
 
445
476
  ```scala
446
- package zio.blocks.telemetry.otel
447
-
448
477
  import zio.blocks.telemetry._
478
+ import zio.blocks.telemetry.otel._
449
479
 
450
480
  // Simulate incoming HTTP headers
451
481
  val incomingHeaders = Map(
@@ -459,19 +489,22 @@ val parentCtx: Option[SpanContext] =
459
489
  val tracer = trace.get("api-service")
460
490
 
461
491
  // Wire the extracted context as the parent by scoping it in ContextStorage
492
+ // A provider's own contextStorage is internal, so create one and pass it in
493
+ val contextStorage = ContextStorage.create[Option[SpanContext]](None)
494
+
462
495
  val tracerProvider = TracerProvider.builder
463
- .addSpanProcessor(yourExporter)
496
+ .addSpanProcessor(SpanProcessor.noop) // your exporter here
497
+ .setContextStorage(contextStorage)
464
498
  .build()
465
499
 
466
- // Use the provider's context storage to set the remote parent
467
- val contextStorage = tracerProvider.contextStorage
500
+ trace.install(tracerProvider)
468
501
 
469
502
  contextStorage.scoped(parentCtx) {
470
503
  tracer.span("handle-checkout", SpanKind.Server) { span =>
471
504
  span.setAttribute("http.method", "POST")
472
505
  span.setAttribute("http.route", "/checkout")
473
506
  // Your handler logic here
474
- processCheckout()
507
+ () // your handler logic here
475
508
  }
476
509
  }
477
510
  ```
@@ -479,9 +512,8 @@ contextStorage.scoped(parentCtx) {
479
512
  **HTTP client (injecting trace into outbound call):**
480
513
 
481
514
  ```scala
482
- package zio.blocks.telemetry.otel
483
-
484
515
  import zio.blocks.telemetry._
516
+ import zio.blocks.telemetry.otel._
485
517
 
486
518
  val tracer = trace.get("api-service")
487
519
 
@@ -494,7 +526,7 @@ tracer.span("call-inventory", SpanKind.Client) { span =>
494
526
  )
495
527
 
496
528
  // headers now contains: "traceparent" -> "00-<traceId>-<spanId>-01"
497
- httpClient.post("http://inventory-service/check", headers)
529
+ headers // hand these to your HTTP client
498
530
  }
499
531
  ```
500
532
 
@@ -541,7 +573,8 @@ When you log inside an active span, the logger automatically attaches `traceId`
541
573
  At provider creation time:
542
574
 
543
575
  ```scala
544
- // TracerProvider.builder.build() creates a ContextStorage
576
+ // TracerProvider.builder.build() falls back to ContextStorage.defaultSpanContextStorage
577
+ // unless you pass one; here we create an explicit instance to share
545
578
  val cs = ContextStorage.create[Option[SpanContext]](None)
546
579
  val tracerProvider = new TracerProvider(resource, sampler, processors, cs)
547
580
 
@@ -551,9 +584,9 @@ val loggerProvider = LoggerProvider.builder
551
584
  .build()
552
585
  ```
553
586
 
554
- When `tracer.span(...)` runs, it calls `contextStorage.scoped(Some(span.spanContext)) { ... }`. This scopes the span context for the duration of the block using `ScopedValue` (on JDK 25+) or `ThreadLocal`. Inside that block, `logger.currentSpanContext()` reads from the same storage and sees the active span. No explicit linkage code at the call site.
587
+ When `tracer.span(...)` runs, it calls `contextStorage.scoped(Some(span.spanContext)) { ... }`. This scopes the span context for the duration of the block using `ScopedValue` on the JVM. On Scala.js there are no threads, so the storage is a plain mutable variable saved and restored around the block. Inside that block, `logger.currentSpanContext()` reads from the same storage and sees the active span. No explicit linkage code at the call site.
555
588
 
556
- If you use the global singletons without explicitly sharing a `ContextStorage`, the default logger gets its own context storage, separate from the default tracer's. Span-log correlation only works automatically when you wire them to share one. The OTEL module's production setup example (Part 5) shows how to do this correctly.
589
+ This works out of the box with the global singletons: a provider built without an explicit `ContextStorage` falls back to the same `ContextStorage.defaultSpanContextStorage` singleton, so the default tracer and the default logger already read and write the same storage. You only need to pass one explicitly when you want the opposite — isolating correlation between test cases, or fitting a custom runtime.
557
590
 
558
591
  ---
559
592
 
@@ -648,15 +681,19 @@ requests.add(7, "method" -> "POST", "status" -> "201")
648
681
 
649
682
  val data = requests.collect()
650
683
  // data: MetricData.SumData(List(
651
- // SumDataPoint(Attributes{method="GET",status="200"}, startNanos, nowNanos, 42),
652
- // SumDataPoint(Attributes{method="POST",status="201"}, startNanos, nowNanos, 7)
684
+ // SumDataPoint(Attributes{method="GET",status="200"}, 0L, nowNanos, 42),
685
+ // SumDataPoint(Attributes{method="POST",status="201"}, 0L, nowNanos, 7)
653
686
  // ))
687
+ // startTimeNanos is always 0 — the instruments do not track a start time.
654
688
  ```
655
689
 
656
690
  Each `SumDataPoint` contains the full attribute set plus the accumulated value. The OTLP exporter's `collectFn` calls `.collect()` on each instrument you register:
657
691
 
658
692
  ```scala
659
- package zio.blocks.telemetry.otel
693
+ import zio.blocks.telemetry._
694
+ import zio.blocks.telemetry.otel._
695
+
696
+
660
697
 
661
698
  val metricExporter = new OtlpJsonMetricExporter(
662
699
  config, resource, scope, sender,
@@ -668,7 +705,7 @@ val metricExporter = new OtlpJsonMetricExporter(
668
705
  )
669
706
  ```
670
707
 
671
- The `collectFn` is called by the `BatchProcessor`'s flush task on the configured interval.
708
+ Unlike the trace and log exporters, the metric exporter has no `BatchProcessor` and no background thread: `collectFn` runs only when you ask for a snapshot, via `exportMetrics()` or `collectAllMetrics()`. Schedule those calls yourself — for example on the same `PlatformExecutor` you use for the other signals.
672
709
 
673
710
  ### Thread safety internals
674
711
 
@@ -692,7 +729,9 @@ The `collectFn` is called by the `BatchProcessor`'s flush task on the configured
692
729
  The OTEL exporters live in `package zio.blocks.telemetry.otel` and are `private[otel]`. Your bootstrap code must be in the same package (or a class in that package):
693
730
 
694
731
  ```scala
695
- package zio.blocks.telemetry.otel
732
+ import zio.blocks.telemetry.otel._
733
+
734
+
696
735
 
697
736
  import zio.blocks.telemetry._
698
737
  import java.util.concurrent.TimeUnit
@@ -747,8 +786,8 @@ object Telemetry {
747
786
 
748
787
  log.install(loggerProvider.get("my-service"), minSeverity = Severity.Info)
749
788
 
750
- // Optional: also write to a local file as backup
751
- log.writer(JsonLogFormatter, FileLogWriter("logs/app.jsonl"))
789
+ // Optional: also emit JSON on stderr (a file needs your own LogWriter, see Part 2)
790
+ log.writer(JsonLogFormatter, StderrWriter)
752
791
 
753
792
  // --- Metrics ---
754
793
  val meterProvider = MeterProvider.builder
@@ -816,6 +855,9 @@ The default values work for most services. Here's when to change them:
816
855
 
817
856
  **High-throughput services (>1000 RPS):**
818
857
  ```scala
858
+ import zio.blocks.telemetry._
859
+ import zio.blocks.telemetry.otel._
860
+
819
861
  ExporterConfig(
820
862
  maxQueueSize = 8192, // larger buffer for spikes
821
863
  maxBatchSize = 1024, // send bigger payloads less often
@@ -825,6 +867,9 @@ ExporterConfig(
825
867
 
826
868
  **Low-latency services (real-time data required):**
827
869
  ```scala
870
+ import zio.blocks.telemetry._
871
+ import zio.blocks.telemetry.otel._
872
+
828
873
  ExporterConfig(
829
874
  maxQueueSize = 1024,
830
875
  maxBatchSize = 128, // smaller batches = lower end-to-end latency
@@ -834,6 +879,9 @@ ExporterConfig(
834
879
 
835
880
  **Development / debugging:**
836
881
  ```scala
882
+ import zio.blocks.telemetry._
883
+ import zio.blocks.telemetry.otel._
884
+
837
885
  ExporterConfig(
838
886
  maxQueueSize = 256,
839
887
  maxBatchSize = 32,
@@ -847,15 +895,16 @@ The `maxQueueSize` and `maxBatchSize` interact: if records arrive faster than yo
847
895
 
848
896
  ### BatchProcessor behavior
849
897
 
850
- `BatchProcessor` runs inside the `PlatformExecutor`, which uses a virtual-thread-per-task executor for export tasks. This means:
898
+ `BatchProcessor` schedules its flush on the `ScheduledExecutorService` it is handed — `PlatformExecutor.create()` supplies a single-threaded scheduled pool whose threads are virtual (`Executors.newScheduledThreadPool(1, Thread.ofVirtual()…factory())`). This means:
851
899
 
852
900
  - The scheduled flush (every `flushIntervalMillis`) runs on a virtual thread
853
- - Export HTTP calls and their retries run on virtual threads, so sleeping during retry backoff doesn't pin a platform thread
901
+ - Export HTTP calls and their retry sleeps run on that same virtual thread, so backoff doesn't pin a platform thread — but the pool has one thread, so a retrying export delays every other flush sharing the executor. Give a high-volume signal its own `PlatformExecutor` if that matters.
902
+ - A full batch does not trigger a send: `maxBatchSize` only chunks what a flush drains. Data leaves on the interval, on `forceFlush()`, or on `shutdown()`.
854
903
  - Queue overflow drops the oldest item, prints to stderr, and continues
855
904
 
856
- The retry schedule uses exponential backoff: attempt 0 waits 1s, attempt 1 waits 2s, attempt 2 waits 4s... up to 30s maximum, for `maxRetries` (default 5) total attempts. Non-retryable failures (e.g., 400 Bad Request from the collector) are dropped immediately.
905
+ The retry schedule uses exponential backoff: attempt 0 waits 1s, attempt 1 waits 2s, attempt 2 waits 4s... up to 30s maximum. `maxRetries` (default 5) counts retries, so a permanently failing batch is attempted 6 times before it is dropped. Non-retryable failures (e.g., 400 Bad Request from the collector) are dropped immediately.
857
906
 
858
- On `shutdown()`, `BatchProcessor` cancels the periodic flush, does a final synchronous flush of all pending items, then shuts down the export executor. Retries still happen during shutdown.
907
+ On `shutdown()`, `BatchProcessor` cancels the periodic flush and does a final synchronous flush of all pending items. Retries are skipped once shutdown has started — a failing batch is dropped with a message on stderr rather than delaying exit. The executor is not owned by the processor; shut down the `PlatformExecutor` yourself, after the providers.
859
908
 
860
909
  ### Graceful shutdown order
861
910
 
@@ -875,6 +924,9 @@ Don't shut down the executor before the providers; the batch processor's retry t
875
924
  The standard OTLP environment variables map naturally:
876
925
 
877
926
  ```scala
927
+ import zio.blocks.telemetry._
928
+ import zio.blocks.telemetry.otel._
929
+
878
930
  val config = ExporterConfig(
879
931
  endpoint = sys.env.getOrElse("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"),
880
932
  headers = sys.env.get("OTEL_EXPORTER_OTLP_HEADERS")
@@ -898,6 +950,8 @@ def parseOtlpHeaders(raw: String): Map[String, String] =
898
950
  For minimum log level:
899
951
 
900
952
  ```scala
953
+ import zio.blocks.telemetry._
954
+
901
955
  val minLevel = sys.env.get("LOG_LEVEL").flatMap {
902
956
  case "TRACE" => Some(Severity.Trace)
903
957
  case "DEBUG" => Some(Severity.Debug)
@@ -907,7 +961,7 @@ val minLevel = sys.env.get("LOG_LEVEL").flatMap {
907
961
  case _ => None
908
962
  }.getOrElse(Severity.Info)
909
963
 
910
- log.install(logger, minSeverity = minLevel)
964
+ log.install(LoggerProvider.builder.build().get("my-service"), minSeverity = minLevel)
911
965
  ```
912
966
 
913
967
  ---
@@ -919,9 +973,8 @@ log.install(logger, minSeverity = minLevel)
919
973
  A typical middleware pattern extracts incoming trace context, starts a server span, and injects context into outbound calls. Here's the conceptual shape (adapting to your specific HTTP library):
920
974
 
921
975
  ```scala
922
- package zio.blocks.telemetry.otel
923
-
924
976
  import zio.blocks.telemetry._
977
+ import zio.blocks.telemetry.otel._
925
978
 
926
979
  // Incoming request middleware
927
980
  def traceIncoming[Req, Resp](
@@ -958,7 +1011,7 @@ def traceOutgoing[Resp](
958
1011
  }
959
1012
  ```
960
1013
 
961
- For the incoming case, propagating the remote parent into the tracer's `ContextStorage` before starting the span requires access to the `TracerProvider`'s `contextStorage` field. This is why `contextStorage` is exposed on `TracerProvider`.
1014
+ For the incoming case, the remote parent has to be scoped in the same `ContextStorage` the tracer reads. A provider's own `contextStorage` field is library-private, so build one with `ContextStorage.create` and hand it to the builder via `setContextStorage` — as the server example above does — then wrap the handler in `contextStorage.scoped(parentCtx) { … }`.
962
1015
 
963
1016
  ### With existing Java logging (SLF4J/JUL)
964
1017
 
@@ -975,7 +1028,7 @@ The core telemetry module (`zio-blocks-telemetry`) compiles for JVM and Scala.js
975
1028
  - All the in-memory processors and default providers
976
1029
 
977
1030
  JVM-only:
978
- - `FileLogWriter`: uses `FileChannel`, not available in JS
1031
+ - File output: a custom `LogWriter` backed by `java.io` or `FileChannel` works on the JVM only
979
1032
  - `ContextStorage` using `ScopedValue`: JDK 25+ specific; on Scala.js, a simpler mutable-variable implementation is used
980
1033
  - `PlatformExecutor`: uses `ScheduledExecutorService` with virtual threads
981
1034
  - The entire `zio-blocks-telemetry-otel` module (OTLP HTTP export, `BatchProcessor`)
@@ -990,10 +1043,10 @@ If you're writing cross-platform code that uses telemetry, keep your call sites
990
1043
 
991
1044
  Two common causes:
992
1045
 
993
- 1. The global minimum level is filtering it out. Check `log.globalMinLevel`. Default is `Severity.Trace.number` (1), so everything passes. If you called `log.install(logger, minSeverity = Severity.Info)`, then trace and debug calls are suppressed.
1046
+ 1. The global minimum level is filtering it out. The default floor is `Severity.Trace` (severity number 1), so everything passes. If you called `log.install(logger, minSeverity = Severity.Info)` or `log.setMinSeverity(Severity.Info)`, then trace and debug calls are suppressed.
994
1047
 
995
1048
  2. The logger was installed but the processor chain is empty. Verify your `LoggerProvider` builder has at least one processor:
996
- ```scala
1049
+ ```scala
997
1050
  LoggerProvider.builder
998
1051
  .addLogRecordProcessor(new ConsoleLogRecordProcessor) // don't forget this
999
1052
  .build()
@@ -1015,7 +1068,7 @@ The compiler error will say something like `no implicit value for LogEnrichment[
1015
1068
 
1016
1069
  Define an implicit in your package object or companion:
1017
1070
 
1018
- ```scala
1071
+ ```scala mdoc:compile-only
1019
1072
  import zio.blocks.telemetry._
1020
1073
  import java.util.UUID
1021
1074
 
@@ -1029,7 +1082,7 @@ Then `log.info("action", "id" -> someUuid)` compiles and works.
1029
1082
 
1030
1083
  **"What's the overhead of a disabled log level?"**
1031
1084
 
1032
- Exactly one volatile read (`globalMinLevel`) plus, if the level passes that check, one array scan through the namespace overrides (usually empty, so O(1)). Both happen before any argument is evaluated. No string formatting, no object allocation, no method calls on your arguments.
1085
+ Exactly one volatile read (the global minimum level) plus, if the level passes that check, one array scan through the namespace overrides (usually empty, so O(1)). Both happen before any argument is evaluated. No string formatting, no object allocation, no method calls on your arguments.
1033
1086
 
1034
1087
  **"Can I use this with ZIO, Cats Effect, or any other effect system?"**
1035
1088
 
@@ -1047,7 +1100,7 @@ For tracing: use `trace.collectedSpans` and `trace.clearSpans()`. The default in
1047
1100
 
1048
1101
  **"What happens when the export queue fills up?"**
1049
1102
 
1050
- When `queueSize` exceeds `maxQueueSize`, `BatchProcessor.dropOldestIfOverCapacity()` polls the head of the queue (the oldest item), decrements the size, and prints a warning to stderr:
1103
+ When `queueSize` exceeds `maxQueueSize`, `enqueue` polls the head of the queue (the oldest item), decrements the size, and prints a warning to stderr:
1051
1104
 
1052
1105
  ```
1053
1106
  [zio-blocks-telemetry] BatchProcessor queue full (2048). Dropping oldest item.
@@ -1057,13 +1110,21 @@ The new item is still enqueued. This is a best-effort, head-dropping strategy. Y
1057
1110
 
1058
1111
  **"Is this virtual-thread safe?"**
1059
1112
 
1060
- Yes. `ContextStorage` on JDK 25+ uses `ScopedValue`, which is the JDK 25 native mechanism for per-virtual-thread context propagation. Each virtual thread inherits its `ScopedValue` bindings from the thread that spawns it, so if you start a virtual thread inside a span's block, the child thread sees the same span context. The `BatchProcessor` export threads also use virtual threads via `Executors.newVirtualThreadPerTaskExecutor()`, so retry sleeps don't pin platform threads.
1113
+ Yes, in the sense that a span's context is bound to the current call stack whether that stack belongs to a platform or a virtual thread: `ContextStorage` on JDK 25+ uses `ScopedValue`, and reads and writes from the running thread are safe. Inheritance is narrower than an `InheritableThreadLocal`, though — a `ScopedValue` binding reaches a child thread only when the child is forked from a `StructuredTaskScope`. A plain `Thread.ofVirtual().start(...)` inside a span's block sees no span context, so records logged there are not correlated; open the span inside the child, or pass the `SpanContext` across explicitly. The `PlatformExecutor` that drives OTLP export runs its tasks on virtual threads, so retry sleeps don't pin platform threads.
1061
1114
 
1062
1115
  ---
1063
1116
 
1064
1117
  ## Where to Go Next
1065
1118
 
1066
- - **Complete API reference:** [Telemetry Reference](../reference/telemetry.md) for all types, methods, and parameter documentation
1119
+ - **Complete API reference:** [Telemetry Reference](../reference/telemetry/index.md) for all types, methods, and parameter documentation
1067
1120
  - **Installation and quick start:** same reference doc, Installation section
1068
1121
  - **Context and dependency injection:** [Context Reference](../reference/context.md) for integrating `OtelContext` with the Context module
1069
- - **Resource management:** [Compile-Time Resource Safety with Scope](compile-time-resource-safety-with-scope.md) for managing provider lifetimes with Scope
1122
+ - **Resource management:** [Compile-Time Resource Safety with Scope](compile-time-resource-safety-with-scope.md) — the telemetry module has no dependency on `Scope`; the providers are plain `AutoCloseable`s you shut down yourself, as in the wiring recipe above
1123
+
1124
+ ## See Also
1125
+
1126
+ - [TracerProvider](../reference/telemetry/tracing/tracer-provider.md) — configure and install the tracing pipeline
1127
+ - [LoggerProvider](../reference/telemetry/logging/logger-provider.md) — configure and install the logging pipeline
1128
+ - [MeterProvider](../reference/telemetry/metrics/meter-provider.md) — configure and install the metrics pipeline
1129
+ - [Attributes](../reference/telemetry/common/attributes.md) — the unboxed key-value type carried by every signal
1130
+ - [Common Types](../reference/telemetry/common/index.md) — `Attributes`, `AttributeKey`, `Resource`, and `InstrumentationScope`
@@ -38,13 +38,13 @@ libraryDependencies += "dev.zio" %% "zio-schema-avro" % "1.x.x"
38
38
  **After (ZIO Blocks Schema):**
39
39
 
40
40
  ```scala
41
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
41
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.56"
42
42
  // Optional codec modules:
43
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-avro" % "0.0.51"
44
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-thrift" % "0.0.51"
45
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-bson" % "0.0.51"
46
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-messagepack" % "0.0.51"
47
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-toon" % "0.0.51"
43
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-avro" % "0.0.56"
44
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-thrift" % "0.0.56"
45
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-bson" % "0.0.56"
46
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-messagepack" % "0.0.56"
47
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-toon" % "0.0.56"
48
48
  ```
49
49
 
50
50
  Key points: