@remix-run/logger-middleware 0.1.5 → 0.2.1

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/README.md CHANGED
@@ -7,6 +7,7 @@ HTTP request/response logging middleware for Remix. It logs request metadata and
7
7
  - **Request/Response Logging** - Logs method, path, status, and response metadata
8
8
  - **Token-Based Formatting** - Customize log output with built-in placeholders
9
9
  - **Structured Timing Data** - Includes request duration and timestamps
10
+ - **Colorized Output** - Highlights method, status, duration, and content length in TTY output
10
11
 
11
12
  ## Installation
12
13
 
@@ -73,6 +74,27 @@ let router = createRouter({
73
74
  })
74
75
  ```
75
76
 
77
+ ### Colorized Output
78
+
79
+ Logger output automatically uses ANSI colors for high-signal tokens when terminal color detection allows them. Set `colors` to `false` to disable colorized output or `true` to force it on. When the `process` global is defined, color detection respects `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, and TTY output streams.
80
+
81
+ ```ts
82
+ let router = createRouter({
83
+ middleware: [
84
+ logger({
85
+ colors: false,
86
+ }),
87
+ ],
88
+ })
89
+ ```
90
+
91
+ The following tokens are colorized when colors are enabled:
92
+
93
+ - `%method`
94
+ - `%status`
95
+ - `%duration`
96
+ - `%contentLength`
97
+
76
98
  ### Custom Logger
77
99
 
78
100
  You can use a custom logger to write logs to a file or other stream.
@@ -36,6 +36,23 @@ export interface LoggerOptions {
36
36
  * @default console.log
37
37
  */
38
38
  log?: (message: string) => void;
39
+ /**
40
+ * Enables ANSI colors for high-signal log tokens.
41
+ *
42
+ * By default, colors are enabled when terminal color detection allows them. Set this to `false`
43
+ * to opt out or `true` to force colors on. When the `process` global is defined, color
44
+ * detection respects `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, and TTY output streams.
45
+ *
46
+ * The following tokens are colorized when colors are enabled:
47
+ *
48
+ * - `%method`
49
+ * - `%status`
50
+ * - `%duration`
51
+ * - `%contentLength`
52
+ *
53
+ * @default undefined
54
+ */
55
+ colors?: boolean;
39
56
  }
40
57
  /**
41
58
  * Creates a middleware handler that logs various request/response info.
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEzD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,UAAU,CAoC9D"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAIzD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,UAAU,CA4C9D"}
@@ -1,3 +1,4 @@
1
+ import { createStyles } from '@remix-run/terminal';
1
2
  /**
2
3
  * Creates a middleware handler that logs various request/response info.
3
4
  *
@@ -5,20 +6,24 @@
5
6
  * @returns The logger middleware
6
7
  */
7
8
  export function logger(options = {}) {
8
- let { format = '[%date] %method %path %status %contentLength', log = console.log } = options;
9
+ let { colors, format = '[%date] %method %path %status %contentLength', log = console.log, } = options;
10
+ let colorizer = getColorizer(colors);
9
11
  return async ({ request, url }, next) => {
10
12
  let start = new Date();
11
13
  let response = await next();
12
14
  let end = new Date();
15
+ let duration = end.getTime() - start.getTime();
16
+ let contentLength = response.headers.get('Content-Length');
17
+ let contentLengthValue = parseContentLength(contentLength);
13
18
  let tokens = {
14
19
  date: () => formatApacheDate(start),
15
20
  dateISO: () => start.toISOString(),
16
- duration: () => String(end.getTime() - start.getTime()),
17
- contentLength: () => response.headers.get('Content-Length') ?? '-',
21
+ duration: () => colorizer.duration(duration),
22
+ contentLength: () => colorizer.contentLength(contentLength ?? '-', contentLengthValue),
18
23
  contentType: () => response.headers.get('Content-Type') ?? '-',
19
24
  host: () => url.host,
20
25
  hostname: () => url.hostname,
21
- method: () => request.method,
26
+ method: () => colorizer.method(request.method),
22
27
  path: () => url.pathname + url.search,
23
28
  pathname: () => url.pathname,
24
29
  port: () => url.port,
@@ -26,7 +31,7 @@ export function logger(options = {}) {
26
31
  query: () => url.search,
27
32
  referer: () => request.headers.get('Referer') ?? '-',
28
33
  search: () => url.search,
29
- status: () => String(response.status),
34
+ status: () => colorizer.status(response.status),
30
35
  statusText: () => response.statusText,
31
36
  url: () => url.href,
32
37
  userAgent: () => request.headers.get('User-Agent') ?? '-',
@@ -37,6 +42,74 @@ export function logger(options = {}) {
37
42
  };
38
43
  }
39
44
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
45
+ function getColorizer(option) {
46
+ let styles = createStyles({ colors: option });
47
+ return {
48
+ contentLength(value, bytes) {
49
+ if (!styles.enabled || bytes === undefined)
50
+ return value;
51
+ if (bytes >= 1024 * 1024)
52
+ return styles.red(value);
53
+ if (bytes >= 100 * 1024)
54
+ return styles.yellow(value);
55
+ if (bytes >= 1024)
56
+ return styles.cyan(value);
57
+ return value;
58
+ },
59
+ duration(ms) {
60
+ let value = String(ms);
61
+ if (!styles.enabled)
62
+ return value;
63
+ if (ms >= 1000)
64
+ return styles.red(value);
65
+ if (ms >= 500)
66
+ return styles.magenta(value);
67
+ if (ms >= 100)
68
+ return styles.yellow(value);
69
+ return styles.green(value);
70
+ },
71
+ method(method) {
72
+ if (!styles.enabled)
73
+ return method;
74
+ switch (method.toUpperCase()) {
75
+ case 'GET':
76
+ case 'HEAD':
77
+ return styles.green(method);
78
+ case 'POST':
79
+ return styles.cyan(method);
80
+ case 'PUT':
81
+ case 'PATCH':
82
+ return styles.yellow(method);
83
+ case 'DELETE':
84
+ return styles.red(method);
85
+ case 'OPTIONS':
86
+ return styles.magenta(method);
87
+ default:
88
+ return method;
89
+ }
90
+ },
91
+ status(status) {
92
+ let value = String(status);
93
+ if (!styles.enabled)
94
+ return value;
95
+ if (status >= 500)
96
+ return styles.red(value);
97
+ if (status >= 400)
98
+ return styles.yellow(value);
99
+ if (status >= 300)
100
+ return styles.cyan(value);
101
+ if (status >= 200)
102
+ return styles.green(value);
103
+ return value;
104
+ },
105
+ };
106
+ }
107
+ function parseContentLength(value) {
108
+ if (value === null)
109
+ return undefined;
110
+ let bytes = Number(value);
111
+ return Number.isSafeInteger(bytes) && bytes >= 0 ? bytes : undefined;
112
+ }
40
113
  /**
41
114
  * Formats a date in Apache/nginx log format: "dd/Mon/yyyy:HH:mm:ss ±zzzz"
42
115
  * Example: "23/Sep/2025:11:34:12 -0700"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/logger-middleware",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Middleware for logging HTTP requests and responses",
5
5
  "author": "Michael Jackson <mjijackson@gmail.com>",
6
6
  "license": "MIT",
@@ -28,12 +28,13 @@
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.6.0",
30
30
  "@typescript/native-preview": "7.0.0-dev.20251125.1",
31
- "@remix-run/fetch-router": "0.18.1",
32
- "@remix-run/assert": "0.1.0",
33
- "@remix-run/test": "0.1.0"
31
+ "@remix-run/assert": "0.2.0",
32
+ "@remix-run/fetch-router": "0.18.2",
33
+ "@remix-run/test": "0.3.0"
34
34
  },
35
35
  "dependencies": {
36
- "@remix-run/fetch-router": "^0.18.1"
36
+ "@remix-run/fetch-router": "^0.18.2",
37
+ "@remix-run/terminal": "^0.1.0"
37
38
  },
38
39
  "keywords": [
39
40
  "fetch",
@@ -47,6 +48,7 @@
47
48
  "build": "tsgo -p tsconfig.build.json",
48
49
  "clean": "git clean -fdX",
49
50
  "test": "remix-test",
51
+ "test:bun": "bun x --bun remix-test",
50
52
  "typecheck": "tsgo --noEmit"
51
53
  }
52
54
  }
package/src/lib/logger.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { Middleware } from '@remix-run/fetch-router'
2
2
 
3
+ import { createStyles } from '@remix-run/terminal'
4
+
3
5
  /**
4
6
  * Options for the {@link logger} middleware.
5
7
  */
@@ -37,6 +39,23 @@ export interface LoggerOptions {
37
39
  * @default console.log
38
40
  */
39
41
  log?: (message: string) => void
42
+ /**
43
+ * Enables ANSI colors for high-signal log tokens.
44
+ *
45
+ * By default, colors are enabled when terminal color detection allows them. Set this to `false`
46
+ * to opt out or `true` to force colors on. When the `process` global is defined, color
47
+ * detection respects `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, and TTY output streams.
48
+ *
49
+ * The following tokens are colorized when colors are enabled:
50
+ *
51
+ * - `%method`
52
+ * - `%status`
53
+ * - `%duration`
54
+ * - `%contentLength`
55
+ *
56
+ * @default undefined
57
+ */
58
+ colors?: boolean
40
59
  }
41
60
 
42
61
  /**
@@ -46,22 +65,30 @@ export interface LoggerOptions {
46
65
  * @returns The logger middleware
47
66
  */
48
67
  export function logger(options: LoggerOptions = {}): Middleware {
49
- let { format = '[%date] %method %path %status %contentLength', log = console.log } = options
68
+ let {
69
+ colors,
70
+ format = '[%date] %method %path %status %contentLength',
71
+ log = console.log,
72
+ } = options
73
+ let colorizer = getColorizer(colors)
50
74
 
51
75
  return async ({ request, url }, next) => {
52
76
  let start = new Date()
53
77
  let response = await next()
54
78
  let end = new Date()
79
+ let duration = end.getTime() - start.getTime()
80
+ let contentLength = response.headers.get('Content-Length')
81
+ let contentLengthValue = parseContentLength(contentLength)
55
82
 
56
83
  let tokens: Record<string, () => string> = {
57
84
  date: () => formatApacheDate(start),
58
85
  dateISO: () => start.toISOString(),
59
- duration: () => String(end.getTime() - start.getTime()),
60
- contentLength: () => response.headers.get('Content-Length') ?? '-',
86
+ duration: () => colorizer.duration(duration),
87
+ contentLength: () => colorizer.contentLength(contentLength ?? '-', contentLengthValue),
61
88
  contentType: () => response.headers.get('Content-Type') ?? '-',
62
89
  host: () => url.host,
63
90
  hostname: () => url.hostname,
64
- method: () => request.method,
91
+ method: () => colorizer.method(request.method),
65
92
  path: () => url.pathname + url.search,
66
93
  pathname: () => url.pathname,
67
94
  port: () => url.port,
@@ -69,7 +96,7 @@ export function logger(options: LoggerOptions = {}): Middleware {
69
96
  query: () => url.search,
70
97
  referer: () => request.headers.get('Referer') ?? '-',
71
98
  search: () => url.search,
72
- status: () => String(response.status),
99
+ status: () => colorizer.status(response.status),
73
100
  statusText: () => response.statusText,
74
101
  url: () => url.href,
75
102
  userAgent: () => request.headers.get('User-Agent') ?? '-',
@@ -85,6 +112,71 @@ export function logger(options: LoggerOptions = {}): Middleware {
85
112
 
86
113
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
87
114
 
115
+ interface Colorizer {
116
+ contentLength(value: string, bytes: number | undefined): string
117
+ duration(ms: number): string
118
+ method(method: string): string
119
+ status(status: number): string
120
+ }
121
+
122
+ function getColorizer(option: boolean | undefined): Colorizer {
123
+ let styles = createStyles({ colors: option })
124
+
125
+ return {
126
+ contentLength(value, bytes) {
127
+ if (!styles.enabled || bytes === undefined) return value
128
+ if (bytes >= 1024 * 1024) return styles.red(value)
129
+ if (bytes >= 100 * 1024) return styles.yellow(value)
130
+ if (bytes >= 1024) return styles.cyan(value)
131
+ return value
132
+ },
133
+ duration(ms) {
134
+ let value = String(ms)
135
+ if (!styles.enabled) return value
136
+ if (ms >= 1000) return styles.red(value)
137
+ if (ms >= 500) return styles.magenta(value)
138
+ if (ms >= 100) return styles.yellow(value)
139
+ return styles.green(value)
140
+ },
141
+ method(method) {
142
+ if (!styles.enabled) return method
143
+
144
+ switch (method.toUpperCase()) {
145
+ case 'GET':
146
+ case 'HEAD':
147
+ return styles.green(method)
148
+ case 'POST':
149
+ return styles.cyan(method)
150
+ case 'PUT':
151
+ case 'PATCH':
152
+ return styles.yellow(method)
153
+ case 'DELETE':
154
+ return styles.red(method)
155
+ case 'OPTIONS':
156
+ return styles.magenta(method)
157
+ default:
158
+ return method
159
+ }
160
+ },
161
+ status(status) {
162
+ let value = String(status)
163
+ if (!styles.enabled) return value
164
+ if (status >= 500) return styles.red(value)
165
+ if (status >= 400) return styles.yellow(value)
166
+ if (status >= 300) return styles.cyan(value)
167
+ if (status >= 200) return styles.green(value)
168
+ return value
169
+ },
170
+ }
171
+ }
172
+
173
+ function parseContentLength(value: string | null): number | undefined {
174
+ if (value === null) return undefined
175
+
176
+ let bytes = Number(value)
177
+ return Number.isSafeInteger(bytes) && bytes >= 0 ? bytes : undefined
178
+ }
179
+
88
180
  /**
89
181
  * Formats a date in Apache/nginx log format: "dd/Mon/yyyy:HH:mm:ss ±zzzz"
90
182
  * Example: "23/Sep/2025:11:34:12 -0700"