diff --git a/apps/docs/content/1.getting-started/2.installation.md b/apps/docs/content/1.getting-started/2.installation.md index 3d17676..27609d0 100644 --- a/apps/docs/content/1.getting-started/2.installation.md +++ b/apps/docs/content/1.getting-started/2.installation.md @@ -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/**'], }, }) ``` @@ -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: diff --git a/packages/evlog/src/nitro/plugin.ts b/packages/evlog/src/nitro/plugin.ts index 0635b74..2b87cc5 100644 --- a/packages/evlog/src/nitro/plugin.ts +++ b/packages/evlog/src/nitro/plugin.ts @@ -8,14 +8,23 @@ interface EvlogConfig { env?: Record 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)) } @@ -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 } diff --git a/packages/evlog/src/nuxt/module.ts b/packages/evlog/src/nuxt/module.ts index 7ab9ac3..ae37240 100644 --- a/packages/evlog/src/nuxt/module.ts +++ b/packages/evlog/src/nuxt/module.ts @@ -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. diff --git a/packages/evlog/test/utils.test.ts b/packages/evlog/test/utils.test.ts index 792e509..d634ddc 100644 --- a/packages/evlog/test/utils.test.ts +++ b/packages/evlog/test/utils.test.ts @@ -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)) +} + describe('formatDuration', () => { it('formats milliseconds for duration < 1s', () => { expect(formatDuration(0)).toBe('0ms') @@ -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) + }) + }) +})