Skip to content

Commit 26d36f5

Browse files
authored
feat: supports route-based service configuration (#40)
1 parent 942178f commit 26d36f5

24 files changed

Lines changed: 1425 additions & 342 deletions

File tree

apps/docs/content/1.getting-started/2.installation.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default defineNuxtConfig({
5656
| `env.environment` | `string` | Auto-detected | Environment name |
5757
| `include` | `string[]` | `undefined` | Route patterns to log. Supports glob (`/api/**`). If not set, all routes are logged |
5858
| `exclude` | `string[]` | `undefined` | Route patterns to exclude from logging. Supports glob (`/api/_nuxt_icon/**`). Exclusions take precedence over inclusions |
59+
| `routes` | `Record<string, RouteConfig>` | `undefined` | Route-specific service configuration. Allows setting different service names for different routes using glob patterns |
5960
| `pretty` | `boolean` | `true` in dev | Pretty print with tree formatting |
6061
| `sampling.rates` | `object` | `undefined` | Head sampling rates per log level (0-100%). See [Sampling](#sampling) |
6162
| `sampling.keep` | `array` | `undefined` | Tail sampling conditions to force-keep logs. See [Sampling](#sampling) |
@@ -86,6 +87,33 @@ export default defineNuxtConfig({
8687
**Exclusions take precedence.** If a path matches both `include` and `exclude`, it will be excluded.
8788
::
8889

90+
### Route-Based Service Configuration
91+
92+
In multi-service architectures, configure different service names for different routes:
93+
94+
```typescript [nuxt.config.ts]
95+
export default defineNuxtConfig({
96+
modules: ['evlog/nuxt'],
97+
evlog: {
98+
env: {
99+
service: 'default-service', // Fallback for unmatched routes
100+
},
101+
routes: {
102+
'/api/auth/**': { service: 'auth-service' },
103+
'/api/payment/**': { service: 'payment-service' },
104+
'/api/booking/**': { service: 'booking-service' },
105+
},
106+
},
107+
})
108+
```
109+
110+
All logs from matching routes will automatically include the configured service name. This is especially useful when:
111+
- Running multiple microservices behind a single Nuxt server
112+
- Organizing logs by business domain (auth, payment, inventory)
113+
- Differentiating between API versions (`/api/v1/**`, `/api/v2/**`)
114+
115+
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.
116+
89117
### Sampling
90118

91119
At scale, logging everything can become expensive. evlog supports two sampling strategies:

apps/docs/content/1.getting-started/3.quick-start.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,59 @@ The logger automatically emits when the request ends. No manual `emit()` call ne
6060
| When you need to accumulate context | Quick debugging messages |
6161
| For wide events (one log per request) | Client-side logging |
6262

63+
### Service Identification
64+
65+
In multi-service architectures, differentiate which service a log belongs to using either route-based configuration or explicit service names.
66+
67+
#### Route-Based Configuration
68+
69+
Configure service names per route pattern in your `nuxt.config.ts`:
70+
71+
```typescript [nuxt.config.ts]
72+
export default defineNuxtConfig({
73+
modules: ['evlog/nuxt'],
74+
75+
evlog: {
76+
env: {
77+
service: 'default-service', // Fallback service name
78+
},
79+
routes: {
80+
'/api/auth/**': { service: 'auth-service' },
81+
'/api/payment/**': { service: 'payment-service' },
82+
'/api/booking/**': { service: 'booking-service' },
83+
},
84+
},
85+
})
86+
```
87+
88+
Logs from routes matching these patterns will automatically include the configured service name:
89+
90+
```bash [Output]
91+
21:57:10.442 INFO [auth-service] POST /api/auth/login 200 in 1ms
92+
├─ requestId: 88ced16a-bef2-4483-86cb-2b4fb677ea52
93+
├─ user: id=user_123 email=demo@example.com
94+
└─ action: login
95+
```
96+
97+
#### Explicit Service Parameter
98+
99+
Override the service name for specific routes using the second parameter of `useLogger`:
100+
101+
```typescript [server/api/legacy/process.post.ts]
102+
export default defineEventHandler((event) => {
103+
// Explicitly set service name for this handler
104+
const log = useLogger(event, 'legacy-service')
105+
106+
log.set({ action: 'process_legacy_request' })
107+
108+
return { success: true }
109+
})
110+
```
111+
112+
::callout{icon="i-lucide-info" color="info"}
113+
**Priority order:** Explicit `useLogger` parameter > Route configuration > `env.service` > Auto-detected from environment
114+
::
115+
63116
## createError (Structured Errors)
64117

65118
Use `createError()` to throw errors with actionable context:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import type { TestSection } from '~/config/tests.config'
3+
4+
const props = defineProps<{
5+
sections: TestSection[]
6+
activeSection: string
7+
}>()
8+
9+
const emit = defineEmits<{
10+
select: [sectionId: string]
11+
}>()
12+
13+
function selectSection(sectionId: string) {
14+
emit('select', sectionId)
15+
}
16+
</script>
17+
18+
<template>
19+
<div class="flex gap-2 overflow-x-auto pb-2 -mb-2">
20+
<button
21+
v-for="section in sections"
22+
:key="section.id"
23+
:class="[
24+
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap',
25+
activeSection === section.id
26+
? 'bg-primary text-primary-foreground'
27+
: 'bg-elevated text-muted hover:bg-muted hover:text-highlighted',
28+
]"
29+
@click="selectSection(section.id)"
30+
>
31+
<UIcon v-if="section.icon" :name="section.icon" class="w-4 h-4" />
32+
{{ section.label }}
33+
</button>
34+
</div>
35+
</template>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
status: 'idle' | 'loading' | 'success' | 'error'
4+
}>()
5+
6+
const config = computed(() => {
7+
switch (props.status) {
8+
case 'loading':
9+
return {
10+
icon: 'i-lucide-loader-circle',
11+
color: 'text-blue-500',
12+
animate: 'animate-spin',
13+
}
14+
case 'success':
15+
return {
16+
icon: 'i-lucide-check-circle',
17+
color: 'text-green-500',
18+
}
19+
case 'error':
20+
return {
21+
icon: 'i-lucide-x-circle',
22+
color: 'text-red-500',
23+
}
24+
default:
25+
return {
26+
icon: 'i-lucide-circle',
27+
color: 'text-gray-400',
28+
}
29+
}
30+
})
31+
</script>
32+
33+
<template>
34+
<UIcon
35+
:name="config.icon"
36+
:class="[config.color, config.animate]"
37+
class="w-4 h-4"
38+
/>
39+
</template>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script setup lang="ts">
2+
import type { TestConfig } from '~/config/tests.config'
3+
4+
const props = defineProps<TestConfig>()
5+
6+
const toast = useToast()
7+
8+
const { execute, isLoading, result, error, status } = useTestRunner(props.id, {
9+
endpoint: props.endpoint,
10+
method: props.method,
11+
onSuccess: (response) => {
12+
if (props.toastOnSuccess) {
13+
toast.add({
14+
...props.toastOnSuccess,
15+
color: 'success',
16+
})
17+
}
18+
},
19+
onError: (err) => {
20+
if (props.toastOnError) {
21+
toast.add({
22+
...props.toastOnError,
23+
color: 'error',
24+
})
25+
}
26+
},
27+
})
28+
29+
async function handleClick() {
30+
try {
31+
if (props.onClick) {
32+
await execute(props.onClick)
33+
} else {
34+
await execute()
35+
}
36+
} catch {
37+
// Error already handled by useTestRunner
38+
}
39+
}
40+
</script>
41+
42+
<template>
43+
<div>
44+
<UButton
45+
:label
46+
:loading="isLoading"
47+
:color
48+
:variant
49+
@click="handleClick"
50+
/>
51+
52+
<PlaygroundTestResult
53+
v-if="showResult"
54+
:status
55+
:response="result"
56+
:error
57+
/>
58+
</div>
59+
</template>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script setup lang="ts">
2+
import type { TestConfig } from '~/config/tests.config'
3+
4+
const props = defineProps<TestConfig>()
5+
6+
const toast = useToast()
7+
8+
const { execute, isLoading, result, error, status } = useTestRunner(props.id, {
9+
endpoint: props.endpoint,
10+
method: props.method,
11+
onSuccess: (response) => {
12+
if (props.toastOnSuccess) {
13+
toast.add({
14+
...props.toastOnSuccess,
15+
color: 'success',
16+
})
17+
}
18+
},
19+
onError: (err) => {
20+
if (props.toastOnError) {
21+
toast.add({
22+
...props.toastOnError,
23+
color: 'error',
24+
})
25+
}
26+
},
27+
})
28+
29+
async function handleClick() {
30+
try {
31+
if (props.onClick) {
32+
await execute(props.onClick)
33+
} else {
34+
await execute()
35+
}
36+
} catch {
37+
// Error already handled
38+
}
39+
}
40+
</script>
41+
42+
<template>
43+
<div class="h-full p-5 rounded-lg bg-elevated border border-primary/5 hover:border-primary/10 transition-colors flex flex-col">
44+
<div class="flex items-start justify-between gap-3 mb-3">
45+
<h3 class="text-sm font-semibold text-highlighted leading-tight">
46+
{{ label }}
47+
</h3>
48+
<UBadge
49+
v-if="badge"
50+
:color="badge.color as any"
51+
variant="subtle"
52+
class="shrink-0"
53+
>
54+
{{ badge.label }}
55+
</UBadge>
56+
</div>
57+
58+
<p v-if="description" class="text-sm text-muted leading-relaxed mb-4 grow">
59+
{{ description }}
60+
</p>
61+
62+
<div class="mt-auto">
63+
<UButton
64+
block
65+
:color="color as any"
66+
:loading="isLoading"
67+
@click="handleClick"
68+
>
69+
{{ label }}
70+
</UButton>
71+
</div>
72+
73+
<PlaygroundTestResult
74+
v-if="showResult"
75+
:status
76+
:response="result"
77+
:error
78+
compact
79+
class="mt-4"
80+
/>
81+
</div>
82+
</template>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
status: 'idle' | 'loading' | 'success' | 'error'
4+
response?: any
5+
error?: any
6+
compact?: boolean
7+
}>()
8+
9+
const copied = ref(false)
10+
11+
const displayData = computed(() => {
12+
if (props.response !== undefined) {
13+
return JSON.stringify(props.response, null, 2)
14+
}
15+
if (props.error) {
16+
return JSON.stringify(props.error, null, 2)
17+
}
18+
return null
19+
})
20+
21+
async function copyToClipboard() {
22+
if (displayData.value) {
23+
try {
24+
await navigator.clipboard.writeText(displayData.value)
25+
copied.value = true
26+
setTimeout(() => {
27+
copied.value = false
28+
}, 2000)
29+
} catch (err) {
30+
console.error('Failed to copy:', err)
31+
}
32+
}
33+
}
34+
</script>
35+
36+
<template>
37+
<div
38+
v-if="status !== 'idle' && displayData"
39+
class="mt-3 space-y-2"
40+
>
41+
<div class="flex items-center justify-between">
42+
<span class="text-xs font-medium text-muted">
43+
{{ status === 'error' ? 'Error' : 'Response' }}
44+
</span>
45+
<UButton
46+
v-if="!compact"
47+
size="xs"
48+
variant="ghost"
49+
:icon="copied ? 'i-lucide-check' : 'i-lucide-copy'"
50+
@click="copyToClipboard"
51+
>
52+
{{ copied ? 'Copied!' : 'Copy' }}
53+
</UButton>
54+
</div>
55+
<pre
56+
:class="[
57+
'text-xs p-3 rounded overflow-auto',
58+
status === 'error'
59+
? 'bg-error/10 text-error border border-error/20'
60+
: 'bg-muted',
61+
compact ? 'max-h-32' : 'max-h-64',
62+
]"
63+
>{{ displayData }}</pre>
64+
</div>
65+
</template>

0 commit comments

Comments
 (0)