Examples
Real-world examples and use cases for Elogs
下面所有示例都基于 apps/elysia/src/routers/ 的真实代码——createElogs 已在 index.ts 里挂上,这些 router 都按 <App extends CreateElogs>(app: App) => ... 模式写,TypeScript 能精确推断 store.logger / log 的类型。
基础
最小化
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
const app = new Elysia()
.use(createElogs())
.get('/', () => 'Hello World')
.listen(3000)
显示启动 banner
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
const app = new Elysia()
.use(
createElogs({
config: {
showStartupMessage: true,
startupMessageFormat: 'banner',
},
})
)
.get('/', () => 'Hello World')
.listen(3000)
dev preset 默认就开 banner,等价于上面这段。
请求作用域 context
累积字段到 access log
不调用任何 logger.info,只要在 handler 里 mergeContext,字段会自动出现在最终的 access log 的 context 节点下:
import type { CreateElogs } from '@eastgold15/elogs'
export const requestContextRouter = <App extends CreateElogs>(app: App) =>
app.get('/checkout', ({ request, store }) => {
store.logger.mergeContext(request, { userId: 'usr_demo' })
store.logger.mergeContext(request, { cart: { items: 2, total: 4999 } })
return { ok: true }
})
深调用栈:AsyncLocalStorage
开启 useAsyncLocalStorage: true 后,service / helper 不用逐层传 request,直接 useLogger() 拿同一份 logger:
import type { CreateElogs } from '@eastgold15/elogs'
import { createElogs, useLogger } from '@eastgold15/elogs'
const dbQueryHelper = async () => {
const log = useLogger()
log.mergeContext({ query: 'SELECT * FROM users' })
await Promise.resolve()
log.info('Running database query in nested service')
}
export const requestContextRouter = <App extends CreateElogs>(app: App) =>
app
.use(createElogs({ config: { useAsyncLocalStorage: true } }))
.get('/async-context', async ({ log }) => {
log.mergeContext({ userId: 'usr_async' })
log.info('Starting async request processing')
await dbQueryHelper()
return { ok: true }
})
请求作用域 log(不传 request)
handler context 上 derive 出来的 log 已经绑定了当前请求,省掉一个参数:
import type { CreateElogs } from '@eastgold15/elogs'
export const requestContextRouter = <App extends CreateElogs>(app: App) =>
app.get('/profile', ({ log }) => {
log.info('profile accessed') // info, 自动绑 request
log.mergeContext({ stage: 'view' }) // 累积 context
return { ok: true }
})
自定义日志
结构化日志
两种姿势:直接走 store.logger.info(Elogs 路径)或者走 store.pino.info(原始 Pino):
import type { CreateElogs } from '@eastgold15/elogs'
export const customRouter = <App extends CreateElogs>(app: App) =>
app.get('/users/:id', ({ request, store, params }) => {
// Elogs 路径:自动关联 request context
store.logger.info(request, 'User profile accessed', {
userId: params.id,
feature: 'custom-route-log',
})
// 原始 Pino 路径(不进 request context)
store.pino.info(
{ userId: params.id, action: 'view_profile' },
'User profile accessed'
)
return { ok: true }
})
Child Loggers
用 pino.child(...) 创建带绑定字段的子 logger:
import type { CreateElogs } from '@eastgold15/elogs'
export const pinoRouter = <App extends CreateElogs>(app: App) =>
app.get('/api/orders/:id', ({ store, params }) => {
const orderLogger = store.pino.child({
orderId: params.id,
module: 'order-service',
})
orderLogger.debug('Fetching order details')
orderLogger.info({ status: 'processing' }, 'Order retrieved')
return { ok: true }
})
性能埋点
import type { CreateElogs } from '@eastgold15/elogs'
export const pinoRouter = <App extends CreateElogs>(app: App) =>
app.post('/api/process', async ({ store, body }) => {
const startTime = Date.now()
const result = await processData(body)
store.pino.info(
{
operation: 'process_data',
duration: Date.now() - startTime,
itemsProcessed: result.count,
memory: process.memoryUsage().heapUsed / 1024 / 1024,
success: true,
},
'Data processing completed'
)
return result
})
错误处理
自动错误日志
Elogs 在单点 onError 钩子里记录所有进入错误管道的 error,不 return value——错误继续按原路径传播:
import type { CreateElogs } from '@eastgold15/elogs'
export const boomRouter = <App extends CreateElogs>(app: App) =>
app.get('/boom', () => {
// 这个 Error 会被 Elogs 自动记录为 ERROR 级别
// 响应格式仍由 Elysia 默认的 application/problem+json 处理
throw new Error('Boom!')
})
自定义响应格式
用自己的 .error() 钩子接管响应(Elogs 仍然负责日志):
import { problem } from 'elysia'
export const errorRouter = <App extends CreateElogs>(app: App) =>
app
.error(({ code, error }) => {
return problem(500, {
detail: error.message,
status: code,
})
})
.get('/risky', ({ store }) => {
try {
return performRiskyOperation()
} catch (error) {
store.pino.error(
{
err: error,
operation: 'risky_operation',
context: { attempted: true, timestamp: Date.now() },
},
'Operation failed'
)
throw error
}
})
JSON 格式的 access log
customLogFormat 一行换成 JSON:
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
const app = new Elysia()
.use(
createElogs({
config: {
customLogFormat:
'{"level": "{level}", "message": "{message}", "method": "{method}", "pathname": "{pathname}", "status": "{status}"}',
},
})
)
.get('/error', () => {
throw new Error('Validation failed')
})
正常请求和错误请求都会被渲染成 JSON:
{"level": "INFO", "message": "", "method": "GET", "pathname": "/hello", "status": "200"}
{"level": "ERROR", "message": "Validation failed", "method": "GET", "pathname": "/error", "status": "500"}
数据库错误翻译
autoTranslate: { db: 'drizzle' } 让 Drizzle driver 错误(唯一约束 / 外键违反 / 连接失败)按映射规则决定日志级别。响应格式完全交给用户的 .error("DrizzleError", ...):
import type { CreateElogs } from '@eastgold15/elogs'
import { createElogs } from '@eastgold15/elogs'
import { problem } from 'elysia'
// 模拟 Drizzle 抛的错误(真实代码里是 drizzle 自己的 DrizzleError)
const makeDrizzleError = (code: string) => {
const e = new Error(`PG driver reported: ${code}`) as Error & {
code: string
name: string
}
e.name = 'DrizzleError'
e.code = code
return e
}
export const dbRouter = <App extends CreateElogs>(app: App) =>
app
// 用户完全控制响应格式
.error('DrizzleError', (ctx) => {
const code = (ctx.error as { code?: string }).code ?? 'UNKNOWN'
if (code === '23505') {
return problem(409, { detail: 'Duplicate key — that value already exists' })
}
if (code === '23503') {
return problem(400, { detail: 'Foreign key violation — referenced row missing' })
}
if (code === '08006') {
return problem(503, { detail: 'Database unavailable — try again later' })
}
return problem(500, { detail: `Unhandled DB error (code=${code})` })
})
.get('/demo/db-error/duplicate', () => {
throw makeDrizzleError('23505')
})
.get('/demo/db-error/foreign-key', () => {
throw makeDrizzleError('23503')
})
.get('/demo/db-error/connect', () => {
throw makeDrizzleError('08006')
})
const app = new Elysia()
.use(
createElogs({
autoTranslate: { db: 'drizzle' },
config: { /* ... */ },
})
)
.use(dbRouter)
.listen(3000)
也可以不依赖 autoTranslate,在 try/catch 里手动调 translateDrizzleError:
import { translateDrizzleError } from '@eastgold15/elogs/translator'
app.post('/users', async ({ store, body }) => {
try {
return await db.insert(users).values(body)
} catch (err) {
// 翻译后抛回去——status / message 已经按 driver code 选好
throw translateDrizzleError(err)
}
})
详见 Database errors 和 translator API。
Custom Transports
Elasticsearch Transport
const elasticsearchTransport = {
log: async (level, message, meta) => {
try {
await fetch('http://elasticsearch:9200/logs/_doc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level,
message,
...meta,
timestamp: new Date().toISOString(),
}),
})
} catch (err) {
console.error('Elasticsearch transport error', err)
}
},
}
const app = new Elysia()
.use(
createElogs({
config: {
transports: [elasticsearchTransport],
},
})
)
.listen(3000)
Slack Transport(仅 ERROR)
const slackTransport = {
log: async (level, message, meta) => {
if (level !== 'ERROR') return
const webhook = process.env.SLACK_WEBHOOK_URL
if (!webhook) return
try {
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `[ERROR] ${message}\n\`\`\`${JSON.stringify(meta, null, 2)}\`\`\``,
}),
})
} catch (err) {
console.error('Slack transport error', err)
}
},
}
MongoDB Transport
import { MongoClient } from 'mongodb'
const client = new MongoClient(process.env.MONGODB_URI)
await client.connect()
const db = client.db('logs')
const mongodbTransport = {
log: async (level, message, meta) => {
try {
await db.collection('logs').insertOne({
level,
message,
...meta,
timestamp: new Date(),
})
} catch (err) {
console.error('MongoDB transport error', err)
}
},
}
完整生产配置
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
const elasticsearchTransport = { /* ... */ }
const slackTransport = { /* ... */ }
const app = new Elysia()
.use(
createElogs({
preset: 'prod',
autoTranslate: { db: 'drizzle' },
config: {
service: 'my-api',
showStartupMessage: false,
// 文件日志 + 轮转
logFilePath: './logs/production.log',
logRotation: {
maxSize: '100m',
interval: '1d',
maxFiles: '30d',
compress: true,
},
// 只输出 ERROR / WARNING
logFilter: { level: ['ERROR', 'WARNING'] },
// Pino 配置
pino: {
level: 'info',
redact: ['password', 'token', 'apiKey', 'creditCard'],
base: {
service: 'my-api',
version: process.env.APP_VERSION,
environment: 'production',
},
},
// 外部 transport
useTransportsOnly: false,
transports: [elasticsearchTransport, slackTransport],
},
})
)
.listen(3000)
按环境动态切换
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
const app = new Elysia()
.use(
createElogs({
preset: isProd ? 'prod' : 'dev',
autoTranslate: isProd ? { db: 'drizzle' } : undefined,
config: {
showStartupMessage: isDev,
startupMessageFormat: isDev ? 'banner' : 'simple',
logFilePath: isProd ? './logs/production.log' : undefined,
logRotation: isProd
? { maxSize: '100m', interval: '1d', maxFiles: '30d', compress: true }
: undefined,
pino: {
level: isDev ? 'debug' : 'info',
prettyPrint: isDev,
redact: isProd ? ['password', 'token', 'apiKey'] : [],
base: {
service: 'my-api',
version: process.env.APP_VERSION,
environment: process.env.NODE_ENV,
},
},
transports: isProd ? [elasticsearchTransport, slackTransport] : [],
},
})
)
.listen(3000)
REST API 完整示例
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
const app = new Elysia()
.use(
createElogs({
config: {
pino: {
level: 'info',
base: { service: 'user-api' },
},
},
})
)
.get('/users', ({ store }) => {
store.pino.info({ action: 'list_users' }, 'Fetching users')
return { users: [] }
})
.get('/users/:id', ({ store, params }) => {
const userLogger = store.pino.child({ userId: params.id })
userLogger.info('Fetching user')
return { user: { id: params.id } }
})
.post('/users', ({ store, body }) => {
store.pino.info({ action: 'create_user' }, 'Creating user')
return { user: body }
})
.put('/users/:id', ({ store, params, body }) => {
store.pino.info({ userId: params.id, action: 'update_user' }, 'Updating user')
return { user: { id: params.id, ...body } }
})
.delete('/users/:id', ({ store, params }) => {
store.pino.info({ userId: params.id, action: 'delete_user' }, 'Deleting user')
return { success: true }
})
.listen(3000)