Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions packages/logger-middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ You can use the `format` option to customize the log format. The following token
- `%date` - Date and time in Apache/nginx format (dd/Mon/yyyy:HH:mm:ss ±zzzz)
- `%dateISO` - Date and time in ISO format
- `%duration` - Request duration in milliseconds
- `%durationPretty` - Request duration in a human-readable format (e.g., `1.20s`, `120ms`)
- `%contentLength` - Response Content-Length header
- `%contentLengthPretty` - Response Content-Length in a human-readable format (e.g., `1.2 kB`, `120 B`)
- `%contentType` - Response Content-Type header
- `%host` - Request URL host
- `%hostname` - Request URL hostname
Expand All @@ -50,11 +52,11 @@ You can use the `format` option to customize the log format. The following token
let router = createRouter({
middleware: [
logger({
format: '%method %path - %status (%duration ms)',
format: '%method %path - %status (%durationPretty) %contentLengthPretty',
}),
],
})
// Logs: GET /users/123 - 200 (42 ms)
// Logs: GET /users/123 - 200 (42ms) 1.2 kB
```

For Apache-style combined log format, you can use the following format:
Expand All @@ -69,6 +71,28 @@ let router = createRouter({
})
```

### Colorized Output

You can enable colorized output by setting the `colors` option to `true`. This is useful for development environments to improve readability. Colorization is automatically disabled in environments that don't support it (e.g., non-TTY terminals or when the `NO_COLOR` environment variable is set).

```ts
let router = createRouter({
middleware: [
logger({
colors: true,
}),
],
})
```

When `colors` is enabled, the following tokens will be color-coded:
- `%method`
- `%status`
- `%duration`
- `%durationPretty`
- `%contentLength`
- `%contentLengthPretty`

### Custom Logger

You can use a custom logger to write logs to a file or other stream.
Expand Down
153 changes: 153 additions & 0 deletions packages/logger-middleware/src/lib/colorizer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { Colorizer } from './colorizer.ts'

const RESET = '\x1b[0m'
const GREEN = '\x1b[32m'
const CYAN = '\x1b[36m'
const YELLOW = '\x1b[33m'
const RED = '\x1b[31m'
const MAGENTA = '\x1b[35m'

describe('Colorizer', () => {
describe('with colors disabled', () => {
let colorizer = new Colorizer(false)

it('status returns plain code', () => {
assert.strictEqual(colorizer.status(200), '200')
assert.strictEqual(colorizer.status(404), '404')
assert.strictEqual(colorizer.status(500), '500')
})

it('method returns plain method', () => {
assert.strictEqual(colorizer.method('GET'), 'GET')
assert.strictEqual(colorizer.method('POST'), 'POST')
})

it('duration returns plain value', () => {
assert.strictEqual(colorizer.duration(50, '50ms'), '50ms')
assert.strictEqual(colorizer.duration(1500, '1.5s'), '1.5s')
})

it('contentLength returns plain value', () => {
assert.strictEqual(colorizer.contentLength(1024, '1KB'), '1KB')
assert.strictEqual(colorizer.contentLength(undefined, 'N/A'), 'N/A')
})
})

describe('status method', () => {
let colorizer = new Colorizer(true)

it('handles 2xx codes', () => {
let result = colorizer.status(200)
assert.ok(result === '200' || result === `${GREEN}200${RESET}`)
})

it('handles 3xx codes', () => {
let result = colorizer.status(302)
assert.ok(result === '302' || result === `${CYAN}302${RESET}`)
})

it('handles 4xx codes', () => {
let result = colorizer.status(404)
assert.ok(result === '404' || result === `${RED}404${RESET}`)
})

it('handles 5xx codes', () => {
let result = colorizer.status(500)
assert.ok(result === '500' || result === `${MAGENTA}500${RESET}`)
})

it('handles 1xx codes without color', () => {
assert.strictEqual(colorizer.status(100), '100')
})
})

describe('method colorization', () => {
let colorizer = new Colorizer(true)

it('handles GET', () => {
let result = colorizer.method('GET')
assert.ok(result === 'GET' || result === `${GREEN}GET${RESET}`)
})

it('handles POST', () => {
let result = colorizer.method('POST')
assert.ok(result === 'POST' || result === `${CYAN}POST${RESET}`)
})

it('handles PUT and PATCH', () => {
let put = colorizer.method('PUT')
let patch = colorizer.method('PATCH')
assert.ok(put === 'PUT' || put === `${YELLOW}PUT${RESET}`)
assert.ok(patch === 'PATCH' || patch === `${YELLOW}PATCH${RESET}`)
})

it('handles DELETE', () => {
let result = colorizer.method('DELETE')
assert.ok(result === 'DELETE' || result === `${RED}DELETE${RESET}`)
})

it('handles HEAD and OPTIONS', () => {
let head = colorizer.method('HEAD')
let options = colorizer.method('OPTIONS')
assert.ok(head === 'HEAD' || head === `${MAGENTA}HEAD${RESET}`)
assert.ok(options === 'OPTIONS' || options === `${MAGENTA}OPTIONS${RESET}`)
})

it('handles unknown methods', () => {
assert.strictEqual(colorizer.method('UNKNOWN'), 'UNKNOWN')
})
})

describe('duration colorization', () => {
let colorizer = new Colorizer(true)

it('handles fast durations', () => {
let result = colorizer.duration(50, '50ms')
assert.ok(result === '50ms' || result === `${GREEN}50ms${RESET}`)
})

it('handles medium durations', () => {
let result = colorizer.duration(150, '150ms')
assert.ok(result === '150ms' || result === `${YELLOW}150ms${RESET}`)
})

it('handles slow durations', () => {
let result = colorizer.duration(600, '600ms')
assert.ok(result === '600ms' || result === `${MAGENTA}600ms${RESET}`)
})

it('handles very slow durations', () => {
let result = colorizer.duration(1500, '1.5s')
assert.ok(result === '1.5s' || result === `${RED}1.5s${RESET}`)
})
})

describe('contentLength colorization', () => {
let colorizer = new Colorizer(true)

it('handles small sizes', () => {
assert.strictEqual(colorizer.contentLength(500, '500B'), '500B')
})

it('handles KB sizes', () => {
let result = colorizer.contentLength(2000, '2KB')
assert.ok(result === '2KB' || result === `${CYAN}2KB${RESET}`)
})

it('handles 100KB+ sizes', () => {
let result = colorizer.contentLength(150000, '150KB')
assert.ok(result === '150KB' || result === `${YELLOW}150KB${RESET}`)
})

it('handles MB+ sizes', () => {
let result = colorizer.contentLength(2000000, '2MB')
assert.ok(result === '2MB' || result === `${RED}2MB${RESET}`)
})

it('handles undefined', () => {
assert.strictEqual(colorizer.contentLength(undefined, 'N/A'), 'N/A')
})
})
})
68 changes: 68 additions & 0 deletions packages/logger-middleware/src/lib/colorizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const RESET = '\x1b[0m'
const GREEN = '\x1b[32m'
const CYAN = '\x1b[36m'
const YELLOW = '\x1b[33m'
const RED = '\x1b[31m'
const MAGENTA = '\x1b[35m'

export class Colorizer {
readonly #enabled?: boolean

constructor(colors?: boolean) {
this.#enabled = colors
}

#colorize(text: string, color: string): string {
return this.#enabled ? `${color}${text}${RESET}` : text
}

status(code: number): string {
let value = String(code)
if (!this.#enabled) return value
if (code >= 500) return this.#colorize(value, MAGENTA)
if (code >= 400) return this.#colorize(value, RED)
if (code >= 300) return this.#colorize(value, CYAN)
if (code >= 200) return this.#colorize(value, GREEN)
return value
}

method(method: string): string {
if (!this.#enabled) return method
switch (method.toUpperCase()) {
case 'GET':
return this.#colorize(method, GREEN)
case 'POST':
return this.#colorize(method, CYAN)
case 'PUT':
case 'PATCH':
return this.#colorize(method, YELLOW)
case 'DELETE':
return this.#colorize(method, RED)
case 'HEAD':
case 'OPTIONS':
return this.#colorize(method, MAGENTA)
default:
return method
}
}

duration(ms: number, prettyValue: string): string {
if (!this.#enabled) return prettyValue
if (ms >= 1000) return this.#colorize(prettyValue, RED)
if (ms >= 500) return this.#colorize(prettyValue, MAGENTA)
if (ms >= 100) return this.#colorize(prettyValue, YELLOW)
return this.#colorize(prettyValue, GREEN)
}

contentLength(bytes: number | undefined, prettyValue: string): string {
let ONE_MB = 1024 * 1024
let ONE_HUNDRED_KB = 100 * 1024
let ONE_KB = 1024

if (!this.#enabled || bytes === undefined) return prettyValue
if (bytes >= ONE_MB) return this.#colorize(prettyValue, RED)
if (bytes >= ONE_HUNDRED_KB) return this.#colorize(prettyValue, YELLOW)
if (bytes >= ONE_KB) return this.#colorize(prettyValue, CYAN)
return prettyValue
}
}
Loading