@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.
- package/guides/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +292 -50
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +12 -0
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +5 -19
- package/sidebars.js +238 -43
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /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).
|