opencode-effect-enforcer 0.2.0
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/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-observability
|
|
3
|
+
description: Implement structured logging, distributed tracing, and metrics in Effect applications. Use this skill when configuring loggers, adding spans/tracing, collecting metrics, or setting up OTLP/Prometheus export.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an Effect TypeScript expert specializing in observability — structured logging, distributed tracing, and metrics collection.
|
|
7
|
+
|
|
8
|
+
## Effect Source Reference
|
|
9
|
+
|
|
10
|
+
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
+
Browse and read files there directly to look up APIs, types, and implementations.
|
|
12
|
+
|
|
13
|
+
Reference this for:
|
|
14
|
+
|
|
15
|
+
- `Logger` module: `packages/effect/src/Logger.ts`
|
|
16
|
+
- `Tracer` module: `packages/effect/src/Tracer.ts`
|
|
17
|
+
- `Metric` module: `packages/effect/src/Metric.ts`
|
|
18
|
+
- OTLP export: `packages/effect/src/unstable/observability/`
|
|
19
|
+
- Observability examples: `ai-docs/src/08_observability/`
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Structured Logging
|
|
24
|
+
|
|
25
|
+
### Log Functions
|
|
26
|
+
|
|
27
|
+
Effect provides log functions at every level. Each is variadic and accepts one or more message values:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { Effect } from 'effect';
|
|
31
|
+
|
|
32
|
+
const program = Effect.gen(function* () {
|
|
33
|
+
yield* Effect.log('general log message');
|
|
34
|
+
yield* Effect.logDebug('debug-level detail');
|
|
35
|
+
yield* Effect.logInfo('informational message');
|
|
36
|
+
yield* Effect.logWarning('something concerning');
|
|
37
|
+
yield* Effect.logError('something failed');
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Pass additional message values after the first. These extra values become part of the log message/body — they are **not** automatically indexed as queryable annotations:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
yield* Effect.log('User action', { userId: 123, action: 'login' });
|
|
45
|
+
yield* Effect.logInfo('Request processed', { duration: 150, statusCode: 200 });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For **queryable dimensions** (fields you want to filter or group on in your logging/tracing backend), prefer `Effect.annotateLogs` or span annotations (`Effect.annotateCurrentSpan` / `Effect.annotateSpans`) rather than passing objects as message values.
|
|
49
|
+
|
|
50
|
+
### Log Annotations
|
|
51
|
+
|
|
52
|
+
Attach key-value metadata to all log lines within a scope:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const checkout = Effect.gen(function* () {
|
|
56
|
+
yield* Effect.logInfo('validating cart');
|
|
57
|
+
yield* Effect.logWarning('inventory low for one line item');
|
|
58
|
+
yield* Effect.logError('payment provider timeout');
|
|
59
|
+
}).pipe(
|
|
60
|
+
Effect.annotateLogs({
|
|
61
|
+
service: 'checkout-api',
|
|
62
|
+
route: 'POST /checkout'
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Single annotation shorthand:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
Effect.annotateLogs('requestId', 'req-abc-123');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Log Spans
|
|
74
|
+
|
|
75
|
+
Add duration metadata to log lines — each log will include `label=<elapsed>ms`:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const withTiming = myEffect.pipe(Effect.withLogSpan('checkout'));
|
|
79
|
+
// Log output includes: checkout=42ms
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 2. Logger Configuration
|
|
85
|
+
|
|
86
|
+
### Built-in Loggers
|
|
87
|
+
|
|
88
|
+
Effect v4 provides several logger implementations:
|
|
89
|
+
|
|
90
|
+
| Logger | Output | Use Case |
|
|
91
|
+
| -------------------------- | --------------------------------- | ------------------------------ |
|
|
92
|
+
| `Logger.defaultLogger` | Default runtime format | General use |
|
|
93
|
+
| `Logger.consolePretty()` | Colorized, human-readable | Development |
|
|
94
|
+
| `Logger.consoleJson` | Single-line JSON to console | Production / log aggregation |
|
|
95
|
+
| `Logger.consoleLogFmt` | logfmt key=value to console | Production / structured search |
|
|
96
|
+
| `Logger.consoleStructured` | JS object to console | Development debugging |
|
|
97
|
+
| `Logger.formatSimple` | String (no console output) | Composition / piping |
|
|
98
|
+
| `Logger.formatJson` | JSON string (no console output) | Composition / piping |
|
|
99
|
+
| `Logger.formatLogFmt` | logfmt string (no console output) | Composition / piping |
|
|
100
|
+
| `Logger.formatStructured` | Structured JS object | Composition / piping |
|
|
101
|
+
| `Logger.tracerLogger` | Emits logs as tracer span events | Included by default |
|
|
102
|
+
|
|
103
|
+
### Installing Loggers via `Logger.layer`
|
|
104
|
+
|
|
105
|
+
`Logger.layer` **replaces** the current loggers by default:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { Effect, Logger } from 'effect';
|
|
109
|
+
|
|
110
|
+
// JSON logging for production
|
|
111
|
+
const JsonLoggerLayer = Logger.layer([Logger.consoleJson]);
|
|
112
|
+
|
|
113
|
+
// Pretty logging for development
|
|
114
|
+
const PrettyLoggerLayer = Logger.layer([Logger.consolePretty()]);
|
|
115
|
+
|
|
116
|
+
// Multiple loggers — both receive every log entry
|
|
117
|
+
const MultiLoggerLayer = Logger.layer([
|
|
118
|
+
Logger.consoleJson,
|
|
119
|
+
Logger.consolePretty()
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// Merge with existing loggers instead of replacing
|
|
123
|
+
const AdditionalLoggerLayer = Logger.layer([Logger.consoleJson], {
|
|
124
|
+
mergeWithExisting: true
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Log Level Filtering
|
|
129
|
+
|
|
130
|
+
Control the minimum log level via `References.MinimumLogLevel`:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { Layer, References } from 'effect';
|
|
134
|
+
|
|
135
|
+
// Only emit Warn and above (skip Debug, Info)
|
|
136
|
+
const WarnAndAbove = Layer.succeed(References.MinimumLogLevel, 'Warn');
|
|
137
|
+
|
|
138
|
+
// Combine with logger
|
|
139
|
+
const ProductionLoggerLayer = Logger.layer([Logger.consoleJson]).pipe(
|
|
140
|
+
Layer.provideMerge(WarnAndAbove)
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Valid levels: `"All"`, `"Trace"`, `"Debug"`, `"Info"`, `"Warn"`, `"Error"`, `"Fatal"`, `"None"`.
|
|
145
|
+
|
|
146
|
+
### Custom Loggers
|
|
147
|
+
|
|
148
|
+
Create loggers with `Logger.make`:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { Logger } from 'effect';
|
|
152
|
+
|
|
153
|
+
const customLogger = Logger.make((options) => {
|
|
154
|
+
console.log(`[${options.logLevel}] ${options.message}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// options fields: message, logLevel, cause, fiber, date
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Batched Logging
|
|
161
|
+
|
|
162
|
+
Aggregate log entries over a time window before flushing:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { Effect, Logger } from 'effect';
|
|
166
|
+
|
|
167
|
+
const batchedLogger = Logger.batched(Logger.formatStructured, {
|
|
168
|
+
window: '1 second',
|
|
169
|
+
flush: Effect.fn(function* (batch) {
|
|
170
|
+
// Send batch to external service
|
|
171
|
+
console.log(`Flushing ${batch.length} log entries`);
|
|
172
|
+
})
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// batchedLogger is an Effect — use it inside Logger.layer
|
|
176
|
+
const BatchedLoggerLayer = Logger.layer([batchedLogger]);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### File Logging
|
|
180
|
+
|
|
181
|
+
Write logs directly to a file (requires `FileSystem` from `@effect/platform`):
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { NodeFileSystem } from '@effect/platform-node';
|
|
185
|
+
import { Layer, Logger } from 'effect';
|
|
186
|
+
|
|
187
|
+
const FileLoggerLayer = Logger.layer([
|
|
188
|
+
Logger.toFile(Logger.formatSimple, 'app.log')
|
|
189
|
+
]).pipe(Layer.provide(NodeFileSystem.layer));
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Pipe syntax with options:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const fileLogger = Logger.formatJson.pipe(
|
|
196
|
+
Logger.toFile('/var/log/myapp.log', {
|
|
197
|
+
flag: 'a',
|
|
198
|
+
batchWindow: '5 seconds'
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Environment-based Logger Selection
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { Config, Effect, Layer, Logger } from 'effect';
|
|
207
|
+
|
|
208
|
+
const LoggerLayer = Layer.unwrap(
|
|
209
|
+
Effect.gen(function* () {
|
|
210
|
+
const env = yield* Config.string('NODE_ENV').pipe(
|
|
211
|
+
Config.withDefault('development')
|
|
212
|
+
);
|
|
213
|
+
if (env === 'production') {
|
|
214
|
+
return Logger.layer([Logger.consoleJson]);
|
|
215
|
+
}
|
|
216
|
+
return Logger.layer([Logger.consolePretty()]);
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 3. Spans and Tracing
|
|
224
|
+
|
|
225
|
+
### `Effect.withSpan`
|
|
226
|
+
|
|
227
|
+
Create a tracing span around any effect:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const processOrder = Effect.gen(function* () {
|
|
231
|
+
yield* chargeCard(orderId);
|
|
232
|
+
yield* persistOrder(orderId);
|
|
233
|
+
}).pipe(Effect.withSpan('processOrder'));
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Spans nest automatically — child effects that also use `withSpan` become child spans.
|
|
237
|
+
|
|
238
|
+
### `Effect.fn` with Auto-Spans
|
|
239
|
+
|
|
240
|
+
When you pass a string name to `Effect.fn`, it automatically wraps the function body in a span:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { Effect } from 'effect';
|
|
244
|
+
|
|
245
|
+
const processCheckout = Effect.fn('Checkout.processCheckout')(function* (
|
|
246
|
+
orderId: string
|
|
247
|
+
) {
|
|
248
|
+
yield* Effect.logInfo('starting checkout', { orderId });
|
|
249
|
+
yield* chargeCard(orderId).pipe(Effect.withSpan('checkout.charge-card'));
|
|
250
|
+
yield* persistOrder(orderId).pipe(
|
|
251
|
+
Effect.withSpan('checkout.persist-order')
|
|
252
|
+
);
|
|
253
|
+
yield* Effect.logInfo('checkout completed', { orderId });
|
|
254
|
+
});
|
|
255
|
+
// Creates a span named "Checkout.processCheckout" wrapping the entire body
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Span Attributes
|
|
259
|
+
|
|
260
|
+
Annotate the current span with key-value attributes:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
// Annotate the current span from inside the effect
|
|
264
|
+
yield* Effect.annotateCurrentSpan('order.id', orderId);
|
|
265
|
+
yield* Effect.annotateCurrentSpan('order.total', 99.95);
|
|
266
|
+
|
|
267
|
+
// Annotate from outside using pipe
|
|
268
|
+
const withAttributes = myEffect.pipe(
|
|
269
|
+
Effect.annotateSpans({
|
|
270
|
+
'checkout.order_id': orderId,
|
|
271
|
+
'checkout.provider': 'acme-pay'
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Layer Spans
|
|
277
|
+
|
|
278
|
+
Attach spans to layer construction:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
const MyLayer = Layer.effectDiscard(setupEffect).pipe(
|
|
282
|
+
Layer.withSpan('my-layer-setup')
|
|
283
|
+
);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## 4. Metrics
|
|
289
|
+
|
|
290
|
+
Effect provides five metric types. All metrics are concurrent-safe and integrated into the runtime.
|
|
291
|
+
|
|
292
|
+
### Counter
|
|
293
|
+
|
|
294
|
+
Tracks cumulative values that only increase:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { Effect, Metric } from 'effect';
|
|
298
|
+
|
|
299
|
+
const requestCount = Metric.counter('http_requests_total', {
|
|
300
|
+
description: 'Total number of HTTP requests'
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
yield* Metric.update(requestCount, 1);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Gauge
|
|
307
|
+
|
|
308
|
+
A single numerical value that can go up or down:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const activeConnections = Metric.gauge('active_connections', {
|
|
312
|
+
description: 'Current active connections'
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
yield* Metric.update(activeConnections, 42);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Histogram
|
|
319
|
+
|
|
320
|
+
Records observations in configurable buckets:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const responseTime = Metric.histogram('http_response_time_ms', {
|
|
324
|
+
description: 'HTTP response time in milliseconds',
|
|
325
|
+
boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 })
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
yield* Metric.update(responseTime, 127);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Summary
|
|
332
|
+
|
|
333
|
+
Calculates quantiles over a sliding time window:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
const dbQueryTime = Metric.summary('db_query_duration', {
|
|
337
|
+
maxAge: '5 minutes',
|
|
338
|
+
maxSize: 1000,
|
|
339
|
+
quantiles: [0.5, 0.9, 0.95, 0.99]
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
yield* Metric.update(dbQueryTime, durationMs);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Frequency
|
|
346
|
+
|
|
347
|
+
Counts occurrences of discrete string values:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const statusCodes = Metric.frequency('http_status_codes', {
|
|
351
|
+
description: 'HTTP status code distribution'
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
yield* Metric.update(statusCodes, '200');
|
|
355
|
+
yield* Metric.update(statusCodes, '404');
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Metric Attributes
|
|
359
|
+
|
|
360
|
+
Tag metrics with key-value attributes for filtering/grouping:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
const taggedCounter = Metric.withAttributes(requestCount, {
|
|
364
|
+
endpoint: '/api/users',
|
|
365
|
+
method: 'GET'
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
yield* Metric.update(taggedCounter, 1);
|
|
369
|
+
|
|
370
|
+
// Or inline
|
|
371
|
+
yield*
|
|
372
|
+
Metric.update(
|
|
373
|
+
Metric.withAttributes(requestCount, {
|
|
374
|
+
endpoint: '/api/posts',
|
|
375
|
+
method: 'POST'
|
|
376
|
+
}),
|
|
377
|
+
1
|
|
378
|
+
);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Reading Metric Values
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
// Get current value of a single metric
|
|
385
|
+
const value = yield* Metric.value(requestCount);
|
|
386
|
+
|
|
387
|
+
// Snapshot all metrics
|
|
388
|
+
const snapshots = yield* Metric.snapshot;
|
|
389
|
+
for (const metric of snapshots) {
|
|
390
|
+
console.log(`${metric.id}: ${JSON.stringify(metric.state)}`);
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 5. OTLP Export
|
|
397
|
+
|
|
398
|
+
Effect v4 includes built-in OTLP exporters in `effect/unstable/observability`. No external OpenTelemetry SDK needed.
|
|
399
|
+
|
|
400
|
+
### All-in-One: `Otlp.layerJson`
|
|
401
|
+
|
|
402
|
+
The simplest setup — exports traces, logs, and metrics to a single OTLP endpoint:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { Layer } from 'effect';
|
|
406
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
407
|
+
import { Otlp } from 'effect/unstable/observability';
|
|
408
|
+
|
|
409
|
+
const ObservabilityLayer = Otlp.layerJson({
|
|
410
|
+
baseUrl: 'http://localhost:4318',
|
|
411
|
+
resource: {
|
|
412
|
+
serviceName: 'my-api',
|
|
413
|
+
serviceVersion: '1.0.0',
|
|
414
|
+
attributes: {
|
|
415
|
+
'deployment.environment': 'staging'
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
This creates layers for all three signals with standard OTLP paths (`/v1/traces`, `/v1/logs`, `/v1/metrics`).
|
|
422
|
+
|
|
423
|
+
Variants:
|
|
424
|
+
|
|
425
|
+
- `Otlp.layerJson` — JSON serialization (simplest, no extra deps)
|
|
426
|
+
- `Otlp.layerProtobuf` — Protobuf serialization (more efficient)
|
|
427
|
+
- `Otlp.layer` — requires you to provide `OtlpSerialization` separately
|
|
428
|
+
|
|
429
|
+
### Individual OTLP Exporters
|
|
430
|
+
|
|
431
|
+
For fine-grained control, configure each exporter independently:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import { Layer } from 'effect';
|
|
435
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
436
|
+
import {
|
|
437
|
+
OtlpLogger,
|
|
438
|
+
OtlpMetrics,
|
|
439
|
+
OtlpSerialization,
|
|
440
|
+
OtlpTracer
|
|
441
|
+
} from 'effect/unstable/observability';
|
|
442
|
+
|
|
443
|
+
const OtlpTracingLayer = OtlpTracer.layer({
|
|
444
|
+
url: 'http://localhost:4318/v1/traces',
|
|
445
|
+
resource: {
|
|
446
|
+
serviceName: 'checkout-api',
|
|
447
|
+
serviceVersion: '1.0.0',
|
|
448
|
+
attributes: { 'deployment.environment': 'staging' }
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const OtlpLoggingLayer = OtlpLogger.layer({
|
|
453
|
+
url: 'http://localhost:4318/v1/logs',
|
|
454
|
+
resource: {
|
|
455
|
+
serviceName: 'checkout-api',
|
|
456
|
+
serviceVersion: '1.0.0'
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const OtlpMetricsLayer = OtlpMetrics.layer({
|
|
461
|
+
url: 'http://localhost:4318/v1/metrics',
|
|
462
|
+
resource: {
|
|
463
|
+
serviceName: 'checkout-api',
|
|
464
|
+
serviceVersion: '1.0.0'
|
|
465
|
+
},
|
|
466
|
+
temporality: 'delta' // or "cumulative" (default)
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const ObservabilityLayer = Layer.mergeAll(
|
|
470
|
+
OtlpTracingLayer,
|
|
471
|
+
OtlpLoggingLayer,
|
|
472
|
+
OtlpMetricsLayer
|
|
473
|
+
).pipe(
|
|
474
|
+
Layer.provide(OtlpSerialization.layerJson),
|
|
475
|
+
Layer.provide(FetchHttpClient.layer)
|
|
476
|
+
);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Manual OTLP Flush
|
|
480
|
+
|
|
481
|
+
Each OTLP signal layer now outputs the shared `OtlpExporter.Flusher` service. Use it before a controlled handoff or shutdown when buffered telemetry must be exported immediately:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import { Effect } from 'effect';
|
|
485
|
+
import { OtlpExporter } from 'effect/unstable/observability';
|
|
486
|
+
|
|
487
|
+
const flushTelemetry = Effect.gen(function* () {
|
|
488
|
+
const flusher = yield* OtlpExporter.Flusher;
|
|
489
|
+
yield* flusher.flush.pipe(Effect.timeoutOption('5 seconds'));
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
All signal layers in the same memoized layer graph share one flusher registry, so one call drains logs, traces, and metrics concurrently. `flush` cannot fail and has no built-in timeout; it waits only for exports it starts, not an export already in flight.
|
|
494
|
+
|
|
495
|
+
### OTLP Layer Options
|
|
496
|
+
|
|
497
|
+
Common options for the individual OTLP exporters (`OtlpLogger.layer`, `OtlpTracer.layer`, `OtlpMetrics.layer`):
|
|
498
|
+
|
|
499
|
+
| Option | Default | Description |
|
|
500
|
+
| ------------------------- | ---------------- | ---------------------------------------- |
|
|
501
|
+
| `url` | required | OTLP endpoint URL |
|
|
502
|
+
| `resource.serviceName` | — | Service name in exported telemetry |
|
|
503
|
+
| `resource.serviceVersion` | — | Service version |
|
|
504
|
+
| `resource.attributes` | — | Additional resource attributes |
|
|
505
|
+
| `headers` | — | HTTP headers for auth etc. |
|
|
506
|
+
| `exportInterval` | signal-specific | How often to flush batches (see below) |
|
|
507
|
+
| `maxBatchSize` | signal-specific | Max items per export batch (see below) |
|
|
508
|
+
| `shutdownTimeout` | `3 seconds` | Timeout for final flush on shutdown |
|
|
509
|
+
|
|
510
|
+
Batch/flush defaults differ by signal:
|
|
511
|
+
|
|
512
|
+
| Exporter | `exportInterval` | `maxBatchSize` | `shutdownTimeout` |
|
|
513
|
+
| ------------------- | ---------------- | ------------------------------ | ----------------- |
|
|
514
|
+
| `OtlpLogger.layer` | `1 second` | `1000` | `3 seconds` |
|
|
515
|
+
| `OtlpTracer.layer` | `5 seconds` | `1000` | `3 seconds` |
|
|
516
|
+
| `OtlpMetrics.layer` | `10 seconds` | disabled (pull-style snapshot) | `3 seconds` |
|
|
517
|
+
|
|
518
|
+
The all-in-one `Otlp.layerJson` / `Otlp.layerProtobuf` use **signal-specific** interval option names instead of a single `exportInterval`: `loggerExportInterval`, `metricsExportInterval`, and `tracerExportInterval` (plus a shared `maxBatchSize`, `shutdownTimeout`, and `metricsTemporality`).
|
|
519
|
+
|
|
520
|
+
`OtlpMetrics.layer` additionally accepts:
|
|
521
|
+
|
|
522
|
+
- `temporality`: `"cumulative"` (default) or `"delta"` — determines how metric values relate to their time interval
|
|
523
|
+
|
|
524
|
+
`OtlpLogger.layer` additionally accepts:
|
|
525
|
+
|
|
526
|
+
- `mergeWithExisting`: `true` (default) — merge with existing loggers instead of replacing
|
|
527
|
+
- `excludeLogSpans`: omit log span annotations from exported logs
|
|
528
|
+
|
|
529
|
+
### Wiring Into Your App
|
|
530
|
+
|
|
531
|
+
Provide the observability layer at the outermost level so all spans and logs are captured:
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
import { NodeRuntime } from '@effect/platform-node';
|
|
535
|
+
import { Layer } from 'effect';
|
|
536
|
+
|
|
537
|
+
const Main = AppLayer.pipe(Layer.provide(ObservabilityLayer));
|
|
538
|
+
|
|
539
|
+
Layer.launch(Main).pipe(NodeRuntime.runMain);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## 6. Prometheus Metrics
|
|
545
|
+
|
|
546
|
+
Export Effect metrics in Prometheus exposition format:
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
import { Effect, Metric } from 'effect';
|
|
550
|
+
import * as PrometheusMetrics from 'effect/unstable/observability/PrometheusMetrics';
|
|
551
|
+
|
|
552
|
+
const program = Effect.gen(function* () {
|
|
553
|
+
const counter = Metric.counter('http_requests_total', {
|
|
554
|
+
description: 'Total HTTP requests'
|
|
555
|
+
});
|
|
556
|
+
yield* Metric.update(counter, 42);
|
|
557
|
+
|
|
558
|
+
// Format metrics as Prometheus text
|
|
559
|
+
const output = yield* PrometheusMetrics.format();
|
|
560
|
+
// # HELP http_requests_total Total HTTP requests
|
|
561
|
+
// # TYPE http_requests_total counter
|
|
562
|
+
// http_requests_total 42
|
|
563
|
+
|
|
564
|
+
// With prefix
|
|
565
|
+
const prefixed = yield* PrometheusMetrics.format({ prefix: 'myapp' });
|
|
566
|
+
// myapp_http_requests_total 42
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Prometheus HTTP Endpoint
|
|
571
|
+
|
|
572
|
+
Automatically register a `/metrics` endpoint on your HTTP router:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
import * as PrometheusMetrics from 'effect/unstable/observability/PrometheusMetrics';
|
|
576
|
+
|
|
577
|
+
// Default: GET /metrics
|
|
578
|
+
const PrometheusLayer = PrometheusMetrics.layerHttp();
|
|
579
|
+
|
|
580
|
+
// Custom path and prefix
|
|
581
|
+
const CustomPrometheusLayer = PrometheusMetrics.layerHttp({
|
|
582
|
+
path: '/prometheus/metrics',
|
|
583
|
+
prefix: 'myapp'
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
`layerHttp` requires `HttpRouter.HttpRouter` in the context — it adds a route to your existing router.
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## 7. Testing Observability
|
|
592
|
+
|
|
593
|
+
### Testing Logged Output
|
|
594
|
+
|
|
595
|
+
Use `TestConsole` from `@effect/vitest` to capture and assert on console output:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
import { Effect } from 'effect';
|
|
599
|
+
import { it } from '@effect/vitest';
|
|
600
|
+
|
|
601
|
+
it.effect('logs checkout flow', () =>
|
|
602
|
+
Effect.gen(function* () {
|
|
603
|
+
yield* myLoggingEffect;
|
|
604
|
+
// TestConsole captures log output for assertion
|
|
605
|
+
})
|
|
606
|
+
);
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Testing Metrics
|
|
610
|
+
|
|
611
|
+
Read metric values directly:
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
it.effect('increments request counter', () =>
|
|
615
|
+
Effect.gen(function* () {
|
|
616
|
+
const counter = Metric.counter('test_requests');
|
|
617
|
+
yield* Metric.update(counter, 5);
|
|
618
|
+
const state = yield* Metric.value(counter);
|
|
619
|
+
expect(state.count).toBe(5);
|
|
620
|
+
})
|
|
621
|
+
);
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Testing Spans
|
|
625
|
+
|
|
626
|
+
Use a test tracer or assert on span attributes captured via `Effect.withSpan`.
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## 8. Common Patterns
|
|
631
|
+
|
|
632
|
+
### Full Observability Stack (Dev + Prod)
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
import { Config, Effect, Layer, Logger, References } from 'effect';
|
|
636
|
+
import { FetchHttpClient } from 'effect/unstable/http';
|
|
637
|
+
import { Otlp } from 'effect/unstable/observability';
|
|
638
|
+
|
|
639
|
+
const DevObservability = Logger.layer([Logger.consolePretty()]);
|
|
640
|
+
|
|
641
|
+
const ProdObservability = Layer.mergeAll(
|
|
642
|
+
Logger.layer([Logger.consoleJson]),
|
|
643
|
+
Layer.succeed(References.MinimumLogLevel, 'Info'),
|
|
644
|
+
Otlp.layerJson({
|
|
645
|
+
baseUrl: 'http://otel-collector:4318',
|
|
646
|
+
resource: {
|
|
647
|
+
serviceName: 'my-api',
|
|
648
|
+
serviceVersion: '1.0.0'
|
|
649
|
+
}
|
|
650
|
+
}).pipe(Layer.provide(FetchHttpClient.layer))
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
const ObservabilityLayer = Layer.unwrap(
|
|
654
|
+
Effect.gen(function* () {
|
|
655
|
+
const env = yield* Config.string('NODE_ENV').pipe(
|
|
656
|
+
Config.withDefault('development')
|
|
657
|
+
);
|
|
658
|
+
return env === 'production' ? ProdObservability : DevObservability;
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Service with Instrumented Methods
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
import { Effect, Layer, Metric, Context } from 'effect';
|
|
667
|
+
|
|
668
|
+
const requestLatency = Metric.histogram('checkout_latency_ms', {
|
|
669
|
+
boundaries: Metric.linearBoundaries({ start: 0, width: 25, count: 40 })
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
class Checkout extends Context.Service<
|
|
673
|
+
Checkout,
|
|
674
|
+
{
|
|
675
|
+
processCheckout(orderId: string): Effect.Effect<void>;
|
|
676
|
+
}
|
|
677
|
+
>()('app/Checkout') {
|
|
678
|
+
static readonly layer = Layer.effect(
|
|
679
|
+
Checkout,
|
|
680
|
+
Effect.gen(function* () {
|
|
681
|
+
return Checkout.of({
|
|
682
|
+
processCheckout: Effect.fn('Checkout.processCheckout')(
|
|
683
|
+
function* (orderId: string) {
|
|
684
|
+
yield* Effect.logInfo('starting checkout', { orderId });
|
|
685
|
+
yield* Effect.annotateCurrentSpan('order.id', orderId);
|
|
686
|
+
|
|
687
|
+
yield* chargeCard(orderId).pipe(
|
|
688
|
+
Effect.withSpan('checkout.charge-card')
|
|
689
|
+
);
|
|
690
|
+
yield* persistOrder(orderId).pipe(
|
|
691
|
+
Effect.withSpan('checkout.persist-order')
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
yield* Effect.logInfo('checkout completed', {
|
|
695
|
+
orderId
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
});
|
|
700
|
+
})
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Critical Rules
|
|
708
|
+
|
|
709
|
+
1. **Use `Logger.layer` to install loggers** — it replaces the default set. Use `{ mergeWithExisting: true }` to add without replacing.
|
|
710
|
+
2. **`Logger.tracerLogger` is included by default** — log messages automatically become span events. If you override loggers, include it explicitly if you want this behavior.
|
|
711
|
+
3. **`Effect.fn("name")` creates auto-spans** — prefer this over manual `Effect.withSpan` for service methods.
|
|
712
|
+
4. **Provide observability layers outermost** — so all application spans and logs are captured for export.
|
|
713
|
+
5. **OTLP exporters require `HttpClient` and `OtlpSerialization`** — `Otlp.layerJson` (and `layerProtobuf`) wires `OtlpSerialization` for you but still requires an `HttpClient`, so provide a client layer such as `FetchHttpClient.layer`. With `Otlp.layer` or the individual exporters, provide both `OtlpSerialization.layerJson` and `FetchHttpClient.layer` manually.
|
|
714
|
+
6. **`References.MinimumLogLevel`** controls filtering — not a logger concern, set it via `Layer.succeed`.
|
|
715
|
+
7. **Metric names should follow conventions** — snake_case with units suffix (e.g., `http_request_duration_ms`).
|
|
716
|
+
8. **`Metric.withAttributes` creates a tagged variant** — it does not mutate the original metric.
|
|
717
|
+
9. **`OtlpMetrics` temporality** — use `"delta"` for backends like Datadog/Dynatrace, `"cumulative"` (default) for Prometheus-style backends.
|
|
718
|
+
10. **All OTLP modules are under `effect/unstable/observability`** — the API may evolve but the patterns are stable.
|
|
719
|
+
11. **Use `OtlpExporter.Flusher` for manual drains** — bound `flusher.flush` with `Effect.timeoutOption` when shutdown latency must be capped.
|