@danieljvdm/dev-kit 0.11.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -78
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/build-effect-clis/SKILL.md +61 -0
- package/skills/build-effect-clis/agents/openai.yaml +4 -0
- package/skills/build-effect-clis/references/command-design.md +93 -0
- package/skills/build-effect-clis/references/entrypoints-and-testing.md +59 -0
- package/skills/build-effect-clis/references/processes-and-platform.md +73 -0
- package/skills/dev-kit/SKILL.md +77 -48
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +86 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +3 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +18 -16
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +181 -131
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
|
@@ -1,746 +0,0 @@
|
|
|
1
|
-
# Observability Guide
|
|
2
|
-
|
|
3
|
-
This guide covers named operations, spans, structured logging, metrics, and
|
|
4
|
-
telemetry wiring.
|
|
5
|
-
|
|
6
|
-
Key source files:
|
|
7
|
-
|
|
8
|
-
- `packages/effect/src/Effect.ts`
|
|
9
|
-
- `packages/effect/src/Tracer.ts`
|
|
10
|
-
- `packages/effect/src/Logger.ts`
|
|
11
|
-
- `packages/effect/src/Metric.ts`
|
|
12
|
-
- `packages/opentelemetry/src/NodeSdk.ts`
|
|
13
|
-
- `packages/opentelemetry/src/OtelTracer.ts`
|
|
14
|
-
|
|
15
|
-
## Mental Model
|
|
16
|
-
|
|
17
|
-
Observable Effect code should make business operations visible by default.
|
|
18
|
-
|
|
19
|
-
That means:
|
|
20
|
-
|
|
21
|
-
- business logic should show up clearly in stack traces
|
|
22
|
-
- important operations should produce spans
|
|
23
|
-
- logs should inherit execution context
|
|
24
|
-
- metrics should be attached at meaningful boundaries
|
|
25
|
-
|
|
26
|
-
The most important best practice is:
|
|
27
|
-
|
|
28
|
-
- prefer `Effect.fn(...)` whenever possible for business logic
|
|
29
|
-
|
|
30
|
-
Why:
|
|
31
|
-
|
|
32
|
-
- it adds stack frames
|
|
33
|
-
- it creates spans automatically
|
|
34
|
-
- it gives you better tracing and debugging for free
|
|
35
|
-
- it keeps business logic observable without extra boilerplate
|
|
36
|
-
|
|
37
|
-
Repo reference:
|
|
38
|
-
|
|
39
|
-
- `packages/effect/src/Effect.ts`
|
|
40
|
-
|
|
41
|
-
## Preferred Rule
|
|
42
|
-
|
|
43
|
-
Use `Effect.fn` as the default constructor for business-logic functions that return `Effect`.
|
|
44
|
-
|
|
45
|
-
Prefer this:
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
import { Effect } from "effect";
|
|
49
|
-
|
|
50
|
-
const loadUser = Effect.fn("loadUser")(function* (userId: string) {
|
|
51
|
-
return { id: userId, name: "Ada" };
|
|
52
|
-
});
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Over this:
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
import { Effect } from "effect";
|
|
59
|
-
|
|
60
|
-
const loadUser = (userId: string) =>
|
|
61
|
-
Effect.gen(function* () {
|
|
62
|
-
return { id: userId, name: "Ada" };
|
|
63
|
-
});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The second version works, but it throws away useful observability structure that `Effect.fn` gives you automatically.
|
|
67
|
-
|
|
68
|
-
## Prefer `Effect.fn` Over Raw `Effect.gen`
|
|
69
|
-
|
|
70
|
-
For business-logic definitions, prefer `Effect.fn` over writing raw `Effect.gen` directly, even when the operation takes no arguments.
|
|
71
|
-
|
|
72
|
-
Prefer this:
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
const refreshCache = Effect.fn("refreshCache")(function* () {
|
|
76
|
-
yield* Effect.logInfo("refreshing cache");
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Over this:
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
const refreshCache = Effect.gen(function* () {
|
|
84
|
-
yield* Effect.logInfo("refreshing cache");
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Why:
|
|
89
|
-
|
|
90
|
-
- `Effect.fn` gives the operation a clear observable identity
|
|
91
|
-
- stack traces are better
|
|
92
|
-
- tracing is more consistent
|
|
93
|
-
- the codebase gets a uniform shape for business operations
|
|
94
|
-
|
|
95
|
-
Use raw `Effect.gen` when necessary, for example:
|
|
96
|
-
|
|
97
|
-
- inline effect blocks inside another `Effect.fn`
|
|
98
|
-
- small one-off composition at call sites
|
|
99
|
-
- top-level assembly code where you are not defining a reusable business operation
|
|
100
|
-
|
|
101
|
-
Rule of thumb:
|
|
102
|
-
|
|
103
|
-
- reusable business operation: `Effect.fn`
|
|
104
|
-
- inline composition block: `Effect.gen`
|
|
105
|
-
|
|
106
|
-
## `Effect.fn` vs `Effect.fnUntraced`
|
|
107
|
-
|
|
108
|
-
### `Effect.fn`
|
|
109
|
-
|
|
110
|
-
Use `Effect.fn` for almost all business logic.
|
|
111
|
-
|
|
112
|
-
It is the preferred default because it adds:
|
|
113
|
-
|
|
114
|
-
- stack frames
|
|
115
|
-
- tracing spans
|
|
116
|
-
- optional post-processing of the produced effect
|
|
117
|
-
|
|
118
|
-
Example:
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
import { Effect } from "effect";
|
|
122
|
-
|
|
123
|
-
const createUser = Effect.fn("createUser")(function* (name: string) {
|
|
124
|
-
return { id: "u_123", name };
|
|
125
|
-
});
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Use this for:
|
|
129
|
-
|
|
130
|
-
- domain operations
|
|
131
|
-
- application services
|
|
132
|
-
- handlers
|
|
133
|
-
- workflows
|
|
134
|
-
- orchestrations
|
|
135
|
-
- repository calls
|
|
136
|
-
|
|
137
|
-
### `Effect.fnUntraced`
|
|
138
|
-
|
|
139
|
-
Use `Effect.fnUntraced` only for edge cases.
|
|
140
|
-
|
|
141
|
-
The canonical Effect source itself uses `fnUntraced` in a number of low-level internals and integration helpers. That does not make it the default recommendation for downstream application or business code.
|
|
142
|
-
|
|
143
|
-
If you do not want an explicit named span, prefer `Effect.fn` without a span name so you still keep stack traces and the normal traced-function behavior.
|
|
144
|
-
|
|
145
|
-
Prefer this:
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
const normalizeUser = Effect.fn(function* (input: string) {
|
|
149
|
-
return input.trim().toLowerCase();
|
|
150
|
-
});
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Over this:
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
const normalizeUser = Effect.fnUntraced(function* (input: string) {
|
|
157
|
-
return input.trim().toLowerCase();
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Typical cases:
|
|
162
|
-
|
|
163
|
-
- extremely hot low-level internal helpers
|
|
164
|
-
- very small internal combinators
|
|
165
|
-
- tight loops where you have measured overhead and need to reduce it
|
|
166
|
-
|
|
167
|
-
Preferred rule:
|
|
168
|
-
|
|
169
|
-
- `Effect.fn` by default
|
|
170
|
-
- `Effect.fn` without a span name when you want to avoid an explicit named span
|
|
171
|
-
- `Effect.fnUntraced` only with a concrete measured reason
|
|
172
|
-
|
|
173
|
-
## Business Logic Patterns
|
|
174
|
-
|
|
175
|
-
### Pattern: one named `Effect.fn` per meaningful operation
|
|
176
|
-
|
|
177
|
-
Good:
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
const parseCommand = Effect.fn("parseCommand")(function* (input: string) {
|
|
181
|
-
return input.trim();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const loadUser = Effect.fn("loadUser")(function* (userId: string) {
|
|
185
|
-
return { id: userId, name: "Ada" };
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const sendWelcomeEmail = Effect.fn("sendWelcomeEmail")(function* (userId: string) {
|
|
189
|
-
const user = yield* loadUser(userId);
|
|
190
|
-
return user.email;
|
|
191
|
-
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Why this is good:
|
|
195
|
-
|
|
196
|
-
- each operation has a clear span name
|
|
197
|
-
- traces reflect business concepts
|
|
198
|
-
- stack traces reflect the actual workflow
|
|
199
|
-
|
|
200
|
-
### Pattern: use meaningful names
|
|
201
|
-
|
|
202
|
-
Span names created through `Effect.fn` should represent business operations, not generic implementation detail.
|
|
203
|
-
|
|
204
|
-
Prefer:
|
|
205
|
-
|
|
206
|
-
- `loadUser`
|
|
207
|
-
- `chargeInvoice`
|
|
208
|
-
- `syncGithubInstallation`
|
|
209
|
-
|
|
210
|
-
Avoid:
|
|
211
|
-
|
|
212
|
-
- `helper`
|
|
213
|
-
- `run`
|
|
214
|
-
- `process`
|
|
215
|
-
- `step1`
|
|
216
|
-
|
|
217
|
-
## Explicit Spans
|
|
218
|
-
|
|
219
|
-
### `Effect.withSpan`
|
|
220
|
-
|
|
221
|
-
Use `withSpan` when you need an explicit span around an effect that is not already naturally represented by a named `Effect.fn`, or when you want a nested sub-operation span.
|
|
222
|
-
|
|
223
|
-
Example:
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
import { Effect } from "effect";
|
|
227
|
-
|
|
228
|
-
const syncUser = Effect.fn("syncUser")(function* (userId: string) {
|
|
229
|
-
const profile = yield* fetchProfile(userId).pipe(Effect.withSpan("fetchProfile"));
|
|
230
|
-
|
|
231
|
-
return yield* persistProfile(profile).pipe(Effect.withSpan("persistProfile"));
|
|
232
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Use this when:
|
|
236
|
-
|
|
237
|
-
- you want a nested span inside a larger operation
|
|
238
|
-
- you are instrumenting an existing effect pipeline
|
|
239
|
-
- you need more detailed trace structure than `Effect.fn` alone provides
|
|
240
|
-
|
|
241
|
-
### `Effect.withSpanScoped`
|
|
242
|
-
|
|
243
|
-
Use `withSpanScoped` when the span should remain open for the lifetime of a scope.
|
|
244
|
-
|
|
245
|
-
This is less common in business logic and more common in long-lived resource or streaming workflows.
|
|
246
|
-
|
|
247
|
-
### `Effect.withParentSpan`
|
|
248
|
-
|
|
249
|
-
Use `withParentSpan` when integrating with an externally created span or continuing a parent span manually.
|
|
250
|
-
|
|
251
|
-
This is useful in framework or interoperability boundaries.
|
|
252
|
-
|
|
253
|
-
## Span Enrichment
|
|
254
|
-
|
|
255
|
-
### `Effect.annotateCurrentSpan`
|
|
256
|
-
|
|
257
|
-
Use `annotateCurrentSpan` to attach important structured fields to the current span.
|
|
258
|
-
|
|
259
|
-
Example:
|
|
260
|
-
|
|
261
|
-
```ts
|
|
262
|
-
import { Effect } from "effect";
|
|
263
|
-
|
|
264
|
-
const loadUser = Effect.fn("loadUser")(function* (userId: string) {
|
|
265
|
-
yield* Effect.annotateCurrentSpan({ userId });
|
|
266
|
-
return { id: userId, name: "Ada" };
|
|
267
|
-
});
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
Good span annotations:
|
|
271
|
-
|
|
272
|
-
- stable identifiers
|
|
273
|
-
- domain-relevant keys
|
|
274
|
-
- request or resource identifiers
|
|
275
|
-
- small structured values
|
|
276
|
-
|
|
277
|
-
Avoid:
|
|
278
|
-
|
|
279
|
-
- giant payloads
|
|
280
|
-
- secrets
|
|
281
|
-
- noisy transient data with little diagnostic value
|
|
282
|
-
|
|
283
|
-
## Logging Patterns
|
|
284
|
-
|
|
285
|
-
### Use Effect logging inside effects
|
|
286
|
-
|
|
287
|
-
Prefer:
|
|
288
|
-
|
|
289
|
-
- `Effect.log`
|
|
290
|
-
- `Effect.logInfo`
|
|
291
|
-
- `Effect.logDebug`
|
|
292
|
-
- `Effect.logWarning`
|
|
293
|
-
- `Effect.logError`
|
|
294
|
-
|
|
295
|
-
These integrate with the current Effect execution context.
|
|
296
|
-
|
|
297
|
-
Example:
|
|
298
|
-
|
|
299
|
-
```ts
|
|
300
|
-
const loadUser = Effect.fn("loadUser")(function* (userId: string) {
|
|
301
|
-
yield* Effect.logDebug("loading user", { userId });
|
|
302
|
-
return { id: userId, name: "Ada" };
|
|
303
|
-
});
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### `Effect.withLogSpan`
|
|
307
|
-
|
|
308
|
-
Use `withLogSpan` when you want log messages to carry a local logical span label even when you are not creating a full tracing span.
|
|
309
|
-
|
|
310
|
-
Example:
|
|
311
|
-
|
|
312
|
-
```ts
|
|
313
|
-
const program = Effect.logInfo("starting sync").pipe(Effect.withLogSpan("user-sync"));
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
This is useful for:
|
|
317
|
-
|
|
318
|
-
- log grouping
|
|
319
|
-
- quick local context
|
|
320
|
-
- correlation in plain log output
|
|
321
|
-
|
|
322
|
-
### Logging Best Practices
|
|
323
|
-
|
|
324
|
-
- log at business boundaries, not every tiny helper
|
|
325
|
-
- prefer structured values over concatenated strings
|
|
326
|
-
- keep logs high-signal
|
|
327
|
-
- avoid duplicate logs at every layer of the stack
|
|
328
|
-
- rely on spans plus a few well-placed logs, not log spam
|
|
329
|
-
- use small structured fields such as identifiers, counts, booleans, and
|
|
330
|
-
operation names
|
|
331
|
-
- attach errors as structured attributes rather than stringifying them into the
|
|
332
|
-
message
|
|
333
|
-
- keep runtime logger Layers and common annotations at runtime boundaries;
|
|
334
|
-
business logic should not create module-level logger instances
|
|
335
|
-
|
|
336
|
-
### Log Annotations
|
|
337
|
-
|
|
338
|
-
Use `Effect.annotateLogs` for metadata that should appear on every log emitted
|
|
339
|
-
inside an operation. Annotate at the highest meaningful owner—request, route,
|
|
340
|
-
tenant, workflow, or job—rather than repeating the same attributes at each log
|
|
341
|
-
site.
|
|
342
|
-
|
|
343
|
-
```ts
|
|
344
|
-
const runCheckout = (request: CheckoutRequest) =>
|
|
345
|
-
Effect.gen(function* () {
|
|
346
|
-
yield* Effect.logInfo("loading checkout state");
|
|
347
|
-
yield* validateCart(request.cart);
|
|
348
|
-
yield* Effect.logInfo("submitting payment");
|
|
349
|
-
yield* submitPayment(request.payment);
|
|
350
|
-
}).pipe(
|
|
351
|
-
Effect.annotateLogs({
|
|
352
|
-
operation: "checkout",
|
|
353
|
-
requestId: request.id,
|
|
354
|
-
cartId: request.cart.id,
|
|
355
|
-
}),
|
|
356
|
-
Effect.withLogSpan("checkout"),
|
|
357
|
-
);
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
Add narrower annotations only when metadata belongs to a nested item, attempt,
|
|
361
|
-
or operation. Use `Effect.annotateLogsScoped` only when an acquired scope should
|
|
362
|
-
carry annotations across multiple effects; ordinary `Effect.annotateLogs` is
|
|
363
|
-
the default for request and operation boundaries.
|
|
364
|
-
|
|
365
|
-
## Metrics Patterns
|
|
366
|
-
|
|
367
|
-
### Track effects at meaningful boundaries
|
|
368
|
-
|
|
369
|
-
Use metric tracking on meaningful operations such as:
|
|
370
|
-
|
|
371
|
-
- requests
|
|
372
|
-
- jobs
|
|
373
|
-
- retries
|
|
374
|
-
- external calls
|
|
375
|
-
- queue handlers
|
|
376
|
-
|
|
377
|
-
Repo reference:
|
|
378
|
-
|
|
379
|
-
- `packages/effect/src/Effect.ts`
|
|
380
|
-
- `Effect.track`
|
|
381
|
-
|
|
382
|
-
Example:
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
import { Effect, Metric } from "effect";
|
|
386
|
-
|
|
387
|
-
const requests = Metric.counter("user_load_requests").pipe(Metric.withConstantInput(1));
|
|
388
|
-
|
|
389
|
-
const loadUser = Effect.fn("loadUser")(function* (userId: string) {
|
|
390
|
-
return { id: userId, name: "Ada" };
|
|
391
|
-
}, Effect.track(requests));
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
### Prefer boundary metrics over micro-metrics
|
|
395
|
-
|
|
396
|
-
Good metrics are usually attached to:
|
|
397
|
-
|
|
398
|
-
- endpoint handlers
|
|
399
|
-
- queue/job handlers
|
|
400
|
-
- repository operations
|
|
401
|
-
- external API boundaries
|
|
402
|
-
|
|
403
|
-
Avoid putting a metric on every tiny internal helper.
|
|
404
|
-
|
|
405
|
-
## OpenTelemetry Integration
|
|
406
|
-
|
|
407
|
-
For real application observability, compose telemetry at the layer level.
|
|
408
|
-
|
|
409
|
-
The canonical source provides `@effect/opentelemetry` layers such as:
|
|
410
|
-
|
|
411
|
-
- `NodeSdk.layer`
|
|
412
|
-
- `OtelTracer.layer`
|
|
413
|
-
- `OtelLogger.layer`
|
|
414
|
-
- `OtelMetrics.layer`
|
|
415
|
-
|
|
416
|
-
Repo references:
|
|
417
|
-
|
|
418
|
-
- `packages/opentelemetry/src/NodeSdk.ts`
|
|
419
|
-
- `packages/opentelemetry/src/OtelTracer.ts`
|
|
420
|
-
|
|
421
|
-
Preferred composition style:
|
|
422
|
-
|
|
423
|
-
```ts
|
|
424
|
-
const AppLayer = Layer.mergeAll(UserLayer, BillingLayer, HttpLayer).pipe(
|
|
425
|
-
Layer.provide(Telemetry),
|
|
426
|
-
Layer.provide(NodeSdk),
|
|
427
|
-
);
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
Why:
|
|
431
|
-
|
|
432
|
-
- business code stays observability-agnostic
|
|
433
|
-
- observability is configured once at the boundary
|
|
434
|
-
- spans, logs, and metrics remain consistent across the app
|
|
435
|
-
|
|
436
|
-
## OpenTelemetry JS Framework Integration
|
|
437
|
-
|
|
438
|
-
The canonical source includes a real integration layer for the OpenTelemetry JavaScript ecosystem in `@effect/opentelemetry`.
|
|
439
|
-
|
|
440
|
-
This is the preferred integration path when the application needs to participate in the standard OpenTelemetry JS framework, exporters, and SDKs.
|
|
441
|
-
|
|
442
|
-
Relevant modules:
|
|
443
|
-
|
|
444
|
-
- `packages/opentelemetry/src/NodeSdk.ts`
|
|
445
|
-
- `packages/opentelemetry/src/OtelTracer.ts`
|
|
446
|
-
- `packages/opentelemetry/src/OtelMetrics.ts`
|
|
447
|
-
- `packages/opentelemetry/src/OtelLogger.ts`
|
|
448
|
-
- `packages/opentelemetry/src/Resource.ts`
|
|
449
|
-
- `packages/opentelemetry/src/WebSdk.ts`
|
|
450
|
-
|
|
451
|
-
### Preferred Integration Model
|
|
452
|
-
|
|
453
|
-
Use `@effect/opentelemetry` layers to bridge Effect observability into OpenTelemetry JS.
|
|
454
|
-
|
|
455
|
-
Do not manually wire OpenTelemetry SDK objects inside business code.
|
|
456
|
-
|
|
457
|
-
Prefer:
|
|
458
|
-
|
|
459
|
-
- configuring tracer, metrics, logger, and resource layers once
|
|
460
|
-
- composing them into the application layer graph
|
|
461
|
-
- keeping business code written against Effect tracing, logging, and metrics APIs
|
|
462
|
-
|
|
463
|
-
This means:
|
|
464
|
-
|
|
465
|
-
- application code should keep using `Effect.fn`, `Effect.withSpan`, `Effect.log*`, and Effect metrics
|
|
466
|
-
- OpenTelemetry JS should be introduced at the infrastructure layer, not inside domain operations
|
|
467
|
-
|
|
468
|
-
### `NodeSdk.layer`
|
|
469
|
-
|
|
470
|
-
`NodeSdk.layer(...)` is the main Node.js integration entrypoint.
|
|
471
|
-
|
|
472
|
-
From the canonical source, it accepts a configuration that can include:
|
|
473
|
-
|
|
474
|
-
- span processors
|
|
475
|
-
- tracer config
|
|
476
|
-
- metric readers
|
|
477
|
-
- temporality preference
|
|
478
|
-
- log record processors
|
|
479
|
-
- logger provider config
|
|
480
|
-
- resource information such as service name and version
|
|
481
|
-
- shutdown timeout
|
|
482
|
-
|
|
483
|
-
It then builds and merges:
|
|
484
|
-
|
|
485
|
-
- resource layer
|
|
486
|
-
- tracer layer
|
|
487
|
-
- metrics layer
|
|
488
|
-
- logger layer
|
|
489
|
-
|
|
490
|
-
This makes it the preferred high-level integration for Node applications.
|
|
491
|
-
|
|
492
|
-
### Resource Configuration
|
|
493
|
-
|
|
494
|
-
OpenTelemetry JS integration should define resource metadata explicitly.
|
|
495
|
-
|
|
496
|
-
From `NodeSdk.layer`, the supported resource configuration includes:
|
|
497
|
-
|
|
498
|
-
- `serviceName`
|
|
499
|
-
- `serviceVersion`
|
|
500
|
-
- additional attributes
|
|
501
|
-
|
|
502
|
-
This is important because tracer and logger setup depend on the configured resource.
|
|
503
|
-
|
|
504
|
-
Best practice:
|
|
505
|
-
|
|
506
|
-
- always provide a meaningful service name
|
|
507
|
-
- provide service version when available
|
|
508
|
-
- use resource attributes for stable deployment or environment metadata
|
|
509
|
-
|
|
510
|
-
### Tracing Integration
|
|
511
|
-
|
|
512
|
-
The `OtelTracer` module bridges Effect spans into OpenTelemetry spans.
|
|
513
|
-
|
|
514
|
-
Important integration points from the canonical source:
|
|
515
|
-
|
|
516
|
-
- `OtelTracer.layer`
|
|
517
|
-
- `OtelTracer.layerGlobal`
|
|
518
|
-
- `OtelTracer.layerGlobalProvider`
|
|
519
|
-
- `OtelTracer.currentOtelSpan`
|
|
520
|
-
- `OtelTracer.makeExternalSpan`
|
|
521
|
-
|
|
522
|
-
Use these when:
|
|
523
|
-
|
|
524
|
-
- you need Effect tracing to export through OpenTelemetry JS
|
|
525
|
-
- you need to continue or bridge external trace context
|
|
526
|
-
- you need access to the current OpenTelemetry span object
|
|
527
|
-
|
|
528
|
-
Best practice:
|
|
529
|
-
|
|
530
|
-
- keep creating spans with Effect APIs in application code
|
|
531
|
-
- use the OpenTelemetry tracer layer to export and bridge them
|
|
532
|
-
- use `makeExternalSpan` or parent-span wiring only at integration boundaries
|
|
533
|
-
|
|
534
|
-
### Metrics Integration
|
|
535
|
-
|
|
536
|
-
The `OtelMetrics` module connects Effect metrics to OpenTelemetry JS metric
|
|
537
|
-
readers.
|
|
538
|
-
|
|
539
|
-
Important details from the canonical implementation:
|
|
540
|
-
|
|
541
|
-
- `OtelMetrics.layer(...)` registers a producer against one or more metric
|
|
542
|
-
readers
|
|
543
|
-
- it supports temporality preferences:
|
|
544
|
-
- `cumulative`
|
|
545
|
-
- `delta`
|
|
546
|
-
- it handles shutdown through scoped layer cleanup
|
|
547
|
-
|
|
548
|
-
Best practice:
|
|
549
|
-
|
|
550
|
-
- choose temporality based on the backend
|
|
551
|
-
- configure metric readers in the telemetry layer
|
|
552
|
-
- keep application code focused on recording Effect metrics, not exporter mechanics
|
|
553
|
-
|
|
554
|
-
### Logger Integration
|
|
555
|
-
|
|
556
|
-
The `OtelLogger` module connects Effect logging to OpenTelemetry JS logs.
|
|
557
|
-
|
|
558
|
-
Important details from the canonical implementation:
|
|
559
|
-
|
|
560
|
-
- it maps Effect log levels to OpenTelemetry severity numbers
|
|
561
|
-
- it includes fiber ID, span context, log annotations, and log span timing in emitted attributes
|
|
562
|
-
- `OtelLogger.layer({ mergeWithExisting })` can merge with or replace existing
|
|
563
|
-
application loggers
|
|
564
|
-
|
|
565
|
-
Best practice:
|
|
566
|
-
|
|
567
|
-
- prefer merging with existing loggers unless there is a strong reason to replace them
|
|
568
|
-
- use Effect log annotations and log spans so the OpenTelemetry logger receives structured context automatically
|
|
569
|
-
|
|
570
|
-
### Shutdown And Lifecycle
|
|
571
|
-
|
|
572
|
-
The canonical layers use scoped acquisition and release for tracer providers,
|
|
573
|
-
metric readers, and logger providers.
|
|
574
|
-
|
|
575
|
-
This is the correct lifecycle model.
|
|
576
|
-
|
|
577
|
-
Do not manually call provider shutdown methods from arbitrary business logic.
|
|
578
|
-
|
|
579
|
-
Instead:
|
|
580
|
-
|
|
581
|
-
- let the OpenTelemetry layers own provider lifecycle
|
|
582
|
-
- compose them into the application layer graph
|
|
583
|
-
- let the runtime or outer layer scope manage shutdown
|
|
584
|
-
|
|
585
|
-
### External Trace Context
|
|
586
|
-
|
|
587
|
-
When integrating with frameworks or inbound protocols that already carry trace context, prefer using the OpenTelemetry integration helpers rather than hand-rolling context propagation.
|
|
588
|
-
|
|
589
|
-
The canonical tracer module provides:
|
|
590
|
-
|
|
591
|
-
- `makeExternalSpan`
|
|
592
|
-
- `currentOtelSpan`
|
|
593
|
-
|
|
594
|
-
Use these only at integration boundaries such as:
|
|
595
|
-
|
|
596
|
-
- HTTP adapters
|
|
597
|
-
- RPC adapters
|
|
598
|
-
- worker or queue adapters
|
|
599
|
-
|
|
600
|
-
Keep business operations oblivious to propagation mechanics.
|
|
601
|
-
|
|
602
|
-
### Recommended Pattern
|
|
603
|
-
|
|
604
|
-
Preferred architecture:
|
|
605
|
-
|
|
606
|
-
1. business code uses Effect observability APIs
|
|
607
|
-
2. infrastructure composes `@effect/opentelemetry` layers
|
|
608
|
-
3. the final app layer provides telemetry once at the top level
|
|
609
|
-
|
|
610
|
-
Example shape:
|
|
611
|
-
|
|
612
|
-
```ts
|
|
613
|
-
const TelemetryLayer = NodeSdk.layer(() => ({
|
|
614
|
-
resource: {
|
|
615
|
-
serviceName: "todo-service",
|
|
616
|
-
serviceVersion: "1.0.0",
|
|
617
|
-
},
|
|
618
|
-
spanProcessor: mySpanProcessor,
|
|
619
|
-
metricReader: myMetricReader,
|
|
620
|
-
logRecordProcessor: myLogProcessor,
|
|
621
|
-
}));
|
|
622
|
-
|
|
623
|
-
const AppLayer = Layer.mergeAll(DomainLayer, HttpLayer).pipe(Layer.provide(TelemetryLayer));
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
This keeps:
|
|
627
|
-
|
|
628
|
-
- app code portable
|
|
629
|
-
- OTel JS setup centralized
|
|
630
|
-
- shutdown semantics correct
|
|
631
|
-
- exported spans, logs, and metrics aligned
|
|
632
|
-
|
|
633
|
-
### Anti-Patterns
|
|
634
|
-
|
|
635
|
-
- constructing OpenTelemetry SDK clients directly inside business services
|
|
636
|
-
- mixing manual exporter setup into domain code
|
|
637
|
-
- bypassing Effect logging and tracing APIs in normal business operations
|
|
638
|
-
- scattering provider shutdown logic across the application
|
|
639
|
-
- configuring telemetry separately in many subsystems instead of one top-level layer
|
|
640
|
-
|
|
641
|
-
## Anti-Patterns
|
|
642
|
-
|
|
643
|
-
### Anti-Pattern: business logic built from anonymous `Effect.gen` functions everywhere
|
|
644
|
-
|
|
645
|
-
Bad:
|
|
646
|
-
|
|
647
|
-
```ts
|
|
648
|
-
const loadUser = (userId: string) =>
|
|
649
|
-
Effect.gen(function* () {
|
|
650
|
-
return { id: userId, name: "Ada" };
|
|
651
|
-
});
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
Why this is bad:
|
|
655
|
-
|
|
656
|
-
- weaker tracing structure
|
|
657
|
-
- poorer stack traces
|
|
658
|
-
- less consistent naming in debugging output
|
|
659
|
-
|
|
660
|
-
Preferred:
|
|
661
|
-
|
|
662
|
-
```ts
|
|
663
|
-
const loadUser = Effect.fn("loadUser")(function* (userId: string) {
|
|
664
|
-
return { id: userId, name: "Ada" };
|
|
665
|
-
});
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
### Anti-Pattern: using `Effect.fnUntraced` by default
|
|
669
|
-
|
|
670
|
-
This throws away free observability.
|
|
671
|
-
|
|
672
|
-
If you just do not want an explicit span name, use `Effect.fn` without a name instead.
|
|
673
|
-
|
|
674
|
-
Only use `fnUntraced` when you have a specific low-level reason.
|
|
675
|
-
|
|
676
|
-
### Anti-Pattern: logging without structure or context
|
|
677
|
-
|
|
678
|
-
Bad:
|
|
679
|
-
|
|
680
|
-
- giant interpolated strings
|
|
681
|
-
- duplicate logs at every layer
|
|
682
|
-
- logs with no business identifiers
|
|
683
|
-
|
|
684
|
-
Prefer:
|
|
685
|
-
|
|
686
|
-
- named operations via `Effect.fn`
|
|
687
|
-
- structured logs with IDs and context
|
|
688
|
-
- a few high-value logs at operation boundaries
|
|
689
|
-
|
|
690
|
-
Also avoid:
|
|
691
|
-
|
|
692
|
-
- `console.*` in new Effect code
|
|
693
|
-
- custom logging wrappers around `Effect.log*`
|
|
694
|
-
- broad module-wide annotations when operation-level ownership is clearer
|
|
695
|
-
- dual legacy and Effect logging paths
|
|
696
|
-
|
|
697
|
-
### Anti-Pattern: hand-instrumenting every helper with spans
|
|
698
|
-
|
|
699
|
-
Do not create explicit spans everywhere just because you can.
|
|
700
|
-
|
|
701
|
-
Preferred order:
|
|
702
|
-
|
|
703
|
-
1. start with `Effect.fn`
|
|
704
|
-
2. add `Effect.withSpan` only where extra detail is actually useful
|
|
705
|
-
3. add metrics at meaningful boundaries
|
|
706
|
-
|
|
707
|
-
## Recommended Patterns
|
|
708
|
-
|
|
709
|
-
### Pattern: observable business operation
|
|
710
|
-
|
|
711
|
-
```ts
|
|
712
|
-
import { Effect } from "effect";
|
|
713
|
-
|
|
714
|
-
const fetchUser = Effect.fn("fetchUser")(function* (userId: string) {
|
|
715
|
-
yield* Effect.annotateCurrentSpan({ userId });
|
|
716
|
-
yield* Effect.logDebug("fetching user", { userId });
|
|
717
|
-
return { id: userId, name: "Ada" };
|
|
718
|
-
});
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
### Pattern: orchestration with nested spans
|
|
722
|
-
|
|
723
|
-
```ts
|
|
724
|
-
const syncUser = Effect.fn("syncUser")(function* (userId: string) {
|
|
725
|
-
const profile = yield* fetchRemoteProfile(userId).pipe(Effect.withSpan("fetchRemoteProfile"));
|
|
726
|
-
|
|
727
|
-
return yield* persistProfile(profile).pipe(Effect.withSpan("persistProfile"));
|
|
728
|
-
});
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
### Pattern: framework boundary with runtime
|
|
732
|
-
|
|
733
|
-
```ts
|
|
734
|
-
const runtime = ManagedRuntime.make(AppLayer);
|
|
735
|
-
|
|
736
|
-
const handleRequest = (userId: string) => runtime.runPromise(fetchUser(userId));
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
## Good Repo Examples To Study
|
|
740
|
-
|
|
741
|
-
- `packages/effect/src/Effect.ts`
|
|
742
|
-
- `packages/effect/src/Tracer.ts`
|
|
743
|
-
- `packages/effect/src/Logger.ts`
|
|
744
|
-
- `packages/effect/src/Metric.ts`
|
|
745
|
-
- `packages/opentelemetry/src/NodeSdk.ts`
|
|
746
|
-
- `packages/opentelemetry/src/OtelTracer.ts`
|