Frameworks

Astro

Wide events and structured errors in Astro server middleware.

Astro doesn't have a dedicated evlog integration. Instead, use the core evlog package with Astro's middleware to create request-scoped loggers manually.

Set up evlog in my Astro app

This is a guide-level integration. It uses the generic createRequestLogger API rather than a framework-specific module.
On Cloudflare Workers (including Astro with @astrojs/cloudflare), set waitUntil on createRequestLogger to your ExecutionContext#waitUntil (properly bound), or use defineWorkerFetch / createWorkersLogger with { executionCtx } on a Worker fetch entry. Otherwise async drains may never finish after the response is returned.For Astro middleware (not the raw Worker handler), there is no defineWorkerFetch; you still pass waitUntil from the adapter-exposed context.The exact way to read ctx from Astro middleware depends on your adapter version, so check the Cloudflare adapter docs.

Quick Start

1. Install

pnpm add evlog

2. Create a middleware

src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
import { initLogger, createRequestLogger } from 'evlog'

initLogger({
  env: { service: 'my-astro-app' },
})

export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
  const url = new URL(request.url)

  const log = createRequestLogger({
    method: request.method,
    path: url.pathname,
  })

  locals.log = log

  try {
    const response = await next()
    log.emit()
    return response
  } catch (error) {
    log.error(error instanceof Error ? error : new Error(String(error)))
    log.emit()
    throw error
  }
})

3. Type your locals

src/env.d.ts
/// <reference types="astro/client" />

import type { RequestLogger } from 'evlog'

declare namespace App {
  interface Locals {
    log: RequestLogger
  }
}

Wide Events

Access the logger from Astro.locals in your pages and API routes:

src/pages/api/users/[id].ts
import type { APIRoute } from 'astro'

export const GET: APIRoute = async ({ params, locals }) => {
  locals.log.set({ user: { id: params.id } })

  const user = await db.findUser(params.id)
  locals.log.set({ user: { name: user.name, plan: user.plan } })

  return new Response(JSON.stringify(user), {
    headers: { 'Content-Type': 'application/json' },
  })
}
Terminal output
14:58:15 INFO [my-astro-app] GET /api/users/usr_123
  ├─ user: id=usr_123 name=Alice plan=pro
  └─ requestId: 4a8ff3a8-...

Error Handling

Use createError for structured errors:

src/pages/api/checkout.ts
import type { APIRoute } from 'astro'
import { createError, parseError } from 'evlog'

export const POST: APIRoute = async ({ request, locals }) => {
  const body = await request.json()
  locals.log.set({ cart: { items: body.items } })

  if (!body.paymentMethod) {
    const error = createError({
      status: 400,
      message: 'Missing payment method',
      why: 'No payment method was provided',
      fix: 'Include a paymentMethod field in the request body',
    })
    locals.log.error(error)
    const parsed = parseError(error)
    return new Response(JSON.stringify(parsed), { status: parsed.status })
  }

  return new Response(JSON.stringify({ success: true }))
}

Configuration

See the Configuration reference for all available options (initLogger, middleware options, sampling, silent mode, etc.).

Keep the options in evlog.config.ts

The options can live in an evlog.config.ts at the root of the app, the file the CLI reads too. Pass it to initLogger in the middleware:

src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
import { initLogger, createRequestLogger, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))

export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
  // ... same as above
})

toLoggerConfig keeps every option initLogger takes, such as the service, sampling, minLevel, redact, the drain and the plugins. This guide builds the request logger with createRequestLogger, not an evlog middleware, so the file's enrich, keep, include, exclude and routes do not apply.

Drain

Configure drain in initLogger inside your middleware:

src/middleware.ts
import { initLogger, createRequestLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
import type { DrainContext } from 'evlog'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
})
const drain = pipeline(createAxiomDrain())

initLogger({
  env: { service: 'my-astro-app' },
  drain,
})
See the Adapters docs for all available drain adapters.

Next Steps

  • Wide Events: Design comprehensive events with context layering
  • Adapters: Send logs to Axiom, Sentry, PostHog, and more
  • Sampling: Control log volume with head and tail sampling
  • Structured Errors: Throw errors with why, fix, and link fields