Near-Zero Cost via ?.
Optional chaining skips the entire call — including argument evaluation — when a level is disabled. ~22x faster than a conventional noop logger in benchmarks.
Clarity without the clutter. One namespace tree. One output pipeline. One ?. pattern for near-zero overhead. Pure TypeScript. ~3KB. Zero dependencies.
Early release (0.x) — API may evolve before 1.0.
npm install loggilybun add loggilypnpm add loggilyyarn add loggilyimport { createLogger } from "loggily"
import { toOtel } from "loggily/otel"
import { withMetrics, createMetricsCollector } from "loggily/metrics"
import * as otelApi from "@opentelemetry/api"
// One pipeline: console + OTEL + a Pino transport
const collector = createMetricsCollector()
const log = withMetrics(collector)(
createLogger("myapp", [
{ level: "debug" },
toOtel({ api: otelApi }),
{ write: (event) => pinoTransport.write(event), objectMode: true },
console,
]),
)
// Structured logging — ?. skips everything when the level is disabled
log.info?.("server started", { port: 3000 })
log.debug?.("cache hit", { key: "user:42" })
log.error?.(new Error("connection lost"))
// Spans — automatic timing, parent-child tracking, trace IDs
{
using span = log.span("db:query", { table: "users" })
const users = await db.query("SELECT * FROM users")
span.spanData.count = users.length
}
// → SPAN myapp:db:query (45ms) {count: 100, table: "users"}
// → also forwarded to OTLP backend and Pino transport
// Metrics — check p50/p95/p99 from explicit collector
for (const [name, s] of collector.all()) {
if (s.p95 > 100) console.warn(`${name} is slow: p95=${s.p95}ms`)
}