Skip to content

Conversation

@bossadizenith
Copy link

@bossadizenith bossadizenith commented Dec 5, 2025

Summary

Implements a first-class logger middleware for Elysia as proposed in #1599.

Features

  • Log HTTP method, path, status code, and response duration
  • Clean integration with Elysia's existing lifecycle hooks (derive, onBeforeHandle, onAfterHandle)
  • Minimal performance overhead
  • Zero dependencies
  • Fully typed with Elysia's typings

Usage

import { logger } from 'elysia/logger'

const app = new Elysia()
  .use(logger())
  .get('/', () => 'hello world')

Closes #1599

Summary by CodeRabbit

  • New Features

    • Introduced HTTP request logger middleware with customizable output, colored indicators for status codes and HTTP methods, and configurable path skip patterns.
  • Documentation

    • Added example demonstrating logger integration with a basic server setup.
  • Tests

    • Added comprehensive test coverage for logger functionality including error handling and path filtering.

✏️ Tip: You can customize this high-level summary in your review settings.

Implements a first-class logger middleware for Elysia with:
- HTTP method, path, status code, and response duration logging
- Clean integration with Elysia's lifecycle hooks
- Minimal performance overhead
- Zero dependencies
- Full TypeScript support

Closes elysiajs#1599
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

Walkthrough

This PR introduces a built-in HTTP request logger middleware for the Elysia framework. The implementation adds a customizable logger with support for custom output functions, pattern-based path skipping, colored terminal output, and request duration tracking. It includes comprehensive tests, example usage, and exports configuration.

Changes

Cohort / File(s) Summary
Logger Core Implementation
src/logger.ts
Implements customizable HTTP request logger middleware with LogEntry interface, LoggerOptions for configuration (output function, skip patterns, colored output), and logger factory. Supports wildcard and glob-like path patterns, colored status/method output, and duration formatting (μs/ms/s). Uses WeakMap for request timing tracking.
Example Usage
example/logger.ts
Demonstrates logger middleware integration with Elysia server setup, defining routes for GET /, GET /users/:id, POST /users, and GET /health with integrated logging.
Package Export Configuration
package.json
Adds "./logger" export entry with distribution paths for types, ES modules, and CommonJS bundles.
Test Suite
test/logger/logger.test.ts
Comprehensive test coverage for logger middleware: basic logging, skip behavior (exact paths, wildcards, glob patterns, predicates), error/status code handling, duration accuracy, multiple HTTP methods, route parameters, 404 handling, colored output, and plugin interactions.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Elysia Server
    participant Logger Middleware
    participant Route Handler
    participant Output Function

    Client->>Elysia Server: HTTP Request
    Elysia Server->>Logger Middleware: onBeforeHandle
    Logger Middleware->>Logger Middleware: Record start time (WeakMap)
    Logger Middleware->>Route Handler: Pass through
    Route Handler->>Elysia Server: Generate Response
    Elysia Server->>Logger Middleware: onAfterHandle
    Logger Middleware->>Logger Middleware: Compute duration<br/>Build LogEntry<br/>Check skip patterns
    alt Path not skipped
        Logger Middleware->>Output Function: Log entry (colored or plain)
        Output Function->>Output Function: Format & display
    end
    Logger Middleware->>Client: Return Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pattern matching logic in src/logger.ts for handling exact, wildcard (/users/*), and glob-like (/api/**) path patterns requires careful verification
  • WeakMap-based timing mechanism for tracking request start times and duration computation needs validation for edge cases and memory safety
  • Colored output helpers with status-code and HTTP-method-based color logic should be reviewed for correctness
  • Comprehensive test suite with numerous test cases covering skip behavior, patterns, duration accuracy, and plugin interactions needs thorough review to ensure complete coverage and correctness
  • Type exports and public API (LogEntry, LoggerFunction, LoggerOptions, logger) should be validated for consistency with Elysia patterns

Poem

🦊 A logger hops through the warren,
Tracking each request—no more sorrow!
Status codes gleam, durations ring true,
Pattern-matched paths skip on cue,
Colored logs dance through the night—
Elysia's debugging just got bright! ✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a built-in logger middleware to Elysia, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #1599: logs HTTP method, path, status, duration; integrates with Elysia lifecycle hooks; maintains zero dependencies; fully typed; provides intuitive API matching the proposed interface.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #1599 objectives: logger middleware implementation, types, tests, and package.json export. No unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bossadizenith
Copy link
Author

it's to mention that more than 50% of the code here was generated with AI however i crossed checked and made sure it doesn't cause harm.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/logger.ts (2)

89-93: Duration formatting edge case: sub-microsecond values.

When duration < 1 (i.e., < 1ms), you multiply by 1000 to get microseconds. However, if duration is extremely small (e.g., 0.0001ms), the result would be 0μs. Consider a floor of 1μs or showing decimal microseconds for accuracy.

 const formatDuration = (duration: number): string => {
-	if (duration < 1) return `${(duration * 1000).toFixed(0)}μs`
+	if (duration < 1) {
+		const us = duration * 1000
+		return us < 1 ? '<1μs' : `${us.toFixed(0)}μs`
+	}
 	if (duration < 1000) return `${duration.toFixed(2)}ms`
 	return `${(duration / 1000).toFixed(2)}s`
 }

122-143: Pattern matching logic works but has a minor redundancy.

The final if (pattern.includes('*')) block on line 137 is only reachable for patterns with * in the middle (e.g., /users/*/posts). The regex approach handles this correctly.

However, the current logic creates a new RegExp on every call for wildcard patterns. For frequently called paths, consider caching compiled regexes if skip patterns are static.

For patterns like /users/*/posts, a compiled regex could be cached during logger initialization:

+const compilePattern = (pattern: string): ((path: string) => boolean) => {
+	if (!pattern.includes('*')) return (path) => pattern === path
+	if (pattern.endsWith('/**')) {
+		const prefix = pattern.slice(0, -3)
+		return (path) => path === prefix || path.startsWith(prefix + '/')
+	}
+	if (pattern.endsWith('/*')) {
+		const prefix = pattern.slice(0, -2)
+		return (path) => {
+			if (!path.startsWith(prefix + '/')) return false
+			const rest = path.slice(prefix.length + 1)
+			return !rest.includes('/')
+		}
+	}
+	const regex = new RegExp('^' + pattern.replace(/\*/g, '[^/]+') + '$')
+	return (path) => regex.test(path)
+}
test/logger/logger.test.ts (2)

236-257: Duration threshold may be flaky in CI.

The test expects duration >= 45 for a 50ms delay. While 5ms buffer is reasonable, slow CI environments might occasionally fail. Consider increasing the delay or widening the threshold.

 	it('measures duration correctly', async () => {
 		const logs: LogEntry[] = []
 
 		const app = new Elysia()
 			.use(
 				logger({
 					output: (entry) => logs.push(entry)
 				})
 			)
 			.get('/slow', async () => {
-				await delay(50)
+				await delay(100)
 				return 'Slow response'
 			})
 
 		await app.handle(req('/slow'))
 
 		await delay(10)
 
 		expect(logs.length).toBe(1)
-		// Duration should be at least 50ms due to delay
-		expect(logs[0].duration).toBeGreaterThanOrEqual(45)
+		// Duration should be at least 100ms due to delay
+		expect(logs[0].duration).toBeGreaterThanOrEqual(90)
 	})

339-355: Default logger tests only verify no-throw behavior.

These tests ensure the default logger and colored: false don't throw, but they don't verify actual console output. Consider capturing console.log to validate output format.

it('uses default logger when no output specified', async () => {
	const originalLog = console.log
	const logs: string[] = []
	console.log = (...args) => logs.push(args.join(' '))

	try {
		const app = new Elysia().use(logger()).get('/', () => 'Hello')
		await app.handle(req('/'))
		await delay(10)
		expect(logs.length).toBe(1)
		expect(logs[0]).toContain('GET')
		expect(logs[0]).toContain('/')
	} finally {
		console.log = originalLog
	}
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07b449a and 960ce2a.

📒 Files selected for processing (4)
  • example/logger.ts (1 hunks)
  • package.json (1 hunks)
  • src/logger.ts (1 hunks)
  • test/logger/logger.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/logger/logger.test.ts (2)
src/logger.ts (2)
  • LogEntry (3-9)
  • logger (197-232)
test/utils.ts (2)
  • req (1-2)
  • delay (69-70)
example/logger.ts (1)
src/logger.ts (1)
  • logger (197-232)
🔇 Additional comments (8)
src/logger.ts (3)

1-11: Well-structured public API.

The LogEntry interface and LoggerFunction type are clean and provide the essential fields for HTTP request logging. The interface is appropriately minimal while covering all stated requirements (method, path, status, duration, timestamp).


13-51: Good documentation and flexibility in LoggerOptions.

The JSDoc examples are helpful, and supporting both string[] and a predicate function for skip provides good flexibility.


197-231: Core logger implementation is solid.

Good use of WeakMap for timing storage (prevents memory leaks), and using onAfterResponse with { as: 'global' } ensures logging works across plugins as intended. The early return when startTime === undefined is a safe guard.

One minor observation: the skip check happens after fetching the start time but before computing duration. This is fine for correctness, but you could move the skip check to onRequest to avoid storing timing for skipped paths entirely.

package.json (1)

165-170: Export entry follows existing conventions.

The new ./logger export is consistent with other module exports in the package, providing types, ESM, and CJS entry points.

test/logger/logger.test.ts (3)

1-6: Clean test setup with appropriate imports.

Good use of test utilities (req, post, delay) from the shared utils file.


8-29: Comprehensive basic test case.

The test validates all LogEntry fields and ensures proper logging for a simple GET request.


7-377: Well-structured and comprehensive test suite.

The tests cover the key functionality: basic logging, multiple HTTP methods, skip patterns (array, function, wildcards), plugins, route parameters, error handling, and 404 responses. Good job ensuring global scope works across plugins.

example/logger.ts (1)

1-10: Good example demonstrating logger usage.

The example clearly shows how to integrate the logger middleware with various route types (GET, POST, route parameters).

Comment on lines +12 to +17
console.log('🦊 Server running at http://localhost:3333')
console.log('Try these endpoints:')
console.log(' GET http://localhost:3000/')
console.log(' GET http://localhost:3000/users/123')
console.log(' POST http://localhost:3000/users')
console.log(' GEeT http://localhost:3000/health')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Port mismatch and typo in console output.

The server listens on port 3333 (line 10) but the console output references port 3000. Also, there's a typo "GEeT" on line 17.

 console.log('🦊 Server running at http://localhost:3333')
 console.log('Try these endpoints:')
-console.log('  GET  http://localhost:3000/')
-console.log('  GET  http://localhost:3000/users/123')
-console.log('  POST http://localhost:3000/users')
-console.log('  GEeT  http://localhost:3000/health')
+console.log('  GET  http://localhost:3333/')
+console.log('  GET  http://localhost:3333/users/123')
+console.log('  POST http://localhost:3333/users')
+console.log('  GET  http://localhost:3333/health')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log('🦊 Server running at http://localhost:3333')
console.log('Try these endpoints:')
console.log(' GET http://localhost:3000/')
console.log(' GET http://localhost:3000/users/123')
console.log(' POST http://localhost:3000/users')
console.log(' GEeT http://localhost:3000/health')
console.log('🦊 Server running at http://localhost:3333')
console.log('Try these endpoints:')
console.log(' GET http://localhost:3333/')
console.log(' GET http://localhost:3333/users/123')
console.log(' POST http://localhost:3333/users')
console.log(' GET http://localhost:3333/health')
🤖 Prompt for AI Agents
In example/logger.ts around lines 12 to 17, the console messages incorrectly
reference port 3000 and contain a typo ("GEeT"); update all endpoint URLs to use
port 3333 to match the server and correct "GEeT" to "GET" so the printed
endpoints are accurate and consistent.

Comment on lines +219 to +220
const duration = performance.now() - startTime
const status = (set.status as number) ?? 200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find files in src directory to understand the structure
find src -type f -name "*.ts" | head -20

Repository: elysiajs/elysia

Length of output: 174


🌐 Web query:

Elysia set.status type definition how does it handle string status names

💡 Result:

Short answer: set.status (Context.set.status) is a legacy, mutable setter that accepts either a numeric HTTP status code or the common status-text string (e.g. "I'm a teapot"). It will set that status for the response, but it does not provide the same TypeScript type-narrowing/inference as the newer status()/error helpers — so using set.status won't let Elysia infer response types for different codes. [1][2]

Sources:

  • Elysia handler docs (set.status examples and note about string names + lack of inference). [1]
  • Elysia blog/docs recommending error/status helpers for proper type inference (shows set.status usage and limitations). [2]

Status code extraction doesn't handle string status names correctly.

The cast (set.status as number) ?? 200 doesn't actually convert string values to numbers. Elysia's set.status accepts both numeric codes and string status names (e.g., 'Created', 'OK', 'I'm a teapot'), so if a handler sets set.status = 'Created', the logger will record the string 'Created' instead of the numeric code 201. The cast to as number only affects TypeScript's type checking, not runtime behavior.

🤖 Prompt for AI Agents
In src/logger.ts around lines 219-220, the code currently uses (set.status as
number) which doesn't convert string status names to numeric codes; update the
logic to determine a numeric status: if typeof set.status === 'number' use it,
if it's a string perform a reverse lookup against Node's http.STATUS_CODES
(case-insensitive) to find the matching numeric code (parseInt the key), and if
no match fall back to 200; ensure you handle numeric strings by trying
Number(set.status) first and use the resolved numeric status for logging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Built-In Logger Middleware

1 participant