feat(logging): 引入 LogTape 替换 console.* 为结构化日志
为什么选 LogTape(2026 实测):
- pino 在 bun build --compile 编译产物里因 worker_threads + 动态 require 在
/\$bunfs/ 虚拟文件系统中崩溃,与单二进制部署核心目标冲突;
- LogTape 零依赖(5.3KB)、零 worker、纯 ESM、原生 Bun 导出条件,runtime
agnostic,配合 configureSync 完美兼容 --bytecode 模式(无裸 top-level await);
- 一等公民集成:@logtape/drizzle-orm(SQL 查询日志)、@logtape/otel(后续
OpenTelemetry sink 留扩展点)。
变更:
- src/server/logger.ts: configureSync 引导 + getLogger 重导出。format 默认
process.stdout.isTTY ? pretty : json,可经 LOG_FORMAT 显式覆盖(绕开 Bun
bundler 把 process.env.NODE_ENV 在 --minify 时 inline 成字面量的特殊处理)。
- src/server/api/interceptors.ts: logError 改用 getLogger(['api']).error(...) +
结构化 properties,弃 logger.error 顶层 API。
- src/cli/migrate.ts: 所有 console.log 改走 getLogger(['cli','migrate']),logger
在 run() 内 lazy-import 以保持 citty subcommand 模块体 side-effect-free。
- src/server/db/index.ts: env.LOG_DB=true 时挂 DrizzleLogger 适配器,SQL 查询
按类别 ['db'] 在 debug 级输出(含 query/params/formattedQuery 三字段)。
新增 env 旋钮(t3-oss 校验):
- LOG_LEVEL: trace|debug|info|warning|error|fatal,默认 info
- LOG_FORMAT: pretty|json,默认 TTY 自动选
- LOG_DB: stringbool,默认 false
端到端验证(compose + Postgres 18-alpine):
- TTY 终端:pretty 输出含 ✨ 图标 + ANSI 彩色 + 类别·路径 ✓
- 管道/Docker:JSON Lines 一行一条,含 @timestamp/level/logger/properties ✓
- LOG_FORMAT=pretty 强制覆盖 ✓
- ./server migrate 应用 migration 并经 logger 输出 ✓
- ./server serve + RPC round-trip:interceptor logError 与 drizzle SQL 日志
在生产 JSON 模式下结构化输出 ✓
- fix / typecheck / test 3/3 / build / compile 117M 二进制全绿
This commit is contained in:
@@ -1 +1,6 @@
|
|||||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
||||||
|
|
||||||
|
# Optional logging knobs (defaults are usually fine):
|
||||||
|
# LOG_LEVEL=info # trace|debug|info|warning|error|fatal
|
||||||
|
# LOG_FORMAT=pretty # pretty|json — defaults to TTY ? pretty : json
|
||||||
|
# LOG_DB=false # true to log every Drizzle SQL query
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Compact, repo-specific notes for AI agents. Generic language/framework knowledge
|
|||||||
- TanStack Start (React 19 SSR, file-routed) + Vite 8 + Nitro (nightly, preset `bun`). Vite dev port is **strict 3000**.
|
- TanStack Start (React 19 SSR, file-routed) + Vite 8 + Nitro (nightly, preset `bun`). Vite dev port is **strict 3000**.
|
||||||
- PostgreSQL + **Drizzle ORM `0.45.2` (0.x, NOT 1.0 beta)** — see "Drizzle" section, this matters a lot.
|
- PostgreSQL + **Drizzle ORM `0.45.2` (0.x, NOT 1.0 beta)** — see "Drizzle" section, this matters a lot.
|
||||||
- ORPC (contract-first), TanStack Query v5, Tailwind v4.
|
- ORPC (contract-first), TanStack Query v5, Tailwind v4.
|
||||||
|
- **Logging via [LogTape](https://logtape.org/)** (zero-dep, runtime-agnostic) — see "Logging" section. `console.*` is forbidden in business code.
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ Contract → Router → Handler → Client, all type-safe from a single contract
|
|||||||
```
|
```
|
||||||
Barrel-aggregated in `contracts/index.ts` as `export const contract = { todo }`.
|
Barrel-aggregated in `contracts/index.ts` as `export const contract = { todo }`.
|
||||||
- Routers (`src/server/api/routers/*.router.ts`) import `db` directly from `@/server/db` in their handlers. There is **no `middlewares/` directory** by default — `db` doesn't need one (module-level const). When you actually need per-request context (auth, tenant, rate-limit), create `src/server/api/middlewares/<name>.middleware.ts` with `os.middleware(...)` and extend `BaseContext` in `context.ts`.
|
- Routers (`src/server/api/routers/*.router.ts`) import `db` directly from `@/server/db` in their handlers. There is **no `middlewares/` directory** by default — `db` doesn't need one (module-level const). When you actually need per-request context (auth, tenant, rate-limit), create `src/server/api/middlewares/<name>.middleware.ts` with `os.middleware(...)` and extend `BaseContext` in `context.ts`.
|
||||||
- **Interceptors are attached at the handler level, not in `server.ts` and not on `os`.** Both `src/routes/api/rpc.$.ts` (`RPCHandler`) and `src/routes/api/$.ts` (`OpenAPIHandler`) register `[onError(logError)]` (server) and `[onError(handleValidationError)]` (client). The validation interceptor rewrites `BAD_REQUEST + ValidationError` into `INPUT_VALIDATION_FAILED` (422) and output validation errors into `OUTPUT_VALIDATION_FAILED`. `logError` calls `logger.error` from `@/server/logger` — never `console.*` directly.
|
- **Interceptors are attached at the handler level, not in `server.ts` and not on `os`.** Both `src/routes/api/rpc.$.ts` (`RPCHandler`) and `src/routes/api/$.ts` (`OpenAPIHandler`) register `[onError(logError)]` (server) and `[onError(handleValidationError)]` (client). The validation interceptor rewrites `BAD_REQUEST + ValidationError` into `INPUT_VALIDATION_FAILED` (422) and output validation errors into `OUTPUT_VALIDATION_FAILED`. `logError` resolves a `getLogger(['api'])` LogTape category — never `console.*` directly. See "Logging" section.
|
||||||
- OpenAPI/Scalar: docs at `/api/docs`, spec at `/api/spec.json` (handler prefix `/api`, plugin paths `/docs` and `/spec.json`).
|
- OpenAPI/Scalar: docs at `/api/docs`, spec at `/api/spec.json` (handler prefix `/api`, plugin paths `/docs` and `/spec.json`).
|
||||||
- **SSR isomorphism** (`src/client/orpc.ts`): `createIsomorphicFn().server(createRouterClient(...)).client(new RPCLink(...))`. Server branch reads `getRequestHeaders()` for context; client branch POSTs to `${origin}/api/rpc`.
|
- **SSR isomorphism** (`src/client/orpc.ts`): `createIsomorphicFn().server(createRouterClient(...)).client(new RPCLink(...))`. Server branch reads `getRequestHeaders()` for context; client branch POSTs to `${origin}/api/rpc`.
|
||||||
- **Mutation invalidation is colocated at the call site** via `mutationOptions({ onSuccess })`, not in `src/client/orpc.ts`. `orpc` in `src/client/orpc.ts` is a plain `createTanstackQueryUtils(client)` — no `experimental_defaults`. Per-feature query helpers live in `src/client/queries/<feature>.ts` (e.g. `useInvalidateTodos`); routes/components compose those hooks rather than holding query keys inline. See `src/client/queries/todo.ts` + `src/routes/index.tsx` for the canonical shape.
|
- **Mutation invalidation is colocated at the call site** via `mutationOptions({ onSuccess })`, not in `src/client/orpc.ts`. `orpc` in `src/client/orpc.ts` is a plain `createTanstackQueryUtils(client)` — no `experimental_defaults`. Per-feature query helpers live in `src/client/queries/<feature>.ts` (e.g. `useInvalidateTodos`); routes/components compose those hooks rather than holding query keys inline. See `src/client/queries/todo.ts` + `src/routes/index.tsx` for the canonical shape.
|
||||||
@@ -120,7 +121,25 @@ Path alias: `@/* → src/*`. For files outside `src/` use `@/../<file>` (example
|
|||||||
|
|
||||||
## Env
|
## Env
|
||||||
|
|
||||||
`src/env.ts` via `@t3-oss/env-core`. Server: `DATABASE_URL` (required, `z.url()`). `client: {}` is empty by default — any client-side env must be `VITE_`-prefixed. Never commit `.env`.
|
`src/env.ts` via `@t3-oss/env-core`. Server: `DATABASE_URL` (required, `z.url()`), `LOG_LEVEL` (`trace|debug|info|warning|error|fatal`, default `info`), `LOG_FORMAT` (`pretty|json`, default = TTY ? `pretty` : `json`), `LOG_DB` (`stringbool`, default `false` — flips on Drizzle SQL query logging). `client: {}` is empty by default — any client-side env must be `VITE_`-prefixed. Never commit `.env`.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
All server-side logging goes through `src/server/logger.ts`, a thin wrapper over [LogTape](https://logtape.org/). The module configures LogTape on import (via `configureSync`, no top-level await — works under `--bytecode`) and re-exports `getLogger`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getLogger } from '@/server/logger'
|
||||||
|
const logger = getLogger(['feature', 'subsystem'])
|
||||||
|
logger.info('Created todo {id}', { id })
|
||||||
|
logger.error('DB write failed: {err}', { err })
|
||||||
|
```
|
||||||
|
|
||||||
|
- Categories are hierarchical arrays — they show up as dot-paths in JSON output (`"logger":"feature.subsystem"`) and let you filter by prefix when shipping logs.
|
||||||
|
- The `{name}` placeholders in the message string are filled from the second-arg properties object. Don't string-concatenate or template-literal — that defeats structured logging.
|
||||||
|
- Format is `pretty` (icons + ANSI) on TTY, `json` (one-line JSON) when piped — perfect for Loki/Datadog/CloudWatch ingestion. Override with `LOG_FORMAT`.
|
||||||
|
- Drizzle SQL queries are logged at `debug` under category `['db']` when `LOG_DB=true`, via `@logtape/drizzle-orm`'s `DrizzleLogger` adapter (constructed in `src/server/db/index.ts`).
|
||||||
|
- `src/server/api/interceptors.ts` calls `getLogger(['api']).error(...)` from `logError`. CLI subcommands lazy-import the logger inside `run()` — they are still required to be side-effect-free at module top (citty eager-loads for `--help`).
|
||||||
|
- Bun-specific: `process.env.NODE_ENV` is **inlined at build time** by `bun build --minify` — do NOT branch on it for logger config (use `process.stdout.isTTY` or `LOG_FORMAT` instead). pino is unusable here because its worker-thread transports crash inside the `/$bunfs/` virtual filesystem of compiled binaries; LogTape has zero workers and zero dynamic require, so it ships cleanly into the single binary.
|
||||||
|
|
||||||
## Docker / deploy
|
## Docker / deploy
|
||||||
|
|
||||||
@@ -148,7 +167,7 @@ src/
|
|||||||
│ ├── $.ts # OpenAPI + Scalar; interceptors registered here
|
│ ├── $.ts # OpenAPI + Scalar; interceptors registered here
|
||||||
│ └── rpc.$.ts # RPC; interceptors registered here
|
│ └── rpc.$.ts # RPC; interceptors registered here
|
||||||
├── server/
|
├── server/
|
||||||
│ ├── logger.ts # the only log entrypoint — wrap before swapping to pino/otel
|
│ ├── logger.ts # LogTape `configureSync` + `getLogger` re-export — the only log entrypoint
|
||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ ├── server.ts # the ONLY place to build `os`
|
│ │ ├── server.ts # the ONLY place to build `os`
|
||||||
│ │ ├── context.ts # BaseContext (add per-request fields when you add middlewares)
|
│ │ ├── context.ts # BaseContext (add per-request fields when you add middlewares)
|
||||||
@@ -203,6 +222,6 @@ These keep the starter from setting bad precedents as it grows. Append, don't re
|
|||||||
5. Interceptors (`src/server/api/interceptors.ts`) do cross-cutting error logging, transport normalization, and validation rewrites. They do NOT read business data.
|
5. Interceptors (`src/server/api/interceptors.ts`) do cross-cutting error logging, transport normalization, and validation rewrites. They do NOT read business data.
|
||||||
6. One file per Drizzle table. Relations live in the same file and are exported as `<entity>Relations`. No global `relations.ts`.
|
6. One file per Drizzle table. Relations live in the same file and are exported as `<entity>Relations`. No global `relations.ts`.
|
||||||
7. One router file per feature (`routers/<feature>.router.ts`). Only introduce `routers/<domain>/index.ts` when a domain grows past ~5 router files or needs shared domain helpers.
|
7. One router file per feature (`routers/<feature>.router.ts`). Only introduce `routers/<domain>/index.ts` when a domain grows past ~5 router files or needs shared domain helpers.
|
||||||
8. All server-side logging goes through `src/server/logger.ts`. Do not call `console.error/info/warn` directly from business code.
|
8. All server-side logging goes through `getLogger([...])` from `@/server/logger`. Use a hierarchical category (`['api']`, `['db']`, `['cli', 'migrate']`, etc.) — these become dot-paths in JSON output and let you filter by prefix. Use the `{name}` placeholder + properties form, not string interpolation. `console.*` is forbidden in business code.
|
||||||
9. CLI subcommand modules keep top-level imports to `citty` + Node built-ins. Env, db, and server code are `await import(...)`-ed inside `run()` (see `bin.ts` comment for why).
|
9. CLI subcommand modules keep top-level imports to `citty` + Node built-ins. Env, db, and server code are `await import(...)`-ed inside `run()` (see `bin.ts` comment for why).
|
||||||
10. Every new business feature ships with at least one `bun test` covering a contract schema, a pure helper, or a router behavior.
|
10. Every new business feature ships with at least one `bun test` covering a contract schema, a pure helper, or a router behavior.
|
||||||
|
|||||||
@@ -5,40 +5,43 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "fullstack-starter",
|
"name": "fullstack-starter",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@orpc/client": "latest",
|
"@logtape/drizzle-orm": "^2.0.5",
|
||||||
"@orpc/contract": "latest",
|
"@logtape/logtape": "^2.0.5",
|
||||||
"@orpc/openapi": "latest",
|
"@logtape/pretty": "^2.0.5",
|
||||||
"@orpc/server": "latest",
|
"@orpc/client": "^1.14.0",
|
||||||
"@orpc/tanstack-query": "latest",
|
"@orpc/contract": "^1.14.0",
|
||||||
"@orpc/zod": "latest",
|
"@orpc/openapi": "^1.14.0",
|
||||||
"@t3-oss/env-core": "latest",
|
"@orpc/server": "^1.14.0",
|
||||||
"@tanstack/react-query": "latest",
|
"@orpc/tanstack-query": "^1.14.0",
|
||||||
"@tanstack/react-router": "latest",
|
"@orpc/zod": "^1.14.0",
|
||||||
"@tanstack/react-router-ssr-query": "latest",
|
"@t3-oss/env-core": "^0.13.11",
|
||||||
"@tanstack/react-start": "latest",
|
"@tanstack/react-query": "^5.100.1",
|
||||||
"citty": "latest",
|
"@tanstack/react-router": "^1.168.24",
|
||||||
"drizzle-orm": "latest",
|
"@tanstack/react-router-ssr-query": "^1.166.11",
|
||||||
"drizzle-zod": "latest",
|
"@tanstack/react-start": "^1.167.48",
|
||||||
"postgres": "latest",
|
"citty": "^0.2.2",
|
||||||
"react": "latest",
|
"drizzle-orm": "0.45.2",
|
||||||
"react-dom": "latest",
|
"drizzle-zod": "^0.8.3",
|
||||||
"uuid": "latest",
|
"postgres": "^3.4.9",
|
||||||
"zod": "latest",
|
"react": "^19.2.5",
|
||||||
|
"react-dom": "^19.2.5",
|
||||||
|
"uuid": "^14.0.0",
|
||||||
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "latest",
|
"@biomejs/biome": "^2.4.13",
|
||||||
"@tailwindcss/vite": "latest",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@tanstack/devtools-vite": "latest",
|
"@tanstack/devtools-vite": "^0.6.0",
|
||||||
"@tanstack/react-devtools": "latest",
|
"@tanstack/react-devtools": "^0.10.2",
|
||||||
"@tanstack/react-query-devtools": "latest",
|
"@tanstack/react-query-devtools": "^5.100.1",
|
||||||
"@tanstack/react-router-devtools": "latest",
|
"@tanstack/react-router-devtools": "^1.166.13",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "^1.3.13",
|
||||||
"@vitejs/plugin-react": "latest",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"drizzle-kit": "latest",
|
"drizzle-kit": "0.31.10",
|
||||||
"nitro": "npm:nitro-nightly@latest",
|
"nitro": "npm:nitro-nightly@3.0.1-20260424-182106-f8cf6ccc",
|
||||||
"tailwindcss": "latest",
|
"tailwindcss": "^4.2.4",
|
||||||
"typescript": "latest",
|
"typescript": "^6.0.3",
|
||||||
"vite": "latest",
|
"vite": "^8.0.10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -173,6 +176,12 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@logtape/drizzle-orm": ["@logtape/drizzle-orm@2.0.5", "", { "peerDependencies": { "@logtape/logtape": "^2.0.5" } }, "sha512-woM4x9J7B4XzoQTY1bzR6uJVlqezn6oY6flV+rDD6lsuwP2llNVvptgCOGjRutDye+6WZldSUIGe7UFK/faBzA=="],
|
||||||
|
|
||||||
|
"@logtape/logtape": ["@logtape/logtape@2.0.5", "", {}, "sha512-UizDkh20ZPJVOddRxG1F77WhHdlNl/sbQgoO8T534R7XvUBMAJ9En9f35u+meW2tRsNLvjz6R87Zanwf53tspQ=="],
|
||||||
|
|
||||||
|
"@logtape/pretty": ["@logtape/pretty@2.0.5", "", { "peerDependencies": { "@logtape/logtape": "^2.0.5" } }, "sha512-jU5pYL0CW0tFmxBS5umMF5VEMq1vXLvkqKrj7KRHnSlb5SrBOSCYl0w4q7FmPPFVADmgTmzVVr6IcIWwK/2Nig=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||||
|
|
||||||
"@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="],
|
"@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="],
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logtape/drizzle-orm": "^2.0.5",
|
||||||
|
"@logtape/logtape": "^2.0.5",
|
||||||
|
"@logtape/pretty": "^2.0.5",
|
||||||
"@orpc/client": "^1.14.0",
|
"@orpc/client": "^1.14.0",
|
||||||
"@orpc/contract": "^1.14.0",
|
"@orpc/contract": "^1.14.0",
|
||||||
"@orpc/openapi": "^1.14.0",
|
"@orpc/openapi": "^1.14.0",
|
||||||
|
|||||||
+8
-5
@@ -6,16 +6,19 @@ export default defineCommand({
|
|||||||
description: 'Apply pending database migrations',
|
description: 'Apply pending database migrations',
|
||||||
},
|
},
|
||||||
async run() {
|
async run() {
|
||||||
const [{ env }, { drizzle }, { sql }, { embeddedMigrations }, { createHash }] = await Promise.all([
|
const [{ env }, { drizzle }, { sql }, { embeddedMigrations }, { createHash }, { getLogger }] = await Promise.all([
|
||||||
import('@/env'),
|
import('@/env'),
|
||||||
import('drizzle-orm/postgres-js'),
|
import('drizzle-orm/postgres-js'),
|
||||||
import('drizzle-orm'),
|
import('drizzle-orm'),
|
||||||
import('@/server/db/migrations.gen'),
|
import('@/server/db/migrations.gen'),
|
||||||
import('node:crypto'),
|
import('node:crypto'),
|
||||||
|
import('@/server/logger'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const logger = getLogger(['cli', 'migrate'])
|
||||||
|
|
||||||
if (embeddedMigrations.length === 0) {
|
if (embeddedMigrations.length === 0) {
|
||||||
console.log('No migrations bundled into this binary.')
|
logger.info('No migrations bundled into this binary.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +54,11 @@ export default defineCommand({
|
|||||||
const appliedWhens = new Set(applied.map((r) => Number(r.created_at)))
|
const appliedWhens = new Set(applied.map((r) => Number(r.created_at)))
|
||||||
const pending = embeddedMigrations.filter((m) => !appliedWhens.has(m.when))
|
const pending = embeddedMigrations.filter((m) => !appliedWhens.has(m.when))
|
||||||
if (pending.length === 0) {
|
if (pending.length === 0) {
|
||||||
console.log('Database is up to date.')
|
logger.info('Database is up to date.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Applying ${pending.length} migration(s)...`)
|
logger.info('Applying {count} migration(s)...', { count: pending.length })
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
for (const m of pending) {
|
for (const m of pending) {
|
||||||
for (const rawStmt of m.sql.split('--> statement-breakpoint')) {
|
for (const rawStmt of m.sql.split('--> statement-breakpoint')) {
|
||||||
@@ -68,7 +71,7 @@ export default defineCommand({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log('Migrations applied.')
|
logger.info('Migrations applied.')
|
||||||
} finally {
|
} finally {
|
||||||
await db.$client.end()
|
await db.$client.end()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { z } from 'zod'
|
|||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
server: {
|
server: {
|
||||||
DATABASE_URL: z.url({ protocol: /^postgres(ql)?$/ }),
|
DATABASE_URL: z.url({ protocol: /^postgres(ql)?$/ }),
|
||||||
|
LOG_DB: z.stringbool().default(false),
|
||||||
|
LOG_FORMAT: z.enum(['pretty', 'json']).optional(),
|
||||||
|
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warning', 'error', 'fatal']).default('info'),
|
||||||
},
|
},
|
||||||
clientPrefix: 'VITE_',
|
clientPrefix: 'VITE_',
|
||||||
client: {},
|
client: {},
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { ORPCError, ValidationError } from '@orpc/server'
|
import { ORPCError, ValidationError } from '@orpc/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { logger } from '@/server/logger'
|
import { getLogger } from '@/server/logger'
|
||||||
|
|
||||||
|
const logger = getLogger(['api'])
|
||||||
|
|
||||||
export const logError = (error: unknown) => {
|
export const logError = (error: unknown) => {
|
||||||
logger.error(error)
|
logger.error('Unhandled error in ORPC handler: {error}', { error })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleValidationError = (error: unknown) => {
|
export const handleValidationError = (error: unknown) => {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { DrizzleLogger } from '@logtape/drizzle-orm'
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||||
import { env } from '@/env'
|
import { env } from '@/env'
|
||||||
import * as schema from '@/server/db/schema'
|
import * as schema from '@/server/db/schema'
|
||||||
|
import { getLogger } from '@/server/logger'
|
||||||
|
|
||||||
export const db = drizzle({
|
export const db = drizzle({
|
||||||
connection: env.DATABASE_URL,
|
connection: env.DATABASE_URL,
|
||||||
schema,
|
schema,
|
||||||
|
logger: env.LOG_DB ? new DrizzleLogger(getLogger(['db'])) : false,
|
||||||
})
|
})
|
||||||
|
|||||||
+19
-5
@@ -1,5 +1,19 @@
|
|||||||
export const logger = {
|
import { configureSync, getConsoleSink, getJsonLinesFormatter, getLogger, type LogLevel } from '@logtape/logtape'
|
||||||
error: (error: unknown) => console.error(error),
|
import { prettyFormatter } from '@logtape/pretty'
|
||||||
warn: (...args: unknown[]) => console.warn(...args),
|
import { env } from '@/env'
|
||||||
info: (...args: unknown[]) => console.info(...args),
|
|
||||||
}
|
const format = env.LOG_FORMAT ?? (process.stdout.isTTY ? 'pretty' : 'json')
|
||||||
|
|
||||||
|
configureSync({
|
||||||
|
reset: true,
|
||||||
|
sinks: {
|
||||||
|
console: getConsoleSink({ formatter: format === 'pretty' ? prettyFormatter : getJsonLinesFormatter() }),
|
||||||
|
},
|
||||||
|
loggers: [
|
||||||
|
{ category: [], lowestLevel: env.LOG_LEVEL, sinks: ['console'] },
|
||||||
|
{ category: ['logtape', 'meta'], lowestLevel: 'warning', sinks: ['console'] },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type { LogLevel }
|
||||||
|
export { getLogger }
|
||||||
|
|||||||
Reference in New Issue
Block a user