@zio.dev/zio-blocks 0.0.33 → 0.0.51

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 (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,693 @@
1
+ ---
2
+ id: telemetry
3
+ title: "Telemetry"
4
+ ---
5
+
6
+ `zio-blocks-telemetry` is an **effect-free, zero-allocation** observability library covering logging, tracing, and metrics. It follows the OpenTelemetry data model but has no dependency on the OpenTelemetry SDK. Hot paths are macro-generated so that a disabled log level costs exactly one volatile read plus one array scan (usually empty). Backends are pluggable: the default writes to the console with no setup, and production exporters live in the companion `zio-blocks-telemetry-otel` module.
7
+
8
+ The three main entry points are:
9
+ - `log` — structured logging with macro-generated call sites
10
+ - `trace` — distributed tracing with automatic parent propagation
11
+ - `metric` — counters, histograms, and gauges
12
+
13
+ All three work out of the box with no wiring. Replace the default provider at any time via the `install` methods.
14
+
15
+ ## Installation
16
+
17
+ ```scala
18
+ // Core: logging, tracing, metrics
19
+ libraryDependencies += "dev.zio" %% "zio-blocks-telemetry" % "0.0.51"
20
+
21
+ // Optional: OTLP JSON export over HTTP (JVM only)
22
+ libraryDependencies += "dev.zio" %% "zio-blocks-telemetry-otel" % "0.0.51"
23
+ ```
24
+
25
+ For Scala.js (telemetry core only):
26
+
27
+ ```scala
28
+ libraryDependencies += "dev.zio" %%% "zio-blocks-telemetry" % "0.0.51"
29
+ ```
30
+
31
+ Supports Scala 2.13.x and 3.x.
32
+
33
+ ## Logging
34
+
35
+ ### Zero-config console logging
36
+
37
+ Import `zio.blocks.telemetry._` and call `log` directly. No setup required. The default backend prints human-readable text to stdout.
38
+
39
+ ```scala
40
+ import zio.blocks.telemetry._
41
+
42
+ log.info("server started")
43
+ log.debug("handling request", "method" -> "GET", "path" -> "/api/v1/users")
44
+ log.warn("slow query", "duration_ms" -> 450L)
45
+ log.error("unhandled exception", new RuntimeException("disk full"))
46
+ log.fatal("shutting down", "reason" -> "out of memory")
47
+ ```
48
+
49
+ Each call is a Scala inline macro. When the log level is disabled, the compiler eliminates the entire call site, including argument evaluation.
50
+
51
+ ### Structured key-value enrichment
52
+
53
+ Every `log.*` method accepts a varargs list of enrichments after the message. Enrichments are resolved via implicit `LogEnrichment[A]` instances.
54
+
55
+ ```scala
56
+ import zio.blocks.telemetry._
57
+
58
+ // String attribute
59
+ log.info("user login", "user_id" -> "u-1234")
60
+
61
+ // Long attribute
62
+ log.info("item purchased", "price_cents" -> 4999L)
63
+
64
+ // Int attribute (auto-widened to Long internally)
65
+ log.info("retry attempt", "attempt" -> 3)
66
+
67
+ // Double attribute
68
+ log.info("cpu load", "load_avg" -> 0.87)
69
+
70
+ // Boolean attribute
71
+ log.info("feature flag", "dark_mode" -> true)
72
+
73
+ // Throwable: adds exception.type, exception.message, and stacktrace
74
+ log.error("payment failed", new IllegalStateException("card declined"))
75
+
76
+ // Severity override: emit at the declared level but record a different severity
77
+ log.info("degraded response", Severity.Warn)
78
+
79
+ // Pre-built Attributes block
80
+ val extra = Attributes.builder
81
+ .put("region", "eu-west-1")
82
+ .put("az", "b")
83
+ .build
84
+ log.info("instance started", extra)
85
+
86
+ // Mix enrichments freely in a single call
87
+ log.warn(
88
+ "request failed",
89
+ "status" -> 503,
90
+ "retries" -> 2L,
91
+ "cached" -> false,
92
+ new RuntimeException("upstream timeout")
93
+ )
94
+ ```
95
+
96
+ ### Supported enrichment types reference
97
+
98
+ | Scala type | OTLP attribute type | Notes |
99
+ |------------------------|---------------------|-------|
100
+ | `(String, String)` | `STRING` | |
101
+ | `(String, Long)` | `INT64` | |
102
+ | `(String, Int)` | `INT64` | auto-promoted via `.toLong` |
103
+ | `(String, Double)` | `DOUBLE` | |
104
+ | `(String, Boolean)` | `BOOL` | |
105
+ | `Attributes` | merged | pre-built attribute set |
106
+ | `Severity` | overrides level | e.g. `Severity.Warn` |
107
+ | `Throwable` | `exception.*` | adds type, message, stacktrace |
108
+
109
+ **Not directly supported:** `(String, Float)`, `(String, Short)`, `(String, Byte)`, `(String, Char)`. Convert with `.toDouble`, `.toLong`, or `.toString`. Same for `UUID`, `Instant`, `LocalDateTime`, and other JDK types: use `.toString`, or provide a custom `LogEnrichment[T]` implicit.
110
+
111
+ ### What happens at runtime
112
+
113
+ When a log call executes:
114
+
115
+ 1. Check `log.globalMinLevel` — one volatile read. If the message severity is below the global floor, stop immediately.
116
+ 2. Check per-namespace level overrides — one array scan over the override list (empty by default, O(1) for the common case).
117
+ 3. Build attributes into a pooled `AttributeBuilder` — no heap allocation on the hot path.
118
+ 4. Merge scoped annotations from `LogAnnotations` (thread-local / ScopedValue on JVM 25+).
119
+ 5. Read the current `SpanContext` from `ContextStorage` and attach `traceId` / `spanId` if present.
120
+ 6. Call `LogRecordProcessor.onEmit` on each processor.
121
+
122
+ Steps 1 and 2 are the entire cost when logging is disabled. No string formatting, no argument boxing.
123
+
124
+ ### Scoped annotations
125
+
126
+ `log.annotated` adds key-value string pairs to every log call within its scope. Annotations are thread-local (ScopedValue on JDK 25+, `ThreadLocal` otherwise).
127
+
128
+ ```scala
129
+ import zio.blocks.telemetry._
130
+
131
+ def handleRequest(requestId: String): Unit =
132
+ log.annotated("requestId" -> requestId, "service" -> "payments") {
133
+ log.info("request received") // includes requestId and service
134
+ processPayment()
135
+ log.info("request complete") // still includes them
136
+ }
137
+ ```
138
+
139
+ Annotations are inherited by nested calls and removed automatically when the block exits.
140
+
141
+ ### Rate-limited logging
142
+
143
+ Use `*Every` to emit only on every Nth invocation at a given call site. Use `*AtMost` to emit at most once per time window (milliseconds). Both are per-call-site, not global.
144
+
145
+ ```scala
146
+ import zio.blocks.telemetry._
147
+
148
+ // Emit on every 100th call at this exact call site
149
+ def onTick(): Unit =
150
+ log.infoEvery(100, "heartbeat")
151
+
152
+ // Emit at most once per 5 seconds at this call site
153
+ def onRequest(): Unit =
154
+ log.warnAtMost(5000L, "high latency detected", "threshold_ms" -> 200L)
155
+
156
+ // All six severity levels have *Every and *AtMost variants
157
+ log.traceEvery(1000, "fine-grained trace")
158
+ log.debugAtMost(1000L, "debug sampling")
159
+ log.errorEvery(10, "repeated error suppressed")
160
+ log.fatalAtMost(60000L, "fatal alert")
161
+ ```
162
+
163
+ The call-site counter/timestamp is stored in a `val` synthesized by the macro — no shared state between call sites.
164
+
165
+ ### Per-namespace log levels
166
+
167
+ Override the minimum severity for any package prefix. The longest matching prefix wins.
168
+
169
+ ```scala
170
+ import zio.blocks.telemetry._
171
+
172
+ // Suppress DEBUG noise from a chatty library
173
+ log.setMinSeverity("com.thirdparty.noisylibrary", Severity.Warn)
174
+
175
+ // Enable TRACE for your own module during debugging
176
+ log.setMinSeverity("com.myapp.payments", Severity.Trace)
177
+
178
+ // Restore a namespace to the global default
179
+ log.clearMinSeverity("com.thirdparty.noisylibrary")
180
+
181
+ // Remove all overrides
182
+ log.clearAllOverrides()
183
+ ```
184
+
185
+ ### File logging (JVM only)
186
+
187
+ `FileLogWriter` writes to a file using `FileChannel` with a shared byte buffer protected by a `ReentrantLock` (virtual-thread safe). ASCII content takes the fast path (no charset encoding step).
188
+
189
+ ```scala
190
+ import zio.blocks.telemetry._
191
+
192
+ // Text format, append mode, 8 KB buffer (defaults)
193
+ val writer = FileLogWriter("logs/app.log")
194
+ log.writer(TextLogFormatter, writer)
195
+
196
+ // JSON format with a larger buffer
197
+ val jsonWriter = FileLogWriter(
198
+ java.nio.file.Paths.get("logs/app.json"),
199
+ append = true,
200
+ bufferSize = 16384
201
+ )
202
+ log.writer(JsonLogFormatter, jsonWriter)
203
+
204
+ // Both outputs are active simultaneously — writer() is additive
205
+ ```
206
+
207
+ `log.writer` adds an output; calling it twice gives you two outputs. Use `log.clearWriters()` to remove all file outputs and revert to processor-based routing.
208
+
209
+ ### JSON logging
210
+
211
+ Swap `TextLogFormatter` for `JsonLogFormatter` to get OTLP-compatible JSON lines:
212
+
213
+ ```scala
214
+ import zio.blocks.telemetry._
215
+
216
+ log.writer(JsonLogFormatter, FileLogWriter("logs/app.jsonl"))
217
+
218
+ // Each line looks like:
219
+ // {"timeUnixNano":"...","severityNumber":9,"severityText":"INFO","body":{"stringValue":"server started"},"attributes":[...]}
220
+ ```
221
+
222
+ ### Custom LogEnrichment
223
+
224
+ Provide a `LogEnrichment[T]` implicit to support your own types:
225
+
226
+ ```scala
227
+ import zio.blocks.telemetry._
228
+ import java.util.UUID
229
+
230
+ implicit val uuidEnrichment: LogEnrichment[(String, UUID)] =
231
+ new LogEnrichment[(String, UUID)] {
232
+ def enrich(record: LogRecord, value: (String, UUID)): LogRecord =
233
+ record.copy(
234
+ attributes = record.attributes ++ Attributes.of(
235
+ AttributeKey.string(value._1), value._2.toString
236
+ )
237
+ )
238
+ }
239
+
240
+ log.info("user action", "traceId" -> UUID.randomUUID())
241
+ ```
242
+
243
+ ## Tracing
244
+
245
+ ### Basic span
246
+
247
+ Get a `Tracer` from `trace` and call `span`. The block receives the active `Span`. The span starts, runs your code, then ends — even if an exception is thrown.
248
+
249
+ ```scala
250
+ import zio.blocks.telemetry._
251
+
252
+ val tracer = trace.get("my-service")
253
+
254
+ val result = tracer.span("compute-order") { span =>
255
+ span.setAttribute("order.id", "ord-7890")
256
+ span.addEvent("validation-complete")
257
+ // ... your logic here
258
+ 42
259
+ }
260
+ ```
261
+
262
+ ### Span kinds
263
+
264
+ ```scala
265
+ import zio.blocks.telemetry._
266
+
267
+ val tracer = trace.get("gateway")
268
+
269
+ // HTTP server handler
270
+ tracer.span("handle-request", SpanKind.Server) { span =>
271
+ span.setAttribute("http.method", "POST")
272
+ span.setAttribute("http.route", "/checkout")
273
+ processCheckout()
274
+ }
275
+
276
+ // Outbound HTTP call
277
+ tracer.span("call-payment-api", SpanKind.Client) { span =>
278
+ span.setAttribute("peer.service", "stripe")
279
+ callStripe()
280
+ }
281
+ ```
282
+
283
+ ### Nested spans and automatic parent propagation
284
+
285
+ Child spans are created by calling `tracer.span` inside a parent span's block. The parent `SpanContext` is stored in `ContextStorage` (ScopedValue on JDK 25+) and automatically picked up.
286
+
287
+ ```scala
288
+ import zio.blocks.telemetry._
289
+
290
+ val tracer = trace.get("checkout-service")
291
+
292
+ tracer.span("checkout") { _ =>
293
+ // This span becomes the parent automatically
294
+ tracer.span("validate-cart") { _ =>
295
+ tracer.span("check-inventory") { _ =>
296
+ // grandchild — all three linked in the same trace
297
+ checkInventory()
298
+ }
299
+ }
300
+ tracer.span("charge-card") { _ =>
301
+ chargeCard()
302
+ }
303
+ }
304
+ ```
305
+
306
+ ### Span attributes and events
307
+
308
+ ```scala
309
+ import zio.blocks.telemetry._
310
+
311
+ val tracer = trace.get("my-service")
312
+
313
+ tracer.span("process-file") { span =>
314
+ // Typed attribute keys
315
+ span.setAttribute(AttributeKey.string("file.path"), "/data/input.csv")
316
+ span.setAttribute(AttributeKey.long("file.size_bytes"), 1048576L)
317
+ span.setAttribute(AttributeKey.boolean("file.compressed"), true)
318
+
319
+ // String/Long/Double/Boolean convenience overloads
320
+ span.setAttribute("file.format", "csv")
321
+ span.setAttribute("row.count", 50000L)
322
+
323
+ // Events mark instants within the span
324
+ span.addEvent("parsing-started")
325
+ val rows = parseFile()
326
+ span.addEvent("parsing-complete", Attributes.builder.put("rows", rows.toLong).build)
327
+
328
+ // Span status
329
+ span.setStatus(SpanStatus.Ok)
330
+ rows
331
+ }
332
+ ```
333
+
334
+ ### Log-trace correlation
335
+
336
+ When you log inside an active span, the logger automatically reads the current `SpanContext` and attaches `traceId` and `spanId` to the log record. No extra code needed.
337
+
338
+ ```scala
339
+ import zio.blocks.telemetry._
340
+
341
+ val tracer = trace.get("order-service")
342
+
343
+ tracer.span("place-order") { _ =>
344
+ log.info("order received", "order_id" -> "ord-5678")
345
+ // This log record automatically includes traceId and spanId
346
+ processOrder()
347
+ log.info("order complete")
348
+ }
349
+ ```
350
+
351
+ ## Metrics
352
+
353
+ ### Counter, Histogram, Gauge
354
+
355
+ ```scala
356
+ import zio.blocks.telemetry._
357
+
358
+ // Counter: monotonically increasing
359
+ val requests = metric.counter("http.server.requests")
360
+ requests.add(1, "method" -> "GET", "status" -> "200")
361
+ requests.add(1, "method" -> "POST", "status" -> "201")
362
+
363
+ // Histogram: records distributions
364
+ val latency = metric.histogram("http.server.duration_ms")
365
+ latency.record(42.5, "method" -> "GET")
366
+ latency.record(310.0, "method" -> "POST")
367
+
368
+ // Gauge: last-write-wins, can go up or down
369
+ val poolSize = metric.gauge("db.connection_pool.size")
370
+ poolSize.record(10.0, "pool" -> "primary")
371
+ poolSize.record(5.0, "pool" -> "replica")
372
+
373
+ // UpDownCounter: like a counter but supports negative deltas
374
+ val queueDepth = metric.upDownCounter("jobs.queue.depth")
375
+ queueDepth.add(3, "queue" -> "high-priority")
376
+ queueDepth.add(-1, "queue" -> "high-priority")
377
+ ```
378
+
379
+ ### Bound instruments for hot paths
380
+
381
+ Pre-binding a fixed attribute set avoids the attribute-lookup cost on every recording. Use `bind` when you record against the same label combination at high frequency.
382
+
383
+ ```scala
384
+ import zio.blocks.telemetry._
385
+
386
+ val requests = metric.counter("http.server.requests")
387
+
388
+ // Bind once — typically at startup or first use
389
+ val getOk = requests.bind(Attributes.builder.put("method", "GET").put("status", "200").build)
390
+ val postOk = requests.bind(Attributes.builder.put("method", "POST").put("status", "201").build)
391
+
392
+ // Hot path: just increment the pre-bound adder
393
+ def onGetSuccess(): Unit = getOk.add(1)
394
+ def onPostSuccess(): Unit = postOk.add(1)
395
+ ```
396
+
397
+ Histograms and gauges have the same `bind` pattern:
398
+
399
+ ```scala
400
+ import zio.blocks.telemetry._
401
+
402
+ val latency = metric.histogram("rpc.duration_ms")
403
+ val getLatency = latency.bind(Attributes.builder.put("method", "GET").build)
404
+ val poolSize = metric.gauge("db.pool.size")
405
+ val primaryPool = poolSize.bind(Attributes.builder.put("pool", "primary").build)
406
+
407
+ def recordGet(ms: Double): Unit = getLatency.record(ms)
408
+ def updatePool(n: Double): Unit = primaryPool.record(n)
409
+ ```
410
+
411
+ ## OTEL Export
412
+
413
+ The `zio-blocks-telemetry-otel` module provides OTLP JSON exporters that batch and send over HTTP. The exporter classes (`OtlpJsonTraceExporter`, `OtlpJsonLogExporter`, `OtlpJsonMetricExporter`) live in `package zio.blocks.telemetry.otel` and are wired by placing your telemetry bootstrap code in the same package, or by creating a thin wrapper class there.
414
+
415
+ Each exporter takes an `ExporterConfig`, a `Resource`, an `InstrumentationScope`, an `HttpSender`, and a `PlatformExecutor`.
416
+
417
+ ### Trace export
418
+
419
+ ```scala
420
+ package zio.blocks.telemetry.otel
421
+
422
+ import zio.blocks.telemetry._
423
+
424
+ val config = ExporterConfig(
425
+ endpoint = "http://otel-collector:4318",
426
+ headers = Map("Authorization" -> "Bearer my-token"),
427
+ timeout = java.time.Duration.ofSeconds(10),
428
+ maxQueueSize = 2048,
429
+ maxBatchSize = 512,
430
+ flushIntervalMillis = 5000L
431
+ )
432
+
433
+ val resource = Resource.create(
434
+ Attributes.builder
435
+ .put("service.name", "my-service")
436
+ .put("service.version", "1.0.0")
437
+ .build
438
+ )
439
+
440
+ val scope = InstrumentationScope("my-service")
441
+ val sender = HttpSender.jdk(java.time.Duration.ofSeconds(10))
442
+ val executor = PlatformExecutor.create()
443
+
444
+ val traceExporter = new OtlpJsonTraceExporter(config, resource, scope, sender, executor)
445
+
446
+ val tracerProvider = TracerProvider.builder
447
+ .addSpanProcessor(traceExporter)
448
+ .setResource(resource)
449
+ .build()
450
+
451
+ trace.install(tracerProvider)
452
+
453
+ // Now all spans are exported to the collector
454
+ val tracer = trace.get("my-service")
455
+ tracer.span("my-operation") { _ => doWork() }
456
+ ```
457
+
458
+ ### Log export
459
+
460
+ ```scala
461
+ package zio.blocks.telemetry.otel
462
+
463
+ import zio.blocks.telemetry._
464
+
465
+ val config = ExporterConfig(endpoint = "http://otel-collector:4318")
466
+ val resource = Resource.create(Attributes.builder.put("service.name", "my-service").build)
467
+ val scope = InstrumentationScope("my-service")
468
+ val sender = HttpSender.jdk()
469
+ val executor = PlatformExecutor.create()
470
+
471
+ val logExporter = new OtlpJsonLogExporter(config, resource, scope, sender, executor)
472
+
473
+ val loggerProvider = LoggerProvider.builder
474
+ .addLogRecordProcessor(logExporter)
475
+ .setResource(resource)
476
+ .build()
477
+
478
+ log.install(loggerProvider.get("my-service"))
479
+
480
+ // Now log.info / log.warn / etc. are batched and exported to the collector
481
+ log.info("application started")
482
+ ```
483
+
484
+ ### Metric export
485
+
486
+ `OtlpJsonMetricExporter` takes a `collectFn: () => Seq[NamedMetric]` callback that the exporter calls on each flush. Wire it with the instruments you create:
487
+
488
+ ```scala
489
+ package zio.blocks.telemetry.otel
490
+
491
+ import zio.blocks.telemetry._
492
+
493
+ val config = ExporterConfig(endpoint = "http://otel-collector:4318", flushIntervalMillis = 15000L)
494
+ val resource = Resource.create(Attributes.builder.put("service.name", "my-service").build)
495
+ val scope = InstrumentationScope("my-service")
496
+ val sender = HttpSender.jdk()
497
+
498
+ // Create your instruments first
499
+ val requestCounter = metric.counter("http.server.requests")
500
+ val latencyHist = metric.histogram("http.server.duration_ms")
501
+
502
+ // Provide a collectFn that harvests each instrument
503
+ val metricExporter = new OtlpJsonMetricExporter(
504
+ config, resource, scope, sender,
505
+ () => Seq(
506
+ NamedMetric("http.server.requests", "Total HTTP requests", "1", requestCounter.collect()),
507
+ NamedMetric("http.server.duration_ms", "Request latency", "ms", latencyHist.collect())
508
+ )
509
+ )
510
+
511
+ // The exporter flushes on the interval from config
512
+ // Call metricExporter.exportMetrics() to trigger a manual flush
513
+ ```
514
+
515
+ ## Context Propagation
516
+
517
+ Propagators extract and inject `SpanContext` from/into a carrier (typically HTTP headers). Two formats ship out of the box.
518
+
519
+ ### W3C TraceContext (recommended)
520
+
521
+ The `traceparent` header format from the W3C TraceContext spec.
522
+
523
+ ```scala
524
+ import zio.blocks.telemetry._
525
+ import zio.blocks.telemetry.otel._
526
+
527
+ // Inject into outgoing HTTP headers
528
+ val tracer = trace.get("gateway")
529
+
530
+ tracer.span("outbound-call") { span =>
531
+ var headers = Map.empty[String, String]
532
+ headers = W3CTraceContextPropagator.inject(
533
+ span.spanContext,
534
+ headers,
535
+ (carrier, k, v) => carrier + (k -> v)
536
+ )
537
+ callDownstream(headers)
538
+ }
539
+
540
+ // Extract from incoming HTTP headers
541
+ def handleIncoming(incomingHeaders: Map[String, String]): Unit = {
542
+ val parentCtx = W3CTraceContextPropagator.extract(
543
+ incomingHeaders,
544
+ (carrier, key) => carrier.get(key)
545
+ )
546
+ // parentCtx: Option[SpanContext] — use to establish parent link
547
+ parentCtx.foreach { ctx =>
548
+ // Store in ContextStorage to make it the active span context
549
+ }
550
+ }
551
+ ```
552
+
553
+ ### B3 (Zipkin)
554
+
555
+ Single-header and multi-header variants.
556
+
557
+ ```scala
558
+ import zio.blocks.telemetry._
559
+ import zio.blocks.telemetry.otel._
560
+
561
+ // Single header: b3: {traceId}-{spanId}-{sampling}
562
+ val incomingB3Single = B3Propagator.single.extract(
563
+ incomingHeaders,
564
+ (carrier, key) => carrier.get(key)
565
+ )
566
+
567
+ // Multi-header: X-B3-TraceId, X-B3-SpanId, X-B3-Sampled, ...
568
+ val incomingB3Multi = B3Propagator.multi.extract(
569
+ incomingHeaders,
570
+ (carrier, key) => carrier.get(key)
571
+ )
572
+
573
+ // Inject
574
+ var outHeaders = Map.empty[String, String]
575
+ outHeaders = B3Propagator.single.inject(
576
+ spanContext,
577
+ outHeaders,
578
+ (c, k, v) => c + (k -> v)
579
+ )
580
+ ```
581
+
582
+ ## Custom Provider Wiring
583
+
584
+ The exporter classes live inside `package zio.blocks.telemetry.otel`, so place your bootstrap code there (or in a thin wrapper class in that package).
585
+
586
+ ### Custom LoggerProvider
587
+
588
+ Build a logger with multiple processors — one for console output, one for OTEL export:
589
+
590
+ ```scala
591
+ package zio.blocks.telemetry.otel
592
+
593
+ import zio.blocks.telemetry._
594
+
595
+ val config = ExporterConfig(endpoint = "http://otel-collector:4318")
596
+ val resource = Resource.create(
597
+ Attributes.builder
598
+ .put("service.name", "checkout")
599
+ .put("service.version", "2.1.0")
600
+ .build
601
+ )
602
+ val scope = InstrumentationScope("checkout-service")
603
+ val sender = HttpSender.jdk()
604
+ val executor = PlatformExecutor.create()
605
+
606
+ val logExporter = new OtlpJsonLogExporter(config, resource, scope, sender, executor)
607
+
608
+ val loggerProvider = LoggerProvider.builder
609
+ .addLogRecordProcessor(new ConsoleLogRecordProcessor)
610
+ .addLogRecordProcessor(logExporter)
611
+ .setResource(resource)
612
+ .build()
613
+
614
+ val logger = loggerProvider.get("checkout-service")
615
+ log.install(logger, minSeverity = Severity.Info)
616
+ ```
617
+
618
+ ### Custom TracerProvider
619
+
620
+ Build a tracer with a custom sampler and processor chain:
621
+
622
+ ```scala
623
+ package zio.blocks.telemetry.otel
624
+
625
+ import zio.blocks.telemetry._
626
+
627
+ val config = ExporterConfig(endpoint = "http://otel-collector:4318")
628
+ val resource = Resource.create(
629
+ Attributes.builder
630
+ .put("service.name", "checkout")
631
+ .put("deployment.environment", "production")
632
+ .build
633
+ )
634
+ val scope = InstrumentationScope("checkout-service")
635
+ val sender = HttpSender.jdk()
636
+ val executor = PlatformExecutor.create()
637
+
638
+ val traceExporter = new OtlpJsonTraceExporter(config, resource, scope, sender, executor)
639
+
640
+ val tracerProvider = TracerProvider.builder
641
+ .setSampler(AlwaysOnSampler)
642
+ .addSpanProcessor(traceExporter)
643
+ .setResource(resource)
644
+ .build()
645
+
646
+ trace.install(tracerProvider)
647
+ ```
648
+
649
+ ## ExporterConfig reference
650
+
651
+ ```scala
652
+ final case class ExporterConfig(
653
+ endpoint: String = "http://localhost:4318",
654
+ headers: Map[String, String] = Map.empty,
655
+ timeout: java.time.Duration = java.time.Duration.ofSeconds(30),
656
+ maxQueueSize: Int = 2048,
657
+ maxBatchSize: Int = 512,
658
+ flushIntervalMillis: Long = 5000
659
+ )
660
+ ```
661
+
662
+ | Field | Default | Description |
663
+ |-------|---------|-------------|
664
+ | `endpoint` | `http://localhost:4318` | OTLP HTTP base URL. Paths `/v1/traces`, `/v1/logs`, `/v1/metrics` are appended automatically. |
665
+ | `headers` | empty | Additional HTTP headers (e.g. authentication). `Content-Type: application/json` is added automatically. |
666
+ | `timeout` | 30 s | Per-request HTTP timeout. |
667
+ | `maxQueueSize` | 2048 | Maximum number of pending records before the oldest are dropped. |
668
+ | `maxBatchSize` | 512 | Maximum records per export batch. |
669
+ | `flushIntervalMillis` | 5000 | Background flush interval in milliseconds. |
670
+
671
+ ## Formatters reference
672
+
673
+ | Formatter | Output |
674
+ |-----------|--------|
675
+ | `TextLogFormatter` | Human-readable: `2026-03-31T17:30:00.123Z INFO [MyClass.doWork:42] message {key="val"}` |
676
+ | `JsonLogFormatter` | OTLP-compatible JSON: `{"timeUnixNano":"...","severityNumber":9,"severityText":"INFO","body":{"stringValue":"message"},"attributes":[...]}` |
677
+
678
+ Both are singleton objects that allocate nothing per log record. They write directly into a pooled `StringBuilder`.
679
+
680
+ ## Severity levels
681
+
682
+ `Severity` follows the OTLP specification with 24 levels in six groups. The most common values are:
683
+
684
+ | Object | Number | Text |
685
+ |--------|--------|------|
686
+ | `Severity.Trace` | 1 | `TRACE` |
687
+ | `Severity.Debug` | 5 | `DEBUG` |
688
+ | `Severity.Info` | 9 | `INFO` |
689
+ | `Severity.Warn` | 13 | `WARN` |
690
+ | `Severity.Error` | 17 | `ERROR` |
691
+ | `Severity.Fatal` | 21 | `FATAL` |
692
+
693
+ Each group has four variants (`Trace`, `Trace2`, `Trace3`, `Trace4`, etc.) for fine-grained filtering. The default global minimum is `Severity.Trace` (all levels enabled).