@dojocho/effect-ts 0.0.1
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/DOJO.md +22 -0
- package/dojo.json +50 -0
- package/katas/001-hello-effect/SENSEI.md +72 -0
- package/katas/001-hello-effect/solution.test.ts +35 -0
- package/katas/001-hello-effect/solution.ts +16 -0
- package/katas/002-transform-with-map/SENSEI.md +72 -0
- package/katas/002-transform-with-map/solution.test.ts +33 -0
- package/katas/002-transform-with-map/solution.ts +16 -0
- package/katas/003-generator-pipelines/SENSEI.md +72 -0
- package/katas/003-generator-pipelines/solution.test.ts +40 -0
- package/katas/003-generator-pipelines/solution.ts +29 -0
- package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
- package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
- package/katas/004-flatmap-and-chaining/solution.ts +18 -0
- package/katas/005-pipe-composition/SENSEI.md +81 -0
- package/katas/005-pipe-composition/solution.test.ts +41 -0
- package/katas/005-pipe-composition/solution.ts +19 -0
- package/katas/006-handle-errors/SENSEI.md +86 -0
- package/katas/006-handle-errors/solution.test.ts +53 -0
- package/katas/006-handle-errors/solution.ts +30 -0
- package/katas/007-tagged-errors/SENSEI.md +79 -0
- package/katas/007-tagged-errors/solution.test.ts +82 -0
- package/katas/007-tagged-errors/solution.ts +37 -0
- package/katas/008-error-patterns/SENSEI.md +89 -0
- package/katas/008-error-patterns/solution.test.ts +41 -0
- package/katas/008-error-patterns/solution.ts +38 -0
- package/katas/009-option-type/SENSEI.md +96 -0
- package/katas/009-option-type/solution.test.ts +49 -0
- package/katas/009-option-type/solution.ts +26 -0
- package/katas/010-either-and-exit/SENSEI.md +86 -0
- package/katas/010-either-and-exit/solution.test.ts +33 -0
- package/katas/010-either-and-exit/solution.ts +17 -0
- package/katas/011-services-and-context/SENSEI.md +82 -0
- package/katas/011-services-and-context/solution.test.ts +23 -0
- package/katas/011-services-and-context/solution.ts +17 -0
- package/katas/012-layers/SENSEI.md +73 -0
- package/katas/012-layers/solution.test.ts +23 -0
- package/katas/012-layers/solution.ts +26 -0
- package/katas/013-testing-effects/SENSEI.md +88 -0
- package/katas/013-testing-effects/solution.test.ts +41 -0
- package/katas/013-testing-effects/solution.ts +20 -0
- package/katas/014-schema-basics/SENSEI.md +81 -0
- package/katas/014-schema-basics/solution.test.ts +35 -0
- package/katas/014-schema-basics/solution.ts +25 -0
- package/katas/015-domain-modeling/SENSEI.md +85 -0
- package/katas/015-domain-modeling/solution.test.ts +46 -0
- package/katas/015-domain-modeling/solution.ts +42 -0
- package/katas/016-retry-and-schedule/SENSEI.md +72 -0
- package/katas/016-retry-and-schedule/solution.test.ts +26 -0
- package/katas/016-retry-and-schedule/solution.ts +23 -0
- package/katas/017-parallel-effects/SENSEI.md +70 -0
- package/katas/017-parallel-effects/solution.test.ts +33 -0
- package/katas/017-parallel-effects/solution.ts +17 -0
- package/katas/018-race-and-timeout/SENSEI.md +75 -0
- package/katas/018-race-and-timeout/solution.test.ts +30 -0
- package/katas/018-race-and-timeout/solution.ts +27 -0
- package/katas/019-ref-and-state/SENSEI.md +72 -0
- package/katas/019-ref-and-state/solution.test.ts +29 -0
- package/katas/019-ref-and-state/solution.ts +16 -0
- package/katas/020-fibers/SENSEI.md +80 -0
- package/katas/020-fibers/solution.test.ts +23 -0
- package/katas/020-fibers/solution.ts +23 -0
- package/katas/021-acquire-release/SENSEI.md +57 -0
- package/katas/021-acquire-release/solution.test.ts +23 -0
- package/katas/021-acquire-release/solution.ts +22 -0
- package/katas/022-scoped-layers/SENSEI.md +52 -0
- package/katas/022-scoped-layers/solution.test.ts +35 -0
- package/katas/022-scoped-layers/solution.ts +19 -0
- package/katas/023-resource-patterns/SENSEI.md +52 -0
- package/katas/023-resource-patterns/solution.test.ts +20 -0
- package/katas/023-resource-patterns/solution.ts +13 -0
- package/katas/024-streams-basics/SENSEI.md +61 -0
- package/katas/024-streams-basics/solution.test.ts +30 -0
- package/katas/024-streams-basics/solution.ts +16 -0
- package/katas/025-stream-operations/SENSEI.md +59 -0
- package/katas/025-stream-operations/solution.test.ts +26 -0
- package/katas/025-stream-operations/solution.ts +17 -0
- package/katas/026-combining-streams/SENSEI.md +54 -0
- package/katas/026-combining-streams/solution.test.ts +20 -0
- package/katas/026-combining-streams/solution.ts +16 -0
- package/katas/027-data-pipelines/SENSEI.md +58 -0
- package/katas/027-data-pipelines/solution.test.ts +22 -0
- package/katas/027-data-pipelines/solution.ts +16 -0
- package/katas/028-logging-and-spans/SENSEI.md +58 -0
- package/katas/028-logging-and-spans/solution.test.ts +50 -0
- package/katas/028-logging-and-spans/solution.ts +20 -0
- package/katas/029-http-client/SENSEI.md +59 -0
- package/katas/029-http-client/solution.test.ts +49 -0
- package/katas/029-http-client/solution.ts +24 -0
- package/katas/030-capstone/SENSEI.md +63 -0
- package/katas/030-capstone/solution.test.ts +67 -0
- package/katas/030-capstone/solution.ts +55 -0
- package/katas/031-config-and-environment/SENSEI.md +77 -0
- package/katas/031-config-and-environment/solution.test.ts +38 -0
- package/katas/031-config-and-environment/solution.ts +11 -0
- package/katas/032-cause-and-defects/SENSEI.md +90 -0
- package/katas/032-cause-and-defects/solution.test.ts +50 -0
- package/katas/032-cause-and-defects/solution.ts +23 -0
- package/katas/033-pattern-matching/SENSEI.md +86 -0
- package/katas/033-pattern-matching/solution.test.ts +36 -0
- package/katas/033-pattern-matching/solution.ts +28 -0
- package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
- package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
- package/katas/034-deferred-and-coordination/solution.ts +24 -0
- package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
- package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
- package/katas/035-queue-and-backpressure/solution.ts +21 -0
- package/katas/036-schema-advanced/SENSEI.md +81 -0
- package/katas/036-schema-advanced/solution.test.ts +55 -0
- package/katas/036-schema-advanced/solution.ts +19 -0
- package/katas/037-cache-and-memoization/SENSEI.md +73 -0
- package/katas/037-cache-and-memoization/solution.test.ts +47 -0
- package/katas/037-cache-and-memoization/solution.ts +24 -0
- package/katas/038-metrics/SENSEI.md +91 -0
- package/katas/038-metrics/solution.test.ts +39 -0
- package/katas/038-metrics/solution.ts +23 -0
- package/katas/039-managed-runtime/SENSEI.md +75 -0
- package/katas/039-managed-runtime/solution.test.ts +29 -0
- package/katas/039-managed-runtime/solution.ts +19 -0
- package/katas/040-request-batching/SENSEI.md +87 -0
- package/katas/040-request-batching/solution.test.ts +56 -0
- package/katas/040-request-batching/solution.ts +32 -0
- package/package.json +22 -0
- package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
- package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
- package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
- package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
- package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
- package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
- package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
- package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
- package/skills/effect-patterns-error-management/SKILL.md +1668 -0
- package/skills/effect-patterns-getting-started/SKILL.md +237 -0
- package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
- package/skills/effect-patterns-observability/SKILL.md +1586 -0
- package/skills/effect-patterns-platform/SKILL.md +1195 -0
- package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
- package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
- package/skills/effect-patterns-resource-management/SKILL.md +827 -0
- package/skills/effect-patterns-scheduling/SKILL.md +451 -0
- package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
- package/skills/effect-patterns-streams/SKILL.md +2052 -0
- package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
- package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
- package/skills/effect-patterns-testing/SKILL.md +1632 -0
- package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
- package/skills/effect-patterns-value-handling/SKILL.md +676 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,1586 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-patterns-observability
|
|
3
|
+
description: Effect-TS patterns for Observability. Use when working with observability in Effect-TS applications.
|
|
4
|
+
---
|
|
5
|
+
# Effect-TS Patterns: Observability
|
|
6
|
+
This skill provides 13 curated Effect-TS patterns for observability.
|
|
7
|
+
Use this skill when working on tasks related to:
|
|
8
|
+
- observability
|
|
9
|
+
- Best practices in Effect-TS applications
|
|
10
|
+
- Real-world patterns and solutions
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🟢 Beginner Patterns
|
|
15
|
+
|
|
16
|
+
### Debug Effect Programs
|
|
17
|
+
|
|
18
|
+
**Rule:** Use Effect.tap and logging to inspect values without changing program flow.
|
|
19
|
+
|
|
20
|
+
**Good Example:**
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Effect, pipe } from "effect"
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// 1. Using tap to inspect values
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
const fetchUser = (id: string) =>
|
|
30
|
+
Effect.succeed({ id, name: "Alice", email: "alice@example.com" })
|
|
31
|
+
|
|
32
|
+
const processUser = (id: string) =>
|
|
33
|
+
fetchUser(id).pipe(
|
|
34
|
+
// tap runs an effect for its side effect, then continues with original value
|
|
35
|
+
Effect.tap((user) => Effect.log(`Fetched user: ${user.name}`)),
|
|
36
|
+
Effect.map((user) => ({ ...user, processed: true })),
|
|
37
|
+
Effect.tap((user) => Effect.log(`Processed: ${JSON.stringify(user)}`))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// ============================================
|
|
41
|
+
// 2. Debug a pipeline
|
|
42
|
+
// ============================================
|
|
43
|
+
|
|
44
|
+
const numbers = [1, 2, 3, 4, 5]
|
|
45
|
+
|
|
46
|
+
const pipeline = Effect.gen(function* () {
|
|
47
|
+
yield* Effect.log("Starting pipeline")
|
|
48
|
+
|
|
49
|
+
const step1 = numbers.filter((n) => n % 2 === 0)
|
|
50
|
+
yield* Effect.log(`After filter (even): ${JSON.stringify(step1)}`)
|
|
51
|
+
|
|
52
|
+
const step2 = step1.map((n) => n * 10)
|
|
53
|
+
yield* Effect.log(`After map (*10): ${JSON.stringify(step2)}`)
|
|
54
|
+
|
|
55
|
+
const step3 = step2.reduce((a, b) => a + b, 0)
|
|
56
|
+
yield* Effect.log(`After reduce (sum): ${step3}`)
|
|
57
|
+
|
|
58
|
+
return step3
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// ============================================
|
|
62
|
+
// 3. Debug errors
|
|
63
|
+
// ============================================
|
|
64
|
+
|
|
65
|
+
const riskyOperation = (shouldFail: boolean) =>
|
|
66
|
+
Effect.gen(function* () {
|
|
67
|
+
yield* Effect.log("Starting risky operation")
|
|
68
|
+
|
|
69
|
+
if (shouldFail) {
|
|
70
|
+
yield* Effect.log("About to fail...")
|
|
71
|
+
return yield* Effect.fail(new Error("Something went wrong"))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
yield* Effect.log("Success!")
|
|
75
|
+
return "result"
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const debugErrors = riskyOperation(true).pipe(
|
|
79
|
+
// Log when operation fails
|
|
80
|
+
Effect.tapError((error) => Effect.log(`Operation failed: ${error.message}`)),
|
|
81
|
+
|
|
82
|
+
// Provide a fallback
|
|
83
|
+
Effect.catchAll((error) => {
|
|
84
|
+
return Effect.succeed(`Recovered from: ${error.message}`)
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// ============================================
|
|
89
|
+
// 4. Trace execution flow
|
|
90
|
+
// ============================================
|
|
91
|
+
|
|
92
|
+
const step = (name: string, value: number) =>
|
|
93
|
+
Effect.gen(function* () {
|
|
94
|
+
yield* Effect.log(`[${name}] Input: ${value}`)
|
|
95
|
+
const result = value * 2
|
|
96
|
+
yield* Effect.log(`[${name}] Output: ${result}`)
|
|
97
|
+
return result
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const tracedWorkflow = Effect.gen(function* () {
|
|
101
|
+
const a = yield* step("Step 1", 5)
|
|
102
|
+
const b = yield* step("Step 2", a)
|
|
103
|
+
const c = yield* step("Step 3", b)
|
|
104
|
+
yield* Effect.log(`Final result: ${c}`)
|
|
105
|
+
return c
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// ============================================
|
|
109
|
+
// 5. Quick debug with console
|
|
110
|
+
// ============================================
|
|
111
|
+
|
|
112
|
+
// Sometimes you just need console.log
|
|
113
|
+
const quickDebug = Effect.gen(function* () {
|
|
114
|
+
const value = yield* Effect.succeed(42)
|
|
115
|
+
|
|
116
|
+
// Effect.sync wraps side effects
|
|
117
|
+
yield* Effect.sync(() => console.log("Quick debug:", value))
|
|
118
|
+
|
|
119
|
+
return value
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// ============================================
|
|
123
|
+
// 6. Run examples
|
|
124
|
+
// ============================================
|
|
125
|
+
|
|
126
|
+
const program = Effect.gen(function* () {
|
|
127
|
+
yield* Effect.log("=== Tap Example ===")
|
|
128
|
+
yield* processUser("123")
|
|
129
|
+
|
|
130
|
+
yield* Effect.log("\n=== Pipeline Debug ===")
|
|
131
|
+
yield* pipeline
|
|
132
|
+
|
|
133
|
+
yield* Effect.log("\n=== Error Debug ===")
|
|
134
|
+
yield* debugErrors
|
|
135
|
+
|
|
136
|
+
yield* Effect.log("\n=== Traced Workflow ===")
|
|
137
|
+
yield* tracedWorkflow
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
Effect.runPromise(program)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Rationale:**
|
|
144
|
+
|
|
145
|
+
Use `Effect.tap` to inspect values and `Effect.log` to trace execution without changing program behavior.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
Debugging Effect code differs from imperative code:
|
|
151
|
+
|
|
152
|
+
1. **No breakpoints** - Effects are descriptions, not executions
|
|
153
|
+
2. **Lazy evaluation** - Code runs later when you call `runPromise`
|
|
154
|
+
3. **Composition** - Effects chain together
|
|
155
|
+
|
|
156
|
+
`tap` and logging let you see inside without breaking the chain.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### Your First Logs
|
|
163
|
+
|
|
164
|
+
**Rule:** Use Effect.log and related functions for structured, contextual logging.
|
|
165
|
+
|
|
166
|
+
**Good Example:**
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { Effect, Logger, LogLevel } from "effect"
|
|
170
|
+
|
|
171
|
+
// ============================================
|
|
172
|
+
// 1. Basic logging
|
|
173
|
+
// ============================================
|
|
174
|
+
|
|
175
|
+
const basicLogging = Effect.gen(function* () {
|
|
176
|
+
// Different log levels
|
|
177
|
+
yield* Effect.logDebug("Debug message - for development")
|
|
178
|
+
yield* Effect.logInfo("Info message - normal operation")
|
|
179
|
+
yield* Effect.log("Default log - same as logInfo")
|
|
180
|
+
yield* Effect.logWarning("Warning - something unusual")
|
|
181
|
+
yield* Effect.logError("Error - something went wrong")
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// ============================================
|
|
185
|
+
// 2. Logging with context
|
|
186
|
+
// ============================================
|
|
187
|
+
|
|
188
|
+
const withContext = Effect.gen(function* () {
|
|
189
|
+
// Add structured data to logs
|
|
190
|
+
yield* Effect.log("User logged in").pipe(
|
|
191
|
+
Effect.annotateLogs({
|
|
192
|
+
userId: "user-123",
|
|
193
|
+
action: "login",
|
|
194
|
+
ipAddress: "192.168.1.1",
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Add a single annotation
|
|
199
|
+
yield* Effect.log("Processing request").pipe(
|
|
200
|
+
Effect.annotateLogs("requestId", "req-456")
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// ============================================
|
|
205
|
+
// 3. Log spans for timing
|
|
206
|
+
// ============================================
|
|
207
|
+
|
|
208
|
+
const withTiming = Effect.gen(function* () {
|
|
209
|
+
yield* Effect.log("Starting operation")
|
|
210
|
+
|
|
211
|
+
// withLogSpan adds timing information
|
|
212
|
+
yield* Effect.sleep("100 millis").pipe(
|
|
213
|
+
Effect.withLogSpan("database-query")
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
yield* Effect.log("Operation complete")
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// ============================================
|
|
220
|
+
// 4. Practical example
|
|
221
|
+
// ============================================
|
|
222
|
+
|
|
223
|
+
interface User {
|
|
224
|
+
id: string
|
|
225
|
+
email: string
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const processOrder = (orderId: string, userId: string) =>
|
|
229
|
+
Effect.gen(function* () {
|
|
230
|
+
yield* Effect.logInfo("Processing order").pipe(
|
|
231
|
+
Effect.annotateLogs({ orderId, userId })
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Simulate work
|
|
235
|
+
yield* Effect.sleep("50 millis")
|
|
236
|
+
|
|
237
|
+
yield* Effect.logInfo("Order processed successfully").pipe(
|
|
238
|
+
Effect.annotateLogs({ orderId, status: "completed" })
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return { orderId, status: "completed" }
|
|
242
|
+
}).pipe(
|
|
243
|
+
Effect.withLogSpan("processOrder")
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// ============================================
|
|
247
|
+
// 5. Configure log level
|
|
248
|
+
// ============================================
|
|
249
|
+
|
|
250
|
+
const debugProgram = basicLogging.pipe(
|
|
251
|
+
// Show all logs including debug
|
|
252
|
+
Logger.withMinimumLogLevel(LogLevel.Debug)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const productionProgram = basicLogging.pipe(
|
|
256
|
+
// Only show warnings and errors
|
|
257
|
+
Logger.withMinimumLogLevel(LogLevel.Warning)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
// ============================================
|
|
261
|
+
// 6. Run
|
|
262
|
+
// ============================================
|
|
263
|
+
|
|
264
|
+
const program = Effect.gen(function* () {
|
|
265
|
+
yield* Effect.log("=== Basic Logging ===")
|
|
266
|
+
yield* basicLogging
|
|
267
|
+
|
|
268
|
+
yield* Effect.log("\n=== With Context ===")
|
|
269
|
+
yield* withContext
|
|
270
|
+
|
|
271
|
+
yield* Effect.log("\n=== With Timing ===")
|
|
272
|
+
yield* withTiming
|
|
273
|
+
|
|
274
|
+
yield* Effect.log("\n=== Process Order ===")
|
|
275
|
+
yield* processOrder("order-789", "user-123")
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
Effect.runPromise(program)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Rationale:**
|
|
282
|
+
|
|
283
|
+
Use Effect's built-in logging functions for structured, contextual logging that works with any logging backend.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
Effect's logging is superior to `console.log`:
|
|
289
|
+
|
|
290
|
+
1. **Structured** - Logs are data, not just strings
|
|
291
|
+
2. **Contextual** - Automatically includes fiber info, timestamps
|
|
292
|
+
3. **Configurable** - Change log levels, formats, destinations
|
|
293
|
+
4. **Type-safe** - Part of the Effect type system
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
## 🟡 Intermediate Patterns
|
|
301
|
+
|
|
302
|
+
### Instrument and Observe Function Calls with Effect.fn
|
|
303
|
+
|
|
304
|
+
**Rule:** Use Effect.fn to wrap functions with effectful instrumentation, such as logging, metrics, or tracing, in a composable and type-safe way.
|
|
305
|
+
|
|
306
|
+
**Good Example:**
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { Effect } from "effect";
|
|
310
|
+
|
|
311
|
+
// A simple function to instrument
|
|
312
|
+
function add(a: number, b: number): number {
|
|
313
|
+
return a + b;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Use Effect.fn to instrument the function with observability
|
|
317
|
+
const addWithLogging = Effect.fn("add")(add).pipe(
|
|
318
|
+
Effect.withSpan("add", { attributes: { "fn.name": "add" } })
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Use the instrumented function in an Effect workflow
|
|
322
|
+
const program = Effect.gen(function* () {
|
|
323
|
+
yield* Effect.logInfo("Calling add function");
|
|
324
|
+
const sum = yield* addWithLogging(2, 3);
|
|
325
|
+
yield* Effect.logInfo(`Sum is ${sum}`);
|
|
326
|
+
return sum;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Run the program
|
|
330
|
+
Effect.runPromise(program);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Explanation:**
|
|
334
|
+
|
|
335
|
+
- `Effect.fn("name")(fn)` wraps a function with instrumentation capabilities, enabling observability.
|
|
336
|
+
- You can add tracing spans, logging, metrics, and other observability logic to function boundaries.
|
|
337
|
+
- Keeps instrumentation separate from business logic and fully composable.
|
|
338
|
+
- The wrapped function integrates seamlessly with Effect's observability and tracing infrastructure.
|
|
339
|
+
|
|
340
|
+
**Anti-Pattern:**
|
|
341
|
+
|
|
342
|
+
Scattering logging, metrics, or tracing logic directly inside business functions, making code harder to test, maintain, and compose.
|
|
343
|
+
|
|
344
|
+
**Rationale:**
|
|
345
|
+
|
|
346
|
+
Use `Effect.fn` to wrap and instrument function calls with effectful logic, such as logging, metrics, or tracing.
|
|
347
|
+
This enables you to observe, monitor, and debug function boundaries in a composable, type-safe way.
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
Instrumenting function calls is essential for observability, especially in complex or critical code paths.
|
|
351
|
+
`Effect.fn` lets you add effectful logic (logging, metrics, tracing, etc.) before, after, or around any function call, without changing the function’s core logic.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
### Leverage Effect's Built-in Structured Logging
|
|
356
|
+
|
|
357
|
+
**Rule:** Use Effect.log, Effect.logInfo, and Effect.logError to add structured, context-aware logging to your Effect code.
|
|
358
|
+
|
|
359
|
+
**Good Example:**
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { Effect } from "effect";
|
|
363
|
+
|
|
364
|
+
// Log a simple message
|
|
365
|
+
const program = Effect.gen(function* () {
|
|
366
|
+
yield* Effect.log("Starting the application");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Log at different levels
|
|
370
|
+
const infoProgram = Effect.gen(function* () {
|
|
371
|
+
yield* Effect.logInfo("User signed in");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const errorProgram = Effect.gen(function* () {
|
|
375
|
+
yield* Effect.logError("Failed to connect to database");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Log with dynamic values
|
|
379
|
+
const userId = 42;
|
|
380
|
+
const logUserProgram = Effect.gen(function* () {
|
|
381
|
+
yield* Effect.logInfo(`Processing user: ${userId}`);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Use logging in a workflow
|
|
385
|
+
const workflow = Effect.gen(function* () {
|
|
386
|
+
yield* Effect.log("Beginning workflow");
|
|
387
|
+
// ... do some work
|
|
388
|
+
yield* Effect.logInfo("Workflow step completed");
|
|
389
|
+
// ... handle errors
|
|
390
|
+
yield* Effect.logError("Something went wrong");
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Explanation:**
|
|
395
|
+
|
|
396
|
+
- `Effect.log` logs a message at the default level.
|
|
397
|
+
- `Effect.logInfo` and `Effect.logError` log at specific levels.
|
|
398
|
+
- Logging is context-aware and can be used anywhere in your Effect workflows.
|
|
399
|
+
|
|
400
|
+
**Anti-Pattern:**
|
|
401
|
+
|
|
402
|
+
Using `console.log` or ad-hoc logging scattered throughout your code, which is not structured, not context-aware, and harder to manage in production.
|
|
403
|
+
|
|
404
|
+
**Rationale:**
|
|
405
|
+
|
|
406
|
+
Use `Effect.log`, `Effect.logInfo`, `Effect.logError`, and related functions to add structured, context-aware logging to your Effect code.
|
|
407
|
+
This enables you to capture important events, errors, and business information in a consistent and configurable way.
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
Structured logging makes it easier to search, filter, and analyze logs in production.
|
|
411
|
+
Effect’s logging functions are context-aware, meaning they automatically include relevant metadata and can be configured globally.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### Add Custom Metrics to Your Application
|
|
416
|
+
|
|
417
|
+
**Rule:** Use Metric.counter, Metric.gauge, and Metric.histogram to instrument code for monitoring.
|
|
418
|
+
|
|
419
|
+
**Good Example:**
|
|
420
|
+
|
|
421
|
+
This example creates a counter to track how many times a user is created and a histogram to track the duration of the database operation.
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { Effect, Metric, Duration } from "effect"; // We don't need MetricBoundaries anymore
|
|
425
|
+
|
|
426
|
+
// 1. Define your metrics
|
|
427
|
+
const userRegisteredCounter = Metric.counter("users_registered_total", {
|
|
428
|
+
description: "A counter for how many users have been registered.",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const dbDurationTimer = Metric.timer(
|
|
432
|
+
"db_operation_duration",
|
|
433
|
+
"A timer for DB operation durations"
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// 2. Simulated database call
|
|
437
|
+
const saveUserToDb = Effect.succeed("user saved").pipe(
|
|
438
|
+
Effect.delay(Duration.millis(Math.random() * 100))
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// 3. Instrument the business logic
|
|
442
|
+
const createUser = Effect.gen(function* () {
|
|
443
|
+
// Time the operation
|
|
444
|
+
yield* saveUserToDb.pipe(Metric.trackDuration(dbDurationTimer));
|
|
445
|
+
|
|
446
|
+
// Increment the counter
|
|
447
|
+
yield* Metric.increment(userRegisteredCounter);
|
|
448
|
+
|
|
449
|
+
return { status: "success" };
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Run the Effect
|
|
453
|
+
const programWithLogging = Effect.gen(function* () {
|
|
454
|
+
const result = yield* createUser;
|
|
455
|
+
yield* Effect.log(`Result: ${JSON.stringify(result)}`);
|
|
456
|
+
return result;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
Effect.runPromise(programWithLogging);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
**Anti-Pattern:**
|
|
465
|
+
|
|
466
|
+
Not adding any metrics to your application. Without metrics, you are flying blind. You have no high-level overview of your application's health, performance, or business KPIs. You can't build dashboards, you can't set up alerts for abnormal behavior (e.g., "error rate is too high"), and you are forced to rely on digging through logs to
|
|
467
|
+
understand the state of your system.
|
|
468
|
+
|
|
469
|
+
**Rationale:**
|
|
470
|
+
|
|
471
|
+
To monitor the health and performance of your application, instrument your code with `Metric`s. The three main types are:
|
|
472
|
+
|
|
473
|
+
- **`Metric.counter("name")`**: To count occurrences of an event (e.g., `users_registered_total`). It only goes up.
|
|
474
|
+
- **`Metric.gauge("name")`**: To track a value that can go up or down (e.g., `active_connections`).
|
|
475
|
+
- **`Metric.histogram("name")`**: To track the distribution of a value (e.g., `request_duration_seconds`).
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
While logs are for events and traces are for requests, metrics are for aggregation. They provide a high-level, numerical view of your system's health over time, which is perfect for building dashboards and setting up alerts.
|
|
481
|
+
|
|
482
|
+
Effect's `Metric` module provides a simple, declarative way to add this instrumentation. By defining your metrics upfront, you can then use operators like `Metric.increment` or `Effect.timed` to update them. This is fully integrated with Effect's context system, allowing you to provide different metric backends (like Prometheus or StatsD) via a `Layer`.
|
|
483
|
+
|
|
484
|
+
This allows you to answer questions like:
|
|
485
|
+
|
|
486
|
+
- "What is our user sign-up rate over the last 24 hours?"
|
|
487
|
+
- "Are we approaching our maximum number of database connections?"
|
|
488
|
+
- "What is the 95th percentile latency for our API requests?"
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
### Add Custom Metrics to Your Application
|
|
495
|
+
|
|
496
|
+
**Rule:** Use Effect's Metric module to define and update custom metrics for business and performance monitoring.
|
|
497
|
+
|
|
498
|
+
**Good Example:**
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
import { Effect, Metric } from "effect";
|
|
502
|
+
|
|
503
|
+
// Define a counter metric for processed jobs
|
|
504
|
+
const jobsProcessed = Metric.counter("jobs_processed");
|
|
505
|
+
|
|
506
|
+
// Increment the counter when a job is processed
|
|
507
|
+
const processJob = Effect.gen(function* () {
|
|
508
|
+
// ... process the job
|
|
509
|
+
yield* Effect.log("Job processed");
|
|
510
|
+
yield* Metric.increment(jobsProcessed);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Define a gauge for current active users
|
|
514
|
+
const activeUsers = Metric.gauge("active_users");
|
|
515
|
+
|
|
516
|
+
// Update the gauge when users sign in or out
|
|
517
|
+
const userSignedIn = Metric.set(activeUsers, 1); // Set to 1 (simplified example)
|
|
518
|
+
const userSignedOut = Metric.set(activeUsers, 0); // Set to 0 (simplified example)
|
|
519
|
+
|
|
520
|
+
// Define a histogram for request durations
|
|
521
|
+
const requestDuration = Metric.histogram("request_duration", [
|
|
522
|
+
0.1, 0.5, 1, 2, 5,
|
|
523
|
+
] as any); // boundaries in seconds
|
|
524
|
+
|
|
525
|
+
// Record a request duration
|
|
526
|
+
const recordDuration = (duration: number) =>
|
|
527
|
+
Metric.update(requestDuration, duration);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**Explanation:**
|
|
531
|
+
|
|
532
|
+
- `Metric.counter` tracks counts of events.
|
|
533
|
+
- `Metric.gauge` tracks a value that can go up or down (e.g., active users).
|
|
534
|
+
- `Metric.histogram` tracks distributions (e.g., request durations).
|
|
535
|
+
- `Effect.updateMetric` updates the metric in your workflow.
|
|
536
|
+
|
|
537
|
+
**Anti-Pattern:**
|
|
538
|
+
|
|
539
|
+
Relying solely on logs for monitoring, or using ad-hoc counters and variables that are not integrated with your observability stack.
|
|
540
|
+
|
|
541
|
+
**Rationale:**
|
|
542
|
+
|
|
543
|
+
Use Effect's `Metric` module to define and update custom metrics such as counters, gauges, and histograms.
|
|
544
|
+
This allows you to track business events, performance indicators, and system health in a type-safe and composable way.
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
Metrics provide quantitative insight into your application's behavior and performance.
|
|
548
|
+
By instrumenting your code with metrics, you can monitor key events, detect anomalies, and drive business decisions.
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
### Trace Operations Across Services with Spans
|
|
553
|
+
|
|
554
|
+
**Rule:** Use Effect.withSpan to create and annotate tracing spans for operations, enabling distributed tracing and performance analysis.
|
|
555
|
+
|
|
556
|
+
**Good Example:**
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
import { Effect } from "effect";
|
|
560
|
+
|
|
561
|
+
// Trace a database query with a custom span
|
|
562
|
+
const fetchUser = Effect.sync(() => {
|
|
563
|
+
// ...fetch user from database
|
|
564
|
+
return { id: 1, name: "Alice" };
|
|
565
|
+
}).pipe(Effect.withSpan("db.fetchUser"));
|
|
566
|
+
|
|
567
|
+
// Trace an HTTP request with additional attributes
|
|
568
|
+
const fetchData = Effect.tryPromise({
|
|
569
|
+
try: () => fetch("https://api.example.com/data").then((res) => res.json()),
|
|
570
|
+
catch: (err) => `Network error: ${String(err)}`,
|
|
571
|
+
}).pipe(
|
|
572
|
+
Effect.withSpan("http.fetchData", {
|
|
573
|
+
attributes: { url: "https://api.example.com/data" },
|
|
574
|
+
})
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// Use spans in a workflow
|
|
578
|
+
const program = Effect.gen(function* () {
|
|
579
|
+
yield* Effect.log("Starting workflow").pipe(
|
|
580
|
+
Effect.withSpan("workflow.start")
|
|
581
|
+
);
|
|
582
|
+
const user = yield* fetchUser;
|
|
583
|
+
yield* Effect.log(`Fetched user: ${user.name}`).pipe(
|
|
584
|
+
Effect.withSpan("workflow.end")
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Explanation:**
|
|
590
|
+
|
|
591
|
+
- `Effect.withSpan` creates a tracing span around an operation.
|
|
592
|
+
- Spans can be named and annotated with attributes for richer context.
|
|
593
|
+
- Tracing enables distributed observability and performance analysis.
|
|
594
|
+
|
|
595
|
+
**Anti-Pattern:**
|
|
596
|
+
|
|
597
|
+
Relying only on logs or metrics for performance analysis, or lacking visibility into the flow of requests and operations across services.
|
|
598
|
+
|
|
599
|
+
**Rationale:**
|
|
600
|
+
|
|
601
|
+
Use `Effect.withSpan` to create custom tracing spans around important operations in your application.
|
|
602
|
+
This enables distributed tracing, performance analysis, and deep visibility into how requests flow through your system.
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
Tracing spans help you understand the flow and timing of operations, especially in distributed systems or complex workflows.
|
|
606
|
+
They allow you to pinpoint bottlenecks, visualize dependencies, and correlate logs and metrics with specific requests.
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
### Trace Operations Across Services with Spans
|
|
611
|
+
|
|
612
|
+
**Rule:** Use Effect.withSpan to create custom tracing spans for important operations.
|
|
613
|
+
|
|
614
|
+
**Good Example:**
|
|
615
|
+
|
|
616
|
+
This example shows a multi-step operation. Each step, and the overall operation, is wrapped in a span. This creates a parent-child hierarchy in the trace that is easy to visualize.
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
import { Effect, Duration } from "effect";
|
|
620
|
+
|
|
621
|
+
const validateInput = (input: unknown) =>
|
|
622
|
+
Effect.gen(function* () {
|
|
623
|
+
yield* Effect.logInfo("Starting input validation...");
|
|
624
|
+
yield* Effect.sleep(Duration.millis(10));
|
|
625
|
+
const result = { email: "paul@example.com" };
|
|
626
|
+
yield* Effect.logInfo(`âś… Input validated: ${result.email}`);
|
|
627
|
+
return result;
|
|
628
|
+
}).pipe(
|
|
629
|
+
// This creates a child span
|
|
630
|
+
Effect.withSpan("validateInput")
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const saveToDatabase = (user: { email: string }) =>
|
|
634
|
+
Effect.gen(function* () {
|
|
635
|
+
yield* Effect.logInfo(`Saving user to database: ${user.email}`);
|
|
636
|
+
yield* Effect.sleep(Duration.millis(50));
|
|
637
|
+
const result = { id: 123, ...user };
|
|
638
|
+
yield* Effect.logInfo(`âś… User saved with ID: ${result.id}`);
|
|
639
|
+
return result;
|
|
640
|
+
}).pipe(
|
|
641
|
+
// This span includes useful attributes
|
|
642
|
+
Effect.withSpan("saveToDatabase", {
|
|
643
|
+
attributes: { "db.system": "postgresql", "db.user.email": user.email },
|
|
644
|
+
})
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const createUser = (input: unknown) =>
|
|
648
|
+
Effect.gen(function* () {
|
|
649
|
+
yield* Effect.logInfo("=== Creating User with Tracing ===");
|
|
650
|
+
yield* Effect.logInfo(
|
|
651
|
+
"This demonstrates how spans trace operations through the call stack"
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const validated = yield* validateInput(input);
|
|
655
|
+
const user = yield* saveToDatabase(validated);
|
|
656
|
+
|
|
657
|
+
yield* Effect.logInfo(
|
|
658
|
+
`âś… User creation completed: ${JSON.stringify(user)}`
|
|
659
|
+
);
|
|
660
|
+
yield* Effect.logInfo(
|
|
661
|
+
"Note: In production, spans would be sent to a tracing system like Jaeger or Zipkin"
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
return user;
|
|
665
|
+
}).pipe(
|
|
666
|
+
// This is the parent span for the entire operation
|
|
667
|
+
Effect.withSpan("createUserOperation")
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
// Demonstrate the tracing functionality
|
|
671
|
+
const program = Effect.gen(function* () {
|
|
672
|
+
yield* Effect.logInfo("=== Trace Operations with Spans Demo ===");
|
|
673
|
+
|
|
674
|
+
// Create multiple users to show tracing in action
|
|
675
|
+
const user1 = yield* createUser({ email: "user1@example.com" });
|
|
676
|
+
|
|
677
|
+
yield* Effect.logInfo("\n--- Creating second user ---");
|
|
678
|
+
const user2 = yield* createUser({ email: "user2@example.com" });
|
|
679
|
+
|
|
680
|
+
yield* Effect.logInfo("\n=== Summary ===");
|
|
681
|
+
yield* Effect.logInfo("Created users with tracing spans:");
|
|
682
|
+
yield* Effect.logInfo(`User 1: ID ${user1.id}, Email: ${user1.email}`);
|
|
683
|
+
yield* Effect.logInfo(`User 2: ID ${user2.id}, Email: ${user2.email}`);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// When run with a tracing SDK, this will produce traces with root spans
|
|
687
|
+
// "createUserOperation" and child spans: "validateInput" and "saveToDatabase".
|
|
688
|
+
Effect.runPromise(program);
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
**Anti-Pattern:**
|
|
694
|
+
|
|
695
|
+
Not adding custom spans to your business logic.
|
|
696
|
+
Without them, your traces will only show high-level information from your framework (e.g., "HTTP POST /users").
|
|
697
|
+
You will have no visibility into the performance of the individual steps _inside_ your request handler, making it very difficult to pinpoint bottlenecks. Your application's logic remains a "black box" in your traces.
|
|
698
|
+
|
|
699
|
+
**Rationale:**
|
|
700
|
+
|
|
701
|
+
To gain visibility into the performance and flow of your application, wrap logical units of work with `Effect.withSpan("span-name")`. You can add contextual information to these spans using the `attributes` option.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
While logs tell you _what_ happened, traces tell you _why it was slow_. In a complex application, a single user request might trigger calls to multiple services (authentication, database, external APIs). Tracing allows you to visualize this entire chain of events as a single, hierarchical "trace."
|
|
707
|
+
|
|
708
|
+
Each piece of work in that trace is a `span`. `Effect.withSpan` allows you to create your own custom spans. This is invaluable for answering questions like:
|
|
709
|
+
|
|
710
|
+
- "For this API request, did we spend most of our time in the database or calling the external payment gateway?"
|
|
711
|
+
- "Which part of our user creation logic is the bottleneck?"
|
|
712
|
+
|
|
713
|
+
Effect's tracing is built on OpenTelemetry, the industry standard, so it integrates seamlessly with tools like Jaeger, Zipkin, and Datadog.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
## đźź Advanced Patterns
|
|
721
|
+
|
|
722
|
+
### Create Observability Dashboards
|
|
723
|
+
|
|
724
|
+
**Rule:** Create focused dashboards that answer specific questions about system health.
|
|
725
|
+
|
|
726
|
+
**Rationale:**
|
|
727
|
+
|
|
728
|
+
Design dashboards that answer specific questions about system health, performance, and user experience.
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
Good dashboards provide:
|
|
734
|
+
|
|
735
|
+
1. **Quick health check** - See problems at a glance
|
|
736
|
+
2. **Trend analysis** - Spot gradual degradation
|
|
737
|
+
3. **Debugging aid** - Correlate metrics during incidents
|
|
738
|
+
4. **Capacity planning** - Forecast resource needs
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
### Set Up Alerting
|
|
745
|
+
|
|
746
|
+
**Rule:** Create alerts based on SLOs and symptoms, not causes.
|
|
747
|
+
|
|
748
|
+
**Good Example:**
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
import { Effect, Metric, Schedule, Duration, Ref } from "effect"
|
|
752
|
+
|
|
753
|
+
// ============================================
|
|
754
|
+
// 1. Define alertable conditions
|
|
755
|
+
// ============================================
|
|
756
|
+
|
|
757
|
+
interface Alert {
|
|
758
|
+
readonly name: string
|
|
759
|
+
readonly severity: "critical" | "warning" | "info"
|
|
760
|
+
readonly message: string
|
|
761
|
+
readonly timestamp: Date
|
|
762
|
+
readonly labels: Record<string, string>
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
interface AlertRule {
|
|
766
|
+
readonly name: string
|
|
767
|
+
readonly condition: Effect.Effect<boolean>
|
|
768
|
+
readonly severity: "critical" | "warning" | "info"
|
|
769
|
+
readonly message: string
|
|
770
|
+
readonly labels: Record<string, string>
|
|
771
|
+
readonly forDuration: Duration.DurationInput
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ============================================
|
|
775
|
+
// 2. Define alert rules
|
|
776
|
+
// ============================================
|
|
777
|
+
|
|
778
|
+
const createAlertRules = (metrics: {
|
|
779
|
+
errorRate: () => Effect.Effect<number>
|
|
780
|
+
latencyP99: () => Effect.Effect<number>
|
|
781
|
+
availability: () => Effect.Effect<number>
|
|
782
|
+
}): AlertRule[] => [
|
|
783
|
+
{
|
|
784
|
+
name: "HighErrorRate",
|
|
785
|
+
condition: metrics.errorRate().pipe(Effect.map((rate) => rate > 0.01)),
|
|
786
|
+
severity: "critical",
|
|
787
|
+
message: "Error rate exceeds 1%",
|
|
788
|
+
labels: { team: "backend", service: "api" },
|
|
789
|
+
forDuration: "5 minutes",
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: "HighLatency",
|
|
793
|
+
condition: metrics.latencyP99().pipe(Effect.map((p99) => p99 > 2)),
|
|
794
|
+
severity: "warning",
|
|
795
|
+
message: "P99 latency exceeds 2 seconds",
|
|
796
|
+
labels: { team: "backend", service: "api" },
|
|
797
|
+
forDuration: "10 minutes",
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: "LowAvailability",
|
|
801
|
+
condition: metrics.availability().pipe(Effect.map((avail) => avail < 99.9)),
|
|
802
|
+
severity: "critical",
|
|
803
|
+
message: "Availability below 99.9% SLO",
|
|
804
|
+
labels: { team: "backend", service: "api" },
|
|
805
|
+
forDuration: "5 minutes",
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
name: "ErrorBudgetLow",
|
|
809
|
+
condition: Effect.succeed(false), // Implement based on error budget calc
|
|
810
|
+
severity: "warning",
|
|
811
|
+
message: "Error budget below 25%",
|
|
812
|
+
labels: { team: "backend", service: "api" },
|
|
813
|
+
forDuration: "0 seconds",
|
|
814
|
+
},
|
|
815
|
+
]
|
|
816
|
+
|
|
817
|
+
// ============================================
|
|
818
|
+
// 3. Alert manager
|
|
819
|
+
// ============================================
|
|
820
|
+
|
|
821
|
+
interface AlertState {
|
|
822
|
+
readonly firing: Map<string, { since: Date; alert: Alert }>
|
|
823
|
+
readonly resolved: Alert[]
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const makeAlertManager = Effect.gen(function* () {
|
|
827
|
+
const state = yield* Ref.make<AlertState>({
|
|
828
|
+
firing: new Map(),
|
|
829
|
+
resolved: [],
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
const checkRule = (rule: AlertRule) =>
|
|
833
|
+
Effect.gen(function* () {
|
|
834
|
+
const isTriggered = yield* rule.condition
|
|
835
|
+
|
|
836
|
+
yield* Ref.modify(state, (s) => {
|
|
837
|
+
const firing = new Map(s.firing)
|
|
838
|
+
const resolved = [...s.resolved]
|
|
839
|
+
const key = rule.name
|
|
840
|
+
|
|
841
|
+
if (isTriggered) {
|
|
842
|
+
if (!firing.has(key)) {
|
|
843
|
+
// New alert
|
|
844
|
+
firing.set(key, {
|
|
845
|
+
since: new Date(),
|
|
846
|
+
alert: {
|
|
847
|
+
name: rule.name,
|
|
848
|
+
severity: rule.severity,
|
|
849
|
+
message: rule.message,
|
|
850
|
+
timestamp: new Date(),
|
|
851
|
+
labels: rule.labels,
|
|
852
|
+
},
|
|
853
|
+
})
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
if (firing.has(key)) {
|
|
857
|
+
// Alert resolved
|
|
858
|
+
const prev = firing.get(key)!
|
|
859
|
+
resolved.push({
|
|
860
|
+
...prev.alert,
|
|
861
|
+
message: `[RESOLVED] ${prev.alert.message}`,
|
|
862
|
+
timestamp: new Date(),
|
|
863
|
+
})
|
|
864
|
+
firing.delete(key)
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return [undefined, { firing, resolved }]
|
|
869
|
+
})
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const getActiveAlerts = () =>
|
|
873
|
+
Ref.get(state).pipe(
|
|
874
|
+
Effect.map((s) => Array.from(s.firing.values()).map((f) => f.alert))
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
const getRecentResolved = () =>
|
|
878
|
+
Ref.get(state).pipe(Effect.map((s) => s.resolved.slice(-10)))
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
checkRule,
|
|
882
|
+
getActiveAlerts,
|
|
883
|
+
getRecentResolved,
|
|
884
|
+
}
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
// ============================================
|
|
888
|
+
// 4. Alert notification
|
|
889
|
+
// ============================================
|
|
890
|
+
|
|
891
|
+
interface NotificationChannel {
|
|
892
|
+
readonly send: (alert: Alert) => Effect.Effect<void>
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const slackChannel: NotificationChannel = {
|
|
896
|
+
send: (alert) =>
|
|
897
|
+
Effect.gen(function* () {
|
|
898
|
+
const emoji =
|
|
899
|
+
alert.severity === "critical"
|
|
900
|
+
? "đź”´"
|
|
901
|
+
: alert.severity === "warning"
|
|
902
|
+
? "🟡"
|
|
903
|
+
: "🔵"
|
|
904
|
+
|
|
905
|
+
yield* Effect.log(`${emoji} [${alert.severity.toUpperCase()}] ${alert.name}`).pipe(
|
|
906
|
+
Effect.annotateLogs({
|
|
907
|
+
message: alert.message,
|
|
908
|
+
labels: JSON.stringify(alert.labels),
|
|
909
|
+
})
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
// In real implementation: call Slack API
|
|
913
|
+
}),
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const pagerDutyChannel: NotificationChannel = {
|
|
917
|
+
send: (alert) =>
|
|
918
|
+
Effect.gen(function* () {
|
|
919
|
+
if (alert.severity === "critical") {
|
|
920
|
+
yield* Effect.log("PagerDuty: Creating incident").pipe(
|
|
921
|
+
Effect.annotateLogs({ alert: alert.name })
|
|
922
|
+
)
|
|
923
|
+
// In real implementation: call PagerDuty API
|
|
924
|
+
}
|
|
925
|
+
}),
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ============================================
|
|
929
|
+
// 5. Alert evaluation loop
|
|
930
|
+
// ============================================
|
|
931
|
+
|
|
932
|
+
const runAlertEvaluation = (
|
|
933
|
+
rules: AlertRule[],
|
|
934
|
+
channels: NotificationChannel[],
|
|
935
|
+
interval: Duration.DurationInput
|
|
936
|
+
) =>
|
|
937
|
+
Effect.gen(function* () {
|
|
938
|
+
const alertManager = yield* makeAlertManager
|
|
939
|
+
const previousAlerts = yield* Ref.make(new Set<string>())
|
|
940
|
+
|
|
941
|
+
yield* Effect.forever(
|
|
942
|
+
Effect.gen(function* () {
|
|
943
|
+
// Check all rules
|
|
944
|
+
for (const rule of rules) {
|
|
945
|
+
yield* alertManager.checkRule(rule)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Get current active alerts
|
|
949
|
+
const active = yield* alertManager.getActiveAlerts()
|
|
950
|
+
const current = new Set(active.map((a) => a.name))
|
|
951
|
+
const previous = yield* Ref.get(previousAlerts)
|
|
952
|
+
|
|
953
|
+
// Find newly firing alerts
|
|
954
|
+
for (const alert of active) {
|
|
955
|
+
if (!previous.has(alert.name)) {
|
|
956
|
+
// New alert - send notifications
|
|
957
|
+
for (const channel of channels) {
|
|
958
|
+
yield* channel.send(alert)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
yield* Ref.set(previousAlerts, current)
|
|
964
|
+
yield* Effect.sleep(interval)
|
|
965
|
+
})
|
|
966
|
+
)
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
// ============================================
|
|
970
|
+
// 6. Prometheus alerting rules (YAML)
|
|
971
|
+
// ============================================
|
|
972
|
+
|
|
973
|
+
const prometheusAlertRules = `
|
|
974
|
+
groups:
|
|
975
|
+
- name: effect-app-alerts
|
|
976
|
+
rules:
|
|
977
|
+
- alert: HighErrorRate
|
|
978
|
+
expr: |
|
|
979
|
+
sum(rate(http_errors_total[5m]))
|
|
980
|
+
/
|
|
981
|
+
sum(rate(http_requests_total[5m]))
|
|
982
|
+
> 0.01
|
|
983
|
+
for: 5m
|
|
984
|
+
labels:
|
|
985
|
+
severity: critical
|
|
986
|
+
annotations:
|
|
987
|
+
summary: "High error rate detected"
|
|
988
|
+
description: "Error rate is {{ $value | humanizePercentage }}"
|
|
989
|
+
|
|
990
|
+
- alert: HighLatency
|
|
991
|
+
expr: |
|
|
992
|
+
histogram_quantile(0.99,
|
|
993
|
+
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
|
|
994
|
+
) > 2
|
|
995
|
+
for: 10m
|
|
996
|
+
labels:
|
|
997
|
+
severity: warning
|
|
998
|
+
annotations:
|
|
999
|
+
summary: "High P99 latency"
|
|
1000
|
+
description: "P99 latency is {{ $value }}s"
|
|
1001
|
+
|
|
1002
|
+
- alert: SLOViolation
|
|
1003
|
+
expr: |
|
|
1004
|
+
sum(rate(http_requests_total{status!~"5.."}[30m]))
|
|
1005
|
+
/
|
|
1006
|
+
sum(rate(http_requests_total[30m]))
|
|
1007
|
+
< 0.999
|
|
1008
|
+
for: 5m
|
|
1009
|
+
labels:
|
|
1010
|
+
severity: critical
|
|
1011
|
+
annotations:
|
|
1012
|
+
summary: "SLO violation"
|
|
1013
|
+
description: "Availability is {{ $value | humanizePercentage }}"
|
|
1014
|
+
`
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**Rationale:**
|
|
1018
|
+
|
|
1019
|
+
Set up alerts based on user-facing symptoms (SLO violations) rather than system metrics (CPU usage).
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
Good alerting:
|
|
1025
|
+
|
|
1026
|
+
1. **Catches real problems** - Alerts when users are affected
|
|
1027
|
+
2. **Reduces noise** - Fewer false positives
|
|
1028
|
+
3. **Enables response** - Actionable information
|
|
1029
|
+
4. **Supports SLOs** - Tracks service level objectives
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
---
|
|
1034
|
+
|
|
1035
|
+
### Export Metrics to Prometheus
|
|
1036
|
+
|
|
1037
|
+
**Rule:** Use Effect metrics and expose a /metrics endpoint for Prometheus scraping.
|
|
1038
|
+
|
|
1039
|
+
**Good Example:**
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
import { Effect, Metric, MetricLabel, Duration } from "effect"
|
|
1043
|
+
import { HttpServerResponse } from "@effect/platform"
|
|
1044
|
+
|
|
1045
|
+
// ============================================
|
|
1046
|
+
// 1. Define application metrics
|
|
1047
|
+
// ============================================
|
|
1048
|
+
|
|
1049
|
+
// Counter - counts events
|
|
1050
|
+
const httpRequestsTotal = Metric.counter("http_requests_total", {
|
|
1051
|
+
description: "Total number of HTTP requests",
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
// Counter with labels
|
|
1055
|
+
const httpRequestsByStatus = Metric.counter("http_requests_by_status", {
|
|
1056
|
+
description: "HTTP requests by status code",
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
// Gauge - current value
|
|
1060
|
+
const activeConnections = Metric.gauge("active_connections", {
|
|
1061
|
+
description: "Number of active connections",
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
// Histogram - distribution of values
|
|
1065
|
+
const requestDuration = Metric.histogram("http_request_duration_seconds", {
|
|
1066
|
+
description: "HTTP request duration in seconds",
|
|
1067
|
+
boundaries: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
// Summary - percentiles
|
|
1071
|
+
const responseSizeBytes = Metric.summary("http_response_size_bytes", {
|
|
1072
|
+
description: "HTTP response size in bytes",
|
|
1073
|
+
maxAge: Duration.minutes(5),
|
|
1074
|
+
maxSize: 100,
|
|
1075
|
+
quantiles: [0.5, 0.9, 0.99],
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
// ============================================
|
|
1079
|
+
// 2. Instrument code with metrics
|
|
1080
|
+
// ============================================
|
|
1081
|
+
|
|
1082
|
+
const handleRequest = (path: string, status: number) =>
|
|
1083
|
+
Effect.gen(function* () {
|
|
1084
|
+
const startTime = Date.now()
|
|
1085
|
+
|
|
1086
|
+
// Increment request counter
|
|
1087
|
+
yield* Metric.increment(httpRequestsTotal)
|
|
1088
|
+
|
|
1089
|
+
// Increment with labels
|
|
1090
|
+
yield* Metric.increment(
|
|
1091
|
+
httpRequestsByStatus.pipe(
|
|
1092
|
+
Metric.tagged("status", String(status)),
|
|
1093
|
+
Metric.tagged("path", path)
|
|
1094
|
+
)
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
// Track active connections
|
|
1098
|
+
yield* Metric.increment(activeConnections)
|
|
1099
|
+
|
|
1100
|
+
// Simulate work
|
|
1101
|
+
yield* Effect.sleep("100 millis")
|
|
1102
|
+
|
|
1103
|
+
// Record duration
|
|
1104
|
+
const duration = (Date.now() - startTime) / 1000
|
|
1105
|
+
yield* Metric.update(requestDuration, duration)
|
|
1106
|
+
|
|
1107
|
+
// Record response size
|
|
1108
|
+
yield* Metric.update(responseSizeBytes, 1024)
|
|
1109
|
+
|
|
1110
|
+
// Decrement active connections
|
|
1111
|
+
yield* Metric.decrement(activeConnections)
|
|
1112
|
+
})
|
|
1113
|
+
|
|
1114
|
+
// ============================================
|
|
1115
|
+
// 3. Prometheus text format exporter
|
|
1116
|
+
// ============================================
|
|
1117
|
+
|
|
1118
|
+
interface MetricSnapshot {
|
|
1119
|
+
name: string
|
|
1120
|
+
type: "counter" | "gauge" | "histogram" | "summary"
|
|
1121
|
+
help: string
|
|
1122
|
+
values: Array<{
|
|
1123
|
+
labels: Record<string, string>
|
|
1124
|
+
value: number
|
|
1125
|
+
}>
|
|
1126
|
+
// For histograms
|
|
1127
|
+
buckets?: Array<{
|
|
1128
|
+
le: number
|
|
1129
|
+
count: number
|
|
1130
|
+
labels?: Record<string, string>
|
|
1131
|
+
}>
|
|
1132
|
+
sum?: number
|
|
1133
|
+
count?: number
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const formatPrometheusMetrics = (metrics: MetricSnapshot[]): string => {
|
|
1137
|
+
const lines: string[] = []
|
|
1138
|
+
|
|
1139
|
+
for (const metric of metrics) {
|
|
1140
|
+
// Help line
|
|
1141
|
+
lines.push(`# HELP ${metric.name} ${metric.help}`)
|
|
1142
|
+
lines.push(`# TYPE ${metric.name} ${metric.type}`)
|
|
1143
|
+
|
|
1144
|
+
// Values
|
|
1145
|
+
for (const { labels, value } of metric.values) {
|
|
1146
|
+
const labelStr = Object.entries(labels)
|
|
1147
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
1148
|
+
.join(",")
|
|
1149
|
+
|
|
1150
|
+
if (labelStr) {
|
|
1151
|
+
lines.push(`${metric.name}{${labelStr}} ${value}`)
|
|
1152
|
+
} else {
|
|
1153
|
+
lines.push(`${metric.name} ${value}`)
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Histogram buckets
|
|
1158
|
+
if (metric.buckets) {
|
|
1159
|
+
for (const bucket of metric.buckets) {
|
|
1160
|
+
const labelStr = Object.entries(bucket.labels || {})
|
|
1161
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
1162
|
+
.concat([`le="${bucket.le}"`])
|
|
1163
|
+
.join(",")
|
|
1164
|
+
lines.push(`${metric.name}_bucket{${labelStr}} ${bucket.count}`)
|
|
1165
|
+
}
|
|
1166
|
+
lines.push(`${metric.name}_sum ${metric.sum}`)
|
|
1167
|
+
lines.push(`${metric.name}_count ${metric.count}`)
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
lines.push("")
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return lines.join("\n")
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ============================================
|
|
1177
|
+
// 4. /metrics endpoint handler
|
|
1178
|
+
// ============================================
|
|
1179
|
+
|
|
1180
|
+
const metricsHandler = Effect.gen(function* () {
|
|
1181
|
+
// In real implementation, read from Effect's MetricRegistry
|
|
1182
|
+
const metrics: MetricSnapshot[] = [
|
|
1183
|
+
{
|
|
1184
|
+
name: "http_requests_total",
|
|
1185
|
+
type: "counter",
|
|
1186
|
+
help: "Total number of HTTP requests",
|
|
1187
|
+
values: [{ labels: {}, value: 1234 }],
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
name: "http_requests_by_status",
|
|
1191
|
+
type: "counter",
|
|
1192
|
+
help: "HTTP requests by status code",
|
|
1193
|
+
values: [
|
|
1194
|
+
{ labels: { status: "200", path: "/api/users" }, value: 1000 },
|
|
1195
|
+
{ labels: { status: "404", path: "/api/users" }, value: 50 },
|
|
1196
|
+
{ labels: { status: "500", path: "/api/users" }, value: 10 },
|
|
1197
|
+
],
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
name: "active_connections",
|
|
1201
|
+
type: "gauge",
|
|
1202
|
+
help: "Number of active connections",
|
|
1203
|
+
values: [{ labels: {}, value: 42 }],
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: "http_request_duration_seconds",
|
|
1207
|
+
type: "histogram",
|
|
1208
|
+
help: "HTTP request duration in seconds",
|
|
1209
|
+
values: [],
|
|
1210
|
+
buckets: [
|
|
1211
|
+
{ le: 0.01, count: 100 },
|
|
1212
|
+
{ le: 0.05, count: 500 },
|
|
1213
|
+
{ le: 0.1, count: 800 },
|
|
1214
|
+
{ le: 0.25, count: 950 },
|
|
1215
|
+
{ le: 0.5, count: 990 },
|
|
1216
|
+
{ le: 1, count: 999 },
|
|
1217
|
+
{ le: Infinity, count: 1000 },
|
|
1218
|
+
],
|
|
1219
|
+
sum: 123.456,
|
|
1220
|
+
count: 1000,
|
|
1221
|
+
},
|
|
1222
|
+
]
|
|
1223
|
+
|
|
1224
|
+
const body = formatPrometheusMetrics(metrics)
|
|
1225
|
+
|
|
1226
|
+
return HttpServerResponse.text(body, {
|
|
1227
|
+
headers: {
|
|
1228
|
+
"Content-Type": "text/plain; version=0.0.4; charset=utf-8",
|
|
1229
|
+
},
|
|
1230
|
+
})
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
// ============================================
|
|
1234
|
+
// 5. Example output
|
|
1235
|
+
// ============================================
|
|
1236
|
+
|
|
1237
|
+
/*
|
|
1238
|
+
# HELP http_requests_total Total number of HTTP requests
|
|
1239
|
+
# TYPE http_requests_total counter
|
|
1240
|
+
http_requests_total 1234
|
|
1241
|
+
|
|
1242
|
+
# HELP http_requests_by_status HTTP requests by status code
|
|
1243
|
+
# TYPE http_requests_by_status counter
|
|
1244
|
+
http_requests_by_status{status="200",path="/api/users"} 1000
|
|
1245
|
+
http_requests_by_status{status="404",path="/api/users"} 50
|
|
1246
|
+
http_requests_by_status{status="500",path="/api/users"} 10
|
|
1247
|
+
|
|
1248
|
+
# HELP active_connections Number of active connections
|
|
1249
|
+
# TYPE active_connections gauge
|
|
1250
|
+
active_connections 42
|
|
1251
|
+
|
|
1252
|
+
# HELP http_request_duration_seconds HTTP request duration in seconds
|
|
1253
|
+
# TYPE http_request_duration_seconds histogram
|
|
1254
|
+
http_request_duration_seconds_bucket{le="0.01"} 100
|
|
1255
|
+
http_request_duration_seconds_bucket{le="0.05"} 500
|
|
1256
|
+
http_request_duration_seconds_bucket{le="0.1"} 800
|
|
1257
|
+
http_request_duration_seconds_bucket{le="+Inf"} 1000
|
|
1258
|
+
http_request_duration_seconds_sum 123.456
|
|
1259
|
+
http_request_duration_seconds_count 1000
|
|
1260
|
+
*/
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
**Rationale:**
|
|
1264
|
+
|
|
1265
|
+
Create metrics with Effect's Metric API and expose them via an HTTP endpoint in Prometheus text format.
|
|
1266
|
+
|
|
1267
|
+
---
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
Prometheus metrics enable:
|
|
1271
|
+
|
|
1272
|
+
1. **Real-time monitoring** - See what's happening now
|
|
1273
|
+
2. **Historical analysis** - Track trends over time
|
|
1274
|
+
3. **Alerting** - Get notified of issues
|
|
1275
|
+
4. **Dashboards** - Visualize system health
|
|
1276
|
+
|
|
1277
|
+
---
|
|
1278
|
+
|
|
1279
|
+
---
|
|
1280
|
+
|
|
1281
|
+
### Implement Distributed Tracing
|
|
1282
|
+
|
|
1283
|
+
**Rule:** Propagate trace context across service boundaries to correlate requests.
|
|
1284
|
+
|
|
1285
|
+
**Good Example:**
|
|
1286
|
+
|
|
1287
|
+
```typescript
|
|
1288
|
+
import { Effect, Context, Layer } from "effect"
|
|
1289
|
+
import { HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "@effect/platform"
|
|
1290
|
+
|
|
1291
|
+
// ============================================
|
|
1292
|
+
// 1. Define trace context
|
|
1293
|
+
// ============================================
|
|
1294
|
+
|
|
1295
|
+
interface TraceContext {
|
|
1296
|
+
readonly traceId: string
|
|
1297
|
+
readonly spanId: string
|
|
1298
|
+
readonly parentSpanId?: string
|
|
1299
|
+
readonly sampled: boolean
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
class CurrentTrace extends Context.Tag("CurrentTrace")<
|
|
1303
|
+
CurrentTrace,
|
|
1304
|
+
TraceContext
|
|
1305
|
+
>() {}
|
|
1306
|
+
|
|
1307
|
+
// W3C Trace Context header names
|
|
1308
|
+
const TRACEPARENT_HEADER = "traceparent"
|
|
1309
|
+
const TRACESTATE_HEADER = "tracestate"
|
|
1310
|
+
|
|
1311
|
+
// ============================================
|
|
1312
|
+
// 2. Generate trace IDs
|
|
1313
|
+
// ============================================
|
|
1314
|
+
|
|
1315
|
+
const generateTraceId = (): string =>
|
|
1316
|
+
Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
1317
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
1318
|
+
.join("")
|
|
1319
|
+
|
|
1320
|
+
const generateSpanId = (): string =>
|
|
1321
|
+
Array.from(crypto.getRandomValues(new Uint8Array(8)))
|
|
1322
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
1323
|
+
.join("")
|
|
1324
|
+
|
|
1325
|
+
// ============================================
|
|
1326
|
+
// 3. Parse and format trace context
|
|
1327
|
+
// ============================================
|
|
1328
|
+
|
|
1329
|
+
const parseTraceparent = (header: string): TraceContext | null => {
|
|
1330
|
+
// Format: 00-traceId-spanId-flags
|
|
1331
|
+
const parts = header.split("-")
|
|
1332
|
+
if (parts.length !== 4) return null
|
|
1333
|
+
|
|
1334
|
+
return {
|
|
1335
|
+
traceId: parts[1],
|
|
1336
|
+
spanId: generateSpanId(), // New span for this service
|
|
1337
|
+
parentSpanId: parts[2],
|
|
1338
|
+
sampled: parts[3] === "01",
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const formatTraceparent = (ctx: TraceContext): string =>
|
|
1343
|
+
`00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? "01" : "00"}`
|
|
1344
|
+
|
|
1345
|
+
// ============================================
|
|
1346
|
+
// 4. Extract trace from incoming request
|
|
1347
|
+
// ============================================
|
|
1348
|
+
|
|
1349
|
+
const extractTraceContext = Effect.gen(function* () {
|
|
1350
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
1351
|
+
|
|
1352
|
+
const traceparent = request.headers[TRACEPARENT_HEADER]
|
|
1353
|
+
|
|
1354
|
+
if (traceparent) {
|
|
1355
|
+
const parsed = parseTraceparent(traceparent)
|
|
1356
|
+
if (parsed) {
|
|
1357
|
+
yield* Effect.log("Extracted trace context").pipe(
|
|
1358
|
+
Effect.annotateLogs({
|
|
1359
|
+
traceId: parsed.traceId,
|
|
1360
|
+
parentSpanId: parsed.parentSpanId,
|
|
1361
|
+
})
|
|
1362
|
+
)
|
|
1363
|
+
return parsed
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// No incoming trace - start a new one
|
|
1368
|
+
const newTrace: TraceContext = {
|
|
1369
|
+
traceId: generateTraceId(),
|
|
1370
|
+
spanId: generateSpanId(),
|
|
1371
|
+
sampled: Math.random() < 0.1, // 10% sampling
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
yield* Effect.log("Started new trace").pipe(
|
|
1375
|
+
Effect.annotateLogs({ traceId: newTrace.traceId })
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
return newTrace
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
// ============================================
|
|
1382
|
+
// 5. Propagate trace to outgoing requests
|
|
1383
|
+
// ============================================
|
|
1384
|
+
|
|
1385
|
+
const makeTracedHttpClient = Effect.gen(function* () {
|
|
1386
|
+
const baseClient = yield* HttpClient.HttpClient
|
|
1387
|
+
const trace = yield* CurrentTrace
|
|
1388
|
+
|
|
1389
|
+
return {
|
|
1390
|
+
get: (url: string) =>
|
|
1391
|
+
Effect.gen(function* () {
|
|
1392
|
+
// Create child span for outgoing request
|
|
1393
|
+
const childSpan: TraceContext = {
|
|
1394
|
+
traceId: trace.traceId,
|
|
1395
|
+
spanId: generateSpanId(),
|
|
1396
|
+
parentSpanId: trace.spanId,
|
|
1397
|
+
sampled: trace.sampled,
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
yield* Effect.log("Making traced HTTP request").pipe(
|
|
1401
|
+
Effect.annotateLogs({
|
|
1402
|
+
traceId: childSpan.traceId,
|
|
1403
|
+
spanId: childSpan.spanId,
|
|
1404
|
+
url,
|
|
1405
|
+
})
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
const request = HttpClientRequest.get(url).pipe(
|
|
1409
|
+
HttpClientRequest.setHeader(
|
|
1410
|
+
TRACEPARENT_HEADER,
|
|
1411
|
+
formatTraceparent(childSpan)
|
|
1412
|
+
)
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
return yield* baseClient.execute(request)
|
|
1416
|
+
}),
|
|
1417
|
+
}
|
|
1418
|
+
})
|
|
1419
|
+
|
|
1420
|
+
// ============================================
|
|
1421
|
+
// 6. Tracing middleware for HTTP server
|
|
1422
|
+
// ============================================
|
|
1423
|
+
|
|
1424
|
+
const withTracing = <A, E, R>(
|
|
1425
|
+
handler: Effect.Effect<A, E, R | CurrentTrace>
|
|
1426
|
+
): Effect.Effect<A, E, R | HttpServerRequest.HttpServerRequest> =>
|
|
1427
|
+
Effect.gen(function* () {
|
|
1428
|
+
const traceContext = yield* extractTraceContext
|
|
1429
|
+
|
|
1430
|
+
return yield* handler.pipe(
|
|
1431
|
+
Effect.provideService(CurrentTrace, traceContext),
|
|
1432
|
+
Effect.withLogSpan(`request-${traceContext.spanId}`),
|
|
1433
|
+
Effect.annotateLogs({
|
|
1434
|
+
"trace.id": traceContext.traceId,
|
|
1435
|
+
"span.id": traceContext.spanId,
|
|
1436
|
+
"parent.span.id": traceContext.parentSpanId ?? "none",
|
|
1437
|
+
})
|
|
1438
|
+
)
|
|
1439
|
+
})
|
|
1440
|
+
|
|
1441
|
+
// ============================================
|
|
1442
|
+
// 7. Example: Service A calls Service B
|
|
1443
|
+
// ============================================
|
|
1444
|
+
|
|
1445
|
+
// Service B handler
|
|
1446
|
+
const serviceBHandler = withTracing(
|
|
1447
|
+
Effect.gen(function* () {
|
|
1448
|
+
const trace = yield* CurrentTrace
|
|
1449
|
+
yield* Effect.log("Service B processing request")
|
|
1450
|
+
|
|
1451
|
+
// Simulate work
|
|
1452
|
+
yield* Effect.sleep("50 millis")
|
|
1453
|
+
|
|
1454
|
+
return HttpServerResponse.json({
|
|
1455
|
+
message: "Hello from Service B",
|
|
1456
|
+
traceId: trace.traceId,
|
|
1457
|
+
})
|
|
1458
|
+
})
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
// Service A handler (calls Service B)
|
|
1462
|
+
const serviceAHandler = withTracing(
|
|
1463
|
+
Effect.gen(function* () {
|
|
1464
|
+
const trace = yield* CurrentTrace
|
|
1465
|
+
yield* Effect.log("Service A processing request")
|
|
1466
|
+
|
|
1467
|
+
// Call Service B with trace propagation
|
|
1468
|
+
const tracedClient = yield* makeTracedHttpClient
|
|
1469
|
+
const response = yield* tracedClient.get("http://service-b/api/data")
|
|
1470
|
+
|
|
1471
|
+
yield* Effect.log("Service A received response from B")
|
|
1472
|
+
|
|
1473
|
+
return HttpServerResponse.json({
|
|
1474
|
+
message: "Hello from Service A",
|
|
1475
|
+
traceId: trace.traceId,
|
|
1476
|
+
})
|
|
1477
|
+
})
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
// ============================================
|
|
1481
|
+
// 8. Run and observe
|
|
1482
|
+
// ============================================
|
|
1483
|
+
|
|
1484
|
+
const program = Effect.gen(function* () {
|
|
1485
|
+
yield* Effect.log("=== Distributed Tracing Demo ===")
|
|
1486
|
+
|
|
1487
|
+
// Simulate incoming request with trace
|
|
1488
|
+
const incomingTrace: TraceContext = {
|
|
1489
|
+
traceId: generateTraceId(),
|
|
1490
|
+
spanId: generateSpanId(),
|
|
1491
|
+
sampled: true,
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
yield* Effect.log("Processing traced request").pipe(
|
|
1495
|
+
Effect.provideService(CurrentTrace, incomingTrace),
|
|
1496
|
+
Effect.annotateLogs({
|
|
1497
|
+
"trace.id": incomingTrace.traceId,
|
|
1498
|
+
"span.id": incomingTrace.spanId,
|
|
1499
|
+
})
|
|
1500
|
+
)
|
|
1501
|
+
})
|
|
1502
|
+
|
|
1503
|
+
Effect.runPromise(program)
|
|
1504
|
+
```
|
|
1505
|
+
|
|
1506
|
+
**Rationale:**
|
|
1507
|
+
|
|
1508
|
+
Implement distributed tracing by propagating trace context through HTTP headers and using consistent span naming across services.
|
|
1509
|
+
|
|
1510
|
+
---
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
Distributed tracing shows the complete request journey:
|
|
1514
|
+
|
|
1515
|
+
1. **End-to-end visibility** - See entire request flow
|
|
1516
|
+
2. **Latency analysis** - Find slow services
|
|
1517
|
+
3. **Error correlation** - Link errors across services
|
|
1518
|
+
4. **Dependency mapping** - Understand service relationships
|
|
1519
|
+
|
|
1520
|
+
---
|
|
1521
|
+
|
|
1522
|
+
---
|
|
1523
|
+
|
|
1524
|
+
### Integrate Effect Tracing with OpenTelemetry
|
|
1525
|
+
|
|
1526
|
+
**Rule:** Integrate Effect.withSpan with OpenTelemetry to export traces and visualize request flows across services.
|
|
1527
|
+
|
|
1528
|
+
**Good Example:**
|
|
1529
|
+
|
|
1530
|
+
```typescript
|
|
1531
|
+
import { Effect } from "effect";
|
|
1532
|
+
// Pseudocode: Replace with actual OpenTelemetry integration for your stack
|
|
1533
|
+
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
|
|
1534
|
+
|
|
1535
|
+
// Wrap an Effect.withSpan to export to OpenTelemetry
|
|
1536
|
+
function withOtelSpan<T>(
|
|
1537
|
+
name: string,
|
|
1538
|
+
effect: Effect.Effect<unknown, T, unknown>
|
|
1539
|
+
) {
|
|
1540
|
+
return Effect.gen(function* () {
|
|
1541
|
+
const otelSpan = trace.getTracer("default").startSpan(name);
|
|
1542
|
+
try {
|
|
1543
|
+
const result = yield* effect;
|
|
1544
|
+
otelSpan.setStatus({ code: SpanStatusCode.OK });
|
|
1545
|
+
return result;
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
|
|
1548
|
+
throw err;
|
|
1549
|
+
} finally {
|
|
1550
|
+
otelSpan.end();
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// Usage
|
|
1556
|
+
const program = withOtelSpan(
|
|
1557
|
+
"fetchUser",
|
|
1558
|
+
Effect.sync(() => {
|
|
1559
|
+
// ...fetch user logic
|
|
1560
|
+
return { id: 1, name: "Alice" };
|
|
1561
|
+
})
|
|
1562
|
+
);
|
|
1563
|
+
```
|
|
1564
|
+
|
|
1565
|
+
**Explanation:**
|
|
1566
|
+
|
|
1567
|
+
- Start an OpenTelemetry span when entering an Effectful operation.
|
|
1568
|
+
- Set status and attributes as needed.
|
|
1569
|
+
- End the span when the operation completes or fails.
|
|
1570
|
+
- This enables full distributed tracing and visualization in your observability platform.
|
|
1571
|
+
|
|
1572
|
+
**Anti-Pattern:**
|
|
1573
|
+
|
|
1574
|
+
Using Effect.withSpan without exporting to OpenTelemetry, or lacking distributed tracing, which limits your ability to diagnose and visualize complex request flows.
|
|
1575
|
+
|
|
1576
|
+
**Rationale:**
|
|
1577
|
+
|
|
1578
|
+
Connect Effect's tracing spans to OpenTelemetry to enable distributed tracing, visualization, and correlation across your entire stack.
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
OpenTelemetry is the industry standard for distributed tracing.
|
|
1582
|
+
By integrating Effect's spans with OpenTelemetry, you gain deep visibility into request flows, performance bottlenecks, and dependencies—across all your services and infrastructure.
|
|
1583
|
+
|
|
1584
|
+
---
|
|
1585
|
+
|
|
1586
|
+
|