From observability-skills
Use when implementing structured logging, setting up Axiom observability, migrating from console.* to a centralized logger, or configuring Vercel Log Drains for log collection in Next.js apps
npx claudepluginhub securityronin/ronin-marketplace --plugin observability-skillsThis skill uses the workspace's default tool permissions.
Zero-dependency structured logging for Next.js with Axiom integration via Vercel Log Drains. No external SDK required.
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
Analyzes competition with Porter's Five Forces, Blue Ocean Strategy, and positioning maps to identify differentiation opportunities and market positioning for startups and pitches.
Zero-dependency structured logging for Next.js with Axiom integration via Vercel Log Drains. No external SDK required.
Code -> createLogger('namespace') -> console.info(JSON) -> Vercel stdout -> Log Drain -> Axiom
Create a centralized logger module that replaces all raw console.* calls:
// lib/logger.ts
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
export interface Logger {
debug(message: string, context?: Record<string, unknown>): void
info(message: string, context?: Record<string, unknown>): void
warn(message: string, context?: Record<string, unknown>): void
error(message: string, error?: Error, context?: Record<string, unknown>): void
child(context: Record<string, unknown>): Logger
}
export function createLogger(namespace: string, parentContext?: Record<string, unknown>): Logger
export function generateCorrelationId(): string
createLogger('analyze-stream') identifies the source modulelog.child({ correlationId }) bakes in context for all subsequent callsLOG_LEVEL env var override without redeployingctx_ prefixgenerateCorrelationId() for request tracing across log entriesimport { createLogger, generateCorrelationId } from '@/lib/logger'
const log = createLogger('my-module')
// Basic usage
log.debug('Verbose detail', { rowIndex: 42 })
log.info('Operation started', { itemCount: 12450 })
log.warn('Threshold exceeded', { sizeMB: 48.2 })
log.error('Parse failed', error, { filename: 'data.csv' })
// Child logger — bakes in context for all subsequent calls
const correlationId = generateCorrelationId()
const reqLog = log.child({ correlationId, requestId: 'abc-123' })
reqLog.info('Processing started') // auto-includes correlationId + requestId
reqLog.debug('Item parsed', { idx }) // merges with parent context
// Chained children accumulate context
const itemLog = reqLog.child({ itemId: 'item-001' })
itemLog.info('Item processed') // has correlationId + requestId + itemId
| Level | When to Use | Production Default |
|---|---|---|
debug | Verbose internals (row parsing, intermediate state) | Suppressed |
info | Significant milestones (analysis started, export complete) | Logged |
warn | Recoverable issues (no result found, threshold exceeded) | Logged |
error | Failures requiring attention (parse crash, API error) | Logged |
Set LOG_LEVEL environment variable in Vercel to temporarily change log verbosity without redeploying:
LOG_LEVEL=debug -> all levels logged
LOG_LEVEL=error -> only errors logged
Validate the env var at startup — invalid values fall back to the environment default.
Development (human-readable):
[2026-02-14T01:30:00.000Z] INFO [my-module] Processing started {"itemCount":12450}
Production (JSON for Axiom):
{"timestamp":"2026-02-14T01:30:00.000Z","level":"info","namespace":"my-module","message":"Processing started","itemCount":12450,"correlationId":"a1b2c3d4-k5m9n2"}
Format is selected at module load time based on NODE_ENV. Context fields are placed at the top level of JSON (not nested) for direct Axiom column indexing.
If context contains keys that collide with log entry fields (timestamp, level, namespace, message, error, stack), they are auto-prefixed with ctx_:
log.info('test', { message: 'user input' })
// JSON output: {"timestamp":"...","level":"info","message":"test","ctx_message":"user input"}
Zero dependencies. Structured JSON goes to stdout, Vercel captures it, Log Drain webhook forwards to Axiom.
logger.ts -> console.info(JSON) -> Vercel stdout -> Log Drain webhook -> Axiom dataset
Pros: No npm packages, no code changes, no API keys in env vars, works with any logger that outputs to stdout Cons: Only captures server-side logs (SSR/API routes), slight delay (~seconds), no client-side browser logs
Setup:
my-app-logs)Send logs directly to Axiom's ingest API. Useful for browser-side logs or when you need guaranteed delivery.
logger.ts -> axiom.ingest([events]) -> HTTPS POST -> Axiom dataset
Pros: Works client-side (browser), guaranteed delivery, batch control, can send from Edge/Workers
Cons: Requires @axiomhq/js package (~8KB), needs AXIOM_TOKEN + AXIOM_DATASET env vars
Setup:
npm install @axiomhq/js
// lib/axiom-transport.ts
import { Axiom } from '@axiomhq/js'
const axiom = new Axiom({ token: process.env.AXIOM_TOKEN! })
export async function flushToAxiom(events: Record<string, unknown>[]) {
axiom.ingest(process.env.AXIOM_DATASET!, events)
await axiom.flush()
}
Add a transport layer to the logger that buffers JSON entries and flushes in batches (e.g., every 5s or 50 events).
When to consider Option B:
| Aspect | Log Drains (A) | Direct API (B) |
|---|---|---|
| Dependencies | None | @axiomhq/js |
| Server-side logs | Yes | Yes |
| Client-side logs | No | Yes |
| Env vars needed | None (Vercel handles auth) | AXIOM_TOKEN, AXIOM_DATASET |
| Delivery guarantee | Best-effort | Guaranteed (with flush) |
| Latency | ~seconds | ~immediate on flush |
| Code changes | Zero | Transport layer needed |
| Works outside Vercel | No | Yes |
// All errors in the last hour
| where level == "error" | order by _time desc
// Trace a single request
| where correlationId == "a1b2c3d4-k5m9n2" | order by _time asc
// Slow operations by namespace
| where namespace == "reverse-geocode" | summarize avg(durationMs) by bin(_time, 5m)
// Error rate by namespace
| where level == "error" | summarize count() by namespace | order by count_ desc
Enforce no-console: 'error' in ESLint flat config to prevent regression after migration:
// eslint.config.mjs
import nextConfig from "eslint-config-next";
const eslintConfig = [
...nextConfig,
{ rules: { "no-console": "error" } },
{
files: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "e2e/**"],
rules: { "no-console": "off" },
},
];
export default eslintConfig;
Only the logger module itself has eslint-disable-next-line exceptions.
When converting existing console.* calls to the structured logger:
| From | To |
|---|---|
console.log('[API] File type:', fileType) | log.debug('File type detected', { fileType }) |
console.error('Failed:', error) | log.error('Failed', error instanceof Error ? error : undefined) |
console.warn('No results for', address) | log.warn('No results found', { address }) |
console.log(\Parsed ${count} rows`)` | log.info('Parsing complete', { count }) |
console.error(error.message); console.error(error.stack) | log.error('Operation failed', error) (logger handles stack automatically) |
[API], [FilePreview], [SSE]) — the namespace handles identificationconsole.log -> log.debug (verbose) or log.info (milestones)console.error -> log.error(message, errorObj?, context?) — pass Error object as 2nd argconsole.warn -> log.warn(message, context?){ key: value }log.error (logger extracts stack automatically)removeConsole in next.config.tsNext.js compiler.removeConsole strips console.* calls from production bundles. This silently kills logger output since the logger calls console.debug/console.info/console.warn/console.error internally.
Fix: Disable removeConsole. The logger handles level filtering internally.
// next.config.ts
compiler: {
// Console stripping disabled — structured logger handles level filtering
// internally and emits JSON for Axiom log drain ingestion.
},
next-axiom is NOT a Drop-inThe next-axiom package does NOT auto-capture console.* calls. It provides its own Logger API that must be used explicitly. If you already have createLogger, adding next-axiom is redundant. Vercel Log Drains achieves the same result with zero dependencies.
IS_PRODUCTION and the format function (JSON vs human) should be evaluated at module load time. This avoids a runtime check on every log call but means test environments always use the human formatter unless you mock NODE_ENV before import.