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
28 changes: 28 additions & 0 deletions apps/docs/content/1.getting-started/2.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default defineNuxtConfig({
| `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 |
| `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 |
| `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) |
Expand Down Expand Up @@ -86,6 +87,33 @@ export default defineNuxtConfig({
**Exclusions take precedence.** If a path matches both `include` and `exclude`, it will be excluded.
::

### Route-Based Service Configuration

In multi-service architectures, configure different service names for different routes:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: {
service: 'default-service', // Fallback for unmatched routes
},
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
'/api/booking/**': { service: 'booking-service' },
},
},
})
```

All logs from matching routes will automatically include the configured service name. This is especially useful when:
- Running multiple microservices behind a single Nuxt server
- Organizing logs by business domain (auth, payment, inventory)
- Differentiating between API versions (`/api/v1/**`, `/api/v2/**`)

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.

### Sampling

At scale, logging everything can become expensive. evlog supports two sampling strategies:
Expand Down
53 changes: 53 additions & 0 deletions apps/docs/content/1.getting-started/3.quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,59 @@ The logger automatically emits when the request ends. No manual `emit()` call ne
| When you need to accumulate context | Quick debugging messages |
| For wide events (one log per request) | Client-side logging |

### Service Identification

In multi-service architectures, differentiate which service a log belongs to using either route-based configuration or explicit service names.

#### Route-Based Configuration

Configure service names per route pattern in your `nuxt.config.ts`:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['evlog/nuxt'],

evlog: {
env: {
service: 'default-service', // Fallback service name
},
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
'/api/booking/**': { service: 'booking-service' },
},
},
})
```

Logs from routes matching these patterns will automatically include the configured service name:

```bash [Output]
21:57:10.442 INFO [auth-service] POST /api/auth/login 200 in 1ms
├─ requestId: 88ced16a-bef2-4483-86cb-2b4fb677ea52
├─ user: id=user_123 email=demo@example.com
└─ action: login
```

#### Explicit Service Parameter

Override the service name for specific routes using the second parameter of `useLogger`:

```typescript [server/api/legacy/process.post.ts]
export default defineEventHandler((event) => {
// Explicitly set service name for this handler
const log = useLogger(event, 'legacy-service')

log.set({ action: 'process_legacy_request' })

return { success: true }
})
```

::callout{icon="i-lucide-info" color="info"}
**Priority order:** Explicit `useLogger` parameter > Route configuration > `env.service` > Auto-detected from environment
::

## createError (Structured Errors)

Use `createError()` to throw errors with actionable context:
Expand Down
35 changes: 35 additions & 0 deletions apps/playground/app/components/playground/NavigationTabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { TestSection } from '~/config/tests.config'

const props = defineProps<{
sections: TestSection[]
activeSection: string
}>()

const emit = defineEmits<{
select: [sectionId: string]
}>()

function selectSection(sectionId: string) {
emit('select', sectionId)
}
</script>

<template>
<div class="flex gap-2 overflow-x-auto pb-2 -mb-2">
<button
v-for="section in sections"
:key="section.id"
:class="[
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap',
activeSection === section.id
? 'bg-primary text-primary-foreground'
: 'bg-elevated text-muted hover:bg-muted hover:text-highlighted',
]"
@click="selectSection(section.id)"
>
<UIcon v-if="section.icon" :name="section.icon" class="w-4 h-4" />
{{ section.label }}
</button>
</div>
</template>
39 changes: 39 additions & 0 deletions apps/playground/app/components/playground/StatusIndicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
const props = defineProps<{
status: 'idle' | 'loading' | 'success' | 'error'
}>()

const config = computed(() => {
switch (props.status) {
case 'loading':
return {
icon: 'i-lucide-loader-circle',
color: 'text-blue-500',
animate: 'animate-spin',
}
case 'success':
return {
icon: 'i-lucide-check-circle',
color: 'text-green-500',
}
case 'error':
return {
icon: 'i-lucide-x-circle',
color: 'text-red-500',
}
default:
return {
icon: 'i-lucide-circle',
color: 'text-gray-400',
}
}
})
</script>

<template>
<UIcon
:name="config.icon"
:class="[config.color, config.animate]"
class="w-4 h-4"
/>
</template>
59 changes: 59 additions & 0 deletions apps/playground/app/components/playground/TestButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
import type { TestConfig } from '~/config/tests.config'

const props = defineProps<TestConfig>()

const toast = useToast()

const { execute, isLoading, result, error, status } = useTestRunner(props.id, {
endpoint: props.endpoint,
method: props.method,
onSuccess: (response) => {
if (props.toastOnSuccess) {
toast.add({
...props.toastOnSuccess,
color: 'success',
})
}
},
onError: (err) => {
if (props.toastOnError) {
toast.add({
...props.toastOnError,
color: 'error',
})
}
},
})

async function handleClick() {
try {
if (props.onClick) {
await execute(props.onClick)
} else {
await execute()
}
} catch {
// Error already handled by useTestRunner
}
}
</script>

<template>
<div>
<UButton
:label
:loading="isLoading"
:color
:variant
@click="handleClick"
/>

<PlaygroundTestResult
v-if="showResult"
:status
:response="result"
:error
/>
</div>
</template>
82 changes: 82 additions & 0 deletions apps/playground/app/components/playground/TestCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script setup lang="ts">
import type { TestConfig } from '~/config/tests.config'

const props = defineProps<TestConfig>()

const toast = useToast()

const { execute, isLoading, result, error, status } = useTestRunner(props.id, {
endpoint: props.endpoint,
method: props.method,
onSuccess: (response) => {
if (props.toastOnSuccess) {
toast.add({
...props.toastOnSuccess,
color: 'success',
})
}
},
onError: (err) => {
if (props.toastOnError) {
toast.add({
...props.toastOnError,
color: 'error',
})
}
},
})

async function handleClick() {
try {
if (props.onClick) {
await execute(props.onClick)
} else {
await execute()
}
} catch {
// Error already handled
}
}
</script>

<template>
<div class="h-full p-5 rounded-lg bg-elevated border border-primary/5 hover:border-primary/10 transition-colors flex flex-col">
<div class="flex items-start justify-between gap-3 mb-3">
<h3 class="text-sm font-semibold text-highlighted leading-tight">
{{ label }}
</h3>
<UBadge
v-if="badge"
:color="badge.color as any"
variant="subtle"
class="shrink-0"
>
{{ badge.label }}
</UBadge>
</div>

<p v-if="description" class="text-sm text-muted leading-relaxed mb-4 grow">
{{ description }}
</p>

<div class="mt-auto">
<UButton
block
:color="color as any"
:loading="isLoading"
@click="handleClick"
>
{{ label }}
</UButton>
</div>

<PlaygroundTestResult
v-if="showResult"
:status
:response="result"
:error
compact
class="mt-4"
/>
</div>
</template>
65 changes: 65 additions & 0 deletions apps/playground/app/components/playground/TestResult.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
const props = defineProps<{
status: 'idle' | 'loading' | 'success' | 'error'
response?: any
error?: any
compact?: boolean
}>()

const copied = ref(false)

const displayData = computed(() => {
if (props.response !== undefined) {
return JSON.stringify(props.response, null, 2)
}
if (props.error) {
return JSON.stringify(props.error, null, 2)
}
return null
})

async function copyToClipboard() {
if (displayData.value) {
try {
await navigator.clipboard.writeText(displayData.value)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
}
</script>

<template>
<div
v-if="status !== 'idle' && displayData"
class="mt-3 space-y-2"
>
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-muted">
{{ status === 'error' ? 'Error' : 'Response' }}
</span>
<UButton
v-if="!compact"
size="xs"
variant="ghost"
:icon="copied ? 'i-lucide-check' : 'i-lucide-copy'"
@click="copyToClipboard"
>
{{ copied ? 'Copied!' : 'Copy' }}
</UButton>
</div>
<pre
:class="[
'text-xs p-3 rounded overflow-auto',
status === 'error'
? 'bg-error/10 text-error border border-error/20'
: 'bg-muted',
compact ? 'max-h-32' : 'max-h-64',
]"
>{{ displayData }}</pre>
</div>
</template>
Loading
Loading