SkillAgentSearch skills...

Evlog

Logging that makes sense. Wide events, structured errors, zero chaos.

Install / Use

/learn @HugoRCD/Evlog

README

evlog

npm version npm downloads CI TypeScript Documentation license

Your logs are lying to you.

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.

evlog fixes this. One log per request. All context included. Errors that explain themselves.

Why evlog?

The Problem

// server/api/checkout.post.ts

// Scattered logs - impossible to debug
console.log('Request received')
console.log('User:', user.id)
console.log('Cart loaded')
console.log('Payment failed')  // Good luck finding this at 3am

throw new Error('Something went wrong')

The Solution

// server/api/checkout.post.ts
import { useLogger } from 'evlog'

// One comprehensive event per request
export default defineEventHandler(async (event) => {
  const log = useLogger(event)  // Auto-injected by evlog

  log.set({ user: { id: user.id, plan: 'premium' } })
  log.set({ cart: { items: 3, total: 9999 } })
  log.error(error, { step: 'payment' })

  // Emits ONE event with ALL context + duration (automatic)
})

Output:

{
  "timestamp": "2025-01-24T10:23:45.612Z",
  "level": "error",
  "service": "my-app",
  "method": "POST",
  "path": "/api/checkout",
  "duration": "1.2s",
  "user": { "id": "123", "plan": "premium" },
  "cart": { "items": 3, "total": 9999 },
  "error": { "message": "Card declined", "step": "payment" }
}

Built for AI-Assisted Development

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.

Traditional logs force agents to grep through noise. evlog gives them:

  • One event per request with all context in one place
  • Self-documenting errors with why and fix fields
  • Structured JSON that's easy to parse and reason about

Your AI copilot will thank you.


Installation

npm install evlog

Nuxt Integration

The recommended way to use evlog. Zero config, everything just works.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],

  evlog: {
    env: {
      service: 'my-app',
    },
    // Optional: only log specific routes (supports glob patterns)
    include: ['/api/**'],
  },
})

Tip: Use $production to enable sampling only in production:

export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: { env: { service: 'my-app' } },
  $production: {
    evlog: { sampling: { rates: { info: 10, warn: 50, debug: 0 } } },
  },
})

That's it. Now use useLogger(event) in any API route:

// server/api/checkout.post.ts
import { useLogger, createError } from 'evlog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  // Authenticate user and add to wide event
  const user = await requireAuth(event)
  log.set({ user: { id: user.id, plan: user.plan } })

  // Load cart and add to wide event
  const cart = await getCart(user.id)
  log.set({ cart: { items: cart.items.length, total: cart.total } })

  // Process payment
  try {
    const payment = await processPayment(cart, user)
    log.set({ payment: { id: payment.id, method: payment.method } })
  } catch (error) {
    log.error(error, { step: 'payment' })

    throw createError({
      message: 'Payment failed',
      status: 402,
      why: error.message,
      fix: 'Try a different payment method or contact your bank',
    })
  }

  // Create order
  const order = await createOrder(cart, user)
  log.set({ order: { id: order.id, status: order.status } })

  return order
  // log.emit() called automatically at request end
})

The wide event emitted at the end contains everything:

{
  "timestamp": "2026-01-24T10:23:45.612Z",
  "level": "info",
  "service": "my-app",
  "method": "POST",
  "path": "/api/checkout",
  "duration": "1.2s",
  "user": { "id": "user_123", "plan": "premium" },
  "cart": { "items": 3, "total": 9999 },
  "payment": { "id": "pay_xyz", "method": "card" },
  "order": { "id": "order_abc", "status": "created" },
  "status": 200
}

Nitro Integration

Works with any framework powered by Nitro: Nuxt, Analog, Vinxi, SolidStart, TanStack Start, and more.

Nitro v3

// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  modules: [
    evlog({ env: { service: 'my-api' } })
  ],
})

Nitro v2

// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'

export default defineNitroConfig({
  modules: [
    evlog({ env: { service: 'my-api' } })
  ],
})

Then use useLogger in any route. Import from evlog/nitro/v3 (v3) or evlog/nitro (v2):

// routes/api/documents/[id]/export.post.ts
// Nitro v3: import { defineHandler } from 'nitro/h3' + import { useLogger } from 'evlog/nitro/v3'
// Nitro v2: import { defineEventHandler } from 'h3' + import { useLogger } from 'evlog/nitro'
import { defineEventHandler } from 'h3'
import { useLogger } from 'evlog/nitro'
import { createError } from 'evlog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  // Get document ID from route params
  const documentId = getRouterParam(event, 'id')
  log.set({ document: { id: documentId } })

  // Parse request body for export options
  const body = await readBody(event)
  log.set({ export: { format: body.format, includeComments: body.includeComments } })

  // Load document from database
  const document = await db.documents.findUnique({ where: { id: documentId } })
  if (!document) {
    throw createError({
      message: 'Document not found',
      status: 404,
      why: `No document with ID "${documentId}" exists`,
      fix: 'Check the document ID and try again',
    })
  }
  log.set({ document: { id: documentId, title: document.title, pages: document.pages.length } })

  // Generate export
  try {
    const exportResult = await generateExport(document, body.format)
    log.set({ export: { format: body.format, size: exportResult.size, pages: exportResult.pages } })

    return { url: exportResult.url, expiresAt: exportResult.expiresAt }
  } catch (error) {
    log.error(error, { step: 'export-generation' })

    throw createError({
      message: 'Export failed',
      status: 500,
      why: `Failed to generate ${body.format} export: ${error.message}`,
      fix: 'Try a different format or contact support',
    })
  }
  // log.emit() called automatically - outputs one comprehensive wide event
})

Output when the export completes:

{
  "timestamp": "2025-01-24T14:32:10.123Z",
  "level": "info",
  "service": "document-api",
  "method": "POST",
  "path": "/api/documents/doc_123/export",
  "duration": "2.4s",
  "document": { "id": "doc_123", "title": "Q4 Report", "pages": 24 },
  "export": { "format": "pdf", "size": 1240000, "pages": 24 },
  "status": 200
}

Standalone TypeScript

For scripts, workers, or any TypeScript project:

// scripts/migrate.ts
import { initLogger, log, createRequestLogger } from 'evlog'

// Initialize once at script start
initLogger({
  env: {
    service: 'migration-script',
    environment: 'production',
  },
})

// Simple logging
log.info('migration', 'Starting database migration')
log.info({ action: 'migration', tables: ['users', 'orders'] })

// Or use request logger for a logical operation
const migrationLog = createRequestLogger({ action: 'full-migration' })

migrationLog.set({ tables: ['users', 'orders', 'products'] })
migrationLog.set({ rowsProcessed: 15000 })
migrationLog.emit()
// workers/sync-job.ts
import { initLogger, createRequestLogger, createError } from 'evlog'

initLogger({
  env: {
    service: 'sync-worker',
    environment: process.env.NODE_ENV,
  },
})

async function processSyncJob(job: Job) {
  const log = createRequestLogger({ jobId: job.id, type: 'sync' })

  try {
    log.set({ source: job.source, target: job.target })

    const result = await performSync(job)
    log.set({ recordsSynced: result.count })

    return result
  } catch (error) {
    log.error(error, { step: 'sync' })
    throw error
  } finally {
    log.emit()
  }
}

Cloudflare Workers

Use the Workers adapter for structured logs and correct platform severity.

// src/index.ts
import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'

initWorkersLogger({
  env: { service: 'edge-api' },
})

export default {
  async fetch(request: Request) {
    const log = createWorkersLogger(request)

    try {
      log.set({ route: 'health' })
      const response = new Response('ok', { status: 200 })
      log.emit({ status: response.status })
      return response
    } catch (error) {
      log.error(error as Error)
      log.emit({ status: 500 })
      throw error
    }
  },
}

Disable invocation logs to avoid duplicate request logs:

# wrangler.toml
[observability.logs]
invocation_logs = false

Notes:

  • requestId defaults to cf-ray when available
  • request.cf is included (colo, countr

Related Skills

View on GitHub
GitHub Stars982
CategoryDevelopment
Updatedjust now
Forks30

Languages

TypeScript

Security Score

100/100

Audited on Mar 28, 2026

No findings