Beginner

How to Ship Vercel and Next.js Logs to a Log Management Platform

Vercel's built-in logs disappear after 1 hour on free/Pro plans. Set up persistent logging for Next.js API routes, server components, and middleware with a log drain or SDK.

LogFlow TeamAugust 17, 202610 minVercelNext.jsServerless

Vercel's built-in Runtime Logs show console.log output from serverless functions, but they have hard limits:

  • Free/Hobby: 1 hour retention, no search, no export
  • Pro: 3 days retention, basic filtering
  • Enterprise: 7 days, log drains available

For any production application, this isn't enough. When a user reports "checkout failed yesterday afternoon," logs from yesterday are already gone on Free/Pro plans.

This tutorial covers three approaches to get persistent, searchable logs from a Next.js application on Vercel.

Approach 1 — SDK in API Routes (fastest setup)

Install a logging SDK and use it in API routes and server components:

npm install @getlogflow/js

Create a shared logger:

// lib/logger.ts
import LogFlow from '@getlogflow/js'

export const logger = new LogFlow({
  apiKey: process.env.LOGFLOW_API_KEY!,
  service: 'web',
})

Use it in API routes:

// app/api/orders/route.ts
import { logger } from '@/lib/logger'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
  const body = await req.json()

  logger.info('Order creation started', {
    userId: body.userId,
    items: body.items.length,
  })

  try {
    const order = await createOrder(body)

    logger.info('Order created', {
      orderId: order.id,
      total: order.total,
    })

    return NextResponse.json(order, { status: 201 })
  } catch (err: any) {
    logger.error('Order creation failed', {
      userId: body.userId,
      error: err.message,
      stack: err.stack,
    })

    return NextResponse.json({ error: 'Failed' }, { status: 500 })
  }
}

Use it in Server Components:

// app/dashboard/page.tsx
import { logger } from '@/lib/logger'

export default async function DashboardPage() {
  const data = await fetchDashboardData()

  if (data.errors.length > 0) {
    logger.warn('Dashboard loaded with errors', {
      errorCount: data.errors.length,
    })
  }

  return <Dashboard data={data} />
}

Pros: Works immediately, no Vercel configuration needed, works on all plans.

Cons: Manual — need to add logger calls to every route. Only captures what's explicitly logged, not raw stdout/stderr.

Approach 2 — Vercel Log Drain (Pro+ plans)

Vercel Log Drains forward all runtime and build logs to an external endpoint. This captures everything — console.log, console.error, build output, edge function logs.

Set up the drain via Vercel CLI:

vercel log-drains create https://api.getlogflow.com/v1/logs/vercel \
  --type json \
  --env production

Or via the Vercel dashboard: Settings → Log Drains → Add.

What the drain sends

Each log drain delivery is a JSON array of events:

[
  {
    "id": "log_xxx",
    "message": "Order created { orderId: 'ORD-9912' }",
    "timestamp": 1692345678901,
    "source": "lambda",
    "projectId": "prj_xxx",
    "deploymentId": "dpl_xxx",
    "level": "info",
    "path": "/api/orders",
    "host": "my-app.vercel.app"
  }
]

Build a receiver endpoint

If the log platform doesn't have native Vercel drain support, build a lightweight receiver:

// A separate serverless function or external API
export async function POST(req: Request) {
  const events = await req.json()

  const logs = events.map((e: any) => ({
    level: mapLevel(e.level),
    message: e.message,
    service: 'vercel',
    timestamp: new Date(e.timestamp).toISOString(),
    attributes: {
      source: e.source,
      path: e.path,
      deploymentId: e.deploymentId,
      host: e.host,
    },
  }))

  await forwardToLogPlatform(logs)
  return new Response('OK', { status: 200 })
}

function mapLevel(vercelLevel: string): string {
  const map: Record<string, string> = {
    info: 'info',
    warning: 'warn',
    error: 'error',
    log: 'info',
  }
  return map[vercelLevel] || 'info'
}

Pros: Captures everything automatically (stdout, stderr, build logs). No code changes to the application.

Cons: Pro plan required ($20/month). Log format is Vercel-specific — needs transformation. Build logs add volume.

Approach 3 — Pino with Custom Transport (recommended for production)

Pino is the fastest Node.js JSON logger. Combined with a custom transport, it gives structured logging with automatic shipping:

npm install pino pino-http
// lib/logger.ts
import pino from 'pino'

const transport = process.env.LOGFLOW_API_KEY
  ? pino.transport({
      target: './lib/logflow-transport.mjs',
      options: {
        apiKey: process.env.LOGFLOW_API_KEY,
        service: 'web',
      },
    })
  : undefined

export const logger = pino(
  {
    level: process.env.LOG_LEVEL || 'info',
    // Don't pretty-print in production
    ...(process.env.NODE_ENV === 'development' && {
      transport: { target: 'pino-pretty' },
    }),
  },
  transport
)
// lib/logflow-transport.mjs
import { Transform } from 'node:stream'

export default function (opts) {
  const buffer = []
  let timer = null

  async function flush() {
    if (buffer.length === 0) return
    const batch = buffer.splice(0)
    const payload = batch.map(entry => ({
      level: levelToString(entry.level),
      message: entry.msg || '',
      service: opts.service,
      attributes: Object.fromEntries(
        Object.entries(entry)
          .filter(([k]) => !['level','time','pid','hostname','msg','v'].includes(k))
          .map(([k, v]) => [k, String(v)])
      ),
    }))

    try {
      await fetch('https://api.getlogflow.com/v1/logs', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${opts.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
      })
    } catch {}
  }

  function levelToString(n) {
    if (n >= 60) return 'fatal'
    if (n >= 50) return 'error'
    if (n >= 40) return 'warn'
    if (n >= 30) return 'info'
    return 'debug'
  }

  return new Transform({
    objectMode: true,
    transform(chunk, enc, cb) {
      try { buffer.push(JSON.parse(chunk)) } catch {}
      if (!timer) timer = setTimeout(() => { flush(); timer = null }, 1000)
      cb()
    },
    flush(cb) { flush().then(() => cb()) }
  })
}

Use in API routes:

import { logger } from '@/lib/logger'

export async function GET(req: NextRequest) {
  logger.info({ path: '/api/data', query: Object.fromEntries(req.nextUrl.searchParams) }, 'Data requested')
  // ...
}

Logging in Middleware

Next.js middleware runs on the Edge runtime, which has limitations (no Node.js fs, limited Buffer). Use the fetch-based approach:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  const start = Date.now()

  const response = NextResponse.next()

  // Fire-and-forget log (don't await in middleware hot path)
  const duration = Date.now() - start
  fetch('https://api.getlogflow.com/v1/logs', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.LOGFLOW_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify([{
      level: 'info',
      message: `Middleware: ${req.method} ${req.nextUrl.pathname}`,
      service: 'web-edge',
      attributes: {
        method: req.method,
        path: req.nextUrl.pathname,
        geo_country: req.geo?.country || 'unknown',
        durationMs: String(duration),
      },
    }]),
  }).catch(() => {})

  return response
}

Environment Variables

Add to your Vercel project (Settings → Environment Variables):

LOGFLOW_API_KEY=lf_your_key_here
LOG_LEVEL=info

Set LOG_LEVEL=debug in Preview deployments for extra visibility during development.

What to Log in a Next.js App

What Level Where
API route errors error API route handlers
Failed external API calls error Server components, API routes
Slow database queries (>2s) warn Anywhere with DB access
Successful business events info API routes (order created, user signed up)
Auth failures warn Middleware, API routes
Page render times debug Server components

Don't log in client components — browser logs are visible to users in DevTools and aren't sent to the server. Use the @getlogflow/browser SDK for client-side error tracking.

Next steps

Start monitoring your logs today

Free plan available. No credit card required. Up and running in 2 minutes.

Get started free