mohen
v1.3.1
Published
Unified request/response logger for Express and tRPC with SSE support (墨痕 - ink trace)
Maintainers
Readme
Features
- Single file logging for both Express and tRPC
- JSON lines format (one JSON object per line)
- SSE streaming support with chunk aggregation
- Arbitrary metadata attachment
- Automatic field redaction (passwords, tokens, etc.)
- File size management with automatic truncation
Installation
npm install mohen
# or
pnpm add mohenQuick Start
import express from 'express';
import { createLogger } from 'mohen';
const logger = createLogger('./logs/app.log');
const app = express();
app.use(express.json());
app.use(logger.express());
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000);That's it. All requests and responses are now logged to ./logs/app.log.
Usage
Express
import { createLogger, attachMetadata } from 'mohen';
const logger = createLogger('./logs/app.log', {
maxSizeBytes: 10 * 1024 * 1024, // 10MB max, then truncate to 25%
redact: ['password', 'token', 'secret'],
});
app.use(logger.express());
// Attach custom metadata to any request
app.get('/api/users/:id', (req, res) => {
attachMetadata(req, {
userId: req.params.id,
source: 'user-service',
cacheHit: false,
});
res.json({ id: req.params.id, name: 'John Doe' });
});tRPC
import { initTRPC } from '@trpc/server';
import { createLogger, attachTrpcMetadata } from 'mohen';
interface Context {
logMetadata?: Record<string, unknown>;
}
const logger = createLogger('./logs/app.log');
const t = initTRPC.context<Context>().create();
const loggedProcedure = t.procedure.use(logger.trpc<Context>());
const appRouter = t.router({
getUser: loggedProcedure
.input((val: unknown) => val as { id: string })
.query(({ input, ctx }) => {
// Attach custom metadata
attachTrpcMetadata(ctx, {
userId: input.id,
source: 'user-service',
});
return { id: input.id, name: 'John Doe' };
}),
});SSE Streaming
SSE responses are automatically detected and all chunks are aggregated into a single log entry:
app.get('/api/stream', (req, res) => {
attachMetadata(req, { streamType: 'events' });
res.setHeader('Content-Type', 'text/event-stream');
res.write(`data: ${JSON.stringify({ count: 1 })}\n\n`);
res.write(`data: ${JSON.stringify({ count: 2 })}\n\n`);
res.end();
});Log output:
{
"type": "http",
"path": "/api/stream",
"response": {
"streaming": true,
"chunks": [{"count": 1}, {"count": 2}]
},
"metadata": {"streamType": "events"}
}Configuration Options
createLogger(filePath, {
maxSizeBytes: 10 * 1024 * 1024, // Max file size before truncation (default: 10MB)
includeHeaders: false, // Log request headers (default: false)
redact: ['password', 'token'], // Fields to redact (default: password, token, authorization, cookie)
ignorePaths: ['/health', '/health/*', '/metrics'], // Paths to skip logging (supports wildcards)
includePaths: ['/api/*'], // Only log these paths (supports wildcards)
});Path Filtering
Use ignorePaths to skip noisy endpoints like health checks:
const logger = createLogger('./logs/app.log', {
ignorePaths: ['/health', '/health/*', '/metrics', '/favicon.ico'],
});Or use includePaths to only log specific routes:
const logger = createLogger('./logs/app.log', {
includePaths: ['/api/*', '/trpc/*'],
});Wildcard patterns:
/health- matches exactly/health/health/*- matches/health/live,/health/ready, etc./api/*- matches any path starting with/api/
Log Format
Each line is a JSON object with the following structure:
{
"timestamp": "2024-01-15T10:30:00.000Z",
"requestId": "m1abc123-xyz789",
"type": "http",
"method": "POST",
"path": "/api/users",
"statusCode": 200,
"duration": 45,
"request": {
"body": {"name": "John", "password": "[REDACTED]"},
"query": {}
},
"response": {
"body": {"id": 1, "name": "John"},
"streaming": false
},
"metadata": {
"userId": "123",
"source": "signup-flow"
}
}For SSE streaming responses with text-delta events (like LLM responses), the text is automatically aggregated:
{
"type": "http",
"path": "/api/chat",
"response": {
"streaming": true,
"chunks": [{"type": "start"}, {"type": "text-delta", "delta": "Hello"}, ...],
"text": "Hello world! This is the complete aggregated response."
}
}File Size Management
When the log file exceeds maxSizeBytes, the oldest 75% of log entries are removed, keeping the most recent 25%. This happens automatically before each write.
API Reference
createLogger(filePath, options?)
Creates a logger instance.
Returns:
express()- Express middleware functiontrpc<TContext>()- tRPC middleware functionwrite(entry)- Direct write access for custom logging
attachMetadata(req, metadata)
Attach arbitrary metadata to an Express request's log entry.
attachMetadata(req, { userId: '123', feature: 'checkout' });attachTrpcMetadata(ctx, metadata)
Attach arbitrary metadata to a tRPC procedure's log entry.
attachTrpcMetadata(ctx, { userId: '123', feature: 'checkout' });License
MIT
