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.
Maintainers
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.
- Levels —
debug | info | warn | errorwith aminLevelfilter. - 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 adapters —
createMysqlTransport,createPostgresTransport,createHttpTransport,createJsonlTransportwith 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-loggerUsage — 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 subscriberOnly 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 bufferlogger.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 5sMySQL 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.fetchnever 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.fetchdirectly. Older Node: passfetchImpl(e.g.node-fetch).
License
MIT
