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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export default defineNuxtConfig({
| `env.environment` | `string` | Auto-detected | Environment name |
| `include` | `string[]` | `undefined` | Route patterns to log (glob). If not set, all routes are logged |
| `pretty` | `boolean` | `true` in dev | Pretty print logs with tree formatting |
| `inset` | `string` | `undefined` | Next evlog data inside a property when pretty is disabled |
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Spelling error: "Next" should be "Nest" in the description "Next evlog data inside a property".

Suggested change
| `inset` | `string` | `undefined` | Next evlog data inside a property when pretty is disabled |
| `inset` | `string` | `undefined` | Nest evlog data inside a property when pretty is disabled |

Copilot uses AI. Check for mistakes.
| `sampling.rates` | `object` | `undefined` | Head sampling rates per log level (0-100%). Error defaults to 100% |
| `sampling.keep` | `array` | `undefined` | Tail sampling conditions to force-keep logs (see below) |
| `transport.enabled` | `boolean` | `false` | Enable sending client logs to the server |
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ initLogger({
pretty?: boolean // Pretty print (default: true in dev)
stringify?: boolean // JSON.stringify output (default: true, false for Workers)
include?: string[] // Route patterns to log (glob), e.g. ['/api/**']
inset?: string // Nest all log data inside this property
sampling?: {
rates?: { // Head sampling (random per level)
info?: number // 0-100, default 100
Expand Down Expand Up @@ -561,6 +562,29 @@ export default defineNitroPlugin((nitroApp) => {
})
```

### Inset - Nesting Logs

By default, `pretty` is disabled, so evlog will log the object at root level when logging in production; however, for services like Cloudflare Observability, you may wish to nest the data inside an arbitrary property name. This can help organize your data and make it easier to query and analyze.

> **Note**: Nesting can have adverse effects if your logging system expects root-level json data, such as requestIds, or tracing ids.


In this example, your logs will then be nested under the `data` property:

```json [Observability Logs]
{
"$data": {
"timestamp": "2026-02-05T06:48:18.122Z",
"level": "info",
"message": "This is an info log",
"method": "GET",
...
},
"$metadata": {...},
"$workers": {...}
}
```

### Pretty Output Format

In development, evlog uses a compact tree format:
Expand Down Expand Up @@ -678,6 +702,8 @@ try {
}
```


Copy link
Owner

Choose a reason for hiding this comment

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

Can you pls remove this blank line


## Framework Support

evlog works with any framework powered by [Nitro](https://nitro.unjs.io/):
Expand Down
36 changes: 36 additions & 0 deletions apps/docs/content/1.getting-started/2.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export default defineNuxtConfig({
include: ['/api/**'],
// Optional: exclude specific routes from logging
exclude: ['/api/_nuxt_icon/**'],
// Optional: nested property name for wide events
inset: 'evlog'
},
})
```
Expand All @@ -60,6 +62,7 @@ export default defineNuxtConfig({
| `exclude` | `string[]` | `undefined` | Route patterns to exclude from logging. Supports glob (`/api/_nuxt_icon/**`). Exclusions take precedence over inclusions |
| `routes` | `Record<string, RouteConfig>` | `undefined` | Route-specific service configuration. Allows setting different service names for different routes using glob patterns |
| `pretty` | `boolean` | `true` in dev | Pretty print with tree formatting |
| `inset` | `string` | `'evlog'` | Nested property name for wide events |
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Incorrect default value in documentation: The table shows inset with a default value of 'evlog', but according to the type definitions and code, the default value is undefined (no inset). The default should be undefined or - to indicate it's not set by default.

Suggested change
| `inset` | `string` | `'evlog'` | Nested property name for wide events |
| `inset` | `string` | `undefined` | Nested property name for wide events |

Copilot uses AI. Check for mistakes.
| `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) |
| `transport.enabled` | `boolean` | `false` | Enable sending client logs to the server. See [Client Transport](#client-transport) |
Expand Down Expand Up @@ -116,6 +119,39 @@ All logs from matching routes will automatically include the configured service

You can also override the service name per handler using `useLogger(event, 'service-name')`. See [Quick Start - Service Identification](/getting-started/quick-start#service-identification) for details.

### Nesting log data

By default, evlog will log the object at root level when logging without ```pretty``` enabled (see [Configuration Options](/getting-started/installation#configuration-options)); however, for services like Cloudflare Observability, you may wish to nest the data inside an arbitrary property name. This can help organize your data and make it easier to query and analyze.
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Inconsistent formatting: There are triple backticks (```) used inline instead of single backticks (`). The word "pretty" should use single backticks for inline code formatting, not triple backticks.

Suggested change
By default, evlog will log the object at root level when logging without ```pretty``` enabled (see [Configuration Options](/getting-started/installation#configuration-options)); however, for services like Cloudflare Observability, you may wish to nest the data inside an arbitrary property name. This can help organize your data and make it easier to query and analyze.
By default, evlog will log the object at root level when logging without `pretty` enabled (see [Configuration Options](/getting-started/installation#configuration-options)); however, for services like Cloudflare Observability, you may wish to nest the data inside an arbitrary property name. This can help organize your data and make it easier to query and analyze.

Copilot uses AI. Check for mistakes.

::callout{icon="i-lucide-info" color="warning"}
**Note:** Nesting can have adverse effects if your logging system expects root-level json data, such as requestIds, or tracing ids.
::

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
inset: "data",
},
});
```

In this example, your logs will then be nested under the `data` property:

```json [Observability Logs]
{
"$data": {
"timestamp": "2026-02-05T06:48:18.122Z",
"level": "info",
"message": "This is an info log",
"method": "GET",
...
},
"$metadata": {...},
"$workers": {...}
}
```

### Sampling

At scale, logging everything can become expensive. evlog supports two sampling strategies:
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/evlog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export type {
TailSamplingContext,
TransportConfig,
WideEvent,
InsetWideEvent
} from './types'
15 changes: 12 additions & 3 deletions packages/evlog/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defu } from 'defu'
import type { EnvironmentContext, Log, LogLevel, LoggerConfig, RequestLogger, RequestLoggerOptions, SamplingConfig, TailSamplingContext, WideEvent } from './types'
import type { EnvironmentContext, InsetWideEvent, Log, LogLevel, LoggerConfig, RequestLogger, RequestLoggerOptions, SamplingConfig, TailSamplingContext, WideEvent } from './types'
import { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isDev, matchesPattern } from './utils'

let globalEnv: EnvironmentContext = {
Expand All @@ -10,6 +10,7 @@ let globalEnv: EnvironmentContext = {
let globalPretty = isDev()
let globalSampling: SamplingConfig = {}
let globalStringify = true
let globalInset: string | undefined = undefined

/**
* Initialize the logger with configuration.
Expand All @@ -29,6 +30,7 @@ export function initLogger(config: LoggerConfig = {}): void {
globalPretty = config.pretty ?? isDev()
globalSampling = config.sampling ?? {}
globalStringify = config.stringify ?? true
globalInset = config.inset ?? undefined
Comment on lines 13 to 33
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Missing test coverage for the inset configuration option. The logger test file (packages/evlog/test/logger.test.ts) has comprehensive tests for other configuration options like pretty, sampling, etc., but there are no tests to verify:

  1. That logs are nested under the configured property name (e.g., $evlog) when inset is set and pretty: false
  2. That the $ prefix is automatically added to the inset property name
  3. That inset has no effect when pretty: true (development mode)
  4. That the nested structure contains all expected fields (timestamp, level, service, custom fields, etc.)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Edge case not handled: If inset is set to an empty string (""), the condition !globalPretty && globalInset will evaluate to false (since empty string is falsy), which is correct behavior. However, this should be explicitly documented or validated. Consider adding validation in initLogger to reject empty strings or falsy values, or document that only truthy string values are valid for inset.

Suggested change
globalInset = config.inset ?? undefined
// Only accept non-empty strings for inset; treat empty or invalid values as undefined
globalInset = typeof config.inset === 'string' && config.inset.trim() ? config.inset : undefined

Copilot uses AI. Check for mistakes.
}

/**
Expand Down Expand Up @@ -75,12 +77,19 @@ export function shouldKeep(ctx: TailSamplingContext): boolean {
})
}

function emitWideEvent(level: LogLevel, event: Record<string, unknown>, skipSamplingCheck = false): WideEvent | null {
function emitWideEvent(level: LogLevel, event: Record<string, unknown>, skipSamplingCheck = false): WideEvent | InsetWideEvent | null {
if (!skipSamplingCheck && !shouldSample(level)) {
return null
}

const formatted: WideEvent = {
const formatted: InsetWideEvent | WideEvent = !globalPretty && globalInset && globalInset !== undefined ? {
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Redundant check: globalInset && globalInset !== undefined is redundant. The first condition globalInset already handles the undefined check since an empty string is falsy. This can be simplified to just globalInset.

Suggested change
const formatted: InsetWideEvent | WideEvent = !globalPretty && globalInset && globalInset !== undefined ? {
const formatted: InsetWideEvent | WideEvent = !globalPretty && globalInset ? {

Copilot uses AI. Check for mistakes.
[`$${globalInset}`]: {
timestamp: new Date().toISOString(),
level,
...globalEnv,
...event,
}
} : {
timestamp: new Date().toISOString(),
level,
...globalEnv,
Expand Down
2 changes: 2 additions & 0 deletions packages/evlog/src/nitro/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface EvlogConfig {
exclude?: string[]
routes?: Record<string, RouteConfig>
sampling?: SamplingConfig
inset?: string
}

function shouldLog(path: string, include?: string[], exclude?: string[]): boolean {
Expand Down Expand Up @@ -135,6 +136,7 @@ export default defineNitroPlugin((nitroApp) => {
env: evlogConfig?.env,
pretty: evlogConfig?.pretty,
sampling: evlogConfig?.sampling,
inset: evlogConfig?.inset,
})

nitroApp.hooks.hook('request', (event) => {
Expand Down
23 changes: 22 additions & 1 deletion packages/evlog/src/nuxt/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,28 @@ export interface ModuleOptions {
headers?: Record<string, string>
/** Request timeout in milliseconds. Default: 5000 */
timeout?: number
}
},
/**
* Nest logs inside a specific property instead of the root of the log object.
*
* @default undefined
* Logs will be root level objects
*
* @example
* ```ts
* inset: "evlog"
*
* // Resulting Logs
* // {
* // $evlog: {
* // level: 'info',
* // message: 'Hello World',
* // timestamp: '2023-03-01T12:00:00.000Z',
* // }
* // }
* ```
*/
inset?: string
}

export default defineNuxtModule<ModuleOptions>({
Expand Down
9 changes: 9 additions & 0 deletions packages/evlog/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export interface LoggerConfig {
* @default true
*/
stringify?: boolean
/** Nested property name for wide events */
inset?: string;
}

/**
Expand All @@ -228,6 +230,13 @@ export interface BaseWideEvent {
region?: string
}

/**
* Wide event inside a nested propery from global config: inset
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Spelling error in comment: "propery" should be "property".

Suggested change
* Wide event inside a nested propery from global config: inset
* Wide event inside a nested property from global config: inset

Copilot uses AI. Check for mistakes.
*/
export type InsetWideEvent = {
[key: string]: BaseWideEvent & Record<string, unknown>
}

/**
* Wide event with arbitrary additional fields
*/
Expand Down
26 changes: 26 additions & 0 deletions skills/evlog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ export default defineNuxtConfig({
},
// Optional: only log specific routes (supports glob patterns)
include: ['/api/**'],
// Optional: nest log data under a property (e.g., for Cloudflare Observability)
inset: 'evlog',
// Optional: send client logs to server (default: false)
transport: {
enabled: true,
Expand Down Expand Up @@ -340,6 +342,29 @@ nitroApp.hooks.hook('evlog:drain', async (ctx) => {
})
```

## Inset (Nested Log Data)

`inset` nests the wide event inside a `$`-prefixed property. Only applies when `pretty: false` (production JSON).

- Use when the platform injects root-level metadata (e.g., Cloudflare Observability adds `$metadata`, `$workers`)
- Do NOT use with Axiom, Datadog, Grafana — they expect flat root-level JSON

```typescript
// inset: 'evlog' → wraps under $evlog
{ "$evlog": { "level": "info", "service": "api", "user": { "id": "123" }, ... } }

// Without inset (default) → flat root
{ "level": "info", "service": "api", "user": { "id": "123" }, ... }
```

```typescript
// ❌ Don't use with flat-log consumers
evlog: { inset: 'data' } // Axiom/Datadog expect root-level fields

// ✅ Use when platform adds root-level metadata
evlog: { inset: 'evlog' } // Cloudflare Observability
```

## Log Draining & Adapters

evlog provides built-in adapters to send logs to external observability platforms.
Expand Down Expand Up @@ -468,6 +493,7 @@ When reviewing code, check for:
8. **Client-side logging** → Use `log` API for debugging in Vue components
9. **Client log centralization** → Enable `transport.enabled: true` to send client logs to server
10. **Missing log draining** → Set up adapters (`evlog/axiom`, `evlog/otlp`) for production log export
11. **Inset misconfiguration** → Only enable `inset` when the platform adds root-level metadata (e.g., Cloudflare Observability). Do not use with systems expecting flat root-level JSON (Axiom, Datadog, Grafana).

## Loading Reference Files

Expand Down
6 changes: 6 additions & 0 deletions skills/evlog/references/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ export default defineEventHandler(async (event) => {
- [ ] Business context is domain-specific and useful for debugging
- [ ] No sensitive data in logs (passwords, tokens, full card numbers)

### Configuration

- [ ] `inset` is only enabled when the platform adds root-level metadata (e.g., Cloudflare Workers Observability)
- [ ] `inset` is not used with systems expecting flat root-level JSON (Axiom, Datadog, Grafana)

## Anti-Pattern Summary

| Anti-Pattern | Fix |
Expand All @@ -283,6 +288,7 @@ export default defineEventHandler(async (event) => {
| No logging in request handlers | Add `useLogger(event)` (Nuxt/Nitro) or `createRequestLogger()` (standalone) |
| Flat log data | Grouped objects: `{ user: {...}, cart: {...} }` |
| Abbreviated field names | Descriptive names: `userId` not `uid` |
| `inset` with flat-log consumers | Only use `inset` for platforms that add root-level metadata (e.g., Cloudflare) |

## Suggested Review Comments

Expand Down
85 changes: 85 additions & 0 deletions skills/evlog/references/wide-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,88 @@ log.set({
pm: 'card',
})
```

## Inset: Nested Wide Events

By default, wide events are emitted as flat root-level JSON. The `inset` option wraps the entire wide event inside a named property (prefixed with `$`), which is useful when your observability platform injects its own metadata at the root level.

### Default Output (no inset)

```json
{
"timestamp": "2026-01-24T10:23:45.235Z",
"level": "info",
"service": "api",
"method": "POST",
"path": "/checkout",
"duration": "234ms",
"user": { "id": "user_123", "plan": "premium" },
"cart": { "items": 3, "total": 9999 }
}
```

### With Inset (`inset: 'evlog'`)

```json
{
"$evlog": {
"timestamp": "2026-01-24T10:23:45.235Z",
"level": "info",
"service": "api",
"method": "POST",
"path": "/checkout",
"duration": "234ms",
"user": { "id": "user_123", "plan": "premium" },
"cart": { "items": 3, "total": 9999 }
}
}
```

### Cloudflare Workers Observability Example

Cloudflare injects `$metadata` and `$workers` at the root level. With inset, your data stays cleanly separated:

```json
{
"$evlog": {
"timestamp": "2026-02-05T06:48:18.122Z",
"level": "info",
"service": "edge-api",
"method": "POST",
"path": "/api/checkout",
"duration": "456ms",
"user": { "id": "user_789", "plan": "enterprise" },
"cart": { "items": 5, "total": 24999 },
"checkout": { "step": "payment", "paymentMethod": "card" },
"fraud": { "score": 12, "riskLevel": "low", "passed": true }
},
"$metadata": { "..." },
"$workers": { "..." }
}
```

This makes it easy to query all your application data under a single namespace (e.g., `$evlog.user.plan` or `$evlog.cart.total`) without collision with platform fields.

### Configuration

```typescript
// Nuxt
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
inset: 'evlog', // → logs nested under $evlog
},
})

// Standalone
initLogger({
env: { service: 'my-worker' },
inset: 'data', // → logs nested under $data
})
```

### Important Considerations

- **Pretty mode is unaffected.** Inset only applies to JSON output (`pretty: false`). Development pretty-print remains unchanged.
- **Property prefix.** The value is prefixed with `$` automatically: `inset: 'evlog'` → `$evlog`.
- **Platform compatibility.** Do not enable inset if your logging system expects fields like `level`, `requestId`, or `timestamp` at the root level (e.g., Axiom, Datadog, Grafana). Only use when the platform adds its own root-level metadata.
Loading