npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@log-kit/core

v0.1.2

Published

Isomorphic, zero-dependency log engine providing structured types, i18n formatting, deduplication, and client sanitization.

Readme

@log-kit/core

Isomorphic, zero-dependency logging primitives, structured types, and a lightweight logger engine.

Designed for high-throughput Node.js, browser, edge runtime, and serverless environments. @log-kit/core provides strongly-typed logging contracts with native i18n support, runtime deduplication, and audience-based filtering, with zero runtime dependencies.


Features

  • Isomorphic & zero-dependency: runs identically in Node.js, browsers, Bun, Deno, and edge environments.
  • Strongly typed contracts: structured interfaces (Log, LogClient, SerializedError) and string-union constants (LogType, LogErrorType, LogAudience).
  • Rust serde parity: constants and properties use lowerCamelCase, matching serde(rename_all = "camelCase") mapping.
  • Context-aware Logger: child scopes, level filtering, time-window deduplication, and pluggable transports.
  • Environment-aware console output: minLevel alone decides whether a log is emitted; the default transport separately uses a resolved isDev flag to decide how much detail to print for logs that pass, keeping raw payloads out of production consoles by default without silently overriding your level configuration. Bundler-specific dev flags (e.g. Vite/SvelteKit's import.meta.env.DEV) can be passed in via new Logger({ isDev }).
  • Built-in utilities: structured error serialization with sensitive-key redaction (including Axios-style .response errors), deduplication, audience sanitization/filtering, multi-log aggregation, and type guards — every utility is a standalone function, independently importable from @log-kit/core/utils.

Installation

npm install @log-kit/core
# or
pnpm add @log-kit/core
# or
yarn add @log-kit/core
# or
bun add @log-kit/core

Quick start

1. Logger

import { Logger, LogType, LogErrorType } from "@log-kit/core";

const logger = new Logger({
  namespace: "api-gateway",
  minLevel: LogType.debug,
  dedupeWindowMs: 1000 // suppress an identical log re-emitted within 1s
});

logger.info({
  code: "USER_AUTH_SUCCESS",
  message: "User usr_1024 logged in from 192.168.1.1"
});

// Scoped child logger — namespaces are joined with ":"
const authLogger = logger.child({ group: "oauth-flow" });

authLogger.warn({
  code: "ERR_RATE_LIMIT",
  message: "IP exceeded maximum auth attempts",
  errorType: LogErrorType.rateLimit,
  status: 429
});

Or use the createLogger factory if you'd rather not use new:

import { createLogger } from "@log-kit/core";
const logger = createLogger({ namespace: "api-gateway" });

Each level method (trace, debug, info, log, warn, error, fatal) returns the normalized Log object it emitted, or null when the entry was filtered out by minLevel or suppressed as a duplicate.

Note: templating/interpolation ({key}-style placeholders) is intentionally out of scope for this package — build the final message string yourself, or reach for a dedicated templating package.

2. Custom transports

By default logs are written to the console. minLevel is the only thing that decides whether a log is emitted — the default console transport never re-filters by severity. It does use isDev for one thing: outside production, it hides log.details (arbitrary, unredacted, caller-supplied context) from the console, since — unlike log.error, which passes through serializeError's redaction — nothing in this package sanitizes details. log.error is always shown for error/fatal logs, since that's the point of logging an error. Provide transports to ship logs anywhere else (a file, an HTTP sink, an observability platform) — transport errors are caught internally so one broken transport can't take down the others or the calling app. An empty transports: [] is treated the same as omitting it (falls back to the console transport) — pass a no-op transport ([() => {}]) if you want to silence output entirely.

import { Logger, type LogTransportHandler } from "@log-kit/core";

const shipToDatadog: LogTransportHandler = (log) => {
  fetch("https://http-intake.logs.datadoghq.com/v1/input", {
    method: "POST",
    body: JSON.stringify(log)
  });
};

const logger = new Logger({ transports: [shipToDatadog] });

isDev (whether log.details gets shown) defaults to this package's own isDev() utility, which only checks process.env.NODE_ENV. That's the standard convention in Node and in bundlers that replace it (webpack, esbuild, Next.js), but Vite and SvelteKit expose their dev flag as import.meta.env.DEV insteadisDev()'s process check runs before any bundler-side text replacement could apply, so it can't see that flag on its own. Pass it in explicitly:

import { Logger, isDev } from "@log-kit/core";

// In a Vite/SvelteKit app:
const logger = new Logger({ isDev: import.meta.env.DEV });

// Or, to keep a graceful fallback for environments without `import.meta.env`
// (e.g. this same code also running under plain Node/Jest):
const logger = new Logger({ isDev: isDev(import.meta.env.DEV) });

isDev is resolved once per Logger instance at construction time (not re-checked on every log), and child() inherits the parent's resolved value unless a child explicitly overrides it.

For transports that need one-time setup (opening a file handle, building an HTTP client bound to a resolved endpoint, ...) or want to read this Logger's resolved config (its resolved isDev, minLevel, etc. — not just the raw options you passed in), use createTransports instead of building the handler ahead of time. It's a factory, called once at construction and appended to transports, kept as a separate option specifically so there's no runtime type-detection between "is this a plain handler or a factory" — a transports entry is always a plain (log) => void, and createTransports entries are always factories:

import { Logger, type ResolvedLoggerConfig, type LogTransportHandler } from "@log-kit/core";

function createFileTransport(config: ResolvedLoggerConfig): LogTransportHandler {
  const stream = openLogFile(config.namespace ?? "app"); // one-time setup
  return (log) => stream.write(JSON.stringify(log) + "\n");
}

const logger = new Logger({
  createTransports: (config) => [createFileTransport(config)]
});

3. Sanitizing logs for public APIs (toClientLog)

Strip internal stack traces, system namespaces, and non-client-visible fields before returning log payloads to frontend clients:

import { toClientLog, type Log } from "@log-kit/core";

const internalLog: Log = {
  code: "ERR_DB_TIMEOUT",
  message: "Database query timed out for query ID q_88",
  namespaces: "internal-db-cluster",
  stack: "Error: Query failed at Pool.query...",
  details: { internalHost: "10.0.0.5" }
};

const publicLog = toClientLog(internalLog);
// { code: "ERR_DB_TIMEOUT", message: "..." }

4. Serializing caught errors (serializeError)

Handles plain Errors, primitive throws, and structured HTTP-client errors (e.g. Axios) that carry a .response. Sensitive-looking keys (password, token, authorization, cookie, ...) are redacted wherever this function touches external data — the error's own properties, response.headers, and response.data — as a best-effort safety net against accidentally logging credentials. Redaction recurses into nested objects/arrays (default 3 levels deep, with circular-reference protection), so a nested response.data.user.password is caught too, not just a top-level field:

import { serializeError } from "@log-kit/core";

try {
  await axios.get("/users/1");
} catch (err) {
  const serialized = serializeError(err);
  // { name: "AxiosError", message: "...", code: "ERR_BAD_REQUEST",
  //   response: { status: 404, statusText: "Not Found",
  //     data: { user: { password: "[REDACTED]" } },
  //     headers: { authorization: "[REDACTED]", "content-type": "application/json" } } }
}

5. Redacting sensitive keys directly (redactObject / redactValue)

The same redaction serializeError uses internally is available standalone, for scrubbing any plain object (e.g. request headers) or arbitrary value (object, array, or primitive) before you hand it to a transport or a details payload:

import { redactObject, redactValue } from "@log-kit/core";

redactObject({ userId: "u1", password: "hunter2" });
// { userId: "u1", password: "[REDACTED]" }

redactObject({ user: { profile: { password: "hunter2" } } });
// { user: { profile: { password: "[REDACTED]" } } } — recurses by default

// redactValue also handles arrays and primitives, for when you don't know the shape ahead of time
redactValue([{ token: "a" }, { token: "b" }]);
// [{ token: "[REDACTED]" }, { token: "[REDACTED]" }]

6. Building Log objects without a logger instance (createLog)

The same normalization Logger uses internally — building a complete Log from a level and payload, serializing payload.error — is exported standalone so you can compose it into your own pipeline:

import { createLog, LogType } from "@log-kit/core";

try {
  await db.query(sql);
} catch (error) {
  const log = createLog(LogType.error, { code: "ERR_DB_QUERY", error });
  // log.error -> SerializedError, log.stack derived automatically
}

7. Aggregating multiple logs (toLog)

Collapse a batch of related logs (e.g. several field-validation failures) into a single, localizable, displayable log:

import { toLog } from "@log-kit/core";

const summary = toLog([
  { code: "ERR_EMAIL_INVALID", type: "warn", errorType: "validation", message: "Invalid email" },
  { code: "ERR_AGE_INVALID", type: "error", errorType: "validation", message: "Invalid age" }
]);
// summary.type === "error", summary.code === "error.validation.error"

summary.audience is only set when you pass config.audiences (to the first requested audience); otherwise it's left undefined, same as any other log with no explicit audience — it does not silently default to LogAudience.admin.

8. Audience-based filtering (filterByAudience)

import { filterByAudience, LogAudience } from "@log-kit/core";

const clientVisibleLogs = filterByAudience(allLogs, LogAudience.client);

9. Checking for errors in a batch (hasError)

import { hasError, LogType } from "@log-kit/core";

if (hasError(aggregatedLogs)) {
  // at least one log is `error` or `fatal` (the ERROR_TYPES default)
}

hasError(aggregatedLogs, [LogType.warn, LogType.error, LogType.fatal]); // custom threshold

Architecture

No Log class

Log is a plain data shape (see Type contracts), not a class. A class would only add value here if Log needed private state, inheritance, or behavior tightly bound to a single instance — it doesn't. Logs are plain objects created, passed around, serialized, and compared by value across process/network boundaries (they need to survive a JSON.stringify round-trip), which is exactly what plain objects are for. createLog() is the functional equivalent of a constructor: it takes a level and a payload and returns a fully normalized Log, with no hidden state and no prototype.

Logger is a class, and that's the right call here

Unlike Log, the logger engine genuinely owns per-instance state: its resolved config, its transports, and a one-slot dedupe cache that has to persist across calls. That's real encapsulated, mutable state tied to one instance's lifetime — exactly what a class is for, and child() needs prototype-free, straightforward instantiation (new Logger({...})) to produce scoped instances cleanly. createLogger() is provided alongside it for callers who'd rather not use new.

Every method on Logger is deliberately thin — dispatch() reads like a short pipeline of calls into ./utils (meetsMinLevel, createLog, isDuplicate, mergeNamespace, isDev) rather than reimplementing that logic inline. The class owns state and orchestration; the actual logic lives in standalone, independently testable functions that other packages can also import without touching Logger at all.


Utilities reference

All utilities live under src/utils/ (one file per concern) and are re-exported from both @log-kit/core and @log-kit/core/utils.

| Function | Description | | --- | --- | | createLog(type, payload) | Builds a normalized Log, serializing payload.error when present. | | serializeError(error) | Converts an unknown thrown value into a SerializedError, including Axios-style .response errors. Never throws. | | isDuplicate(logA, logB, options?) | Structural equivalence check between two logs. | | toClientLog(log) | Strips server-only fields for public API responses. | | filterByAudience(logs, targetAudience) | Filters logs by visibility scope. | | toLog(logs, config?) | Reduces multiple logs into one representative log. | | pickLogType(types) | Picks the most severe LogType from a list. | | pickErrorType(errorTypes) | Picks the most specific LogErrorType from a list. | | summarizeMessages(messages, type?) | Joins multiple messages into one summary string. | | hasError(logs, types?) | Checks whether any log's type is in types (defaults to ERROR_TYPES: fatal, error). | | meetsMinLevel(type, minLevel?) | Checks whether a severity level meets a minimum threshold. | | mergeNamespace(parent?, child?) | Joins a parent/child namespace pair the same way Logger.child does. | | isDev(fallback?) | Detects whether the current environment is non-production, via process.env.NODE_ENV (works in Node and in any bundler that statically replaces it — webpack, esbuild, Vite, Next.js, ...). | | redactObject(obj, depth?) | Recursively redacts sensitive-looking keys (password, token, ...) on a plain object, up to depth levels (default 3), with circular-reference protection. | | redactValue(value, depth?) | Same as redactObject, but for a value of unknown shape — object, array, or primitive. | | isSensitiveKey(key) | Checks whether a key name looks like it holds a secret. | | isLogType, isLogErrorType, isLogAudience | Runtime type guards for the string-union constants. |


Type contracts

All interfaces and type aliases live in src/types/types.d.ts.

export interface Log extends LogClient, LogErrorPayload, LogContext {
  details?: Record<string, unknown>;
}

export interface LogClient extends LogLocalization {
  type?: LogType;
  status?: number;
  logs?: Log[];
}

export interface SerializedError {
  name: string;
  message: string;
  stack?: string;
  code?: string | number;
  response?: SerializedErrorResponse;
  raw?: unknown;
  [key: string]: unknown;
}

export interface LoggerOptions {
  namespace?: string;
  group?: string;
  defaultAudience?: LogAudience;
  minLevel?: LogType;
  dedupeWindowMs?: number;
  transports?: LogTransportHandler[];
  isDev?: boolean;
  createTransports?: (config: ResolvedLoggerConfig) => LogTransportHandler[];
}

export interface ResolvedLoggerConfig {
  namespace?: string;
  group?: string;
  defaultAudience: LogAudience;
  minLevel: LogType;
  dedupeWindowMs: number;
  isDev: boolean;
}

LogType, LogErrorType, and LogAudience are as const string-union objects (not TypeScript enums), so they compile away to plain string literals with no runtime enum object overhead beyond the small lookup table itself, and compare correctly across module/bundle boundaries.


License

MIT © livacom.com