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

@qianxude/logger

v0.1.12

Published

Production-ready pino-based logger implementations for qianxude stack

Readme

@qianxude/logger

Production-ready pino-based logger implementations for the qianxude stack.

Installation

bun add @qianxude/logger

Overview

This package provides:

  1. Factory functions to create raw pino loggers configured for common use cases
  2. PinoLogger class that wraps pino and implements the stack-standard Logger interface from @qianxude/shared

Quick Start

Pretty Console Logger (Development)

import { createPrettyPino, PinoLogger } from '@qianxude/logger';

// Create a raw pino logger with pretty output
const rawPino = createPrettyPino({
  level: 'debug',
  colorize: true,
});

// Wrap it as a stack-standard Logger
const logger = new PinoLogger(rawPino, { service: 'my-app' });

logger.info('Application started');
logger.debug({ userId: 123 }, 'User logged in');
logger.error({ err: new Error('Oops') }, 'Something went wrong');

Duo-Target Logger (Production)

Outputs to both console (pretty) and file (with rotation):

import { createDuoTargetPino, PinoLogger } from '@qianxude/logger';

const rawPino = createDuoTargetPino({
  level: 'info',
  file: './logs/app.log',
  frequency: 'daily',      // Rotate daily
  limit: { count: 7 },     // Keep 7 days of logs
});

const logger = new PinoLogger(rawPino);
logger.info('Application started');

File-Only Logger

import { createRollFilePino, PinoLogger } from '@qianxude/logger';

const rawPino = createRollFilePino({
  level: 'info',
  file: './logs/app.log',
  size: '10m',             // Rotate at 10MB
  limit: { count: 5 },     // Keep 5 rotated files
});

const logger = new PinoLogger(rawPino);

Logger Interface

PinoLogger implements the Logger interface from @qianxude/shared:

interface Logger {
  // Log methods
  log(level: LogLevel, message: string, ctx?: Context): void;
  fatal(message: string, ctx?: Context): void;
  error(message: string, ctx?: Context): void;
  warn(message: string, ctx?: Context): void;
  info(message: string, ctx?: Context): void;
  debug(message: string, ctx?: Context): void;
  trace(message: string, ctx?: Context): void;

  // Context management
  get ctx(): Context;
  attach(key: string, value: any): void;
  attach(ctx: Context): void;
  detach(ctx: Context): void;
  spawn(ctx?: Context): this;
}

Context Management

// Attach context
logger.attach('userId', 123);
logger.attach({ requestId: 'abc-123', method: 'GET' });

// Create child logger with extended context
const childLogger = logger.spawn({ operation: 'query' });
childLogger.info('Executing query'); // Includes userId, requestId, method, operation

// Detach context
logger.detach({ userId: undefined });

Log Levels

  • trace (10) - Ultra-verbose tracing
  • debug (20) - Debugging information
  • info (30) - General information (default)
  • warn (40) - Warnings
  • error (50) - Errors
  • fatal (60) - Critical errors

Factory Functions

createPrettyPino(options)

Creates a pino logger with pretty console output.

interface PrettyPinoOptions {
  level?: LogLevel;        // Default: 'info'
  colorize?: boolean;      // Default: true
  translateTime?: string;  // Default: 'SYS:standard'
  prettyOptions?: pino.PrettyOptions;
}

createDuoTargetPino(options)

Creates a pino logger that outputs to both console (pretty) and file (with rotation).

interface DuoTargetPinoOptions {
  level?: LogLevel;        // Default: 'info'
  file: string;            // Required: log file path
  frequency?: 'daily' | 'hourly' | number;  // Time-based rotation
  size?: string | number;  // Size-based rotation (e.g., '10m')
  mkdir?: boolean;         // Create parent dirs (default: true)
  symlink?: boolean;       // Create symlink to current log (default: true)
  limit?: { count: number }; // Number of rotated files to keep
  colorize?: boolean;      // Console colorize (default: true)
  translateTime?: string;  // Default: 'SYS:standard'
}

createRollFilePino(options)

Creates a pino logger that outputs only to file (with rotation).

interface RotationOptions {
  level?: LogLevel;
  file: string;            // Required
  frequency?: 'daily' | 'hourly' | number;
  size?: string | number;
  mkdir?: boolean;
  symlink?: boolean;
  limit?: { count: number };
}

createPino(options)

Creates a basic pino logger with custom options.

const logger = createPino({
  level: 'debug',
  base: { service: 'my-service' },
});

Environment Variables

The logger package supports configuration via environment variables. Use the parsing functions to convert environment variables into logger options.

Required vs Optional

| Variable | Required For | Optional For | Default | |----------|--------------|--------------|---------| | LOG_FILE | parsePinoRollOptionsEnv, parseDuoTargetPinoOptionsEnv | parsePrettyPinoOptionsEnv | - | | LOG_LEVEL | - | All functions | 'info' | | LOG_COLORIZE | - | All functions | true | | LOG_TRANSLATE_TIME | - | All functions | 'SYS:standard' | | LOG_FILE_SIZE | - | File logging | - | | LOG_FILE_FREQUENCY | - | File logging | - | | LOG_FILE_EXTENSION | - | File logging | - | | LOG_FILE_SYMLINK | - | File logging | true | | LOG_FILE_LIMIT_COUNT | - | File logging | - | | LOG_FILE_DATE_FORMAT | - | File logging | - | | LOG_FILE_MKDIR | - | File logging | true |

Important: LOG_FILE is the only required environment variable, and it's only required for file-based logging functions (parsePinoRollOptionsEnv and parseDuoTargetPinoOptionsEnv).

Available Environment Variables

Common/Pretty Options (All Optional)

| Variable | Description | Example Values | |----------|-------------|----------------| | LOG_LEVEL | Minimum log level | fatal, error, warn, info, debug, trace | | LOG_COLORIZE | Enable colorized output | true, false | | LOG_TRANSLATE_TIME | Time format string | SYS:standard, SYS:iso |

File Roll Options

| Variable | Description | Required | Example Values | |----------|-------------|----------|----------------| | LOG_FILE | Log file path | Yes (for file logging) | ./logs/app.log | | LOG_FILE_SIZE | Max file size | No | 10m, 100k, 1g, or number (MB) | | LOG_FILE_FREQUENCY | Rotation frequency | No | daily, hourly, or number (ms) | | LOG_FILE_EXTENSION | File extension to append | No | .log | | LOG_FILE_SYMLINK | Create symlink to current log | No | true, false | | LOG_FILE_LIMIT_COUNT | Number of rotated files to keep | No | 7 | | LOG_FILE_DATE_FORMAT | Date format for filename | No | yyyy-MM-dd | | LOG_FILE_MKDIR | Create parent directories | No | true, false |

Usage Examples

Parse Pretty Logger Options

import { parsePrettyPinoOptionsEnv, createPrettyPino } from '@qianxude/logger';

// Parse from process.env
const options = parsePrettyPinoOptionsEnv();

// Or pass a custom env object
const options = parsePrettyPinoOptionsEnv({
  LOG_LEVEL: 'debug',
  LOG_COLORIZE: 'true',
});

const logger = createPrettyPino(options);

Parse File Roll Options

import { parsePinoRollOptionsEnv, createRollFilePino } from '@qianxude/logger';

const options = parsePinoRollOptionsEnv({
  LOG_FILE: './logs/app.log',
  LOG_FILE_FREQUENCY: 'daily',
  LOG_FILE_LIMIT_COUNT: '7',
});

if (options.file) {
  const logger = createRollFilePino({ level: 'info', ...options });
}

Parse Duo-Target Options

import { parseDuoTargetPinoOptionsEnv, createDuoTargetPino } from '@qianxude/logger';

const options = parseDuoTargetPinoOptionsEnv(process.env);

if (options.file) {
  const logger = createDuoTargetPino(options);
}

Environment Variable Constants

Use the exported LOG_ENV_VARS constant for type-safe env var names:

import { LOG_ENV_VARS } from '@qianxude/logger';

console.log(LOG_ENV_VARS.LOG_LEVEL);     // 'LOG_LEVEL'
console.log(LOG_ENV_VARS.LOG_FILE);      // 'LOG_FILE'

Error Handling

The parsing functions throw descriptive errors for invalid values:

import { parsePrettyPinoOptionsEnv } from '@qianxude/logger';

try {
  parsePrettyPinoOptionsEnv({ LOG_LEVEL: 'verbose' });
} catch (err) {
  // Error: Invalid LOG_LEVEL value 'verbose'. Must be one of: fatal, error, warn, info, debug, trace.
}

Graceful Error Handling with noThrow

When you want to handle missing required env vars gracefully without exceptions, use the noThrow option:

import { parsePinoRollOptionsEnv, createRollFilePino } from '@qianxude/logger';

const options = parsePinoRollOptionsEnv(process.env, { noThrow: true });

if (options._invalid) {
  console.warn('File logging disabled:', options._invalidVars?.join(', '));
  // Fall back to console-only logging
} else {
  const logger = createRollFilePino({ level: 'info', ...options });
}

This is useful when file logging is optional and you want to check at runtime whether it's available.

import { parseDuoTargetPinoOptionsEnv, createDuoTargetPino, createPrettyPino } from '@qianxude/logger';

const options = parseDuoTargetPinoOptionsEnv(process.env, { noThrow: true });

// Use duo-target if LOG_FILE is set, otherwise fall back to pretty console
const logger = options._invalid
  ? createPrettyPino({ level: 'info' })
  : createDuoTargetPino(options);

Integration with @qianxude/shared

Use with the shared logger:

import { logger } from '@qianxude/shared';
import { createDuoTargetPino, PinoLogger } from '@qianxude/logger';

// At app startup, configure the shared logger
const pinoLogger = new PinoLogger(createDuoTargetPino({
  level: 'info',
  file: './logs/app.log',
  frequency: 'daily',
}));

// Use configureLogger to set the delegate
import { configureLogger } from '@qianxude/shared';
configureLogger(logger, pinoLogger);

// Now all packages using the shared logger will use pino
logger.info('Application initialized');

LLM Call Lifecycle Logging

@qianxude/logger includes an opt-in, file-backed LLM lifecycle controller. It does not modify @qianxude/shared and does not intercept ordinary Logger calls.

Resolve the deployment configuration first, then pass the resolved options to the controller:

import { logger } from '@qianxude/shared';
import {
  createLLMCallLifecycle,
  parseLLMCallLifecycleOptionsEnv,
} from '@qianxude/logger';

const options = parseLLMCallLifecycleOptionsEnv();
const llm = createLLMCallLifecycle(logger, options);

The controller supports an already-completed call:

await llm.record({
  request: {
    headers: {
      'content-type': 'application/json',
    },
    body: {
      model: 'gpt-4o',
    },
  },
  response: {
    content: 'Hello',
  },
  task: 'chat/title-generation',
});

For accurate lifecycle timing, use begin():

const call = llm.begin({
  request,
  task: 'chat/title-generation',
});

const response = await invokeModel(request);
await call.complete(response);

Model and query

The effective model is resolved from a first-class model parameter, falling back to request.body.model. A caller-supplied model overrides the body value.

const call = llm.begin({
  request,
  model: 'gpt-4o-mini', // overrides request.body.model when present
});

A human-readable query is captured in _meta for log scannability. When omitted, it is derived from the content of the last role: 'user' message in request.body.messages (handling both string content and array text-block content), normalized and truncated to 500 characters.

await llm.record({
  request,
  response: { content: 'Hello' },
  query: 'Summarize the meeting', // optional, otherwise derived from the last user message
});

Failure and cancellation

A non-stream failure that happens between begin() and complete() produces no record unless you finalize it explicitly. Use error() to record a failed call, or abort() to record a cancellation:

const call = llm.begin({ request, task: 'completion' });

try {
  const response = await invokeModel(request);
  await call.complete(response);
} catch (error) {
  await call.error(error); // records error metadata, does not rethrow
}

// Or, for a timeout / client disconnect:
await call.abort('timeout');

Each handle finalizes exactly once; calling a second method afterwards throws. Streams already record errors and partial chunks in their own teardown, so error()/abort() are intended for non-stream lifecycle handles.

Durability: flush() and shutdown()

File writes are fire-and-forget: record(), complete(), error(), abort(), and stream() resolve once the write is scheduled, not necessarily after it lands. Call flush() (or its alias shutdown()) before a clean process exit to await all pending writes:

await llm.flush();  // await all in-flight file writes
// or
await llm.shutdown();

Bounded stream buffering

By default stream() buffers every yielded chunk in memory. Set maxBufferedChunks to keep only the newest N chunks on a rolling basis; older chunks are dropped and _meta.truncated is set to true so analysts know data was elided:

const llm = createLLMCallLifecycle(logger, {
  ...options,
  maxBufferedChunks: 100, // 0 = unbounded (default)
});

Streaming calls are buffered until completion and re-yielded unchanged. On stream end, the raw chunks are also aggregated into a single response object (see Stream response aggregation):

const call = llm.begin({ request, task: 'chat/title-generation' });

for await (const chunk of call.stream(modelStream)) {
  consume(chunk);
}

Each enabled call produces one JSON file. The record contains request, response or top-level chunks, and a final _meta property with lightweight lifecycle metadata. _meta includes an outcome field (success, error, or aborted) so batch analysis can filter records without testing for the presence of specific payload fields:

{
  "id": "...",
  "request": {
    "headers": {},
    "body": { "model": "gpt-4o" }
  },
  "response": { "text": "Hello" },
  "_meta": {
    "timestamp": "...",
    "startedAt": "...",
    "finishedAt": "...",
    "durationMs": 123,
    "task": "chat/title-generation",
    "query": "Summarize the meeting",
    "model": "gpt-4o",
    "stream": false,
    "outcome": "success"
  }
}

The truncated flag is only written (as true) when a bounded stream buffer dropped older chunks.

Stream response aggregation

When a stream completes, the buffered raw chunks are kept and folded into a single aggregated response object written to the record. This mirrors the non-stream response contract so batch analysis can treat stream and non-stream records uniformly via response:

{
  "request": { "body": { "model": "qwen3.6-27b" } },
  "chunks": [
    { "type": "start" },
    { "type": "reasoning-delta", "text": "The user is" },
    { "type": "text-delta", "text": "Hello world" },
    { "type": "finish", "finishReason": "stop", "totalUsage": { "inputTokens": 546, "outputTokens": 66, "totalTokens": 612 } }
  ],
  "response": {
    "text": "Hello world",
    "reasoning": "The user is",
    "finishReason": "stop",
    "usage": { "inputTokens": 546, "outputTokens": 66, "totalTokens": 612 },
    "model": "Qwen3.6-27B"
  },
  "_meta": { "stream": true, "outcome": "success" }
}

Aggregation reads chunk fields generically and is designed for AI-SDK-style typed stream parts, so the logger stays decoupled from any provider protocol. The following parts are aggregated:

| Part type | Aggregates to | | --- | --- | | text-delta | Concatenated into response.text | | reasoning-delta | Concatenated into response.reasoning | | finish-step | usage (preferred) and model from response.modelId | | finish | totalUsage (fallback) and finishReason | | plain chunk { content } / { text } | Concatenated into response.text |

Usage is normalized to { inputTokens, outputTokens, totalTokens }; when a finish-step chunk carries a nested raw payload (e.g. prompt_tokens / completion_tokens / total_tokens), it is preserved as usage.raw. finishReason prefers the terminal finish part and falls back to finish-step.

response is omitted (and only chunks written) when a stream exposes no text, reasoning, usage, or model — e.g. control or tool-only streams. Truncation still applies to the buffered chunks; the aggregated response is built from the retained chunks and the _meta.truncated flag is set so consumers know the result may be partial.

The logger does not calculate usage, token counts, cost, or TPS on its own. When a provider includes usage on the terminal stream parts, it is captured as-is; otherwise usage is absent. Batch analysis should still handle missing or partial usage.

LLM Call Environment Variables

The switch and output directory are deployment-controlled:

| Variable | Description | Default | | --- | --- | --- | | LOG_LLM_CALL_ENABLED | Enable or disable LLM call files | false when unset | | LOG_LLM_CALL_DIR | Output directory; required when enabled | unset | | LOG_LLM_CALL_LATEST | symlink or none for the latest pointer | symlink | | LOG_LLM_CALL_MAX_BUFFERED | Max buffered chunks per stream (0 = unbounded) | unbounded |

When LOG_LLM_CALL_ENABLED=true and LOG_LLM_CALL_DIR is missing or empty, parseLLMCallLifecycleOptionsEnv() throws immediately:

LLM call logging is enabled, but LOG_LLM_CALL_DIR is not configured.
Set LOG_LLM_CALL_DIR to a writable directory, or set
LOG_LLM_CALL_ENABLED=false to disable LLM call logging.

When disabled, record() and complete() are no-ops, and stream() directly passes through the source iterator without buffering or creating files.

Request headers are recorded as supplied. Callers should redact sensitive values such as Authorization before passing requests to the controller.

License

MIT