Structured JSON logging for the @vllnt ecosystem.
Zero runtime dependencies. Tree-shakeable. Works in Node.js, Convex, browser, and edge runtimes.
pnpm add @vllnt/loggerimport { createBackendLogger } from "@vllnt/logger";
const logger = createBackendLogger("auth");
logger.info("login", { userId: "123" });
// → {"event":"auth.login","level":"info","timestamp":"...","userId":"123"}
logger.warn("rate-limited", { ip: "1.2.3.4" });Set LOG_LEVEL env var to control verbosity (debug, info, warn, error). Defaults to warn.
| Import | Use Case |
|---|---|
@vllnt/logger |
Core API + Node.js backend preset |
@vllnt/logger/convex |
Convex-safe logger (no process.env) |
@vllnt/logger/posthog |
PostHog output adapter |
@vllnt/logger/testing |
In-memory test output for assertions |
Pre-configured for Node.js. Reads LOG_LEVEL from process.env lazily on each log call.
const logger = createBackendLogger("payments");
logger.info("charge", { amount: 99, currency: "usd" });Core factory. Bring your own level strategy and output.
import { createLogger, consoleOutput } from "@vllnt/logger";
const logger = createLogger("api", {
getLogLevel: () => "debug",
output: consoleOutput,
});Same as createLogger but adds withTiming and withTimingSync helpers.
const result = await logger.withTiming("db.query", async () => {
return db.query("SELECT ...");
});
// Logs: scope.db.query.start → scope.db.query.complete (with durationMs)
// On error: scope.db.query.error (with durationMs + error message)Fan-out to multiple outputs. Each output is error-isolated.
import { composeOutputs, consoleOutput } from "@vllnt/logger";
import { createPostHogOutput } from "@vllnt/logger/posthog";
const output = composeOutputs(
consoleOutput,
createPostHogOutput(posthog.capture.bind(posthog)),
);Safe for Convex queries, mutations, and actions. No process.env access.
import { createConvexLogger } from "@vllnt/logger/convex";
const logger = createConvexLogger("myFunction", "debug");
logger.info("started");Wraps a PostHog-compatible capture function as a LogOutput. Swallows thrown errors and rejected promises.
import { createPostHogOutput } from "@vllnt/logger/posthog";
const output = createPostHogOutput(posthog.capture.bind(posthog));Captures log entries in-memory for test assertions.
import { createTestOutput } from "@vllnt/logger/testing";
import { createLogger } from "@vllnt/logger";
const { entries, output } = createTestOutput();
const logger = createLogger("test", {
getLogLevel: () => "debug",
output,
});
logger.info("hello", { a: 1 });
expect(entries[0].event).toBe("test.hello");| Function | Description |
|---|---|
formatLogEntry(level, event, data?) |
Build a LogEntry manually |
parseLogLevel(value, default?) |
Safe string to LogLevel parsing |
consoleOutput(entry) |
JSON to console.debug/info/warn/error (crash-safe) |
type LogLevel = "debug" | "info" | "warn" | "error";
type LogData = Record<string, unknown>;
type LogEntry = { event: string; level: LogLevel; timestamp: string } & Record<
string,
unknown
>;
type LogOutput = (entry: LogEntry) => void;
type Logger = {
debug;
info;
warn;
error: (event: string, data?: LogData) => void;
};
type ExtendedLogger = Logger & { withTiming; withTimingSync };
type LoggerConfig = { getLogLevel: () => LogLevel; output: LogOutput };- Scoped events: All events are prefixed with the logger scope (
auth.login,payments.charge) - Reserved field protection: User data cannot overwrite
event,level, ortimestamp - Log calls never throw: a failing
getLogLevel, output, or data getter is swallowed; an invalid level falls back toinfo - Crash-safe serialization:
consoleOutputwritesBigIntas a string,Erroras{ name, message, stack }, and circular refs as"[Circular]"; a throwingtoJSONdegrades to a minimalevent/levelline - No built-in redaction: pass only data you are willing to log.
withTiminglogserror.messageverbatim, so sanitize sensitive errors before throwing them - Synchronous outputs:
LogOutputis synchronous by design — async adapters manage their own promises - Lazy level evaluation:
getLogLevel()is called per-emit, so env changes are picked up without restart - Zero side effects: Core module has zero module-level side effects (
sideEffects: false)
Every publish runs in .github/workflows/publish.yml with npm trusted publishing and provenance;
nothing is published from a local machine. Maintainers only.
- Canary (
@canary): every push tomainthat touches the package publishes one (pnpm add @vllnt/logger@canary). - Stable (
@latest): merge a PR that setspackage.jsonversionand adds a matching## [X.Y.Z]entry toCHANGELOG.md, then push the tagvX.Y.Zon thatmaincommit.