@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.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +6 -0
- package/guides/getting-started-with-mux.md +0 -112
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +395 -1
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +131 -70
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +200 -559
- package/package.json +1 -1
- package/reference/async.md +1379 -531
- package/reference/chunk.md +3 -3
- package/reference/codegen/index.md +1 -1
- package/reference/combinators.md +4 -4
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +6 -49
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +2 -2
- package/reference/docs.md +2 -2
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +1 -0
- package/reference/endpoint/index.md +9 -89
- package/reference/endpoint/path-codec.md +12 -24
- package/reference/endpoint/route-pattern.md +4 -6
- package/reference/endpoint/segment-codec.md +19 -32
- package/reference/html.md +313 -9
- package/reference/htmx/index.md +4 -52
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +3 -1
- package/reference/http-model/model.md +107 -71
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +6 -3
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +128 -11
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +7 -2
- package/reference/openapi.md +3 -3
- package/reference/projection.md +654 -0
- package/reference/resource-management/index.md +1 -1
- package/reference/resource-management/resource.md +2 -98
- package/reference/resource-management/scope.md +1 -209
- package/reference/resource-management/wire.md +4 -50
- package/reference/ringbuffer/advanced.mdx +1 -1
- package/reference/ringbuffer/index.mdx +3 -3
- package/reference/ringbuffer/mpmc.mdx +38 -4
- package/reference/ringbuffer/mpsc.mdx +36 -4
- package/reference/ringbuffer/spmc.mdx +1 -1
- package/reference/ringbuffer/spsc.mdx +87 -15
- package/reference/schema/allows.md +0 -96
- package/reference/schema/binding.md +2 -2
- package/reference/schema/built-in-codecs/avro.md +2 -2
- package/reference/schema/built-in-codecs/bson.md +50 -20
- package/reference/schema/built-in-codecs/csv.md +2 -2
- package/reference/schema/built-in-codecs/index.md +3 -3
- package/reference/schema/built-in-codecs/json/index.md +2 -2
- package/reference/schema/built-in-codecs/json/json.md +1 -0
- package/reference/schema/built-in-codecs/messagepack.md +3 -3
- package/reference/schema/built-in-codecs/thrift.md +2 -2
- package/reference/schema/built-in-codecs/toon.md +3 -3
- package/reference/schema/built-in-codecs/yaml.md +2 -2
- package/reference/schema/codec.md +11 -11
- package/reference/schema/dynamic-optic.md +48 -3
- package/reference/schema/dynamic-schema.md +3 -3
- package/reference/schema/index.md +2 -0
- package/reference/schema/path-interpolator.md +2 -0
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/schema/schema-evolution/as.md +4 -4
- package/reference/schema/schema-evolution/into.md +2 -2
- package/reference/schema/schema-expr.md +2 -2
- package/reference/schema/schema-search.md +263 -0
- package/reference/schema/schema.md +10 -2
- package/reference/schema/type-class-derivation.md +1 -1
- package/reference/smithy.md +502 -3
- package/reference/sql/db-codec-deriver.md +3 -3
- package/reference/sql/db-codec.md +22 -22
- package/reference/sql/db-con.md +4 -4
- package/reference/sql/db-connection.md +1 -1
- package/reference/sql/db-param.md +1 -1
- package/reference/sql/db-result-reader.md +4 -2
- package/reference/sql/db-tx.md +46 -14
- package/reference/sql/ddl.md +1 -1
- package/reference/sql/frag.md +44 -10
- package/reference/sql/index.md +7 -7
- package/reference/sql/repo.md +15 -15
- package/reference/sql/sql-dialect.md +1 -1
- package/reference/sql/sql-logger.md +1 -1
- package/reference/sql/sql-name-mapper.md +3 -3
- package/reference/sql/table-metadata.md +3 -3
- package/reference/sql/table.md +10 -10
- package/reference/sql/transactor-zio.md +1 -1
- package/reference/sql/transactor.md +21 -11
- package/reference/sql-zio.md +2 -2
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
- package/reference/streams/{sink.md → core/sink.md} +331 -353
- package/reference/streams/{stream.md → core/stream.md} +919 -209
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +140 -67
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/{writer.md → primitives/writer.md} +254 -98
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +0 -64
- package/sidebars.js +365 -185
- package/undocumented-report.md +528 -270
- package/reference/config.md +0 -158
- package/reference/streams/concurrent-operators.md +0 -106
- package/reference/streams/reader.md +0 -1284
- package/reference/streams/scala-2-compatibility.md +0 -55
- package/reference/streams/zero-boxing.md +0 -275
- package/reference/telemetry.md +0 -693
|
@@ -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
|
|
14
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-telemetry" % "0.0.
|
|
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.
|
|
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 >=
|
|
82
|
+
if (severity.number >= GlobalLogState.globalMinLevel) { ... }
|
|
77
83
|
```
|
|
78
84
|
|
|
79
|
-
`globalMinLevel` is a `@volatile Int
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
149
|
-
log.writer(JsonLogFormatter,
|
|
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
|
|
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 >=
|
|
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`),
|
|
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
|
|
208
|
+
For `*Every(N, msg)`, the macro emits a guard against a shared global table:
|
|
192
209
|
|
|
193
210
|
```scala
|
|
194
|
-
//
|
|
195
|
-
|
|
211
|
+
// siteId is a compile-time constant: (sourcePath + ":" + line).hashCode
|
|
212
|
+
if (LogRateLimit.shouldLogEvery(siteId, N)) { log.emit(...) }
|
|
196
213
|
|
|
197
|
-
//
|
|
198
|
-
|
|
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
|
-
|
|
221
|
+
if (LogRateLimit.shouldLogAtMost(siteId, windowMs)) { log.emit(...) }
|
|
205
222
|
|
|
206
|
-
|
|
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,
|
|
267
|
-
log.writer(JsonLogFormatter,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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`,
|
|
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(
|
|
496
|
+
.addSpanProcessor(SpanProcessor.noop) // your exporter here
|
|
497
|
+
.setContextStorage(contextStorage)
|
|
464
498
|
.build()
|
|
465
499
|
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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`
|
|
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
|
-
|
|
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"},
|
|
652
|
-
// SumDataPoint(Attributes{method="POST",status="201"},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
751
|
-
log.writer(JsonLogFormatter,
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
- `
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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`, `
|
|
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
|
|
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)
|
|
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.
|
|
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.
|
|
44
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-schema-thrift" % "0.0.
|
|
45
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-schema-bson" % "0.0.
|
|
46
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-schema-messagepack" % "0.0.
|
|
47
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-schema-toon" % "0.0.
|
|
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:
|