Skip to content
Merged
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
27 changes: 27 additions & 0 deletions apps/docs/content/1.getting-started/2.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export default defineNuxtConfig({
},
// Optional: only log specific routes (supports glob patterns)
include: ['/api/**'],
// Optional: exclude specific routes from logging
exclude: ['/api/_nuxt_icon/**'],
},
})
```
Expand All @@ -46,10 +48,35 @@ export default defineNuxtConfig({
| `env.service` | `string` | `'app'` | Service name shown in logs |
| `env.environment` | `string` | Auto-detected | Environment name |
| `include` | `string[]` | `undefined` | Route patterns to log. Supports glob (`/api/**`). If not set, all routes are logged |
| `exclude` | `string[]` | `undefined` | Route patterns to exclude from logging. Supports glob (`/api/_nuxt_icon/**`). Exclusions take precedence over inclusions |
| `pretty` | `boolean` | `true` in dev | Pretty print with tree formatting |
| `sampling.rates` | `object` | `undefined` | Head sampling rates per log level (0-100%). See [Sampling](#sampling) |
| `sampling.keep` | `array` | `undefined` | Tail sampling conditions to force-keep logs. See [Sampling](#sampling) |

### Route Filtering

Use `include` and `exclude` to control which routes are logged. Both support glob patterns.

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
// Log all API and auth routes...
include: ['/api/**', '/auth/**'],
// ...except internal/noisy routes
exclude: [
'/api/_nuxt_icon/**', // Nuxt Icon requests
'/api/_content/**', // Nuxt Content queries
'/api/health', // Health checks
],
},
})
```

::callout{icon="i-lucide-info" color="info"}
**Exclusions take precedence.** If a path matches both `include` and `exclude`, it will be excluded.
::

### Sampling

At scale, logging everything can become expensive. evlog supports two sampling strategies:
Expand Down
17 changes: 13 additions & 4 deletions packages/evlog/src/nitro/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@ interface EvlogConfig {
env?: Record<string, unknown>
pretty?: boolean
include?: string[]
exclude?: string[]
sampling?: SamplingConfig
}

function shouldLog(path: string, include?: string[]): boolean {
// If no include patterns, log everything
function shouldLog(path: string, include?: string[], exclude?: string[]): boolean {
// Check exclusions first (they take precedence)
if (exclude && exclude.length > 0) {
if (exclude.some(pattern => matchesPattern(path, pattern))) {
return false
}
}

// If no include patterns, log everything (that wasn't excluded)
if (!include || include.length === 0) {
return true
}

// Log only if path matches at least one include pattern
return include.some(pattern => matchesPattern(path, pattern))
}
Expand Down Expand Up @@ -63,8 +72,8 @@ export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', (event) => {
const e = event as ServerEvent

// Skip logging for routes not matching include patterns
if (!shouldLog(e.path, evlogConfig?.include)) {
// Skip logging for routes not matching include/exclude patterns
if (!shouldLog(e.path, evlogConfig?.include, evlogConfig?.exclude)) {
return
}

Expand Down
8 changes: 8 additions & 0 deletions packages/evlog/src/nuxt/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export interface ModuleOptions {
*/
include?: string[]

/**
* Route patterns to exclude from logging.
* Supports glob patterns like '/api/_nuxt_icon/**'.
* Exclusions take precedence over inclusions.
* @example ['/api/_nuxt_icon/**', '/health']
*/
exclude?: string[]

/**
* Sampling configuration for filtering logs.
* Allows configuring what percentage of logs to keep per level.
Expand Down
121 changes: 121 additions & 0 deletions packages/evlog/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { describe, expect, it, vi } from 'vitest'
import { colors, formatDuration, getLevelColor, isClient, isDev, isServer, matchesPattern } from '../src/utils'

// Helper to test shouldLog logic (mirrors plugin.ts implementation)
function shouldLog(path: string, include?: string[], exclude?: string[]): boolean {
if (exclude && exclude.length > 0) {
if (exclude.some(pattern => matchesPattern(path, pattern))) {
return false
}
}
if (!include || include.length === 0) {
return true
}
return include.some(pattern => matchesPattern(path, pattern))
}
Comment thread
HugoRCD marked this conversation as resolved.

describe('formatDuration', () => {
it('formats milliseconds for duration < 1s', () => {
expect(formatDuration(0)).toBe('0ms')
Expand Down Expand Up @@ -208,3 +221,111 @@ describe('matchesPattern', () => {
})
})
})

describe('shouldLog', () => {
describe('no filters', () => {
it('logs everything when no include or exclude', () => {
expect(shouldLog('/api/users')).toBe(true)
expect(shouldLog('/health')).toBe(true)
expect(shouldLog('/')).toBe(true)
})

it('logs everything with empty arrays', () => {
expect(shouldLog('/api/users', [], [])).toBe(true)
expect(shouldLog('/health', [], [])).toBe(true)
})
})

describe('include only', () => {
it('logs matching paths', () => {
expect(shouldLog('/api/users', ['/api/**'])).toBe(true)
expect(shouldLog('/api/posts/123', ['/api/**'])).toBe(true)
})

it('does not log non-matching paths', () => {
expect(shouldLog('/health', ['/api/**'])).toBe(false)
expect(shouldLog('/auth/login', ['/api/**'])).toBe(false)
})

it('supports multiple include patterns', () => {
expect(shouldLog('/api/users', ['/api/**', '/auth/**'])).toBe(true)
expect(shouldLog('/auth/login', ['/api/**', '/auth/**'])).toBe(true)
expect(shouldLog('/health', ['/api/**', '/auth/**'])).toBe(false)
})
})

describe('exclude only', () => {
it('logs non-matching paths', () => {
expect(shouldLog('/api/users', undefined, ['/health'])).toBe(true)
expect(shouldLog('/api/posts', undefined, ['/api/_nuxt_icon/**'])).toBe(true)
})

it('does not log matching exclude paths', () => {
expect(shouldLog('/health', undefined, ['/health'])).toBe(false)
expect(shouldLog('/api/_nuxt_icon/foo', undefined, ['/api/_nuxt_icon/**'])).toBe(false)
})

it('supports multiple exclude patterns', () => {
expect(shouldLog('/api/users', undefined, ['/health', '/api/_nuxt_icon/**'])).toBe(true)
expect(shouldLog('/health', undefined, ['/health', '/api/_nuxt_icon/**'])).toBe(false)
expect(shouldLog('/api/_nuxt_icon/bar', undefined, ['/health', '/api/_nuxt_icon/**'])).toBe(false)
})
})

describe('include and exclude combined', () => {
it('excludes take precedence over includes', () => {
// Path matches include but also matches exclude -> excluded
expect(shouldLog('/api/_nuxt_icon/foo', ['/api/**'], ['/api/_nuxt_icon/**'])).toBe(false)
})

it('logs paths matching include but not exclude', () => {
expect(shouldLog('/api/users', ['/api/**'], ['/api/_nuxt_icon/**'])).toBe(true)
expect(shouldLog('/api/posts/123', ['/api/**'], ['/api/_nuxt_icon/**'])).toBe(true)
})

it('does not log paths not matching include', () => {
expect(shouldLog('/health', ['/api/**'], ['/api/_nuxt_icon/**'])).toBe(false)
})

it('handles complex filtering scenarios', () => {
const include = ['/api/**', '/auth/**']
const exclude = ['/api/_nuxt_icon/**', '/api/health', '/auth/internal/**']

expect(shouldLog('/api/users', include, exclude)).toBe(true)
expect(shouldLog('/auth/login', include, exclude)).toBe(true)
expect(shouldLog('/api/_nuxt_icon/icon.svg', include, exclude)).toBe(false)
expect(shouldLog('/api/health', include, exclude)).toBe(false)
expect(shouldLog('/auth/internal/debug', include, exclude)).toBe(false)
expect(shouldLog('/public/assets', include, exclude)).toBe(false)
})
})

describe('real-world use cases', () => {
it('excludes nuxt icon routes from API logging', () => {
const include = ['/api/**']
const exclude = ['/api/_nuxt_icon/**']

expect(shouldLog('/api/users', include, exclude)).toBe(true)
expect(shouldLog('/api/_nuxt_icon/mdi:home', include, exclude)).toBe(false)
expect(shouldLog('/api/_nuxt_icon/lucide:check', include, exclude)).toBe(false)
})

it('excludes health checks', () => {
const exclude = ['/health', '/healthz', '/ready']

expect(shouldLog('/api/users', undefined, exclude)).toBe(true)
expect(shouldLog('/health', undefined, exclude)).toBe(false)
expect(shouldLog('/healthz', undefined, exclude)).toBe(false)
expect(shouldLog('/ready', undefined, exclude)).toBe(false)
})

it('excludes static assets', () => {
const include = ['/api/**']
const exclude = ['/api/_content/**', '/api/_nuxt_icon/**']

expect(shouldLog('/api/users', include, exclude)).toBe(true)
expect(shouldLog('/api/_content/query', include, exclude)).toBe(false)
expect(shouldLog('/api/_nuxt_icon/foo', include, exclude)).toBe(false)
})
})
})
Loading