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

react-request-logger

v0.1.0

Published

Zero-dependency HTTP request logger for fetch and axios. Logs method, url, request/response bodies, status, and duration.

Readme

request-logger

Zero-dependency HTTP request logger for fetch and axios. Logs the method, url, request/response bodies, status, and duration — with secret redaction and body truncation built in.

Works in the browser (works with globalThis.fetch) and in Node 18+ (which has native fetch).

Features

  • Wraps fetch — time it, capture request + response, never blocks the actual request.
  • Axios support — one-line interceptor attachment.
  • Redaction — automatically masks secrets (authorization, password, token, x-api-key, ...) in headers and bodies.
  • Truncation — caps huge bodies so your logs stay readable.
  • Levelsdebug | info | warn | error with a minLevel filter.
  • Custom sink — pipe entries anywhere with subscribe/onLog (console, file, server, your UI).
  • In-memory history — keep the last N entries, page through them, and export to CSV or Excel.
  • Batch sink — buffer entries and flush them to any database / ingest endpoint with retry + backoff.
  • Ready-made adapterscreateMysqlTransport, createPostgresTransport, createHttpTransport, createJsonlTransport with auto schema.
  • React component — optional <RequestLog /> panel with toolbar, card/table views, filter/sort, pagination, and a detail modal.
  • Zero dependencies — no runtime deps at all.

Install

npm install request-logger

Usage — fetch

import { createRequestLogger } from 'request-logger'

const logger = createRequestLogger({
  redact: ['authorization', 'x-api-key'],
  onLog: (entry) => console.log(entry),
})

// Use logger.fetch everywhere you'd use fetch
const res = await logger.fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer sk-1234' },
  body: JSON.stringify({ name: 'Ada' }),
})

The onLog callback receives an HttpEntry:

{
  id: 'k3f9d2',
  timestamp: '2026-08-01T11:00:00.000Z',
  level: 'info',
  method: 'POST',
  url: 'https://api.example.com/users',
  status: 201,
  statusText: 'Created',
  durationMs: 143,
  request: {
    headers: { 'content-type': 'application/json', authorization: '***' },
    body: { name: 'Ada' },
  },
  response: {
    headers: { 'content-type': 'application/json' },
    body: { id: 42, name: 'Ada' },
  },
}

Multiple sinks / subscribe

const logger = createRequestLogger()
logger.subscribe((entry) => sendToServer(entry)) // e.g. POST /logs
logger.subscribe((entry) => console.log(entry))  // replaces the previous subscriber

Only one subscriber is kept — if you need multiple sinks, fan out inside one callback.

Manual entries

Log anything, not just HTTP:

logger.log({
  level: 'error',
  method: 'SYSTEM',
  url: 'db.query',
  durationMs: 12,
  error: new Error('timeout'),
})

Export to CSV / Excel

Enable history to keep a rolling buffer of entries, then export or page through them:

const logger = createRequestLogger({ history: 100 }) // keep last 100 entries

// In the browser — downloads request-log-<timestamp>.csv / .xls
logger.export('csv')
logger.export('xls', 'my-report.xls') // optional filename

// Pagination — page 1 is the most recent entries, newest first
const page = logger.getEntries({ page: 1, pageSize: 25 })
const total = logger.getCount()

const all = logger.getEntries() // full copy of the buffer, chronological
logger.clear()                  // empty the buffer

logger.export(format, filename?) only works where Blob/URL.createObjectURL exist (the browser, or Node 18+ with those globals).

Standalone helpers are also exported:

import { exportEntries, downloadEntries } from 'request-logger'

const blob = exportEntries(entries, 'csv') // Blob — 'csv' | 'xls'
downloadEntries(entries, { format: 'xls', filename: 'logs.xls' })

Usage — axios

import axios from 'axios'
import { attachAxiosLogger } from 'request-logger'

const logger = attachAxiosLogger(axios, { onLog: (entry) => console.log(entry) })

await axios.get('https://api.example.com/users')

Attaching mutates the instance's interceptors (request timing + response/error logging). It returns the same RequestLogger object, so subscribe, log, and setEnabled work too. axios is not a dependency of this package — pass in your own instance.

Persisting logs — batch sink

Every entry is emitted in-memory only. For production, stream them to a database with the built-in batch sink: it buffers entries, flushes on a size/time threshold, and retries with exponential backoff on failure. Pass any transport — a DB insert, an ingest endpoint, S3, etc. Use one of the ready-made adapters or write your own:

import { createRequestLogger } from 'request-logger'

const logger = createRequestLogger({
  history: 200, // keep last 200 in memory for the UI / export
  batch: {
    maxSize: 100,        // flush when 100 entries accumulate
    intervalMs: 5000,    // or every 5s, whichever comes first
    retries: 3,          // retry attempts per batch (exponential backoff)
    retryDelayMs: 1000,  // base delay; attempts wait 1s, 2s, 4s
    transport: async (entries) => {
      await fetch('https://logs.mycompany.com/ingest', {
        method: 'POST',
        body: JSON.stringify(entries),
      })
    },
    onError: (error, entries) => {
      // last retry failed — write to a fallback file/queue, or drop
      console.error('dropped batch', error)
    },
  },
})

logger.flush() flushes the pending buffer immediately (handy on process shutdown). The standalone createBatchSink(options) returns a { push, flush, size, start, stop } object if you want to manage the sink yourself.

Ready-made adapters

Don't write the SQL yourself — the package ships transport factories that take your DB client and handle schema + insert. Zero extra dependencies; pass in your own driver.

MySQL

import mysql from 'mysql2/promise'
import { createRequestLogger, createMysqlTransport } from 'request-logger'

const pool = mysql.createPool({ /* host, user, password, database */ })

const logger = createRequestLogger({
  batch: {
    maxSize: 100,
    intervalMs: 5000,
    transport: createMysqlTransport(pool), // table 'api_logs' auto-created on first flush
  },
})

const res = await logger.fetch('https://api.example.com/users')

createMysqlTransport(client, options?):

| Option | Type | Default | Description | | ------------- | --------- | ----------- | -------------------------------------- | | client | { query } | — | Any mysql2 pool/connection (mysql2/promise). | | table | string | 'api_logs'| Table name. | | createTable | boolean | true | Run CREATE TABLE IF NOT EXISTS once. | | chunkSize | number | 500 | Max rows per INSERT. |

Postgres

import { Client } from 'pg'
import { createRequestLogger, createPostgresTransport } from 'request-logger'

const client = new Client({ /* host, user, password, database */ })
await client.connect()

const logger = createRequestLogger({
  batch: {
    transport: createPostgresTransport(client, { table: 'api_logs' }),
  },
})

HTTP ingest (ClickHouse, Elasticsearch, your own API)

import { createRequestLogger, createHttpTransport } from 'request-logger'

const logger = createRequestLogger({
  batch: {
    transport: createHttpTransport({
      url: 'https://logs.mycompany.com/ingest',
      headers: { Authorization: 'Bearer token' },
    }),
  },
})

For ClickHouse's HTTP interface, add a transform that returns JSON lines:

transport: createHttpTransport({
  url: 'https://clickhouse.example.com:8443/?database=logs&query=' +
    encodeURIComponent('INSERT INTO api_logs FORMAT JSONEachRow'),
  transform: (entries) => entries.map((e) => JSON.stringify(e)).join('\n'),
})

JSONL / S3 / files

import { appendFile } from 'node:fs/promises'
import { createRequestLogger, createJsonlTransport } from 'request-logger'

const logger = createRequestLogger({
  batch: {
    transport: createJsonlTransport({ write: (lines) => appendFile('logs.jsonl', lines + '\n') }),
  },
})

Storing logs in MySQL (manual)

Prefer createMysqlTransport above — this section shows what it does under the hood, and is useful if you need a custom schema. Install mysql2 in your project and write entries in the transport. Run this once to create the table:

CREATE TABLE api_logs (
  id          BIGINT AUTO_INCREMENT PRIMARY KEY,
  timestamp   DATETIME(3) NOT NULL,
  level       VARCHAR(10) NOT NULL,
  method      VARCHAR(10) NOT NULL,
  url         TEXT NOT NULL,
  status      INT NULL,
  duration_ms INT NOT NULL,
  request     JSON NULL,
  response    JSON NULL,
  error       TEXT NULL,
  created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_timestamp (timestamp),
  INDEX idx_level (level)
);
import mysql from 'mysql2/promise'
import { createRequestLogger } from 'request-logger'

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'secret',
  database: 'app_logs',
  connectionLimit: 10,
})

const logger = createRequestLogger({
  batch: {
    maxSize: 100,
    intervalMs: 5000,
    transport: async (entries) => {
      const values = entries.map((e) => [
        new Date(e.timestamp),
        e.level,
        e.method,
        e.url,
        e.status ?? null,
        e.durationMs,
        e.request ? JSON.stringify(e.request) : null,
        e.response ? JSON.stringify(e.response) : null,
        e.error ? String(e.error) : null,
      ])
      await pool.query(
        `INSERT INTO api_logs
           (timestamp, level, method, url, status, duration_ms, request, response, error)
         VALUES ?`,
        [values],
      )
    },
  },
})

const res = await logger.fetch('https://api.example.com/users')
// rows land in api_logs in batches of 100 or every 5s

MySQL is Node-only (via mysql2). In the browser, use the transport to POST to your own ingest API instead of connecting to a database directly.

React component

A ready-to-use panel that renders a live feed of entries — toolbar (level filter, sort, date range, export), card/table view switcher, pagination, and a detail modal for headers/bodies. react is an optional peer dependency; import from the request-logger/react subpath:

import { createRequestLogger } from 'request-logger'
import { RequestLog } from 'request-logger/react'

const logger = createRequestLogger({ history: 200, includeHeaders: true })

export function App() {
  return <RequestLog logger={logger} title="API calls" />
}

The component subscribes to the logger and refreshes automatically on every new entry. Bring your own fetch calls — call logger.fetch(...) (or attach the axios interceptor) anywhere and the panel updates.

Props:

| Prop | Type | Default | Description | | -------------------- | ----------------------- | ---------- | -------------------------------------------- | | logger | RequestLogger | (required) | Logger instance to render and subscribe to. | | title | string | — | Optional heading shown above the toolbar. | | pageSize | number | 10 | Entries per page. | | label | string | 'entries'| Word used in the pager ("5 of 50 entries"). | | defaultTheme | 'dark' \| 'light' | 'dark' | Initial theme. | | enableThemeToggle | boolean | true | Show/hide the Light/Dark toggle. | | colors | Record<string,string> | — | CSS variables (e.g. { '--accent': '#f00' }). | | className | string | — | Extra class on the root element. |

The styles are bundled into the component's CSS (emitted as dist/react/style.css), so nothing extra needs to be imported.

Options

| Option | Type | Default | Description | | ----------------------- | -------------------- | ------------------------------------------- | ---------------------------------------------- | | enabled | boolean | true | When false, logger.fetch is a passthrough. | | minLevel | LogLevel | 'debug' | Only emit entries at or above this level. | | redact | string[] | see list below | Header/body keys to replace with ***. | | redactReplacement | string | '***' | Value used for redacted keys. | | maxBodyLength | number | 2000 | Max chars before a body is truncated. | | includeHeaders | boolean | true | Capture request + response headers. | | includeRequest | boolean | true | Capture request url/method/headers/body. | | includeResponseBody | boolean | true | Capture response bodies. | | onLog | (entry) => void | — | Called with every emitted HttpEntry. | | history | number | 0 (off) | Keep the last N entries for getEntries/getCount/export. | | batch | BatchSinkOptions | — | Stream entries to a database/endpoint. See below. | | now | () => Date | new Date | Custom clock (useful in tests). | | id | () => string | random base36 | Custom entry id generator. | | fetchImpl | typeof fetch | globalThis.fetch | Custom fetch implementation. |

Default redaction keys: authorization, cookie, set-cookie, x-api-key, x-auth-token, password, passwd, token, access_token, refresh_token, secret, api-key, apikey.

HttpEntry

| Field | Type | Description | | ------------ | ----------------------- | ------------------------------------ | | id | string | Unique entry id. | | timestamp | string | ISO timestamp. | | level | LogLevel | debug / info / warn / error. | | method | string | HTTP method (uppercase). | | url | string | Request url. | | status | number? | Response status code. | | statusText | string? | Response status text. | | durationMs | number | Total time from start to completion. | | request | { headers?, body? }? | Request headers/body (redacted). | | response | { headers?, body? }? | Response headers/body (redacted). | | error | unknown? | Error for failed/errored requests. |

Notes

  • logger.fetch never throws differently — errors still reject normally; they're just logged first.
  • Response bodies are read via response.clone(), so your code still consumes the response normally.
  • Node 18+: use logger.fetch directly. Older Node: pass fetchImpl (e.g. node-fetch).

License

MIT