Ghost

struxadotcloud/struxa

public
Fork0
CodeIssues0Pull requests0
1branch
struxa.claudeskillsreview-logging-patternsreferenceswide-events.md
initial commitd4cb9e15 months ago340 lines · 8.2 KB
1# Wide Events Guide
2
3Wide events are comprehensive log entries that capture all context for a single logical operation (usually a request) in one place.
4
5## Why Wide Events?
6
7Traditional logging scatters information across many log lines:
8
9```
1010:23:45.001 Request received POST /checkout
1110:23:45.012 User authenticated: user_123
1210:23:45.045 Cart loaded: 3 items, $99.99
1310:23:45.089 Payment initiated: Stripe
1410:23:45.234 Payment failed: card_declined
1510:23:45.235 Request completed: 500
16```
17
18During an incident, you're grep-ing through thousands of these trying to reconstruct what happened.
19
20**Wide events emit once with everything:**
21
22Development (pretty format):
23
24```
2510:23:45.235 ERROR [api] POST /checkout 500 in 234ms
26 ├─ user: id=user_123 plan=premium accountAge=847
27 ├─ cart: items=3 total=9999
28 ├─ payment: provider=stripe method=card
29 └─ error: code=card_declined retriable=false
30```
31
32Production (JSON format):
33
34```json
35{
36 "timestamp":
37 "level":
38 "service":
39 "method":
40 "path":
41 "duration":
42 "user":
43 "cart":
44 "payment":
45 "error":
46}
47```
48
49## When to Use Wide Events
50
51| Scenario | Use Wide Event? |
52| ----------------------------- | --------------------------------- |
53| HTTP request handling | Yes - one event per request |
54| Background job execution | Yes - one event per job |
55| Database query | No - use simple log |
56| Cache hit/miss | No - include in parent wide event |
57| User action (login, checkout) | Yes - one event per action |
58| Debug statements | No - remove in production |
59
60## Required Fields
61
62Every wide event should include:
63
64### Request Context
65
66```typescript
67log.set({
68 method: "POST",
69 path: "/api/checkout",
70 requestId: "req_abc123", //
71 traceId: "trace_xyz", //
72});
73```
74
75### User Context
76
77```typescript
78log.set({
79 user: {
80 id: "user_123",
81 plan: "premium", //
82 accountAge: 847, //
83 subscription: "annual",
84 },
85});
86```
87
88### Business Context
89
90Add domain-specific data relevant to the operation:
91
92```typescript
93// E-commerce checkout
94log.set({
95 cart: { id: "
96 payment: { method: "
97 order: { id: "
98});
99
100// API rate limiting
101log.set({
102 rateLimit: {
103 limit: 1000,
104 remaining: 42,
105 resetAt: "2025-01-24T11:00:00Z",
106 },
107});
108
109// File upload
110log.set({
111 upload: {
112 filename: "document.pdf",
113 size: 1024000,
114 mimeType: "application/pdf",
115 },
116});
117```
118
119### Outcome
120
121```typescript
122// Success
123log.set({
124 status: 200,
125 // duration is added automatically by emit()
126});
127
128// Error
129log.error(error,
130 step: "payment",
131 retriable: false,
132});
133```
134
135## Pattern: Request Logger in API Routes
136
137### Nuxt/Nitro (Recommended)
138
139With the evlog module, use `useLogger(event)` - it's auto-created and auto-emitted:
140
141```typescript
142// server/api/checkout.post.ts
143// Nuxt: useLogger and createError are auto-imported
144// Nitro v3: import { useLogger } from 'evlog/nitro/v3'
145// Nitro v2: import { useLogger } from 'evlog/nitro'
146import { createError }
147
148export default defineEventHandler(async
149 const log =
150
151 const user =
152 log.set({
153
154 const cart =
155 log.set({
156
157 try {
158 const payment =
159 log.set({
160 } catch (error)
161 log.error(error
162 throw createError({
163 message: "Payment failed",
164 why: error.message
165 fix: "Try a different payment method",
166 });
167 }
168
169 const order =
170 log.set({
171
172 return order;
173 // log.emit() is called automatically at request end
174});
175```
176
177### Standalone TypeScript (Scripts, Workers)
178
179Without Nuxt/Nitro, use `createRequestLogger()` and call `emit()` manually:
180
181```typescript
182// scripts/sync-job.ts
183import { initLogger, createRequestLogger
184
185initLogger({ env: {
186
187async function processJob(
188 const log =
189
190 try {
191 log.set({
192
193 const result =
194 log.set({
195
196 return result;
197 } catch (error)
198 log.error(error
199 throw error;
200 } finally {
201 log.emit();
202 }
203}
204```
205
206## Transformation Examples
207
208### Before: Console.log Spam
209
210```typescript
211// server/api/checkout.post.ts
212
213export default defineEventHandler(async
214 console.log("Checkout started
215
216 const user =
217 console.log("User loaded:
218
219 const cart =
220 console.log("Cart loaded:
221
222 try {
223 const payment =
224 console.log("Payment successful:
225 return { orderId:
226 } catch (error)
227 console.error("Payment failed:
228 throw error;
229 }
230});
231```
232
233### After: Single Wide Event
234
235```typescript
236// server/api/checkout.post.ts
237// Nuxt: useLogger and createError are auto-imported
238// Nitro v3: import { useLogger } from 'evlog/nitro/v3'
239// Nitro v2: import { useLogger } from 'evlog/nitro'
240import { createError }
241
242export default defineEventHandler(async
243 const log =
244
245 const user =
246 log.set({
247
248 const cart =
249 log.set({
250
251 try {
252 const payment =
253 log.set({
254
255 return { orderId:
256 } catch (error)
257 log.error(error
258 throw createError({
259 message: "Payment failed",
260 why: error.message
261 fix: "Try a different payment method",
262 });
263 }
264 // emit() called automatically
265});
266```
267
268## Best Practices
269
270### Do
271
272- Include business-relevant context (user plan, cart value, etc.)
273- Add enough context to debug without looking elsewhere
274- Use consistent field names across your codebase
275- Let `emit()` calculate duration automatically
276
277### Don't
278
279- Log sensitive data (passwords, tokens, full credit card numbers)
280- Create multiple wide events for one logical operation
281- Forget to call `emit()` (or use the Nuxt module for auto-emit)
282- Include debugging logs inside wide events (remove them)
283
284## Security: Preventing Sensitive Data Leakage
285
286Always explicitly select which fields to log:
287
288```typescript
289// ❌ DANGEROUS - logs everything including password
290log.set({ user
291
292// ✅ SAFE - explicitly select fields
293log.set({
294 user: {
295 id: body.id
296 email: maskEmail(body
297 // password: body.password ← NEVER include
298 },
299});
300```
301
302**Never log:** passwords, API keys, tokens, secrets, full card numbers, CVV, SSN, PII, session tokens, JWTs.
303
304**Sanitization helpers:**
305
306```typescript
307// server/utils/sanitize.ts
308export function maskEmail(email
309 const [local,
310 if (!domain)
311 return `${local[
312}
313
314export function maskCard(card
315 return `****${card
316}
317```
318
319See [code-review.md](code-review.md
320
321## Field Naming Conventions
322
323Use consistent, descriptive field names:
324
325```typescript
326// ✅ Good - grouped, descriptive
327log.set({
328 user: { id,
329 cart: { items,
330 payment: { method,
331});
332
333// ❌ Bad - flat, abbreviated
334log.set({
335 uid: "123",
336 n: 3,
337 t: 9999,
338 pm: "card",
339});
340```
"
2025-01-24T10:23:45.235Z
"
,
"
error
"
,
"
api
"
,
"
POST
"
,
"
/checkout
"
,
"
234ms
"
,
{
"
id
"
:
"
user_123
"
,
"
plan
"
:
"
premium
"
,
"
accountAge
"
:
847
}
,
{
"
items
"
:
3
,
"
total
"
:
9999
}
,
{
"
provider
"
:
"
stripe
"
,
"
method
"
:
"
card
"
}
,
{
"
code
"
:
"
card_declined
"
,
"
retriable
"
:
false
}
For tracing
Distributed tracing
Business-relevant
Days since signup
cart_xyz
"
,
items
:
3
,
total
:
9999
}
,
card
"
,
provider
:
"
stripe
"
}
,
order_123
"
,
status
:
"
created
"
}
,
{
from
"
evlog
"
;
(
event
)
=>
{
useLogger
(
event
)
;
//
Auto-created by evlog
await
requireAuth
(
event
)
;
user
:
{
id
:
user
.
id
,
plan
:
user
.
plan
}
}
)
;
await
getCart
(
user
.
id
)
;
cart
:
{
items
:
cart
.
items
.
length
,
total
:
cart
.
total
}
}
)
;
await
processPayment
(
cart
,
user
)
;
payment
:
{
id
:
payment
.
id
,
method
:
payment
.
method
}
}
)
;
{
,
{
step
:
"
payment
"
}
)
;
,
await
createOrder
(
cart
,
user
)
;
order
:
{
id
:
order
.
id
,
status
:
order
.
status
}
}
)
;
}
from
"
evlog
"
;
service
:
"
sync-worker
"
,
environment
:
"
production
"
}
}
)
;
job
:
Job
)
{
createRequestLogger
(
{
jobId
:
job
.
id
,
type
:
"
sync
"
}
)
;
source
:
job
.
source
,
target
:
job
.
target
}
)
;
await
performSync
(
job
)
;
recordsSynced
:
result
.
count
}
)
;
{
,
{
step
:
"
sync
"
}
)
;
//
Manual emit required
(
event
)
=>
{
"
)
;
await
getUser
(
event
)
;
"
,
user
.
id
)
;
await
getCart
(
user
.
id
)
;
"
,
cart
.
items
.
length
,
"
items
"
)
;
await
processPayment
(
cart
)
;
"
,
payment
.
id
)
;
payment
.
orderId
}
;
{
"
,
error
.
message
)
;
from
"
evlog
"
;
(
event
)
=>
{
useLogger
(
event
)
;
await
getUser
(
event
)
;
user
:
{
id
:
user
.
id
,
plan
:
user
.
plan
}
}
)
;
await
getCart
(
user
.
id
)
;
cart
:
{
items
:
cart
.
items
.
length
,
total
:
cart
.
total
}
}
)
;
await
processPayment
(
cart
)
;
payment
:
{
id
:
payment
.
id
}
,
order
:
{
id
:
payment
.
orderId
}
}
)
;
payment
.
orderId
}
;
{
,
{
step
:
"
payment
"
}
)
;
,
:
body
}
)
;
,
.
email
)
,
:
string
)
:
string
{
domain
]
=
email
.
split
(
"
@
"
)
;
return
"
***
"
;
0
]
}
***@
${
domain
[
0
]
}
***.
${
domain
.
split
(
"
.
"
)[
1
]
}
`
;
:
string
)
:
string
{
.
slice
(
-
4
)
}
`
;
)
for the full security review checklist.
plan
,
accountAge
}
,
total
}
,
provider
}
,