A modern, runtime-agnostic structured logging library with automatic PII sanitization and context propagation.
v0.22.0 Β· Beta Β· Active Development
Vestig is in active beta with continuous development. The API is stable and production-ready for most use cases.
| Metric | Status |
|---|---|
| Version | v0.22.0 |
| Stage | Beta - API stable |
| Tests | 1273 passing |
| Core Coverage | 81%+ |
| Releases | 34 versions published |
| Packages | 2 (vestig, @vestig/next) |
| Package | Version | Description |
|---|---|---|
vestig |
Core logging library | |
@vestig/next |
Next.js 15+ integration (App Router, RSC, middleware) |
Vestig β from Latin vestigium (trace, footprint). Leave a trace of what happened.
| Feature | Vestig | Pino | Winston | Bunyan |
|---|---|---|---|---|
| Runtime Agnostic | β | β | β | β |
| Auto PII Sanitization | β | β | β | β |
| GDPR/HIPAA/PCI-DSS Presets | β | β | β | β |
| Wide Events / Tail Sampling | β | β | β | β |
| OTLP Export (Jaeger, Honeycomb) | β | β | β | β |
| Auto Fetch Instrumentation | β | β | β | β |
| Auto Database Instrumentation | β | β | β | β |
| Zero Config | β | β | β | β |
| TypeScript First | β | β | β | |
| Edge Runtime Support | β | β | β | β |
| Browser Support | β | β | β | |
| Context Propagation | β | β | β | β |
| Multiple Transports | β | β | β | β |
| Zero Dependencies | β | β | β | β |
Vestig is the only logging library that:
- Works everywhere (Node.js, Bun, Deno, Edge, Browser)
- Automatically sanitizes PII with compliance presets
- Propagates context through async operations
- Exports spans to OTLP backends (Jaeger, Honeycomb, Vercel, Grafana)
- Auto-instruments fetch() and database queries with zero code changes
- Has zero runtime dependencies
# bun
bun add vestig
# npm
npm install vestig
# pnpm
pnpm add vestig# Next.js 15+ (App Router, Server Components, Middleware)
bun add @vestig/nextimport { log } from 'vestig'
// Simple logging
log.info('Hello world')
log.error('Something failed', { userId: 123 })
// Sensitive data is automatically redacted
log.info('User login', {
email: '[email protected]', // β us***@example.com
password: 'secret123', // β [REDACTED]
creditCard: '4111111111111111', // β ****1111
})// app/page.tsx
import { getLogger } from '@vestig/next'
export default async function Page() {
const log = await getLogger('home')
log.info('Rendering home page')
return <h1>Welcome</h1>
}Automatic OTLP span creation for PostgreSQL queries with precise timing:
// lib/db.ts
import postgres from 'postgres'
import { instrumentPostgres } from '@vestig/next/db'
import { drizzle } from 'drizzle-orm/postgres-js'
// Wrap postgres-js client for automatic instrumentation
const client = instrumentPostgres(postgres(process.env.DATABASE_URL!), {
slowQueryThreshold: 100, // Flag queries over 100ms
onQuery: (entry) => {
// Custom metrics callback (e.g., for noisy neighbor detection)
if (entry.isSlow) {
metrics.record('slow_query', { table: entry.table, duration: entry.duration })
}
}
})
export const db = drizzle(client)Send logs to multiple destinations simultaneously:
import { createLogger, HTTPTransport, DatadogTransport, SentryTransport } from 'vestig'
const log = createLogger()
// Add HTTP transport for centralized logging
log.addTransport(new HTTPTransport({
name: 'api-logs',
url: 'https://logs.example.com/ingest',
headers: { 'Authorization': 'Bearer token' },
}))
// Add Datadog for observability
log.addTransport(new DatadogTransport({
name: 'datadog',
apiKey: process.env.DD_API_KEY,
service: 'my-app',
tags: ['env:production'],
}))
// Add Sentry for error tracking
log.addTransport(new SentryTransport({
name: 'sentry',
dsn: process.env.SENTRY_DSN,
environment: 'production',
minLevel: 'warn', // Only send warn/error to Sentry
}))
// Initialize transports (starts flush timers)
await log.init()
// All logs go to console, HTTP endpoint, Datadog, and Sentry
log.info('Server started', { port: 3000 })| Transport | Description | Use Case |
|---|---|---|
ConsoleTransport |
Console output with colors | Development, debugging |
HTTPTransport |
Send to any HTTP endpoint | Custom log aggregation |
FileTransport |
Write to files with rotation | Server-side logging |
DatadogTransport |
Datadog Log Management | Production observability |
SentryTransport |
Sentry error tracking | Error monitoring, alerting |
Choose from compliance-focused presets:
import { Sanitizer } from 'vestig'
// GDPR compliance (EU data protection)
const gdprSanitizer = Sanitizer.fromPreset('gdpr')
// HIPAA compliance (healthcare)
const hipaaSanitizer = Sanitizer.fromPreset('hipaa')
// PCI-DSS compliance (payment cards)
const pciSanitizer = Sanitizer.fromPreset('pci-dss')
// Apply to logger
const log = createLogger({
sanitize: 'gdpr', // Use preset name directly
})| Preset | Fields Protected | Patterns Applied |
|---|---|---|
none |
None | None |
minimal |
password, secret, token, key | None |
default |
26 common fields | Email, Credit Card, JWT |
gdpr |
+ name, address, phone, IP | + IP addresses, phone |
hipaa |
+ patient, medical, SSN | + SSN pattern |
pci-dss |
+ card, CVV, PIN | Full card detection |
import { span } from 'vestig'
// Trace async operations
await span('api:request', async (s) => {
s.setAttribute('method', 'GET')
s.setAttribute('path', '/users')
await span('db:query', async () => {
return await db.query('SELECT * FROM users')
})
s.setStatus('ok')
})Automatically trace all fetch() calls with zero code changes:
import { instrumentFetch } from 'vestig'
// One line - all fetch() calls now create spans
instrumentFetch({
ignoreUrls: ['/health', /^\/_next/],
captureHeaders: ['content-type', 'x-request-id'],
})
// Now every fetch is traced automatically
await fetch('https://api.example.com/users')
// β Span: "http.client GET api.example.com/users"Export spans to any OpenTelemetry-compatible backend:
import { registerSpanProcessor, span } from 'vestig'
import { OTLPExporter } from 'vestig/otlp'
// Connect to Jaeger, Honeycomb, Vercel, Grafana, etc.
const exporter = new OTLPExporter({
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
serviceName: 'my-app',
headers: { 'Authorization': `Bearer ${process.env.OTEL_TOKEN}` },
})
registerSpanProcessor(exporter)
// All spans automatically exported
await span('checkout', async (s) => {
s.setAttribute('orderId', order.id)
await processPayment()
})One-liner setup for Next.js with OTLP and fetch instrumentation:
// instrumentation.ts
import { registerVestig } from '@vestig/next/instrumentation'
export function register() {
registerVestig({
serviceName: 'my-nextjs-app',
otlp: {
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
},
autoInstrument: {
fetch: true, // Auto-trace all fetch() calls
},
})
}Control log volume in high-throughput applications:
import { createLogger, createProbabilitySampler, createRateLimitSampler } from 'vestig'
// Probability sampling - keep 10% of logs
const logger = createLogger({
sampling: {
sampler: createProbabilitySampler({ rate: 0.1 }),
alwaysSample: ['error', 'warn'] // Always keep errors and warnings
}
})
// Rate limit sampling - max 100 logs per second
const rateLimitedLogger = createLogger({
sampling: {
sampler: createRateLimitSampler({ maxPerSecond: 100 })
}
})Available Samplers:
| Sampler | Description | Use Case |
|---|---|---|
createProbabilitySampler |
Random sampling by percentage | General log reduction |
createRateLimitSampler |
Max logs per time window | Protect against log storms |
createNamespaceSampler |
Different rates per namespace | Fine-grained control |
createCompositeSampler |
Combine multiple samplers | Complex sampling logic |
For more details, see the Sampling documentation.
Wide Events capture all context about a complete operation in ONE structured event. Instead of scattered logs, you get a single comprehensive record per request.
import { createLogger, createWideEvent } from 'vestig'
const log = createLogger({
tailSampling: {
enabled: true,
alwaysKeepStatuses: ['error'], // 100% of errors
slowThresholdMs: 2000, // 100% of slow requests
successSampleRate: 0.1, // 10% of successful requests
}
})
// Create and enrich the wide event throughout the request
const event = createWideEvent({ type: 'http.request' })
// Add context as you go
event.merge('http', { method: 'POST', path: '/api/checkout', status_code: 200 })
event.merge('user', { id: 'user-456', subscription: 'premium' })
event.merge('performance', { db_query_ms: 45, external_api_ms: 230 })
// End and emit the event
const completedEvent = event.end({ status: 'success' })
log.emitWideEvent(completedEvent)
// Output: ONE event with 50+ fields, easily queryableWhy Wide Events?
- Debug faster: All context in one place, no log correlation needed
- Reduce costs: Tail sampling keeps 100% of errors, samples success
- Better queries: "Show me slow requests from premium users with payment errors"
For more details, see the Wide Events documentation.
import { Sanitizer } from 'vestig'
const sanitizer = new Sanitizer({
fields: [
'customSecret',
{ type: 'prefix', value: 'private_' },
{ type: 'contains', value: 'token' },
],
patterns: [{
name: 'internal-id',
pattern: /ID-[A-Z0-9]+/g,
replacement: '[ID_REDACTED]',
}],
})
const safe = sanitizer.sanitize({
private_key: 'abc123', // β [REDACTED]
auth_token: 'xyz789', // β [REDACTED]
internalId: 'ID-ABC123', // β [ID_REDACTED]
})const log = createLogger({ namespace: 'app' })
const dbLog = log.child('database')
const cacheLog = log.child('cache')
dbLog.info('Query executed') // [app:database] Query executed
cacheLog.info('Cache hit') // [app:cache] Cache hitimport { withContext, createCorrelationContext } from 'vestig'
// Next.js API Route
export async function GET(req: Request) {
const context = createCorrelationContext({
requestId: req.headers.get('x-request-id') ?? undefined
})
return withContext(context, async () => {
log.info('Request started')
// All logs include: requestId, traceId, spanId
const result = await fetchData()
log.info('Request completed')
return Response.json(result)
})
}VESTIG_LEVEL=debug # trace | debug | info | warn | error
VESTIG_ENABLED=true # Enable/disable logging
VESTIG_STRUCTURED=true # JSON output (auto-enabled in production)
VESTIG_SANITIZE=true # PII sanitization (default: true)
# Add to context
VESTIG_CONTEXT_SERVICE=api
VESTIG_CONTEXT_VERSION=1.0.0const log = createLogger({
level: 'debug',
enabled: true,
structured: false,
sanitize: 'gdpr', // or true, false, or SanitizeConfig
context: { environment: 'development' }
})| Level | Description |
|---|---|
trace |
Very detailed debugging information |
debug |
Development debugging |
info |
General information |
warn |
Warning messages |
error |
Error messages (includes stack traces) |
Vestig automatically detects and adapts to:
- Node.js - Full features with AsyncLocalStorage
- Bun - Full features with AsyncLocalStorage
- Deno - Full features with AsyncLocalStorage (via
node:async_hooks) - Edge Runtime - Vercel Edge, Cloudflare Workers
- Browser - Client-side logging (use with
@vestig/nextor custom HTTPTransport)
import { RUNTIME, IS_SERVER, IS_DENO } from 'vestig'
console.log(RUNTIME) // 'node' | 'bun' | 'deno' | 'edge' | 'browser' | 'worker' | 'unknown'Browser Usage: For client-side logging, we recommend using
@vestig/nextwhich providesVestigProvideranduseLogger()hook with automatic server sync. For other frameworks, configureHTTPTransportto send logs to your backend.
In production (NODE_ENV=production), Vestig automatically:
- Sets log level to
warn - Enables structured (JSON) output
- Keeps sanitization enabled
import { HTTPTransport } from 'vestig'
const transport = new HTTPTransport({
name: 'my-http',
url: 'https://logs.example.com/ingest',
method: 'POST',
headers: {
'Authorization': 'Bearer my-token',
'X-Custom-Header': 'value',
},
batchSize: 100, // Send when 100 logs accumulated
flushInterval: 5000, // Or every 5 seconds
maxRetries: 3, // Retry failed requests
timeout: 30000, // Request timeout
transform: (entries) => ({
logs: entries,
timestamp: Date.now(),
}),
})import { FileTransport } from 'vestig'
const transport = new FileTransport({
name: 'file',
path: '/var/log/app/app.log',
maxSize: 10 * 1024 * 1024, // 10MB before rotation
maxFiles: 5, // Keep 5 rotated files
compress: true, // Gzip rotated files
})import { DatadogTransport } from 'vestig'
const transport = new DatadogTransport({
name: 'datadog',
apiKey: process.env.DD_API_KEY!,
site: 'datadoghq.com', // or datadoghq.eu, us3, us5
service: 'my-service',
source: 'vestig',
tags: ['env:production', 'team:backend'],
})Create a new logger instance.
Log at the specified level.
Create a namespaced child logger.
Add a transport to the logger.
Remove a transport by name.
Flush all buffered logs.
Cleanup all transports (call on shutdown).
Run a function with the given context.
Generate correlation IDs (requestId, traceId, spanId).
Create a sanitizer from a preset name.
// Before (Pino)
import pino from 'pino'
const logger = pino({ level: 'info' })
logger.info({ userId: 123 }, 'User logged in')
// After (Vestig)
import { createLogger } from 'vestig'
const logger = createLogger({ level: 'info' })
logger.info('User logged in', { userId: 123 })// Before (Winston)
import winston from 'winston'
const logger = winston.createLogger({
level: 'info',
transports: [new winston.transports.Console()],
})
logger.info('Hello', { meta: 'data' })
// After (Vestig)
import { createLogger } from 'vestig'
const logger = createLogger({ level: 'info' })
logger.info('Hello', { meta: 'data' })For comprehensive documentation, visit our documentation site:
We love contributions! Please read our Contributing Guide to get started.
- π Report bugs
- π‘ Request features
- π Improve documentation
MIT Β© Arakiss
See LICENSE for more details.