Zero-dependency, universal, library-first structured logging with scoped wide events.
Your logs are lying to you. Scattered console.log calls, unstructured strings, context spread across 20 log lines. When something breaks, you're grep-ing through noise hoping to find signal.
logscope fixes this. Every log is structured data. Quick logs emit immediately. Scoped logs accumulate context over a unit of work and emit once—with everything an engineer needs to understand what happened.
// Scattered, unstructured, impossible to query
console.log('Request received')
console.log('User:', user.id)
console.log('Cart loaded, items:', cart.items.length)
console.log('Payment failed') // Good luck correlating this at 3amimport { createLogger } from 'logscope'
const log = createLogger('checkout')
// Quick structured log — immediate, queryable
log.info('payment processed', { userId: '123', amount: 99.99 })
// Scoped wide event — accumulate context, emit once
const scope = log.scope({ method: 'POST', path: '/checkout' })
scope.set({ user: { id: '123', plan: 'premium' } })
scope.set({ cart: { items: 3, total: 99.99 } })
scope.set({ payment: { method: 'card', processor: 'stripe' } })
scope.emit()
// → One structured event with ALL context + durationMost logging libraries force configuration on consumers. logscope doesn't. When unconfigured, all logging is completely silent—zero output, zero errors, zero side effects.
// In your library — safe to ship, no config required
import { createLogger } from 'logscope'
const log = createLogger('my-awesome-lib')
export function doSomething() {
log.debug('processing started', { step: 'init' })
// If the app using your library never configures logscope,
// this produces nothing. No noise, no errors, no side effects.
}If the application does configure logscope, those logs become visible and routable:
// In the application entry point
import { configure, getConsoleSink } from 'logscope'
await configure({
sinks: {
console: getConsoleSink(),
},
loggers: [
{ category: 'my-app', level: 'debug', sinks: ['console'] },
// See logs from that library too:
{ category: 'my-awesome-lib', level: 'info', sinks: ['console'] },
],
})npm install logscopepnpm add logscopebun add logscopeimport { configure, getConsoleSink } from 'logscope'
await configure({
sinks: {
console: getConsoleSink(),
},
loggers: [
{ category: 'my-app', level: 'debug', sinks: ['console'] },
],
})import { createLogger } from 'logscope'
const log = createLogger('my-app')// Structured logs — immediate emission
log.info({ action: 'page_view', path: '/home' })
log.info('user logged in', { userId: '123', method: 'oauth' })
log.warn('slow query', { duration: 1200, table: 'users' })
log.error('payment failed', { orderId: 'abc', reason: 'card_declined' })For operations that span multiple steps, accumulate context and emit once:
const scope = log.scope({ method: 'POST', path: '/api/checkout' })
// Accumulate context as you go
scope.set({ user: { id: '123', plan: 'premium' } })
scope.set({ cart: { items: 3, total: 99.99 } })
try {
const payment = await processPayment(cart, user)
scope.set({ payment: { id: payment.id, method: 'card' } })
} catch (error) {
scope.error('payment failed', { reason: error.message })
}
scope.emit()
// → One event with all context + durationLoggers have categories that form a tree. Child loggers inherit from parents:
const appLog = createLogger('my-app')
const dbLog = appLog.child('db') // category: ['my-app', 'db']
const authLog = appLog.child('auth') // category: ['my-app', 'auth']
// Configure different levels per category
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: 'my-app', level: 'info', sinks: ['console'] },
{ category: ['my-app', 'db'], level: 'warn' }, // only warnings+ from DB
],
})Attach reusable properties to a logger:
const reqLog = log.with({ requestId: 'req_abc', userId: '123' })
reqLog.info('processing started') // requestId and userId attached
reqLog.info('step completed') // same context, no repetitionA sink is just a function:
type Sink = (record: LogRecord) => voidBuilt-in sinks:
getConsoleSink()— outputs toconsole.log/console.warn/console.error
Custom sinks are trivial:
await configure({
sinks: {
myApi(record) {
fetch('/api/logs', {
method: 'POST',
body: JSON.stringify(record),
})
},
},
loggers: [
{ category: 'my-app', sinks: ['myApi'] },
],
})Six levels, from lowest to highest severity:
| Level | Use Case |
|---|---|
trace |
Fine-grained diagnostic info |
debug |
Development-time debugging |
info |
Normal operational events |
warning |
Something unexpected but recoverable |
error |
Something failed |
fatal |
Application cannot continue |
Filters are predicate functions on log records:
await configure({
sinks: { console: getConsoleSink() },
filters: {
slowOnly(record) {
return record.properties.duration > 100
},
},
loggers: [
{ category: 'my-app', sinks: ['console'], filters: ['slowOnly'] },
],
})Inspired by Logging Sucks, evlog, and LogTape.
- Structured Data, Not Strings — Every log is queryable, parseable, machine-readable
- Wide Events — One comprehensive event per unit of work, not 20 scattered lines
- Library-First — Safe for library authors. Unconfigured = silent
- Zero Dependencies — No supply chain risk. Works everywhere
- Simple Sinks —
(record) => void. Compose complexity, don't bake it in
logscope works everywhere JavaScript runs:
- Node.js (>= 24)
- Deno
- Bun
- Browsers
- Edge functions (Cloudflare Workers, Vercel Edge, etc.)