@jagreehal/workflow 1.13.0 → 1.14.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/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/visualize.cjs +73 -67
- package/dist/visualize.cjs.map +1 -1
- package/dist/visualize.d.cts +13 -1
- package/dist/visualize.d.ts +13 -1
- package/dist/visualize.js +73 -67
- package/dist/visualize.js.map +1 -1
- package/docs/pino-logging-example.md +293 -0
- package/docs/visualization.md +35 -0
- package/package.json +6 -2
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Logging Workflow Events to Pino
|
|
2
|
+
|
|
3
|
+
Workflow events are structured JSON objects that work perfectly with Pino's structured logging. You can collect events during execution and log them after completion.
|
|
4
|
+
|
|
5
|
+
## Logging ASCII Visualization
|
|
6
|
+
|
|
7
|
+
You can also log the ASCII visualization output to Pino. The multi-line ASCII diagram will be included as a string field in the log entry.
|
|
8
|
+
|
|
9
|
+
## Basic Example
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createWorkflow, createEventCollector } from '@jagreehal/workflow';
|
|
13
|
+
import pino from 'pino';
|
|
14
|
+
|
|
15
|
+
const logger = pino();
|
|
16
|
+
|
|
17
|
+
const collector = createEventCollector();
|
|
18
|
+
const workflow = createWorkflow({ fetchUser, fetchPosts }, {
|
|
19
|
+
onEvent: collector.handleEvent,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const result = await workflow(async (step) => {
|
|
23
|
+
const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
|
|
24
|
+
const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
|
|
25
|
+
return { user, posts };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// After workflow completes, log all events
|
|
29
|
+
const events = collector.getWorkflowEvents();
|
|
30
|
+
|
|
31
|
+
// Log summary
|
|
32
|
+
if (result.ok) {
|
|
33
|
+
logger.info({
|
|
34
|
+
workflowId: events[0]?.workflowId,
|
|
35
|
+
duration: events.find(e => e.type === 'workflow_success')?.durationMs,
|
|
36
|
+
stepCount: events.filter(e => e.type === 'step_success').length,
|
|
37
|
+
}, 'Workflow completed successfully');
|
|
38
|
+
} else {
|
|
39
|
+
logger.error({
|
|
40
|
+
workflowId: events[0]?.workflowId,
|
|
41
|
+
error: result.error,
|
|
42
|
+
duration: events.find(e => e.type === 'workflow_error')?.durationMs,
|
|
43
|
+
}, 'Workflow failed');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Log all events (optional - can be verbose)
|
|
47
|
+
events.forEach(event => {
|
|
48
|
+
logger.debug({ event }, 'Workflow event');
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Pino Output Format
|
|
53
|
+
|
|
54
|
+
Pino will serialize workflow events as clean JSON. Here's what the output looks like:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"level": 30,
|
|
59
|
+
"time": 1704067200000,
|
|
60
|
+
"workflowId": "abc-123",
|
|
61
|
+
"duration": 245,
|
|
62
|
+
"stepCount": 2,
|
|
63
|
+
"msg": "Workflow completed successfully"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For individual events:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"level": 20,
|
|
72
|
+
"time": 1704067200000,
|
|
73
|
+
"event": {
|
|
74
|
+
"type": "step_success",
|
|
75
|
+
"workflowId": "abc-123",
|
|
76
|
+
"stepId": "step-1",
|
|
77
|
+
"name": "Fetch user",
|
|
78
|
+
"stepKey": "user:1",
|
|
79
|
+
"ts": 1704067200100,
|
|
80
|
+
"durationMs": 45,
|
|
81
|
+
"context": {
|
|
82
|
+
"requestId": "req-456",
|
|
83
|
+
"userId": "user-789"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"msg": "Workflow event"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Helper Function
|
|
91
|
+
|
|
92
|
+
Create a reusable Pino logger helper:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { createEventCollector, type WorkflowEvent } from '@jagreehal/workflow';
|
|
96
|
+
import pino from 'pino';
|
|
97
|
+
|
|
98
|
+
export function createPinoLogger(logger: pino.Logger) {
|
|
99
|
+
const collector = createEventCollector();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
handleEvent: collector.handleEvent,
|
|
103
|
+
|
|
104
|
+
logAfterCompletion: (result: { ok: boolean; error?: unknown }) => {
|
|
105
|
+
const events = collector.getWorkflowEvents();
|
|
106
|
+
const workflowId = events[0]?.workflowId;
|
|
107
|
+
|
|
108
|
+
// Find workflow completion event
|
|
109
|
+
const completion = events.find(
|
|
110
|
+
e => e.type === 'workflow_success' || e.type === 'workflow_error'
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (result.ok && completion?.type === 'workflow_success') {
|
|
114
|
+
logger.info({
|
|
115
|
+
workflowId,
|
|
116
|
+
duration: completion.durationMs,
|
|
117
|
+
stepCount: events.filter(e => e.type === 'step_success').length,
|
|
118
|
+
cacheHits: events.filter(e => e.type === 'step_cache_hit').length,
|
|
119
|
+
context: completion.context,
|
|
120
|
+
}, 'Workflow completed');
|
|
121
|
+
} else if (!result.ok && completion?.type === 'workflow_error') {
|
|
122
|
+
logger.error({
|
|
123
|
+
workflowId,
|
|
124
|
+
error: result.error,
|
|
125
|
+
duration: completion.durationMs,
|
|
126
|
+
failedStep: events.find(e => e.type === 'step_error')?.name,
|
|
127
|
+
context: completion.context,
|
|
128
|
+
}, 'Workflow failed');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Log slow steps
|
|
132
|
+
events
|
|
133
|
+
.filter((e): e is WorkflowEvent<unknown> & { durationMs: number } =>
|
|
134
|
+
e.type === 'step_success' && e.durationMs > 1000
|
|
135
|
+
)
|
|
136
|
+
.forEach(step => {
|
|
137
|
+
logger.warn({
|
|
138
|
+
workflowId,
|
|
139
|
+
stepName: step.name,
|
|
140
|
+
stepKey: step.stepKey,
|
|
141
|
+
duration: step.durationMs,
|
|
142
|
+
}, 'Slow step detected');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Log retries
|
|
146
|
+
events
|
|
147
|
+
.filter(e => e.type === 'step_retry')
|
|
148
|
+
.forEach(retry => {
|
|
149
|
+
logger.warn({
|
|
150
|
+
workflowId,
|
|
151
|
+
stepName: retry.name,
|
|
152
|
+
attempt: retry.attempt,
|
|
153
|
+
maxAttempts: retry.maxAttempts,
|
|
154
|
+
error: retry.error,
|
|
155
|
+
}, 'Step retry');
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Usage
|
|
162
|
+
const pinoLogger = createPinoLogger(pino());
|
|
163
|
+
const workflow = createWorkflow(deps, {
|
|
164
|
+
onEvent: pinoLogger.handleEvent,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = await workflow(async (step) => {
|
|
168
|
+
// ... workflow logic
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
pinoLogger.logAfterCompletion(result);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Why This Works Well
|
|
175
|
+
|
|
176
|
+
1. **Structured Events**: All workflow events are plain objects with consistent properties
|
|
177
|
+
2. **JSON-Serializable**: Events contain only JSON-safe values (strings, numbers, booleans, objects)
|
|
178
|
+
3. **Context Support**: Events include `context` field for correlation IDs, user IDs, etc.
|
|
179
|
+
4. **Type Safety**: TypeScript knows all event properties
|
|
180
|
+
5. **Pino-Friendly**: Pino automatically serializes objects - no custom formatting needed
|
|
181
|
+
|
|
182
|
+
## Logging ASCII Visualization
|
|
183
|
+
|
|
184
|
+
You can log the ASCII diagram directly to Pino:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { createWorkflow, createVisualizer } from '@jagreehal/workflow';
|
|
188
|
+
import pino from 'pino';
|
|
189
|
+
|
|
190
|
+
const logger = pino();
|
|
191
|
+
const viz = createVisualizer({ workflowName: 'checkout' });
|
|
192
|
+
|
|
193
|
+
const workflow = createWorkflow({ fetchUser, fetchPosts }, {
|
|
194
|
+
onEvent: viz.handleEvent,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const result = await workflow(async (step) => {
|
|
198
|
+
const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
|
|
199
|
+
const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
|
|
200
|
+
return { user, posts };
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Log ASCII visualization
|
|
204
|
+
const asciiDiagram = viz.render();
|
|
205
|
+
logger.info({
|
|
206
|
+
workflowId: result.ok ? 'success' : 'failed',
|
|
207
|
+
visualization: asciiDiagram,
|
|
208
|
+
}, 'Workflow completed');
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Pino Output with ASCII
|
|
212
|
+
|
|
213
|
+
In JSON format, the ASCII will be escaped with `\n`:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"level": 30,
|
|
218
|
+
"time": 1704067200000,
|
|
219
|
+
"workflowId": "success",
|
|
220
|
+
"visualization": "┌── checkout ───────────────────────────────┐\n│ │\n│ ✓ Fetch user [45ms] │\n│ ✓ Fetch posts [67ms] │\n│ │\n│ Completed in 112ms │\n│ │\n└──────────────────────────────────────────┘",
|
|
221
|
+
"msg": "Workflow completed"
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### With pino-pretty
|
|
226
|
+
|
|
227
|
+
When using `pino-pretty` or viewing in a terminal, the ASCII renders correctly:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
[2024-01-01 12:00:00] INFO: Workflow completed
|
|
231
|
+
workflowId: "success"
|
|
232
|
+
visualization: "
|
|
233
|
+
┌── checkout ───────────────────────────────┐
|
|
234
|
+
│ │
|
|
235
|
+
│ ✓ Fetch user [45ms] │
|
|
236
|
+
│ ✓ Fetch posts [67ms] │
|
|
237
|
+
│ │
|
|
238
|
+
│ Completed in 112ms │
|
|
239
|
+
│ │
|
|
240
|
+
└──────────────────────────────────────────┘
|
|
241
|
+
"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### ANSI Colors
|
|
245
|
+
|
|
246
|
+
The ASCII output includes ANSI color codes. They work in:
|
|
247
|
+
- ✅ Terminal output (pino-pretty, console)
|
|
248
|
+
- ✅ Log viewers that support ANSI (most modern ones)
|
|
249
|
+
- ⚠️ Raw JSON logs (colors are included as escape sequences)
|
|
250
|
+
|
|
251
|
+
To strip colors for cleaner JSON logs:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Simple regex to strip ANSI codes
|
|
255
|
+
function stripAnsi(str: string): string {
|
|
256
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const asciiDiagram = viz.render();
|
|
260
|
+
const asciiWithoutColors = stripAnsi(asciiDiagram);
|
|
261
|
+
|
|
262
|
+
logger.info({
|
|
263
|
+
visualization: asciiWithoutColors, // Clean box-drawing, no ANSI codes
|
|
264
|
+
}, 'Workflow completed');
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Note:** The ASCII output includes ANSI color codes. They render correctly in terminals and log viewers that support ANSI (like `pino-pretty`), but appear as escape sequences in raw JSON logs. Use `stripAnsi()` if you want clean box-drawing characters without color codes.
|
|
268
|
+
|
|
269
|
+
## Example Output
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"level": 30,
|
|
274
|
+
"time": 1704067200000,
|
|
275
|
+
"workflowId": "checkout-abc-123",
|
|
276
|
+
"duration": 1234,
|
|
277
|
+
"stepCount": 5,
|
|
278
|
+
"cacheHits": 2,
|
|
279
|
+
"context": {
|
|
280
|
+
"requestId": "req-456",
|
|
281
|
+
"userId": "user-789",
|
|
282
|
+
"orderId": "order-123"
|
|
283
|
+
},
|
|
284
|
+
"msg": "Workflow completed"
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The format looks great in Pino because:
|
|
289
|
+
- All fields are at the top level (easy to query/filter)
|
|
290
|
+
- Context is nested but accessible
|
|
291
|
+
- Duration and counts are numeric (good for metrics)
|
|
292
|
+
- Error objects serialize cleanly
|
|
293
|
+
- ASCII visualization can be included as a multi-line string field
|
package/docs/visualization.md
CHANGED
|
@@ -95,6 +95,41 @@ flowchart TD
|
|
|
95
95
|
|
|
96
96
|
Paste into GitHub, Notion, or any Mermaid-compatible tool.
|
|
97
97
|
|
|
98
|
+
#### Enhanced Mermaid Edges
|
|
99
|
+
|
|
100
|
+
Mermaid diagrams now show retry loops, error paths, and timeouts as visual edges (not just labels):
|
|
101
|
+
|
|
102
|
+
```mermaid
|
|
103
|
+
flowchart TD
|
|
104
|
+
A["✓ Fetch user 45ms"] --> B["✗ Process payment 201ms"]
|
|
105
|
+
A -.->|"↻ 2 retries"| A
|
|
106
|
+
B -->|error| ERR{{PAYMENT_FAILED}}
|
|
107
|
+
B -.->|timeout| TO{{⏱ Timeout 5000ms}}
|
|
108
|
+
style ERR fill:#fee2e2,stroke:#dc2626
|
|
109
|
+
style TO fill:#fef3c7,stroke:#f59e0b
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**What the edges show:**
|
|
113
|
+
|
|
114
|
+
- **Retry loops** (`-.->`) - Self-loop showing how many retries occurred
|
|
115
|
+
- **Error paths** (`-->|error|`) - Flow to error node showing the error value
|
|
116
|
+
- **Timeout paths** (`-.->|timeout|`) - Dashed edge to timeout node
|
|
117
|
+
|
|
118
|
+
These are enabled by default. To disable, pass options when rendering:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import type { MermaidRenderOptions } from '@jagreehal/workflow/visualize';
|
|
122
|
+
|
|
123
|
+
const options: MermaidRenderOptions = {
|
|
124
|
+
showTimings: true,
|
|
125
|
+
showKeys: false,
|
|
126
|
+
colors: defaultColorScheme,
|
|
127
|
+
showRetryEdges: false, // Hide retry self-loops
|
|
128
|
+
showErrorEdges: false, // Hide error path edges
|
|
129
|
+
showTimeoutEdges: false, // Hide timeout path edges
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
98
133
|
### JSON (IR)
|
|
99
134
|
|
|
100
135
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jagreehal/workflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed async workflows with automatic error inference. Build type-safe workflows with Result types, step caching, resume state, and human-in-the-loop support.",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"@total-typescript/tsconfig": "^1.0.4",
|
|
101
101
|
"@types/node": "^25.0.9",
|
|
102
102
|
"@types/picomatch": "^4.0.2",
|
|
103
|
+
"@types/ws": "^8.18.1",
|
|
103
104
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
104
105
|
"@typescript-eslint/parser": "^8.53.0",
|
|
105
106
|
"@typescript-eslint/rule-tester": "^8.53.0",
|
|
@@ -108,11 +109,14 @@
|
|
|
108
109
|
"eslint": "9.39.2",
|
|
109
110
|
"eslint-config-prettier": "^10.1.8",
|
|
110
111
|
"eslint-plugin-unicorn": "^62.0.0",
|
|
112
|
+
"jsdom": "^27.4.0",
|
|
113
|
+
"mermaid": "^11.12.2",
|
|
111
114
|
"tsd": "^0.33.0",
|
|
112
115
|
"tsup": "^8.5.1",
|
|
113
116
|
"typescript": "^5.9.3",
|
|
114
117
|
"typescript-eslint": "^8.53.0",
|
|
115
|
-
"vitest": "^4.0.17"
|
|
118
|
+
"vitest": "^4.0.17",
|
|
119
|
+
"ws": "^8.19.0"
|
|
116
120
|
},
|
|
117
121
|
"engines": {
|
|
118
122
|
"node": ">=18.0.0"
|