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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. 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.