mohen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +40 -0
- package/.github/workflows/publish.yml +38 -0
- package/README.md +198 -0
- package/dist/logger.d.ts +72 -0
- package/dist/logger.js +389 -0
- package/example/usage.ts +161 -0
- package/logo.png +0 -0
- package/package.json +40 -0
- package/src/logger.ts +454 -0
- package/test/logger.test.ts +499 -0
- package/test/test-server.ts +52 -0
- package/tsconfig.json +29 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master, main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [20.x, 22.x]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: ${{ matrix.node-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install pnpm
|
|
26
|
+
uses: pnpm/action-setup@v2
|
|
27
|
+
with:
|
|
28
|
+
version: 9
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: pnpm install
|
|
32
|
+
|
|
33
|
+
- name: Build
|
|
34
|
+
run: pnpm build
|
|
35
|
+
|
|
36
|
+
- name: Check TypeScript types
|
|
37
|
+
run: pnpm exec tsc --noEmit
|
|
38
|
+
|
|
39
|
+
- name: Run tests
|
|
40
|
+
run: pnpm test
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Use Node.js 20.x
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20.x
|
|
19
|
+
registry-url: 'https://registry.npmjs.org'
|
|
20
|
+
|
|
21
|
+
- name: Install pnpm
|
|
22
|
+
uses: pnpm/action-setup@v2
|
|
23
|
+
with:
|
|
24
|
+
version: 9
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: pnpm install
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: pnpm build
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: pnpm test
|
|
34
|
+
|
|
35
|
+
- name: Publish to npm
|
|
36
|
+
run: npm publish --access public
|
|
37
|
+
env:
|
|
38
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# mohen 墨痕
|
|
2
|
+
|
|
3
|
+
A simple, unified request/response logger for Express and tRPC that writes to a single file with JSON lines format.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Single file logging for both Express and tRPC
|
|
8
|
+
- JSON lines format (one JSON object per line)
|
|
9
|
+
- SSE streaming support with chunk aggregation
|
|
10
|
+
- Arbitrary metadata attachment
|
|
11
|
+
- Automatic field redaction (passwords, tokens, etc.)
|
|
12
|
+
- File size management with automatic truncation
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install mohen
|
|
18
|
+
# or
|
|
19
|
+
pnpm add mohen
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import express from 'express';
|
|
26
|
+
import { createLogger } from 'mohen';
|
|
27
|
+
|
|
28
|
+
const logger = createLogger('./logs/app.log');
|
|
29
|
+
|
|
30
|
+
const app = express();
|
|
31
|
+
app.use(express.json());
|
|
32
|
+
app.use(logger.express());
|
|
33
|
+
|
|
34
|
+
app.get('/api/health', (req, res) => {
|
|
35
|
+
res.json({ status: 'ok' });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.listen(3000);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. All requests and responses are now logged to `./logs/app.log`.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Express
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { createLogger, attachMetadata } from 'mohen';
|
|
49
|
+
|
|
50
|
+
const logger = createLogger('./logs/app.log', {
|
|
51
|
+
maxSizeBytes: 10 * 1024 * 1024, // 10MB max, then truncate to 25%
|
|
52
|
+
redact: ['password', 'token', 'secret'],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
app.use(logger.express());
|
|
56
|
+
|
|
57
|
+
// Attach custom metadata to any request
|
|
58
|
+
app.get('/api/users/:id', (req, res) => {
|
|
59
|
+
attachMetadata(req, {
|
|
60
|
+
userId: req.params.id,
|
|
61
|
+
source: 'user-service',
|
|
62
|
+
cacheHit: false,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
res.json({ id: req.params.id, name: 'John Doe' });
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### tRPC
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { initTRPC } from '@trpc/server';
|
|
73
|
+
import { createLogger, attachTrpcMetadata } from 'mohen';
|
|
74
|
+
|
|
75
|
+
interface Context {
|
|
76
|
+
logMetadata?: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const logger = createLogger('./logs/app.log');
|
|
80
|
+
const t = initTRPC.context<Context>().create();
|
|
81
|
+
|
|
82
|
+
const loggedProcedure = t.procedure.use(logger.trpc<Context>());
|
|
83
|
+
|
|
84
|
+
const appRouter = t.router({
|
|
85
|
+
getUser: loggedProcedure
|
|
86
|
+
.input((val: unknown) => val as { id: string })
|
|
87
|
+
.query(({ input, ctx }) => {
|
|
88
|
+
// Attach custom metadata
|
|
89
|
+
attachTrpcMetadata(ctx, {
|
|
90
|
+
userId: input.id,
|
|
91
|
+
source: 'user-service',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { id: input.id, name: 'John Doe' };
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### SSE Streaming
|
|
100
|
+
|
|
101
|
+
SSE responses are automatically detected and all chunks are aggregated into a single log entry:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
app.get('/api/stream', (req, res) => {
|
|
105
|
+
attachMetadata(req, { streamType: 'events' });
|
|
106
|
+
|
|
107
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
108
|
+
res.write(`data: ${JSON.stringify({ count: 1 })}\n\n`);
|
|
109
|
+
res.write(`data: ${JSON.stringify({ count: 2 })}\n\n`);
|
|
110
|
+
res.end();
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Log output:
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"type": "http",
|
|
118
|
+
"path": "/api/stream",
|
|
119
|
+
"response": {
|
|
120
|
+
"streaming": true,
|
|
121
|
+
"chunks": [{"count": 1}, {"count": 2}]
|
|
122
|
+
},
|
|
123
|
+
"metadata": {"streamType": "events"}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Configuration Options
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
createLogger(filePath, {
|
|
131
|
+
maxSizeBytes: 10 * 1024 * 1024, // Max file size before truncation (default: 10MB)
|
|
132
|
+
includeHeaders: false, // Log request headers (default: false)
|
|
133
|
+
redact: ['password', 'token'], // Fields to redact (default: password, token, authorization, cookie)
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Log Format
|
|
138
|
+
|
|
139
|
+
Each line is a JSON object with the following structure:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"timestamp": "2024-01-15T10:30:00.000Z",
|
|
144
|
+
"requestId": "m1abc123-xyz789",
|
|
145
|
+
"type": "http",
|
|
146
|
+
"method": "POST",
|
|
147
|
+
"path": "/api/users",
|
|
148
|
+
"statusCode": 200,
|
|
149
|
+
"duration": 45,
|
|
150
|
+
"request": {
|
|
151
|
+
"body": {"name": "John", "password": "[REDACTED]"},
|
|
152
|
+
"query": {}
|
|
153
|
+
},
|
|
154
|
+
"response": {
|
|
155
|
+
"body": {"id": 1, "name": "John"},
|
|
156
|
+
"streaming": false
|
|
157
|
+
},
|
|
158
|
+
"metadata": {
|
|
159
|
+
"userId": "123",
|
|
160
|
+
"source": "signup-flow"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## File Size Management
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
## API Reference
|
|
170
|
+
|
|
171
|
+
### `createLogger(filePath, options?)`
|
|
172
|
+
|
|
173
|
+
Creates a logger instance.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
- `express()` - Express middleware function
|
|
177
|
+
- `trpc<TContext>()` - tRPC middleware function
|
|
178
|
+
- `write(entry)` - Direct write access for custom logging
|
|
179
|
+
|
|
180
|
+
### `attachMetadata(req, metadata)`
|
|
181
|
+
|
|
182
|
+
Attach arbitrary metadata to an Express request's log entry.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
attachMetadata(req, { userId: '123', feature: 'checkout' });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `attachTrpcMetadata(ctx, metadata)`
|
|
189
|
+
|
|
190
|
+
Attach arbitrary metadata to a tRPC procedure's log entry.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
attachTrpcMetadata(ctx, { userId: '123', feature: 'checkout' });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
interface LogEntry {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
requestId: string;
|
|
5
|
+
type: 'http' | 'trpc';
|
|
6
|
+
method: string;
|
|
7
|
+
path: string;
|
|
8
|
+
statusCode?: number;
|
|
9
|
+
duration: number;
|
|
10
|
+
request?: {
|
|
11
|
+
body?: unknown;
|
|
12
|
+
query?: unknown;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
response?: {
|
|
16
|
+
body?: unknown;
|
|
17
|
+
streaming?: boolean;
|
|
18
|
+
chunks?: unknown[];
|
|
19
|
+
};
|
|
20
|
+
error?: {
|
|
21
|
+
message: string;
|
|
22
|
+
stack?: string;
|
|
23
|
+
};
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
interface LoggerOptions {
|
|
27
|
+
maxSizeBytes?: number;
|
|
28
|
+
includeHeaders?: boolean;
|
|
29
|
+
redact?: string[];
|
|
30
|
+
}
|
|
31
|
+
declare global {
|
|
32
|
+
namespace Express {
|
|
33
|
+
interface Request {
|
|
34
|
+
logMetadata?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export declare function createLogger(filePath: string, options?: LoggerOptions): {
|
|
39
|
+
/** Express middleware - use with app.use() */
|
|
40
|
+
express: () => (req: Request, res: Response, next: NextFunction) => void;
|
|
41
|
+
/** tRPC middleware - use with t.procedure.use() */
|
|
42
|
+
trpc: <TContext extends Record<string, unknown> = Record<string, unknown>>() => (opts: {
|
|
43
|
+
path: string;
|
|
44
|
+
type: "query" | "mutation" | "subscription";
|
|
45
|
+
input: unknown;
|
|
46
|
+
ctx: TContext & {
|
|
47
|
+
logMetadata?: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
next: () => Promise<{
|
|
50
|
+
ok: boolean;
|
|
51
|
+
data?: unknown;
|
|
52
|
+
error?: Error;
|
|
53
|
+
}>;
|
|
54
|
+
}) => Promise<{
|
|
55
|
+
ok: boolean;
|
|
56
|
+
data?: unknown;
|
|
57
|
+
error?: Error;
|
|
58
|
+
}>;
|
|
59
|
+
/** Direct write access for custom logging */
|
|
60
|
+
write: (entry: Partial<LogEntry>) => void;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Attach metadata to the current request log entry (Express)
|
|
64
|
+
*/
|
|
65
|
+
export declare function attachMetadata(req: Request, metadata: Record<string, unknown>): void;
|
|
66
|
+
/**
|
|
67
|
+
* Attach metadata to the current request log entry (tRPC)
|
|
68
|
+
*/
|
|
69
|
+
export declare function attachTrpcMetadata<TContext extends {
|
|
70
|
+
logMetadata?: Record<string, unknown>;
|
|
71
|
+
}>(ctx: TContext, metadata: Record<string, unknown>): void;
|
|
72
|
+
export default createLogger;
|