Skip to content

Commit 4b03cd3

Browse files
authored
feat(pino): Logging Plugin (#1191)
Fixes #908 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Pino-based structured per-request logging plugin with configurable lifecycle logging, abort handling, ID generation, and streaming error handling. * **Documentation** * Added a Pino integration guide and new navigation links in the docs. * **Chores** * Introduced a new experimental Pino package with package metadata, README, tsconfig and ignore rules. * **Tests** * Added tests covering logger context, plugin behavior, streaming/abort scenarios, and public exports. * **Other** * Broadened utility key typing to accept additional property key types. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent fb5a52a commit 4b03cd3

File tree

14 files changed

+1171
-119
lines changed

14 files changed

+1171
-119
lines changed

apps/content/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export default withMermaid(defineConfig({
153153
{ text: 'Body Limit', link: '/docs/plugins/body-limit' },
154154
{ text: 'Simple CSRF Protection', link: '/docs/plugins/simple-csrf-protection' },
155155
{ text: 'Strict GET method', link: '/docs/plugins/strict-get-method' },
156+
{ text: 'Logging', link: '/docs/integrations/pino' },
156157
],
157158
},
158159
{
@@ -190,6 +191,7 @@ export default withMermaid(defineConfig({
190191
{ text: 'Hey API', link: '/docs/integrations/hey-api' },
191192
{ text: 'OpenTelemetry', link: '/docs/integrations/opentelemetry' },
192193
{ text: 'Pinia Colada', link: '/docs/integrations/pinia-colada' },
194+
{ text: 'Pino', link: '/docs/integrations/pino' },
193195
{ text: 'React SWR', link: '/docs/integrations/react-swr' },
194196
{ text: 'Sentry', link: '/docs/integrations/sentry' },
195197
{ text: 'Tanstack Query', link: '/docs/integrations/tanstack-query' },
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
title: Pino Integration
3+
description: Integrate oRPC with Pino for structured logging and request tracking.
4+
---
5+
6+
# Pino Integration
7+
8+
[Pino](https://getpino.io/) is a fast and lightweight JSON logger. This guide explains how to integrate oRPC with Pino to add structured logging, request tracking, and error monitoring to your applications.
9+
10+
::: warning
11+
This guide assumes familiarity with [Pino](https://getpino.io/). Review the official documentation if needed.
12+
:::
13+
14+
## Installation
15+
16+
::: code-group
17+
18+
```sh [npm]
19+
npm install @orpc/experimental-pino@latest pino@latest
20+
```
21+
22+
```sh [yarn]
23+
yarn add @orpc/experimental-pino@latest pino@latest
24+
```
25+
26+
```sh [pnpm]
27+
pnpm add @orpc/experimental-pino@latest pino@latest
28+
```
29+
30+
```sh [bun]
31+
bun add @orpc/experimental-pino@latest pino@latest
32+
```
33+
34+
```sh [deno]
35+
deno add npm:@orpc/experimental-pino@latest npm:pino@latest
36+
```
37+
38+
:::
39+
40+
## Setup
41+
42+
To set up Pino with oRPC, use the `LoggingHandlerPlugin` class. This plugin automatically instruments your handler with structured logging, request tracking, and error monitoring.
43+
44+
```ts
45+
import { LoggingHandlerPlugin } from '@orpc/experimental-pino'
46+
import pino from 'pino'
47+
48+
const logger = pino()
49+
50+
const handler = new RPCHandler(router, {
51+
plugins: [
52+
new LoggingHandlerPlugin({
53+
logger, // Custom logger instance
54+
generateId: ({ request }) => crypto.randomUUID(), // Custom ID generator
55+
logRequestResponse: true, // Log request start/end (disabled by default)
56+
logRequestAbort: true, // Log when requests are aborted (disabled by default)
57+
}),
58+
],
59+
})
60+
```
61+
62+
::: info
63+
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler.
64+
:::
65+
66+
::: tip
67+
For improved log readability during development, consider using [pino-pretty](https://github.com/pinojs/pino-pretty) to format your logs in a human-friendly way.
68+
69+
```bash
70+
npm run dev | npx pino-pretty
71+
```
72+
73+
:::
74+
75+
## Using the Logger in Your Code
76+
77+
You can access the logger from the context object using the `getLogger` function:
78+
79+
```ts
80+
import { getLogger } from '@orpc/experimental-pino'
81+
82+
const procedure = os.handler(({ context }) => {
83+
const logger = getLogger(context) // [!code highlight]
84+
85+
logger?.info('Processing request')
86+
logger?.debug({ userId: 123 }, 'User data')
87+
88+
return { success: true }
89+
})
90+
```
91+
92+
## Providing Custom Logger per Request
93+
94+
You can provide a custom logger instance for specific requests by passing it through the context. This is especially useful when integrating with [pino-http](https://github.com/pinojs/pino-http) for enhanced HTTP logging:
95+
96+
```ts
97+
import {
98+
CONTEXT_LOGGER_SYMBOL,
99+
LoggerContext,
100+
LoggingHandlerPlugin
101+
} from '@orpc/experimental-pino'
102+
103+
const logger = pino()
104+
const httpLogger = pinoHttp({ logger })
105+
106+
interface ORPCContext extends LoggerContext {} // [!code highlight]
107+
108+
const router = {
109+
ping: os.$context<ORPCContext>().handler(() => 'pong')
110+
}
111+
112+
const handler = new RPCHandler(router, {
113+
plugins: [
114+
new LoggingHandlerPlugin({ logger }), // [!code highlight]
115+
],
116+
})
117+
118+
const server = createServer(async (req, res) => {
119+
httpLogger(req, res)
120+
121+
const { matched } = await handler.handle(req, res, {
122+
prefix: '/api',
123+
context: {
124+
[CONTEXT_LOGGER_SYMBOL]: req.log, // [!code highlight]
125+
},
126+
})
127+
128+
if (!matched) {
129+
res.statusCode = 404
130+
res.end('Not Found')
131+
}
132+
})
133+
```

packages/pino/.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Hidden folders and files
2+
.*
3+
!.gitignore
4+
!.*.example
5+
6+
# Common generated folders
7+
logs/
8+
node_modules/
9+
out/
10+
dist/
11+
dist-ssr/
12+
build/
13+
coverage/
14+
temp/
15+
16+
# Common generated files
17+
*.log
18+
*.log.*
19+
*.tsbuildinfo
20+
*.vitest-temp.json
21+
vite.config.ts.timestamp-*
22+
vitest.config.ts.timestamp-*
23+
24+
# Common manual ignore files
25+
*.local
26+
*.pem
27+
28+
## Hey API Code gen
29+
/tests/client/

packages/pino/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<div align="center">
2+
<image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" />
3+
</div>
4+
5+
<h1></h1>
6+
7+
<div align="center">
8+
<a href="https://codecov.io/gh/unnoq/orpc">
9+
<img alt="codecov" src="https://codecov.io/gh/unnoq/orpc/branch/main/graph/badge.svg">
10+
</a>
11+
<a href="https://www.npmjs.com/package/@orpc/experimental-pino">
12+
<img alt="weekly downloads" src="https://img.shields.io/npm/dw/%40orpc%2Fexperimental-pino?logo=npm" />
13+
</a>
14+
<a href="https://github.com/unnoq/orpc/blob/main/LICENSE">
15+
<img alt="MIT License" src="https://img.shields.io/github/license/unnoq/orpc?logo=open-source-initiative" />
16+
</a>
17+
<a href="https://discord.gg/TXEbwRBvQn">
18+
<img alt="Discord" src="https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff" />
19+
</a>
20+
<a href="https://deepwiki.com/unnoq/orpc">
21+
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
22+
</a>
23+
</div>
24+
25+
<h3 align="center">Typesafe APIs Made Simple 🪄</h3>
26+
27+
**oRPC is a powerful combination of RPC and OpenAPI**, makes it easy to build APIs that are end-to-end type-safe and adhere to OpenAPI standards
28+
29+
---
30+
31+
## Highlights
32+
33+
- **🔗 End-to-End Type Safety**: Ensure type-safe inputs, outputs, and errors from client to server.
34+
- **📘 First-Class OpenAPI**: Built-in support that fully adheres to the OpenAPI standard.
35+
- **📝 Contract-First Development**: Optionally define your API contract before implementation.
36+
- **🔍 First-Class OpenTelemetry**: Seamlessly integrate with OpenTelemetry for observability.
37+
- **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte, Angular), SWR, Pinia Colada, and more.
38+
- **🚀 Server Actions**: Fully compatible with React Server Actions on Next.js, TanStack Start, and other platforms.
39+
- **🔠 Standard Schema Support**: Works out of the box with Zod, Valibot, ArkType, and other schema validators.
40+
- **🗃️ Native Types**: Supports native types like Date, File, Blob, BigInt, URL, and more.
41+
- **⏱️ Lazy Router**: Enhance cold start times with our lazy routing feature.
42+
- **📡 SSE & Streaming**: Enjoy full type-safe support for SSE and streaming.
43+
- **🌍 Multi-Runtime Support**: Fast and lightweight on Cloudflare, Deno, Bun, Node.js, and beyond.
44+
- **🔌 Extendability**: Easily extend functionality with plugins, middleware, and interceptors.
45+
46+
## Documentation
47+
48+
You can find the full documentation [here](https://orpc.unnoq.com).
49+
50+
## Packages
51+
52+
- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
53+
- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
54+
- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
55+
- [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Generate OpenAPI specs and handle OpenAPI requests.
56+
- [@orpc/otel](https://www.npmjs.com/package/@orpc/otel): [OpenTelemetry](https://opentelemetry.io/) integration for observability.
57+
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Deeply integrate oRPC with [NestJS](https://nestjs.com/).
58+
- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
59+
- [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): [TanStack Query](https://tanstack.com/query/latest) integration.
60+
- [@orpc/experimental-react-swr](https://www.npmjs.com/package/@orpc/experimental-react-swr): [SWR](https://swr.vercel.app/) integration.
61+
- [@orpc/vue-colada](https://www.npmjs.com/package/@orpc/vue-colada): Integration with [Pinia Colada](https://pinia-colada.esm.dev/).
62+
- [@orpc/hey-api](https://www.npmjs.com/package/@orpc/hey-api): [Hey API](https://heyapi.dev/) integration.
63+
- [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): More schemas that [Zod](https://zod.dev/) doesn't support yet.
64+
- [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): OpenAPI spec generation from [Valibot](https://valibot.dev/).
65+
- [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): OpenAPI spec generation from [ArkType](https://arktype.io/).
66+
67+
## `@orpc/experimental-pino`
68+
69+
Integration with [Pino](https://getpino.io/) for structured logging.
70+
71+
## Sponsors
72+
73+
<p align="center">
74+
<a href="https://cdn.jsdelivr.net/gh/unnoq/unnoq/sponsors.svg">
75+
<img src='https://cdn.jsdelivr.net/gh/unnoq/unnoq/sponsors.svg'/>
76+
</a>
77+
</p>
78+
79+
## License
80+
81+
Distributed under the MIT License. See [LICENSE](https://github.com/unnoq/orpc/blob/main/LICENSE) for more information.

packages/pino/package.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@orpc/experimental-pino",
3+
"type": "module",
4+
"version": "0.0.1",
5+
"license": "MIT",
6+
"homepage": "https://orpc.unnoq.com",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/unnoq/orpc.git",
10+
"directory": "packages/pino"
11+
},
12+
"keywords": [
13+
"orpc",
14+
"pino"
15+
],
16+
"publishConfig": {
17+
"exports": {
18+
".": {
19+
"types": "./dist/index.d.mts",
20+
"import": "./dist/index.mjs",
21+
"default": "./dist/index.mjs"
22+
}
23+
}
24+
},
25+
"exports": {
26+
".": "./src/index.ts"
27+
},
28+
"files": [
29+
"dist"
30+
],
31+
"scripts": {
32+
"build": "unbuild",
33+
"build:watch": "pnpm run build --watch",
34+
"type:check": "tsc -b"
35+
},
36+
"peerDependencies": {
37+
"pino": ">=10.1.0"
38+
},
39+
"dependencies": {
40+
"@orpc/client": "workspace:*",
41+
"@orpc/server": "workspace:*",
42+
"@orpc/shared": "workspace:*"
43+
},
44+
"devDependencies": {
45+
"pino": "^10.1.0"
46+
}
47+
}

packages/pino/src/context.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import pino from 'pino'
2+
import { CONTEXT_LOGGER_SYMBOL, getLogger } from './context'
3+
4+
it('getLogger', async () => {
5+
expect(getLogger({})).toBeUndefined()
6+
expect(getLogger({ something: true })).toBeUndefined()
7+
8+
const logger = pino()
9+
expect(getLogger({ [CONTEXT_LOGGER_SYMBOL]: logger })).toBe(logger)
10+
})

packages/pino/src/context.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Logger } from 'pino'
2+
import { get } from '@orpc/shared'
3+
4+
export const CONTEXT_LOGGER_SYMBOL: unique symbol = Symbol('ORPC_PINO_CONTEXT_LOGGER_SYMBOL')
5+
6+
export interface LoggerContext {
7+
[CONTEXT_LOGGER_SYMBOL]?: Logger
8+
}
9+
10+
export function getLogger(context: object): Logger | undefined {
11+
return get(context, [CONTEXT_LOGGER_SYMBOL]) as Logger | undefined
12+
}

0 commit comments

Comments
 (0)