Skip to content

Commit 3898a3f

Browse files
feat(evlog): add deterministic minLevel for log api (#266)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4a99f29 commit 3898a3f

31 files changed

Lines changed: 407 additions & 23 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'evlog': minor
3+
---
4+
5+
Add `minLevel` for a deterministic severity threshold on the global `log` API and client `initLog`, plus `setMinLevel()` for runtime toggling in the browser. Orthogonal to probabilistic `sampling.rates`; request wide events from `useLogger` / `createLogger().emit()` are unchanged. Includes `isLevelEnabled()` helper and wiring for Nuxt, Vite, and Next.js.
6+
7+
**2026-04-11** — Playground: interactive panel to try client `minLevel` / `setMinLevel` and trigger logs per level.

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,8 @@ log.error({ action: 'payment', error: 'validation_failed' })
985985

986986
Client logs output to the browser console with colored tags in development.
987987

988+
`setMinLevel` is auto-imported in Nuxt (from `evlog/client`) — call it to change the client severity threshold at runtime (same as `initLog({ minLevel })`).
989+
988990
#### Client Transport
989991

990992
To send client logs to your server for centralized logging, enable the transport:

apps/docs/content/2.logging/4.client-logging.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ log.info({ action: 'app_init', path: window.location.pathname })
5656

5757
The `log` object works anywhere in your client code: components, composables, event handlers.
5858

59+
## Minimum level (`minLevel`)
60+
61+
Use `initLog({ minLevel: 'warn' })` to keep the browser console quiet (warnings and errors only). Severity order: `debug` < `info` < `warn` < `error`. Default is `'debug'` (all levels).
62+
63+
For a **debug toggle** without reloading, call `setMinLevel('debug')` or `setMinLevel('warn')` from `evlog/client` when the user opts in or out of verbose logs.
64+
65+
`minLevel` applies to both console output and [server transport](#sending-logs-to-the-server) payloads.
66+
5967
## Two Call Signatures
6068

6169
The `log` API accepts two forms depending on the context.
@@ -88,6 +96,8 @@ log.info('auth', 'User logged in')
8896
8997
Both forms support four levels: `log.info()`, `log.warn()`, `log.error()`, and `log.debug()`.
9098
99+
In the browser, `log.debug()` is emitted with `console.log` (not `console.debug`) so lines stay visible with the default DevTools **Info** filter; the structured event still has `level: 'debug'`.
100+
91101
## Identity Context
92102
93103
Track which user generated a log with `setIdentity()`:
@@ -116,6 +126,7 @@ Identity fields are automatically merged into every log event until cleared. Thi
116126
| `enabled` | `true` | Enable or disable all client logging |
117127
| `console` | `true` | Output logs to the browser console |
118128
| `pretty` | `true` | Use colored, formatted console output |
129+
| `minLevel` | `'debug'` | Minimum severity: `debug` < `info` < `warn` < `error` |
119130
| `service` | `'client'` | Service name included in every log event |
120131
| `transport` | - | Send logs to a server endpoint (see below) |
121132

apps/docs/content/3.core-concepts/1.configuration.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ initLogger({
3232
pretty: false,
3333
silent: false,
3434
stringify: true,
35+
minLevel: 'info',
3536
sampling: { rates: { info: 10 }, keep: [{ status: 400 }] },
3637
drain: createAxiomDrain(),
3738
})
@@ -44,9 +45,17 @@ initLogger({
4445
| `pretty` | `boolean` | `true` in dev | Pretty print with tree formatting. Auto-detected based on `NODE_ENV` |
4546
| `silent` | `boolean` | `false` | Suppress console output. Events are still built, sampled, and passed to drains |
4647
| `stringify` | `boolean` | `true` | Emit JSON strings when `pretty` is disabled. Set to `false` for Cloudflare Workers |
48+
| `minLevel` | `'debug' \| 'info' \| 'warn' \| 'error'` | `'debug'` | Minimum severity for the global `log` API only (not `createLogger` / request wide events). Order: debug < info < warn < error |
4749
| `sampling` | `SamplingConfig` | `undefined` | Head and tail sampling configuration. See [Sampling](/core-concepts/sampling) |
4850
| `drain` | `(ctx: DrainContext) => void` | `undefined` | Drain callback for sending events to external services |
4951

52+
### `minLevel` vs sampling
53+
54+
- **`minLevel`** is a **hard threshold** on the simple `log.*` API: levels below the threshold are never emitted. It does **not** apply to wide events from `useLogger` / `createLogger().emit()` — use **`sampling.rates`** (and tail `keep`) for request volume.
55+
- **Head sampling** (`sampling.rates`) is **probabilistic** on what is already allowed by `minLevel` for simple logs.
56+
57+
Evaluation order for `log.info` / `log.debug` / etc.: `enabled``minLevel` → head sampling → output.
58+
5059
### Environment Context
5160

5261
The `env` option controls the fields included in every log event. Most values are auto-detected from environment variables and `package.json`.

apps/docs/skills/review-logging-patterns/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@ All options work in Nuxt (`evlog` key), Nitro (passed to `evlog()`), Next.js (`c
720720
| `include` | `string[]` | All routes | Route glob patterns to log |
721721
| `exclude` | `string[]` | None | Route patterns to exclude (takes precedence) |
722722
| `routes` | `Record<string, { service }>` | -- | Route-specific service names |
723+
| `minLevel` | `'debug' \| 'info' \| 'warn' \| 'error'` | `'debug'` | Hard threshold for the global `log` API and client `log` (not request wide events). Use `sampling.rates` for probabilistic volume on requests |
723724
| `sampling.rates` | `object` | -- | Head sampling: `{ info: 10, warn: 50 }` (0-100%) |
724725
| `sampling.keep` | `array` | -- | Tail sampling: `[{ status: 400 }, { duration: 1000 }]` |
725726
| `drain` | `(ctx) => void` | -- | Drain callback (Next.js, standalone) |
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<script setup lang="ts">
2+
import type { LogLevel } from 'evlog'
3+
import { log, setMinLevel } from 'evlog/client'
4+
5+
const levels: LogLevel[] = ['debug', 'info', 'warn', 'error']
6+
7+
const minLevel = ref<LogLevel>('debug')
8+
9+
const order: Record<LogLevel, number> = {
10+
debug: 0,
11+
info: 1,
12+
warn: 2,
13+
error: 3,
14+
}
15+
16+
function passes(level: LogLevel): boolean {
17+
return order[level] >= order[minLevel.value]
18+
}
19+
20+
function applyMinLevel(level: LogLevel) {
21+
minLevel.value = level
22+
setMinLevel(level)
23+
}
24+
25+
function emitAt(level: LogLevel) {
26+
const payload = { panel: 'min-level', ts: Date.now() }
27+
if (level === 'debug') {
28+
log.debug(payload)
29+
} else if (level === 'info') {
30+
log.info(payload)
31+
} else if (level === 'warn') {
32+
log.warn('playground', 'warn sample')
33+
} else {
34+
log.error('playground', 'error sample')
35+
}
36+
}
37+
38+
onMounted(() => {
39+
const raw = useRuntimeConfig().public.evlog as { minLevel?: LogLevel } | undefined
40+
const fromConfig = raw?.minLevel
41+
if (fromConfig && levels.includes(fromConfig)) {
42+
minLevel.value = fromConfig
43+
setMinLevel(fromConfig)
44+
}
45+
})
46+
</script>
47+
48+
<template>
49+
<ClientOnly>
50+
<div class="max-w-3xl space-y-6">
51+
<div
52+
class="rounded-lg border border-[var(--ui-border)] bg-elevated p-5 space-y-4"
53+
>
54+
<div>
55+
<p class="text-xs font-medium text-highlighted uppercase tracking-wide">
56+
Minimum level
57+
</p>
58+
<p class="text-xs text-muted mt-1 leading-relaxed">
59+
Only the global client <code class="text-[11px]">log.*</code> API is filtered. Open DevTools and watch the console: levels below the threshold produce no output and no transport when ingest is enabled.
60+
</p>
61+
</div>
62+
63+
<div class="flex flex-wrap gap-2">
64+
<UButton
65+
v-for="lvl in levels"
66+
:key="lvl"
67+
size="sm"
68+
:variant="minLevel === lvl ? 'solid' : 'outline'"
69+
:color="minLevel === lvl ? 'primary' : 'neutral'"
70+
@click="applyMinLevel(lvl)"
71+
>
72+
{{ lvl }}
73+
</UButton>
74+
</div>
75+
76+
<p class="text-[11px] text-muted leading-relaxed">
77+
<span class="text-highlighted">Passes:</span>
78+
{{ levels.filter(l => passes(l)).join(', ') }}
79+
</p>
80+
<p class="text-[11px] text-muted leading-relaxed">
81+
<code class="text-[11px]">log.debug()</code> uses <code class="text-[11px]">console.log</code> in the browser so lines show with the default console filter (payload still has <code class="text-[11px]">level: &quot;debug&quot;</code>).
82+
</p>
83+
</div>
84+
85+
<div
86+
class="rounded-lg border border-[var(--ui-border)] bg-elevated p-5 space-y-4"
87+
>
88+
<p class="text-xs font-medium text-highlighted uppercase tracking-wide">
89+
Trigger logs
90+
</p>
91+
<div class="flex flex-wrap gap-2">
92+
<UButton
93+
v-for="lvl in levels"
94+
:key="`emit-${lvl}`"
95+
size="sm"
96+
variant="soft"
97+
:color="lvl === 'error' ? 'error' : lvl === 'warn' ? 'warning' : lvl === 'info' ? 'primary' : 'neutral'"
98+
@click="emitAt(lvl)"
99+
>
100+
log.{{ lvl }}()
101+
</UButton>
102+
</div>
103+
<p class="text-[11px] text-muted">
104+
Set minimum to <code class="text-[11px]">warn</code>, then trigger <code class="text-[11px]">log.info</code> — nothing should appear. Switch back to <code class="text-[11px]">debug</code> and trigger again to verify.
105+
</p>
106+
</div>
107+
</div>
108+
<template #fallback>
109+
<p class="text-sm text-muted">
110+
Loading…
111+
</p>
112+
</template>
113+
</ClientOnly>
114+
</template>

apps/playground/app/config/tests.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ function makeDrainEvent(action: string, extra?: Record<string, unknown>) {
5151

5252
export const testConfig = {
5353
sections: [
54+
{
55+
id: 'min-level',
56+
label: 'Min level',
57+
icon: 'i-lucide-sliders-horizontal',
58+
title: 'Minimum log level (client)',
59+
description:
60+
'Deterministic filter for the client `log` API: choose a minimum severity, trigger each level, and confirm blocked lines never reach the console or ingest. Use `setMinLevel` at runtime without reload.',
61+
tests: [],
62+
} as TestSection,
5463
{
5564
id: 'client-logging',
5665
label: 'Client Logging',

apps/playground/app/pages/index.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ const currentSection = computed(() =>
4646
:title="currentSection.title"
4747
:description="currentSection.description"
4848
>
49-
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
49+
<PlaygroundMinLevelPanel v-if="currentSection.id === 'min-level'" />
50+
<div
51+
v-else
52+
class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"
53+
>
5054
<PlaygroundTestCard
5155
v-for="test in currentSection.tests"
5256
:key="test.id"

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/evlog/src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { initLog, log, setIdentity, clearIdentity } from './runtime/client/log'
1+
export { initLog, log, setIdentity, clearIdentity, setMinLevel } from './runtime/client/log'

0 commit comments

Comments
 (0)