@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.
Files changed (131) hide show
  1. package/README.md +1040 -0
  2. package/dist/_http-DmaJ426Z.mjs +76 -0
  3. package/dist/_http-DmaJ426Z.mjs.map +1 -0
  4. package/dist/_severity-D_IU9-90.mjs +17 -0
  5. package/dist/_severity-D_IU9-90.mjs.map +1 -0
  6. package/dist/adapters/axiom.d.mts +64 -0
  7. package/dist/adapters/axiom.d.mts.map +1 -0
  8. package/dist/adapters/axiom.mjs +100 -0
  9. package/dist/adapters/axiom.mjs.map +1 -0
  10. package/dist/adapters/better-stack.d.mts +63 -0
  11. package/dist/adapters/better-stack.d.mts.map +1 -0
  12. package/dist/adapters/better-stack.mjs +98 -0
  13. package/dist/adapters/better-stack.mjs.map +1 -0
  14. package/dist/adapters/otlp.d.mts +85 -0
  15. package/dist/adapters/otlp.d.mts.map +1 -0
  16. package/dist/adapters/otlp.mjs +196 -0
  17. package/dist/adapters/otlp.mjs.map +1 -0
  18. package/dist/adapters/posthog.d.mts +107 -0
  19. package/dist/adapters/posthog.d.mts.map +1 -0
  20. package/dist/adapters/posthog.mjs +166 -0
  21. package/dist/adapters/posthog.mjs.map +1 -0
  22. package/dist/adapters/sentry.d.mts +80 -0
  23. package/dist/adapters/sentry.d.mts.map +1 -0
  24. package/dist/adapters/sentry.mjs +221 -0
  25. package/dist/adapters/sentry.mjs.map +1 -0
  26. package/dist/browser.d.mts +63 -0
  27. package/dist/browser.d.mts.map +1 -0
  28. package/dist/browser.mjs +95 -0
  29. package/dist/browser.mjs.map +1 -0
  30. package/dist/enrichers.d.mts +74 -0
  31. package/dist/enrichers.d.mts.map +1 -0
  32. package/dist/enrichers.mjs +172 -0
  33. package/dist/enrichers.mjs.map +1 -0
  34. package/dist/error.d.mts +65 -0
  35. package/dist/error.d.mts.map +1 -0
  36. package/dist/error.mjs +112 -0
  37. package/dist/error.mjs.map +1 -0
  38. package/dist/index.d.mts +6 -0
  39. package/dist/index.mjs +6 -0
  40. package/dist/logger.d.mts +46 -0
  41. package/dist/logger.d.mts.map +1 -0
  42. package/dist/logger.mjs +287 -0
  43. package/dist/logger.mjs.map +1 -0
  44. package/dist/next/client.d.mts +55 -0
  45. package/dist/next/client.d.mts.map +1 -0
  46. package/dist/next/client.mjs +44 -0
  47. package/dist/next/client.mjs.map +1 -0
  48. package/dist/next/index.d.mts +169 -0
  49. package/dist/next/index.d.mts.map +1 -0
  50. package/dist/next/index.mjs +280 -0
  51. package/dist/next/index.mjs.map +1 -0
  52. package/dist/nitro/errorHandler.d.mts +15 -0
  53. package/dist/nitro/errorHandler.d.mts.map +1 -0
  54. package/dist/nitro/errorHandler.mjs +41 -0
  55. package/dist/nitro/errorHandler.mjs.map +1 -0
  56. package/dist/nitro/module.d.mts +11 -0
  57. package/dist/nitro/module.d.mts.map +1 -0
  58. package/dist/nitro/module.mjs +23 -0
  59. package/dist/nitro/module.mjs.map +1 -0
  60. package/dist/nitro/plugin.d.mts +7 -0
  61. package/dist/nitro/plugin.d.mts.map +1 -0
  62. package/dist/nitro/plugin.mjs +145 -0
  63. package/dist/nitro/plugin.mjs.map +1 -0
  64. package/dist/nitro/v3/errorHandler.d.mts +24 -0
  65. package/dist/nitro/v3/errorHandler.d.mts.map +1 -0
  66. package/dist/nitro/v3/errorHandler.mjs +36 -0
  67. package/dist/nitro/v3/errorHandler.mjs.map +1 -0
  68. package/dist/nitro/v3/index.d.mts +5 -0
  69. package/dist/nitro/v3/index.mjs +5 -0
  70. package/dist/nitro/v3/middleware.d.mts +25 -0
  71. package/dist/nitro/v3/middleware.d.mts.map +1 -0
  72. package/dist/nitro/v3/middleware.mjs +45 -0
  73. package/dist/nitro/v3/middleware.mjs.map +1 -0
  74. package/dist/nitro/v3/module.d.mts +10 -0
  75. package/dist/nitro/v3/module.d.mts.map +1 -0
  76. package/dist/nitro/v3/module.mjs +22 -0
  77. package/dist/nitro/v3/module.mjs.map +1 -0
  78. package/dist/nitro/v3/plugin.d.mts +14 -0
  79. package/dist/nitro/v3/plugin.d.mts.map +1 -0
  80. package/dist/nitro/v3/plugin.mjs +162 -0
  81. package/dist/nitro/v3/plugin.mjs.map +1 -0
  82. package/dist/nitro/v3/useLogger.d.mts +24 -0
  83. package/dist/nitro/v3/useLogger.d.mts.map +1 -0
  84. package/dist/nitro/v3/useLogger.mjs +27 -0
  85. package/dist/nitro/v3/useLogger.mjs.map +1 -0
  86. package/dist/nitro-CrFBjY1Y.d.mts +42 -0
  87. package/dist/nitro-CrFBjY1Y.d.mts.map +1 -0
  88. package/dist/nitro-Dsv6dSzv.mjs +39 -0
  89. package/dist/nitro-Dsv6dSzv.mjs.map +1 -0
  90. package/dist/nuxt/module.d.mts +164 -0
  91. package/dist/nuxt/module.d.mts.map +1 -0
  92. package/dist/nuxt/module.mjs +84 -0
  93. package/dist/nuxt/module.mjs.map +1 -0
  94. package/dist/pipeline.d.mts +46 -0
  95. package/dist/pipeline.d.mts.map +1 -0
  96. package/dist/pipeline.mjs +122 -0
  97. package/dist/pipeline.mjs.map +1 -0
  98. package/dist/routes-BNbrnm14.mjs +39 -0
  99. package/dist/routes-BNbrnm14.mjs.map +1 -0
  100. package/dist/runtime/client/log.d.mts +15 -0
  101. package/dist/runtime/client/log.d.mts.map +1 -0
  102. package/dist/runtime/client/log.mjs +92 -0
  103. package/dist/runtime/client/log.mjs.map +1 -0
  104. package/dist/runtime/client/plugin.d.mts +5 -0
  105. package/dist/runtime/client/plugin.d.mts.map +1 -0
  106. package/dist/runtime/client/plugin.mjs +17 -0
  107. package/dist/runtime/client/plugin.mjs.map +1 -0
  108. package/dist/runtime/server/routes/_mxllog/ingest.post.d.mts +7 -0
  109. package/dist/runtime/server/routes/_mxllog/ingest.post.d.mts.map +1 -0
  110. package/dist/runtime/server/routes/_mxllog/ingest.post.mjs +123 -0
  111. package/dist/runtime/server/routes/_mxllog/ingest.post.mjs.map +1 -0
  112. package/dist/runtime/server/useLogger.d.mts +39 -0
  113. package/dist/runtime/server/useLogger.d.mts.map +1 -0
  114. package/dist/runtime/server/useLogger.mjs +43 -0
  115. package/dist/runtime/server/useLogger.mjs.map +1 -0
  116. package/dist/runtime/utils/parseError.d.mts +7 -0
  117. package/dist/runtime/utils/parseError.d.mts.map +1 -0
  118. package/dist/runtime/utils/parseError.mjs +29 -0
  119. package/dist/runtime/utils/parseError.mjs.map +1 -0
  120. package/dist/types.d.mts +496 -0
  121. package/dist/types.d.mts.map +1 -0
  122. package/dist/types.mjs +1 -0
  123. package/dist/utils.d.mts +34 -0
  124. package/dist/utils.d.mts.map +1 -0
  125. package/dist/utils.mjs +78 -0
  126. package/dist/utils.mjs.map +1 -0
  127. package/dist/workers.d.mts +46 -0
  128. package/dist/workers.d.mts.map +1 -0
  129. package/dist/workers.mjs +81 -0
  130. package/dist/workers.mjs.map +1 -0
  131. package/package.json +195 -0
package/README.md ADDED
@@ -0,0 +1,1040 @@
1
+ # mxllog
2
+
3
+ [![npm version](https://img.shields.io/npm/v/mxllog?color=black)](https://npmjs.com/package/mxllog)
4
+ [![npm downloads](https://img.shields.io/npm/dm/mxllog?color=black)](https://npm.chart.dev/mxllog)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/HugoRCD/mxllog/ci.yml?branch=main&color=black)](https://github.com/HugoRCD/mxllog/actions/workflows/ci.yml)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-black?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
+ [![Documentation](https://img.shields.io/badge/Documentation-black?logo=readme&logoColor=white)](https://mxllog.dev)
8
+ [![license](https://img.shields.io/github/license/HugoRCD/mxllog?color=black)](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)