@safaricom-mxl/log 0.0.3
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 +1040 -0
- package/dist/_http-DmaJ426Z.mjs +76 -0
- package/dist/_http-DmaJ426Z.mjs.map +1 -0
- package/dist/_severity-D_IU9-90.mjs +17 -0
- package/dist/_severity-D_IU9-90.mjs.map +1 -0
- package/dist/adapters/axiom.d.mts +64 -0
- package/dist/adapters/axiom.d.mts.map +1 -0
- package/dist/adapters/axiom.mjs +100 -0
- package/dist/adapters/axiom.mjs.map +1 -0
- package/dist/adapters/better-stack.d.mts +63 -0
- package/dist/adapters/better-stack.d.mts.map +1 -0
- package/dist/adapters/better-stack.mjs +98 -0
- package/dist/adapters/better-stack.mjs.map +1 -0
- package/dist/adapters/otlp.d.mts +85 -0
- package/dist/adapters/otlp.d.mts.map +1 -0
- package/dist/adapters/otlp.mjs +196 -0
- package/dist/adapters/otlp.mjs.map +1 -0
- package/dist/adapters/posthog.d.mts +107 -0
- package/dist/adapters/posthog.d.mts.map +1 -0
- package/dist/adapters/posthog.mjs +166 -0
- package/dist/adapters/posthog.mjs.map +1 -0
- package/dist/adapters/sentry.d.mts +80 -0
- package/dist/adapters/sentry.d.mts.map +1 -0
- package/dist/adapters/sentry.mjs +221 -0
- package/dist/adapters/sentry.mjs.map +1 -0
- package/dist/browser.d.mts +63 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.mjs +95 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/enrichers.d.mts +74 -0
- package/dist/enrichers.d.mts.map +1 -0
- package/dist/enrichers.mjs +172 -0
- package/dist/enrichers.mjs.map +1 -0
- package/dist/error.d.mts +65 -0
- package/dist/error.d.mts.map +1 -0
- package/dist/error.mjs +112 -0
- package/dist/error.mjs.map +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +6 -0
- package/dist/logger.d.mts +46 -0
- package/dist/logger.d.mts.map +1 -0
- package/dist/logger.mjs +287 -0
- package/dist/logger.mjs.map +1 -0
- package/dist/next/client.d.mts +55 -0
- package/dist/next/client.d.mts.map +1 -0
- package/dist/next/client.mjs +44 -0
- package/dist/next/client.mjs.map +1 -0
- package/dist/next/index.d.mts +169 -0
- package/dist/next/index.d.mts.map +1 -0
- package/dist/next/index.mjs +280 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/nitro/errorHandler.d.mts +15 -0
- package/dist/nitro/errorHandler.d.mts.map +1 -0
- package/dist/nitro/errorHandler.mjs +41 -0
- package/dist/nitro/errorHandler.mjs.map +1 -0
- package/dist/nitro/module.d.mts +11 -0
- package/dist/nitro/module.d.mts.map +1 -0
- package/dist/nitro/module.mjs +23 -0
- package/dist/nitro/module.mjs.map +1 -0
- package/dist/nitro/plugin.d.mts +7 -0
- package/dist/nitro/plugin.d.mts.map +1 -0
- package/dist/nitro/plugin.mjs +145 -0
- package/dist/nitro/plugin.mjs.map +1 -0
- package/dist/nitro/v3/errorHandler.d.mts +24 -0
- package/dist/nitro/v3/errorHandler.d.mts.map +1 -0
- package/dist/nitro/v3/errorHandler.mjs +36 -0
- package/dist/nitro/v3/errorHandler.mjs.map +1 -0
- package/dist/nitro/v3/index.d.mts +5 -0
- package/dist/nitro/v3/index.mjs +5 -0
- package/dist/nitro/v3/middleware.d.mts +25 -0
- package/dist/nitro/v3/middleware.d.mts.map +1 -0
- package/dist/nitro/v3/middleware.mjs +45 -0
- package/dist/nitro/v3/middleware.mjs.map +1 -0
- package/dist/nitro/v3/module.d.mts +10 -0
- package/dist/nitro/v3/module.d.mts.map +1 -0
- package/dist/nitro/v3/module.mjs +22 -0
- package/dist/nitro/v3/module.mjs.map +1 -0
- package/dist/nitro/v3/plugin.d.mts +14 -0
- package/dist/nitro/v3/plugin.d.mts.map +1 -0
- package/dist/nitro/v3/plugin.mjs +162 -0
- package/dist/nitro/v3/plugin.mjs.map +1 -0
- package/dist/nitro/v3/useLogger.d.mts +24 -0
- package/dist/nitro/v3/useLogger.d.mts.map +1 -0
- package/dist/nitro/v3/useLogger.mjs +27 -0
- package/dist/nitro/v3/useLogger.mjs.map +1 -0
- package/dist/nitro-CrFBjY1Y.d.mts +42 -0
- package/dist/nitro-CrFBjY1Y.d.mts.map +1 -0
- package/dist/nitro-Dsv6dSzv.mjs +39 -0
- package/dist/nitro-Dsv6dSzv.mjs.map +1 -0
- package/dist/nuxt/module.d.mts +164 -0
- package/dist/nuxt/module.d.mts.map +1 -0
- package/dist/nuxt/module.mjs +84 -0
- package/dist/nuxt/module.mjs.map +1 -0
- package/dist/pipeline.d.mts +46 -0
- package/dist/pipeline.d.mts.map +1 -0
- package/dist/pipeline.mjs +122 -0
- package/dist/pipeline.mjs.map +1 -0
- package/dist/routes-BNbrnm14.mjs +39 -0
- package/dist/routes-BNbrnm14.mjs.map +1 -0
- package/dist/runtime/client/log.d.mts +15 -0
- package/dist/runtime/client/log.d.mts.map +1 -0
- package/dist/runtime/client/log.mjs +92 -0
- package/dist/runtime/client/log.mjs.map +1 -0
- package/dist/runtime/client/plugin.d.mts +5 -0
- package/dist/runtime/client/plugin.d.mts.map +1 -0
- package/dist/runtime/client/plugin.mjs +17 -0
- package/dist/runtime/client/plugin.mjs.map +1 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.d.mts +7 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.d.mts.map +1 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.mjs +123 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.mjs.map +1 -0
- package/dist/runtime/server/useLogger.d.mts +39 -0
- package/dist/runtime/server/useLogger.d.mts.map +1 -0
- package/dist/runtime/server/useLogger.mjs +43 -0
- package/dist/runtime/server/useLogger.mjs.map +1 -0
- package/dist/runtime/utils/parseError.d.mts +7 -0
- package/dist/runtime/utils/parseError.d.mts.map +1 -0
- package/dist/runtime/utils/parseError.mjs +29 -0
- package/dist/runtime/utils/parseError.mjs.map +1 -0
- package/dist/types.d.mts +496 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +1 -0
- package/dist/utils.d.mts +34 -0
- package/dist/utils.d.mts.map +1 -0
- package/dist/utils.mjs +78 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/workers.d.mts +46 -0
- package/dist/workers.d.mts.map +1 -0
- package/dist/workers.mjs +81 -0
- package/dist/workers.mjs.map +1 -0
- package/package.json +195 -0
package/README.md
ADDED
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
# mxllog
|
|
2
|
+
|
|
3
|
+
[](https://npmjs.com/package/mxllog)
|
|
4
|
+
[](https://npm.chart.dev/mxllog)
|
|
5
|
+
[](https://github.com/HugoRCD/mxllog/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://mxllog.dev)
|
|
8
|
+
[](https://github.com/HugoRCD/mxllog/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
**Your logs are lying to you.**
|
|
11
|
+
|
|
12
|
+
A single request generates 10+ log lines. When production breaks at 3am, you're grep-ing through noise, praying you'll find signal. Your errors say "Something went wrong" -- thanks, very helpful.
|
|
13
|
+
|
|
14
|
+
**mxllog fixes this.** One log per request. All context included. Errors that explain themselves.
|
|
15
|
+
|
|
16
|
+
## Why mxllog?
|
|
17
|
+
|
|
18
|
+
### The Problem
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// server/api/checkout.post.ts
|
|
22
|
+
|
|
23
|
+
// Scattered logs - impossible to debug
|
|
24
|
+
console.log('Request received')
|
|
25
|
+
console.log('User:', user.id)
|
|
26
|
+
console.log('Cart loaded')
|
|
27
|
+
console.log('Payment failed') // Good luck finding this at 3am
|
|
28
|
+
|
|
29
|
+
throw new Error('Something went wrong')
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### The Solution
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// server/api/checkout.post.ts
|
|
36
|
+
import { useLogger } from '@safaricom-mxl/log'
|
|
37
|
+
|
|
38
|
+
// One comprehensive event per request
|
|
39
|
+
export default defineEventHandler(async (event) => {
|
|
40
|
+
const log = useLogger(event) // Auto-injected by mxllog
|
|
41
|
+
|
|
42
|
+
log.set({ user: { id: user.id, plan: 'premium' } })
|
|
43
|
+
log.set({ cart: { items: 3, total: 9999 } })
|
|
44
|
+
log.error(error, { step: 'payment' })
|
|
45
|
+
|
|
46
|
+
// Emits ONE event with ALL context + duration (automatic)
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Output:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"timestamp": "2025-01-24T10:23:45.612Z",
|
|
55
|
+
"level": "error",
|
|
56
|
+
"service": "my-app",
|
|
57
|
+
"method": "POST",
|
|
58
|
+
"path": "/api/checkout",
|
|
59
|
+
"duration": "1.2s",
|
|
60
|
+
"user": { "id": "123", "plan": "premium" },
|
|
61
|
+
"cart": { "items": 3, "total": 9999 },
|
|
62
|
+
"error": { "message": "Card declined", "step": "payment" }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Built for AI-Assisted Development
|
|
67
|
+
|
|
68
|
+
We're in the age of AI agents writing and debugging code. When an agent encounters an error, it needs **clear, structured context** to understand what happened and how to fix it.
|
|
69
|
+
|
|
70
|
+
Traditional logs force agents to grep through noise. mxllog gives them:
|
|
71
|
+
- **One event per request** with all context in one place
|
|
72
|
+
- **Self-documenting errors** with `why` and `fix` fields
|
|
73
|
+
- **Structured JSON** that's easy to parse and reason about
|
|
74
|
+
|
|
75
|
+
Your AI copilot will thank you.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install mxllog
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Nuxt Integration
|
|
86
|
+
|
|
87
|
+
The recommended way to use mxllog. Zero config, everything just works.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// nuxt.config.ts
|
|
91
|
+
export default defineNuxtConfig({
|
|
92
|
+
modules: ['@safaricom-mxl/log/nuxt'],
|
|
93
|
+
|
|
94
|
+
mxllog: {
|
|
95
|
+
env: {
|
|
96
|
+
service: 'my-app',
|
|
97
|
+
},
|
|
98
|
+
// Optional: only log specific routes (supports glob patterns)
|
|
99
|
+
include: ['/api/**'],
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Tip:** Use `$production` to enable [sampling](#sampling) only in production:
|
|
105
|
+
> ```typescript
|
|
106
|
+
> export default defineNuxtConfig({
|
|
107
|
+
> modules: ['@safaricom-mxl/log/nuxt'],
|
|
108
|
+
> mxllog: { env: { service: 'my-app' } },
|
|
109
|
+
> $production: {
|
|
110
|
+
> mxllog: { sampling: { rates: { info: 10, warn: 50, debug: 0 } } },
|
|
111
|
+
> },
|
|
112
|
+
> })
|
|
113
|
+
> ```
|
|
114
|
+
|
|
115
|
+
That's it. Now use `useLogger(event)` in any API route:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// server/api/checkout.post.ts
|
|
119
|
+
import { useLogger, createError } from '@safaricom-mxl/log'
|
|
120
|
+
|
|
121
|
+
export default defineEventHandler(async (event) => {
|
|
122
|
+
const log = useLogger(event)
|
|
123
|
+
|
|
124
|
+
// Authenticate user and add to wide event
|
|
125
|
+
const user = await requireAuth(event)
|
|
126
|
+
log.set({ user: { id: user.id, plan: user.plan } })
|
|
127
|
+
|
|
128
|
+
// Load cart and add to wide event
|
|
129
|
+
const cart = await getCart(user.id)
|
|
130
|
+
log.set({ cart: { items: cart.items.length, total: cart.total } })
|
|
131
|
+
|
|
132
|
+
// Process payment
|
|
133
|
+
try {
|
|
134
|
+
const payment = await processPayment(cart, user)
|
|
135
|
+
log.set({ payment: { id: payment.id, method: payment.method } })
|
|
136
|
+
} catch (error) {
|
|
137
|
+
log.error(error, { step: 'payment' })
|
|
138
|
+
|
|
139
|
+
throw createError({
|
|
140
|
+
message: 'Payment failed',
|
|
141
|
+
status: 402,
|
|
142
|
+
why: error.message,
|
|
143
|
+
fix: 'Try a different payment method or contact your bank',
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create order
|
|
148
|
+
const order = await createOrder(cart, user)
|
|
149
|
+
log.set({ order: { id: order.id, status: order.status } })
|
|
150
|
+
|
|
151
|
+
return order
|
|
152
|
+
// log.emit() called automatically at request end
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The wide event emitted at the end contains **everything**:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"timestamp": "2026-01-24T10:23:45.612Z",
|
|
161
|
+
"level": "info",
|
|
162
|
+
"service": "my-app",
|
|
163
|
+
"method": "POST",
|
|
164
|
+
"path": "/api/checkout",
|
|
165
|
+
"duration": "1.2s",
|
|
166
|
+
"user": { "id": "user_123", "plan": "premium" },
|
|
167
|
+
"cart": { "items": 3, "total": 9999 },
|
|
168
|
+
"payment": { "id": "pay_xyz", "method": "card" },
|
|
169
|
+
"order": { "id": "order_abc", "status": "created" },
|
|
170
|
+
"status": 200
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Nitro Integration
|
|
175
|
+
|
|
176
|
+
Works with **any framework powered by Nitro**: Nuxt, Analog, Vinxi, SolidStart, TanStack Start, and more.
|
|
177
|
+
|
|
178
|
+
### Nitro v3
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// nitro.config.ts
|
|
182
|
+
import { defineConfig } from 'nitro'
|
|
183
|
+
import mxllog from '@safaricom-mxl/log/nitro/v3'
|
|
184
|
+
|
|
185
|
+
export default defineConfig({
|
|
186
|
+
modules: [
|
|
187
|
+
mxllog({ env: { service: 'my-api' } })
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Nitro v2
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// nitro.config.ts
|
|
196
|
+
import { defineNitroConfig } from 'nitropack/config'
|
|
197
|
+
import mxllog from '@safaricom-mxl/log/nitro'
|
|
198
|
+
|
|
199
|
+
export default defineNitroConfig({
|
|
200
|
+
modules: [
|
|
201
|
+
mxllog({ env: { service: 'my-api' } })
|
|
202
|
+
],
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Then use `useLogger` in any route. Import from `mxllog/nitro/v3` (v3) or `mxllog/nitro` (v2):
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// routes/api/documents/[id]/export.post.ts
|
|
210
|
+
// Nitro v3: import { defineHandler } from 'nitro/h3' + import { useLogger } from '@safaricom-mxl/log/nitro/v3'
|
|
211
|
+
// Nitro v2: import { defineEventHandler } from 'h3' + import { useLogger } from '@safaricom-mxl/log/nitro'
|
|
212
|
+
import { defineEventHandler } from 'h3'
|
|
213
|
+
import { useLogger } from '@safaricom-mxl/log/nitro'
|
|
214
|
+
import { createError } from '@safaricom-mxl/log'
|
|
215
|
+
|
|
216
|
+
export default defineEventHandler(async (event) => {
|
|
217
|
+
const log = useLogger(event)
|
|
218
|
+
|
|
219
|
+
// Get document ID from route params
|
|
220
|
+
const documentId = getRouterParam(event, 'id')
|
|
221
|
+
log.set({ document: { id: documentId } })
|
|
222
|
+
|
|
223
|
+
// Parse request body for export options
|
|
224
|
+
const body = await readBody(event)
|
|
225
|
+
log.set({ export: { format: body.format, includeComments: body.includeComments } })
|
|
226
|
+
|
|
227
|
+
// Load document from database
|
|
228
|
+
const document = await db.documents.findUnique({ where: { id: documentId } })
|
|
229
|
+
if (!document) {
|
|
230
|
+
throw createError({
|
|
231
|
+
message: 'Document not found',
|
|
232
|
+
status: 404,
|
|
233
|
+
why: `No document with ID "${documentId}" exists`,
|
|
234
|
+
fix: 'Check the document ID and try again',
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
log.set({ document: { id: documentId, title: document.title, pages: document.pages.length } })
|
|
238
|
+
|
|
239
|
+
// Generate export
|
|
240
|
+
try {
|
|
241
|
+
const exportResult = await generateExport(document, body.format)
|
|
242
|
+
log.set({ export: { format: body.format, size: exportResult.size, pages: exportResult.pages } })
|
|
243
|
+
|
|
244
|
+
return { url: exportResult.url, expiresAt: exportResult.expiresAt }
|
|
245
|
+
} catch (error) {
|
|
246
|
+
log.error(error, { step: 'export-generation' })
|
|
247
|
+
|
|
248
|
+
throw createError({
|
|
249
|
+
message: 'Export failed',
|
|
250
|
+
status: 500,
|
|
251
|
+
why: `Failed to generate ${body.format} export: ${error.message}`,
|
|
252
|
+
fix: 'Try a different format or contact support',
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
// log.emit() called automatically - outputs one comprehensive wide event
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Output when the export completes:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"timestamp": "2025-01-24T14:32:10.123Z",
|
|
264
|
+
"level": "info",
|
|
265
|
+
"service": "document-api",
|
|
266
|
+
"method": "POST",
|
|
267
|
+
"path": "/api/documents/doc_123/export",
|
|
268
|
+
"duration": "2.4s",
|
|
269
|
+
"document": { "id": "doc_123", "title": "Q4 Report", "pages": 24 },
|
|
270
|
+
"export": { "format": "pdf", "size": 1240000, "pages": 24 },
|
|
271
|
+
"status": 200
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Standalone TypeScript
|
|
276
|
+
|
|
277
|
+
For scripts, workers, or any TypeScript project:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// scripts/migrate.ts
|
|
281
|
+
import { initLogger, log, createRequestLogger } from '@safaricom-mxl/log'
|
|
282
|
+
|
|
283
|
+
// Initialize once at script start
|
|
284
|
+
initLogger({
|
|
285
|
+
env: {
|
|
286
|
+
service: 'migration-script',
|
|
287
|
+
environment: 'production',
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// Simple logging
|
|
292
|
+
log.info('migration', 'Starting database migration')
|
|
293
|
+
log.info({ action: 'migration', tables: ['users', 'orders'] })
|
|
294
|
+
|
|
295
|
+
// Or use request logger for a logical operation
|
|
296
|
+
const migrationLog = createRequestLogger({ action: 'full-migration' })
|
|
297
|
+
|
|
298
|
+
migrationLog.set({ tables: ['users', 'orders', 'products'] })
|
|
299
|
+
migrationLog.set({ rowsProcessed: 15000 })
|
|
300
|
+
migrationLog.emit()
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// workers/sync-job.ts
|
|
305
|
+
import { initLogger, createRequestLogger, createError } from '@safaricom-mxl/log'
|
|
306
|
+
|
|
307
|
+
initLogger({
|
|
308
|
+
env: {
|
|
309
|
+
service: 'sync-worker',
|
|
310
|
+
environment: process.env.NODE_ENV,
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
async function processSyncJob(job: Job) {
|
|
315
|
+
const log = createRequestLogger({ jobId: job.id, type: 'sync' })
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
log.set({ source: job.source, target: job.target })
|
|
319
|
+
|
|
320
|
+
const result = await performSync(job)
|
|
321
|
+
log.set({ recordsSynced: result.count })
|
|
322
|
+
|
|
323
|
+
return result
|
|
324
|
+
} catch (error) {
|
|
325
|
+
log.error(error, { step: 'sync' })
|
|
326
|
+
throw error
|
|
327
|
+
} finally {
|
|
328
|
+
log.emit()
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Cloudflare Workers
|
|
334
|
+
|
|
335
|
+
Use the Workers adapter for structured logs and correct platform severity.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// src/index.ts
|
|
339
|
+
import { initWorkersLogger, createWorkersLogger } from '@safaricom-mxl/log/workers'
|
|
340
|
+
|
|
341
|
+
initWorkersLogger({
|
|
342
|
+
env: { service: 'edge-api' },
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
export default {
|
|
346
|
+
async fetch(request: Request) {
|
|
347
|
+
const log = createWorkersLogger(request)
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
log.set({ route: 'health' })
|
|
351
|
+
const response = new Response('ok', { status: 200 })
|
|
352
|
+
log.emit({ status: response.status })
|
|
353
|
+
return response
|
|
354
|
+
} catch (error) {
|
|
355
|
+
log.error(error as Error)
|
|
356
|
+
log.emit({ status: 500 })
|
|
357
|
+
throw error
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Disable invocation logs to avoid duplicate request logs:
|
|
364
|
+
|
|
365
|
+
```toml
|
|
366
|
+
# wrangler.toml
|
|
367
|
+
[observability.logs]
|
|
368
|
+
invocation_logs = false
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Notes:
|
|
372
|
+
- `requestId` defaults to `cf-ray` when available
|
|
373
|
+
- `request.cf` is included (colo, country, asn) unless disabled
|
|
374
|
+
- Use `headerAllowlist` to avoid logging sensitive headers
|
|
375
|
+
|
|
376
|
+
## Hono
|
|
377
|
+
|
|
378
|
+
Use the standalone API to create one wide event per request from a Hono middleware.
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// src/index.ts
|
|
382
|
+
import { serve } from '@hono/node-server'
|
|
383
|
+
import { Hono } from 'hono'
|
|
384
|
+
import { createRequestLogger, initLogger } from '@safaricom-mxl/log'
|
|
385
|
+
|
|
386
|
+
initLogger({
|
|
387
|
+
env: { service: 'hono-api' },
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const app = new Hono()
|
|
391
|
+
|
|
392
|
+
app.use('*', async (c, next) => {
|
|
393
|
+
const startedAt = Date.now()
|
|
394
|
+
const log = createRequestLogger({ method: c.req.method, path: c.req.path })
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
await next()
|
|
398
|
+
} catch (error) {
|
|
399
|
+
log.error(error as Error)
|
|
400
|
+
throw error
|
|
401
|
+
} finally {
|
|
402
|
+
log.emit({
|
|
403
|
+
status: c.res.status,
|
|
404
|
+
duration: Date.now() - startedAt,
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
app.get('/health', (c) => c.json({ ok: true }))
|
|
410
|
+
|
|
411
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
See the full [hono example](https://github.com/HugoRCD/mxllog/tree/main/examples/hono) for a complete working project.
|
|
415
|
+
|
|
416
|
+
## Browser
|
|
417
|
+
|
|
418
|
+
Use the `log` API on the client side for structured browser logging:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { log } from '@safaricom-mxl/log/browser'
|
|
422
|
+
|
|
423
|
+
log.info('checkout', 'User initiated checkout')
|
|
424
|
+
log.error({ action: 'payment', error: 'validation_failed' })
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
In Nuxt, `log` is auto-imported -- no import needed in Vue components:
|
|
428
|
+
|
|
429
|
+
```vue
|
|
430
|
+
<script setup>
|
|
431
|
+
log.info('checkout', 'User initiated checkout')
|
|
432
|
+
</script>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Client logs output to the browser console with colored tags in development.
|
|
436
|
+
|
|
437
|
+
### Client Transport
|
|
438
|
+
|
|
439
|
+
To send client logs to the server for centralized logging, enable the transport:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// nuxt.config.ts
|
|
443
|
+
export default defineNuxtConfig({
|
|
444
|
+
modules: ['@safaricom-mxl/log/nuxt'],
|
|
445
|
+
mxllog: {
|
|
446
|
+
transport: {
|
|
447
|
+
enabled: true, // Send client logs to server
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
When enabled:
|
|
454
|
+
1. Client logs are sent to `/api/_mxllog/ingest` via POST
|
|
455
|
+
2. Server enriches with environment context (service, version, etc.)
|
|
456
|
+
3. `mxllog:drain` hook is called with `source: 'client'`
|
|
457
|
+
4. External services receive the log
|
|
458
|
+
|
|
459
|
+
## Structured Errors
|
|
460
|
+
|
|
461
|
+
Errors should tell you **what** happened, **why**, and **how to fix it**.
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
// server/api/repos/sync.post.ts
|
|
465
|
+
import { useLogger, createError } from '@safaricom-mxl/log'
|
|
466
|
+
|
|
467
|
+
export default defineEventHandler(async (event) => {
|
|
468
|
+
const log = useLogger(event)
|
|
469
|
+
|
|
470
|
+
log.set({ repo: { owner: 'acme', name: 'my-project' } })
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const result = await syncWithGitHub()
|
|
474
|
+
log.set({ sync: { commits: result.commits, files: result.files } })
|
|
475
|
+
return result
|
|
476
|
+
} catch (error) {
|
|
477
|
+
log.error(error, { step: 'github-sync' })
|
|
478
|
+
|
|
479
|
+
throw createError({
|
|
480
|
+
message: 'Failed to sync repository',
|
|
481
|
+
status: 503,
|
|
482
|
+
why: 'GitHub API rate limit exceeded',
|
|
483
|
+
fix: 'Wait 1 hour or use a different token',
|
|
484
|
+
link: 'https://docs.github.com/en/rest/rate-limit',
|
|
485
|
+
cause: error,
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Console output (development):
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
Error: Failed to sync repository
|
|
495
|
+
Why: GitHub API rate limit exceeded
|
|
496
|
+
Fix: Wait 1 hour or use a different token
|
|
497
|
+
More info: https://docs.github.com/en/rest/rate-limit
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Enrichment Hook
|
|
501
|
+
|
|
502
|
+
Use the `mxllog:enrich` hook to add derived context after emit, before drain.
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// server/plugins/mxllog-enrich.ts
|
|
506
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
507
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:enrich', (ctx) => {
|
|
508
|
+
ctx.event.deploymentId = process.env.DEPLOYMENT_ID
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Built-in Enrichers
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// server/plugins/mxllog-enrich.ts
|
|
517
|
+
import {
|
|
518
|
+
createGeoEnricher,
|
|
519
|
+
createRequestSizeEnricher,
|
|
520
|
+
createTraceContextEnricher,
|
|
521
|
+
createUserAgentEnricher,
|
|
522
|
+
} from '@safaricom-mxl/log/enrichers'
|
|
523
|
+
|
|
524
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
525
|
+
const enrich = [
|
|
526
|
+
createUserAgentEnricher(),
|
|
527
|
+
createGeoEnricher(),
|
|
528
|
+
createRequestSizeEnricher(),
|
|
529
|
+
createTraceContextEnricher(),
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:enrich', (ctx) => {
|
|
533
|
+
for (const enricher of enrich) enricher(ctx)
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Each enricher adds a specific field to the event:
|
|
539
|
+
|
|
540
|
+
| Enricher | Event Field | Shape |
|
|
541
|
+
|----------|-------------|-------|
|
|
542
|
+
| `createUserAgentEnricher()` | `event.userAgent` | `{ raw, browser?: { name, version? }, os?: { name, version? }, device?: { type } }` |
|
|
543
|
+
| `createGeoEnricher()` | `event.geo` | `{ country?, region?, regionCode?, city?, latitude?, longitude? }` |
|
|
544
|
+
| `createRequestSizeEnricher()` | `event.requestSize` | `{ requestBytes?, responseBytes? }` |
|
|
545
|
+
| `createTraceContextEnricher()` | `event.traceContext` + `event.traceId` + `event.spanId` | `{ traceparent?, tracestate?, traceId?, spanId? }` |
|
|
546
|
+
|
|
547
|
+
All enrichers accept an optional `{ overwrite?: boolean }` option. By default (`overwrite: false`), user-provided data on the event takes precedence over enricher-computed values. Set `overwrite: true` to always replace existing fields.
|
|
548
|
+
|
|
549
|
+
> **Cloudflare geo note:** Only `cf-ipcountry` is a real Cloudflare HTTP header. The `cf-region`, `cf-city`, `cf-latitude`, `cf-longitude` headers are NOT standard -- they are properties of `request.cf`. For full geo data on Cloudflare, write a custom enricher that reads `request.cf`, or use a Workers middleware to forward `cf` properties as custom headers.
|
|
550
|
+
|
|
551
|
+
### Custom Enrichers
|
|
552
|
+
|
|
553
|
+
The `mxllog:enrich` hook receives an `EnrichContext` with these fields:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
interface EnrichContext {
|
|
557
|
+
event: WideEvent // The emitted wide event (mutable -- modify it directly)
|
|
558
|
+
request?: { // Request metadata
|
|
559
|
+
method?: string
|
|
560
|
+
path?: string
|
|
561
|
+
requestId?: string
|
|
562
|
+
}
|
|
563
|
+
headers?: Record<string, string> // Safe HTTP headers (sensitive headers filtered)
|
|
564
|
+
response?: { // Response metadata
|
|
565
|
+
status?: number
|
|
566
|
+
headers?: Record<string, string>
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Example custom enricher:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// server/plugins/mxllog-enrich.ts
|
|
575
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
576
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:enrich', (ctx) => {
|
|
577
|
+
// Add deployment metadata
|
|
578
|
+
ctx.event.deploymentId = process.env.DEPLOYMENT_ID
|
|
579
|
+
ctx.event.region = process.env.FLY_REGION
|
|
580
|
+
|
|
581
|
+
// Extract data from headers
|
|
582
|
+
const tenantId = ctx.headers?.['x-tenant-id']
|
|
583
|
+
if (tenantId) {
|
|
584
|
+
ctx.event.tenantId = tenantId
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## Adapters
|
|
591
|
+
|
|
592
|
+
Send your logs to external observability platforms with built-in adapters.
|
|
593
|
+
|
|
594
|
+
### Axiom
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
// server/plugins/mxllog-drain.ts
|
|
598
|
+
import { createAxiomDrain } from '@safaricom-mxl/log/axiom'
|
|
599
|
+
|
|
600
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
601
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', createAxiomDrain())
|
|
602
|
+
})
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Set environment variables:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
NUXT_AXIOM_TOKEN=xaat-your-token
|
|
609
|
+
NUXT_AXIOM_DATASET=your-dataset
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### OTLP (OpenTelemetry)
|
|
613
|
+
|
|
614
|
+
Works with Grafana, Datadog, Honeycomb, and any OTLP-compatible backend.
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
// server/plugins/mxllog-drain.ts
|
|
618
|
+
import { createOTLPDrain } from '@safaricom-mxl/log/otlp'
|
|
619
|
+
|
|
620
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
621
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', createOTLPDrain())
|
|
622
|
+
})
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Set environment variables:
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
NUXT_OTLP_ENDPOINT=http://localhost:4318
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### PostHog
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// server/plugins/mxllog-drain.ts
|
|
635
|
+
import { createPostHogDrain } from '@safaricom-mxl/log/posthog'
|
|
636
|
+
|
|
637
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
638
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', createPostHogDrain())
|
|
639
|
+
})
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Set environment variables:
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
NUXT_POSTHOG_API_KEY=phc_your-key
|
|
646
|
+
NUXT_POSTHOG_HOST=https://us.i.posthog.com # Optional: for EU or self-hosted
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Sentry
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
// server/plugins/mxllog-drain.ts
|
|
653
|
+
import { createSentryDrain } from '@safaricom-mxl/log/sentry'
|
|
654
|
+
|
|
655
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
656
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', createSentryDrain())
|
|
657
|
+
})
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Set environment variables:
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
NUXT_SENTRY_DSN=https://public@o0.ingest.sentry.io/123
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Better Stack
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// server/plugins/mxllog-drain.ts
|
|
670
|
+
import { createBetterStackDrain } from '@safaricom-mxl/log/better-stack'
|
|
671
|
+
|
|
672
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
673
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', createBetterStackDrain())
|
|
674
|
+
})
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Set environment variables:
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
NUXT_BETTER_STACK_SOURCE_TOKEN=your-source-token
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Multiple Destinations
|
|
684
|
+
|
|
685
|
+
Send logs to multiple services:
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
// server/plugins/mxllog-drain.ts
|
|
689
|
+
import { createAxiomDrain } from '@safaricom-mxl/log/axiom'
|
|
690
|
+
import { createOTLPDrain } from '@safaricom-mxl/log/otlp'
|
|
691
|
+
|
|
692
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
693
|
+
const axiom = createAxiomDrain()
|
|
694
|
+
const otlp = createOTLPDrain()
|
|
695
|
+
|
|
696
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', async (ctx) => {
|
|
697
|
+
await Promise.allSettled([axiom(ctx), otlp(ctx)])
|
|
698
|
+
})
|
|
699
|
+
})
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Custom Adapters
|
|
703
|
+
|
|
704
|
+
Build your own adapter for any destination:
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
// server/plugins/mxllog-drain.ts
|
|
708
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
709
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', async (ctx) => {
|
|
710
|
+
await fetch('https://your-service.com/logs', {
|
|
711
|
+
method: 'POST',
|
|
712
|
+
headers: { 'Content-Type': 'application/json' },
|
|
713
|
+
body: JSON.stringify(ctx.event),
|
|
714
|
+
})
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
> See the [full documentation](https://mxllog.hrcd.fr/adapters/overview) for adapter configuration options, troubleshooting, and advanced patterns.
|
|
720
|
+
|
|
721
|
+
## Drain Pipeline
|
|
722
|
+
|
|
723
|
+
For production use, wrap your drain adapter with `createDrainPipeline` to get **batching**, **retry with backoff**, and **buffer overflow protection**.
|
|
724
|
+
|
|
725
|
+
Without a pipeline, each event triggers a separate network call. The pipeline buffers events and sends them in batches, reducing overhead and handling transient failures automatically.
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
// server/plugins/mxllog-drain.ts
|
|
729
|
+
import type { DrainContext } from '@safaricom-mxl/log'
|
|
730
|
+
import { createDrainPipeline } from '@safaricom-mxl/log/pipeline'
|
|
731
|
+
import { createAxiomDrain } from '@safaricom-mxl/log/axiom'
|
|
732
|
+
|
|
733
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
734
|
+
const pipeline = createDrainPipeline<DrainContext>({
|
|
735
|
+
batch: { size: 50, intervalMs: 5000 },
|
|
736
|
+
retry: { maxAttempts: 3, backoff: 'exponential', initialDelayMs: 1000 },
|
|
737
|
+
onDropped: (events, error) => {
|
|
738
|
+
console.error(`[mxllog] Dropped ${events.length} events:`, error?.message)
|
|
739
|
+
},
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
const drain = pipeline(createAxiomDrain())
|
|
743
|
+
|
|
744
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:drain', drain)
|
|
745
|
+
nitroApp.hooks.hook('close', () => drain.flush())
|
|
746
|
+
})
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### How it works
|
|
750
|
+
|
|
751
|
+
1. Events are buffered in memory as they arrive
|
|
752
|
+
2. A batch is flushed when either the **batch size** is reached or the **interval** expires (whichever comes first)
|
|
753
|
+
3. If the drain function fails, the batch is retried with the configured **backoff strategy**
|
|
754
|
+
4. If all retries are exhausted, `onDropped` is called with the lost events
|
|
755
|
+
5. If the buffer exceeds `maxBufferSize`, the oldest events are dropped to prevent memory leaks
|
|
756
|
+
|
|
757
|
+
### Options
|
|
758
|
+
|
|
759
|
+
| Option | Default | Description |
|
|
760
|
+
|--------|---------|-------------|
|
|
761
|
+
| `batch.size` | `50` | Maximum events per batch |
|
|
762
|
+
| `batch.intervalMs` | `5000` | Max time (ms) before flushing a partial batch |
|
|
763
|
+
| `retry.maxAttempts` | `3` | Total attempts (including first) |
|
|
764
|
+
| `retry.backoff` | `'exponential'` | `'exponential'` \| `'linear'` \| `'fixed'` |
|
|
765
|
+
| `retry.initialDelayMs` | `1000` | Base delay for first retry |
|
|
766
|
+
| `retry.maxDelayMs` | `30000` | Upper bound for any retry delay |
|
|
767
|
+
| `maxBufferSize` | `1000` | Max buffered events before dropping oldest |
|
|
768
|
+
| `onDropped` | -- | Callback when events are dropped |
|
|
769
|
+
|
|
770
|
+
### Returned drain function
|
|
771
|
+
|
|
772
|
+
The function returned by `pipeline(drain)` is hook-compatible and exposes:
|
|
773
|
+
|
|
774
|
+
- **`drain(ctx)`** -- Push a single event into the buffer
|
|
775
|
+
- **`drain.flush()`** -- Force-flush all buffered events (call on server shutdown)
|
|
776
|
+
- **`drain.pending`** -- Number of events currently buffered
|
|
777
|
+
|
|
778
|
+
## API Reference
|
|
779
|
+
|
|
780
|
+
### `initLogger(config)`
|
|
781
|
+
|
|
782
|
+
Initialize the logger. Required for standalone usage, automatic with Nuxt/Nitro plugins.
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
initLogger({
|
|
786
|
+
enabled: boolean // Optional. Enable/disable all logging (default: true)
|
|
787
|
+
env: {
|
|
788
|
+
service: string // Service name
|
|
789
|
+
environment: string // 'production' | 'development' | 'test'
|
|
790
|
+
version?: string // App version
|
|
791
|
+
commitHash?: string // Git commit
|
|
792
|
+
region?: string // Deployment region
|
|
793
|
+
},
|
|
794
|
+
pretty?: boolean // Pretty print (default: true in dev)
|
|
795
|
+
stringify?: boolean // JSON.stringify output (default: true, false for Workers)
|
|
796
|
+
include?: string[] // Route patterns to log (glob), e.g. ['/api/**']
|
|
797
|
+
sampling?: {
|
|
798
|
+
rates?: { // Head sampling (random per level)
|
|
799
|
+
info?: number // 0-100, default 100
|
|
800
|
+
warn?: number // 0-100, default 100
|
|
801
|
+
debug?: number // 0-100, default 100
|
|
802
|
+
error?: number // 0-100, default 100 (always logged unless set to 0)
|
|
803
|
+
}
|
|
804
|
+
keep?: Array<{ // Tail sampling (force keep based on outcome)
|
|
805
|
+
status?: number // Keep if status >= value
|
|
806
|
+
duration?: number // Keep if duration >= value (ms)
|
|
807
|
+
path?: string // Keep if path matches glob pattern
|
|
808
|
+
}>
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### Sampling
|
|
814
|
+
|
|
815
|
+
At scale, logging everything can become expensive. mxllog supports two sampling strategies:
|
|
816
|
+
|
|
817
|
+
#### Head Sampling (rates)
|
|
818
|
+
|
|
819
|
+
Random sampling based on log level, decided before the request completes:
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
initLogger({
|
|
823
|
+
sampling: {
|
|
824
|
+
rates: {
|
|
825
|
+
info: 10, // Keep 10% of info logs
|
|
826
|
+
warn: 50, // Keep 50% of warning logs
|
|
827
|
+
debug: 0, // Disable debug logs
|
|
828
|
+
// error defaults to 100% (always logged)
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
})
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
#### Tail Sampling (keep)
|
|
835
|
+
|
|
836
|
+
Force-keep logs based on request outcome, evaluated after the request completes. Useful to always capture slow requests or critical paths:
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
// nuxt.config.ts
|
|
840
|
+
export default defineNuxtConfig({
|
|
841
|
+
modules: ['@safaricom-mxl/log/nuxt'],
|
|
842
|
+
mxllog: {
|
|
843
|
+
sampling: {
|
|
844
|
+
rates: { info: 10 }, // Only 10% of info logs
|
|
845
|
+
keep: [
|
|
846
|
+
{ duration: 1000 }, // Always keep if duration >= 1000ms
|
|
847
|
+
{ status: 400 }, // Always keep if status >= 400
|
|
848
|
+
{ path: '/api/critical/**' }, // Always keep critical paths
|
|
849
|
+
],
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
})
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
#### Custom Tail Sampling Hook
|
|
856
|
+
|
|
857
|
+
For business-specific conditions (premium users, feature flags), use the `mxllog:emit:keep` Nitro hook:
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
// server/plugins/mxllog-custom.ts
|
|
861
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
862
|
+
nitroApp.hooks.hook('@safaricom-mxl/log:emit:keep', (ctx) => {
|
|
863
|
+
// Always keep logs for premium users
|
|
864
|
+
if (ctx.context.user?.premium) {
|
|
865
|
+
ctx.shouldKeep = true
|
|
866
|
+
}
|
|
867
|
+
})
|
|
868
|
+
})
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### Pretty Output Format
|
|
872
|
+
|
|
873
|
+
In development, mxllog uses a compact tree format:
|
|
874
|
+
|
|
875
|
+
```
|
|
876
|
+
16:45:31.060 INFO [my-app] GET /api/checkout 200 in 234ms
|
|
877
|
+
|- user: id=123 plan=premium
|
|
878
|
+
|- cart: items=3 total=9999
|
|
879
|
+
+- payment: id=pay_xyz method=card
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
In production (`pretty: false`), logs are emitted as JSON for machine parsing.
|
|
883
|
+
|
|
884
|
+
### `log`
|
|
885
|
+
|
|
886
|
+
Simple logging API.
|
|
887
|
+
|
|
888
|
+
```typescript
|
|
889
|
+
log.info('tag', 'message') // Tagged log
|
|
890
|
+
log.info({ key: 'value' }) // Wide event
|
|
891
|
+
log.error('tag', 'message')
|
|
892
|
+
log.warn('tag', 'message')
|
|
893
|
+
log.debug('tag', 'message')
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### `createRequestLogger(options)`
|
|
897
|
+
|
|
898
|
+
Create a request-scoped logger for wide events.
|
|
899
|
+
|
|
900
|
+
```typescript
|
|
901
|
+
const log = createRequestLogger({
|
|
902
|
+
method: 'POST',
|
|
903
|
+
path: '/checkout',
|
|
904
|
+
requestId: 'req_123',
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
log.set({ user: { id: '123' } }) // Add context
|
|
908
|
+
log.error(error, { step: 'x' }) // Log error with context
|
|
909
|
+
log.emit() // Emit final event
|
|
910
|
+
log.getContext() // Get current context
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### `initWorkersLogger(options?)`
|
|
914
|
+
|
|
915
|
+
Initialize mxllog for Cloudflare Workers (object logs + correct severity).
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
import { initWorkersLogger } from '@safaricom-mxl/log/workers'
|
|
919
|
+
|
|
920
|
+
initWorkersLogger({
|
|
921
|
+
env: { service: 'edge-api' },
|
|
922
|
+
})
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### `createWorkersLogger(request, options?)`
|
|
926
|
+
|
|
927
|
+
Create a request-scoped logger for Workers. Auto-extracts `cf-ray`, `request.cf`, method, and path.
|
|
928
|
+
|
|
929
|
+
```typescript
|
|
930
|
+
import { createWorkersLogger } from '@safaricom-mxl/log/workers'
|
|
931
|
+
|
|
932
|
+
const log = createWorkersLogger(request, {
|
|
933
|
+
requestId: 'custom-id', // Override cf-ray (default: cf-ray header)
|
|
934
|
+
headers: ['x-request-id'], // Headers to include (default: none)
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
log.set({ user: { id: '123' } })
|
|
938
|
+
log.emit({ status: 200 })
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
### `createError(options)`
|
|
942
|
+
|
|
943
|
+
Create a structured error with HTTP status support. Import from `mxllog` directly to avoid conflicts with Nuxt/Nitro's `createError`.
|
|
944
|
+
|
|
945
|
+
> **Note**: `createMxllogError` is also available as an auto-imported alias in Nuxt/Nitro to avoid conflicts.
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
import { createError } from '@safaricom-mxl/log'
|
|
949
|
+
|
|
950
|
+
createError({
|
|
951
|
+
message: string // What happened
|
|
952
|
+
status?: number // HTTP status code (default: 500)
|
|
953
|
+
why?: string // Why it happened
|
|
954
|
+
fix?: string // How to fix it
|
|
955
|
+
link?: string // Documentation URL
|
|
956
|
+
cause?: Error // Original error
|
|
957
|
+
})
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
### `parseError(error)`
|
|
961
|
+
|
|
962
|
+
Parse a caught error into a flat structure with all mxllog fields. Auto-imported in Nuxt.
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
import { parseError } from '@safaricom-mxl/log'
|
|
966
|
+
|
|
967
|
+
try {
|
|
968
|
+
await $fetch('/api/checkout')
|
|
969
|
+
} catch (err) {
|
|
970
|
+
const error = parseError(err)
|
|
971
|
+
|
|
972
|
+
// Direct access to all fields
|
|
973
|
+
console.log(error.message) // "Payment failed"
|
|
974
|
+
console.log(error.status) // 402
|
|
975
|
+
console.log(error.why) // "Card declined"
|
|
976
|
+
console.log(error.fix) // "Try another card"
|
|
977
|
+
console.log(error.link) // "https://docs.example.com/..."
|
|
978
|
+
|
|
979
|
+
// Use with toast
|
|
980
|
+
toast.add({
|
|
981
|
+
title: error.message,
|
|
982
|
+
description: error.why,
|
|
983
|
+
color: 'error',
|
|
984
|
+
})
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
## Framework Support
|
|
989
|
+
|
|
990
|
+
| Framework | Integration |
|
|
991
|
+
|-----------|-------------|
|
|
992
|
+
| **Nuxt** | `modules: ['@safaricom-mxl/log/nuxt']` |
|
|
993
|
+
| **Next.js** | `createMxllog()` factory with `import { createMxllog } from '@safaricom-mxl/log/next'` ([example](./examples/nextjs)) |
|
|
994
|
+
| **Nitro v3** | `modules: [mxllog()]` with `import mxllog from '@safaricom-mxl/log/nitro/v3'` |
|
|
995
|
+
| **Nitro v2** | `modules: [mxllog()]` with `import mxllog from '@safaricom-mxl/log/nitro'` |
|
|
996
|
+
| **Analog** | Nitro v2 module setup |
|
|
997
|
+
| **Vinxi** | Nitro v2 module setup |
|
|
998
|
+
| **SolidStart** | Nitro v2 module setup ([example](./examples/solidstart)) |
|
|
999
|
+
| **TanStack Start** | Nitro v3 module setup ([example](./examples/tanstack-start)) |
|
|
1000
|
+
|
|
1001
|
+
## Agent Skills
|
|
1002
|
+
|
|
1003
|
+
mxllog provides [Agent Skills](https://github.com/boristane/agent-skills) to help AI coding assistants understand and implement proper logging patterns in your codebase.
|
|
1004
|
+
|
|
1005
|
+
### Installation
|
|
1006
|
+
|
|
1007
|
+
```bash
|
|
1008
|
+
npx add-skill hugorcd/mxllog
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### What it does
|
|
1012
|
+
|
|
1013
|
+
Once installed, your AI assistant will:
|
|
1014
|
+
- Review your logging code and suggest wide event patterns
|
|
1015
|
+
- Help refactor scattered `console.log` calls into structured events
|
|
1016
|
+
- Guide you to use `createError()` for self-documenting errors
|
|
1017
|
+
- Ensure proper use of `useLogger(event)` in Nuxt/Nitro routes
|
|
1018
|
+
|
|
1019
|
+
### Examples
|
|
1020
|
+
|
|
1021
|
+
```
|
|
1022
|
+
Add logging to this endpoint
|
|
1023
|
+
Review my logging code
|
|
1024
|
+
Help me set up logging for this service
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
## Philosophy
|
|
1028
|
+
|
|
1029
|
+
Inspired by [Logging Sucks](https://loggingsucks.com/) by [Boris Tane](https://x.com/boristane).
|
|
1030
|
+
|
|
1031
|
+
1. **Wide Events**: One log per request with all context
|
|
1032
|
+
2. **Structured Errors**: Errors that explain themselves
|
|
1033
|
+
3. **Request Scoping**: Accumulate context, emit once
|
|
1034
|
+
4. **Pretty for Dev, JSON for Prod**: Human-readable locally, machine-parseable in production
|
|
1035
|
+
|
|
1036
|
+
## License
|
|
1037
|
+
|
|
1038
|
+
[MIT](./LICENSE)
|
|
1039
|
+
|
|
1040
|
+
Made by [@HugoRCD](https://github.com/HugoRCD)
|