You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`evlog/orpc` ships two primitives that together turn every oRPC procedure call into a single wide event:
16
-
17
-
-`withEvlog(handler)` — wraps an `RPCHandler` (or `OpenAPIHandler`) so each HTTP request creates a request-scoped logger and emits one wide event when the response completes.
18
-
-`evlog()` — an oRPC procedure middleware that tags the wide event with the procedure path (`operation`) and forwards the logger via `context.log`.
15
+
`evlog/orpc` ships two primitives: `withEvlog(handler)` wraps any oRPC handler (`RPCHandler`, `OpenAPIHandler`) so each request becomes one wide event, and `evlog()` is a procedure middleware that exposes `context.log` and tags the wide event with the procedure path as `operation`.
**Using Vite?** The [`evlog/vite` plugin](/reference/vite-plugin) replaces the `initLogger()` call with compile-time auto-initialization, strips `log.debug()` from production builds, and injects source locations.
92
92
::
93
93
94
-
`EvlogOrpcContext` declares `log: RequestLogger` on the procedure context — the wrapper injects it for every matched request. `os.use(evlog())` on the base then exposes typed `context.log` to every procedure that descends from `base`.
94
+
`EvlogOrpcContext` declares `log: RequestLogger` on the procedure context so `context.log` is fully typed in every procedure that descends from `base`.
95
95
96
96
## Wide Events
97
97
98
-
Build context up over the procedure call. One request = one wide event:
98
+
Build up context progressively through your handler. One request = one wide event:
All fields are merged into a single wide event emitted when the request completes. The `operation` field is filled automatically from the procedure path (nested routers like `users.profile.get` surface as `operation: 'users.profile.get'`):
117
117
118
118
```bash [Terminal output]
119
119
14:58:15 INFO [my-rpc] POST /rpc/getUser 200 in 12ms
120
120
├─ operation: getUser
121
+
├─ orders: count=2 totalRevenue=6298
121
122
├─ user: id=usr_123 name=Alice plan=pro
122
-
├─ orders: count=2
123
123
└─ requestId: 4a8ff3a8-...
124
124
```
125
125
126
-
The `operation` field comes from the procedure path joined with `.`. Nested routers like `router.users.profile.get` surface as `operation: 'users.profile.get'`, which makes filtering by procedure trivial in your observability backend.
127
-
128
-
## useLogger() — accessing the logger off-context
126
+
## useLogger()
129
127
130
-
When you don't have direct access to `context` (utility modules, deep service functions), use `useLogger()`:
128
+
Use `useLogger()` to access the request-scoped logger from anywhere in the call stack without passing the context through your service layer:
131
129
132
-
```typescript [server/services/billing.ts]
130
+
```typescript [server/services/user.ts]
133
131
import { useLogger } from'evlog/orpc'
134
132
135
-
exportasyncfunctionchargeCard(amount:number) {
133
+
exportasyncfunctionfindUser(id:string) {
136
134
const log =useLogger()
137
-
log.set({ payment: { amount } })
138
-
// …
139
-
}
140
-
```
135
+
log.set({ user: { id } })
141
136
142
-
`useLogger()` resolves to the same logger as `context.log` and throws when called outside of a request that flowed through `withEvlog()`.
Author errors with `defineErrorCatalog` / `createError` — the same way you do with every other evlog integration. The `evlog()` procedure middleware bridges them to oRPC at throw time, so the wire response keeps your catalog `code`, status, message, and the `why` / `fix` / `link` guidance.
147
-
148
-
Both authoring styles flow through the same bridge:
The catalog is just sugar over `createError()`— both produce an `EvlogError`, both carry the same metadata, both come out of oRPC with the same wire shape. If `createError()`is called without `code`, the wire `code` falls back to `'EVLOG_ERROR'`.
152
+
Both `context.log` and `useLogger()`return the same logger instance. `useLogger()`uses `AsyncLocalStorage` to propagate the logger across async boundaries.
Use `createError` for structured errors with `why`, `fix`, and `link` fields. The `evlog()` middleware catches the throw, records it on the wide event, and bridges it to an `ORPCError` so the wire response carries your `code`, `status`, `message`, and the human-guidance fields:
Inside the procedure middleware, evlog catches the `EvlogError` thrown by the catalog factory, records it on the wide event (so the level is promoted to `error`), and re-throws an `ORPCError` carrying the same `code`/`status`/`message` plus `why`/`fix`/`link` under `data`:
176
+
The error is captured and logged with both the custom context and structured error fields:
196
177
197
178
```bash [Terminal output]
198
-
14:58:20 ERROR [my-rpc] POST /payments/charge 402 in 3ms
**Why are `why` / `fix` / `link` under `data` and not at the response root?** The other evlog framework integrations put those fields at the root next to `message` and `status`. oRPC's wire format is fixed: every error is serialized as `{ defined, code, status, message, data }`so that typed clients (`safe()` from `@orpc/client`) can deserialize them as a typed union. Anything user-provided lives inside `data`. evlog follows the protocol — the *authoring* surface (catalogs, `createError()`) is the same everywhere; only the wire shape varies because oRPC has its own contract.
203
+
oRPC's error envelope is `{ defined, code, status, message, data }`— clients deserialize errors as a typed union via `safe()` from `@orpc/client`. evlog follows the protocol, so `why`/`fix`/`link` live under `data` instead of at the response root. The authoring API (`createError` / [`defineErrorCatalog`](/learn/structured-errors#error-catalogs)) is identical to the rest of evlog.
221
204
::
222
205
223
-
`defined: false` here means the error was authored with evlog's catalog rather than registered as an oRPC typed error via `os.errors({...})`. If you want typed-client-side narrowing on the catalog codes, you can also feed the catalog into `os.errors()` and throw with `errors.<NAME>(...)`; both styles flow through the same wide-event pipeline.
224
-
225
-
## Middleware Composition
226
-
227
-
`evlog()` plays well with other oRPC middleware. Chain them with `.use()` — every middleware sees `context.log` so each layer can append its own keys to the wide event without coordinating with the next:
228
-
229
-
```typescript [server/orpc.ts]
230
-
const base =os
231
-
.$context<EvlogOrpcContext>()
232
-
.errors(errors)
233
-
.use(evlog())
234
-
235
-
const authed =base.use(async ({ context, next }) => {
When a route is excluded, the wrapper still injects a no-op logger into `context.log` so your procedures never crash on missing fields — the wide event just isn't emitted and drain/enrich aren't called.
338
-
339
-
## Streaming Procedures
340
-
341
-
oRPC's [Event Iterator](https://orpc.dev/docs/event-iterator) lets procedures stream chunks back over Server-Sent Events. The wrapper emits the wide event when `handler.handle()` returns the `Response`, which is **before** the stream has fully drained. Token counts or per-chunk fields written via `context.log.set()` after the procedure returns are dropped (and surface a `[evlog]` warning) — accumulate them inside the procedure body before yielding the iterator, or use a separate drain pipeline for stream metrics.
279
+
When a route is filtered out, the wrapper still injects a no-op `context.log` so procedures never crash on missing fields — the wide event simply isn't emitted and drain/enrich aren't called.
0 commit comments