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.
@@ -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
@@ -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;