feat(server): implement unified structured logging with pino and enforce usage
Introduce `serverLogger` utility based on pino for consistent, structured logging across all server and API modules. Replace direct console logging with `serverLogger` and add request-scoped logging helpers. Update environment variable handling, documentation, and deployment guides to reflect new logging configuration (`LOG_FORMAT`, `LOG_LEVEL`, `COMPUTE_LOG_LEVEL`). Enforce no-console in server code via ESLint and add pino/pino-pretty dependencies. This change standardizes log output, improves observability, and prepares the codebase for ingestion by log platforms.
This commit is contained in:
parent
7732426963
commit
59d0101c0c
54 changed files with 416 additions and 193 deletions
|
|
@ -1,4 +1,10 @@
|
|||
|
||||
# Logging
|
||||
# pretty (default): human-readable logs
|
||||
# json: structured logs for log platforms
|
||||
# LOG_FORMAT=pretty
|
||||
# LOG_LEVEL=info
|
||||
|
||||
# Local / OpenAI TTS API Configuration (default)
|
||||
# Suggest using https://github.com/remsky/Kokoro-FastAPI
|
||||
#
|
||||
|
|
@ -93,6 +99,8 @@ IMPORT_LIBRARY_DIRS=
|
|||
# EMBEDDED_NATS_STORE_DIR=docstore/nats/jetstream
|
||||
# `NATS_URL` here is for embedded startup only. In external worker mode, set `NATS_URL` on the worker service env.
|
||||
# NATS_URL=nats://127.0.0.1:4222
|
||||
# Optional worker log level:
|
||||
# COMPUTE_LOG_LEVEL=info
|
||||
# Optional shared compute tuning:
|
||||
# COMPUTE_JOB_CONCURRENCY=1
|
||||
# COMPUTE_WHISPER_TIMEOUT_MS=30000
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
# - Railway/Render/Fly/etc: platform injects PORT
|
||||
# COMPUTE_WORKER_HOST=0.0.0.0
|
||||
# PORT=8081
|
||||
# COMPUTE_LOG_FORMAT=pretty
|
||||
# LOG_FORMAT=pretty
|
||||
# COMPUTE_LOG_LEVEL=info
|
||||
|
||||
# Must match app env when app uses COMPUTE_WORKER_URL
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ function parseBoolEnv(name: string, fallback: boolean): boolean {
|
|||
}
|
||||
|
||||
function buildLoggerConfig(): boolean | Record<string, unknown> {
|
||||
const format = (process.env.COMPUTE_LOG_FORMAT?.trim().toLowerCase() || 'pretty');
|
||||
const format = (process.env.LOG_FORMAT?.trim().toLowerCase() || 'pretty');
|
||||
const level = process.env.COMPUTE_LOG_LEVEL?.trim() || 'info';
|
||||
if (format === 'json') {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Common optional:
|
|||
- `S3_PREFIX=openreader`
|
||||
- `COMPUTE_WORKER_HOST=0.0.0.0`
|
||||
- `PORT=8081` (local/manual; on Railway platform injects this)
|
||||
- `COMPUTE_LOG_FORMAT=pretty` (default) or `json`
|
||||
- `LOG_FORMAT=pretty` (default) or `json`
|
||||
|
||||
Advanced tuning (usually leave unset unless you need overrides):
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,20 @@ ADMIN_EMAILS=you@example.com # comma-separated; admins manage TTS + features in
|
|||
COMPUTE_WORKER_URL=https://<railway-worker-domain>
|
||||
COMPUTE_WORKER_TOKEN=...
|
||||
|
||||
# Logging (recommended for Vercel log ingestion)
|
||||
LOG_FORMAT=json
|
||||
LOG_LEVEL=info
|
||||
|
||||
# First-boot seed for the TTS shared provider (optional; manage in-app afterwards)
|
||||
# API_KEY=your_replicate_key
|
||||
# API_BASE only needed for OpenAI-compatible self-hosted providers
|
||||
```
|
||||
|
||||
If you also run an external worker service (for example Railway), set these there too:
|
||||
|
||||
- `LOG_FORMAT=json`
|
||||
- `COMPUTE_LOG_LEVEL=info`
|
||||
|
||||
:::note Env vars vs. admin panel (important for Vercel)
|
||||
`API_KEY` / `API_BASE` are one-shot bootstrap seeds on first deploy. After boot, manage providers and site features in **Settings → Admin**. Changes there apply on refresh without a redeploy. See [Admin Panel](../configure/admin-panel).
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -13,12 +13,10 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o
|
|||
|
||||
| Variable | Area | Default | When to set |
|
||||
| --- | --- | --- | --- |
|
||||
| `ADMIN_EMAILS` | Auth/Admin | empty | Comma-separated emails auto-promoted to admin (requires auth enabled) |
|
||||
| `LOG_FORMAT` | Runtime logging | `pretty` | Set `json` for structured logs; shared by app server + compute worker |
|
||||
| `LOG_LEVEL` | Runtime logging | `info` | Set app server log level |
|
||||
| `API_BASE` | Legacy bootstrap seed | none | Optional first-boot seed into `default-openai`; then manage in Settings → Admin → Shared providers |
|
||||
| `API_KEY` | Legacy bootstrap seed | none | Optional first-boot seed into `default-openai`; then manage in Settings → Admin → Shared providers |
|
||||
| `RUNTIME_SEED_*` runtime seeds | Legacy bootstrap seed | varies | Optional first-boot seeds for site features; then manage in Settings → Admin → Site features |
|
||||
| `RUNTIME_SEED_CHANGELOG_FEED_URL` | Legacy bootstrap seed | `https://docs.openreader.richardr.dev/changelog/manifest.json` | Optional first-boot seed for changelog feed URL; then manage in Settings → Admin → Site features |
|
||||
| `RUNTIME_SEED_ENABLE_USER_SIGNUPS` | Legacy bootstrap seed | `true` | Optional first-boot seed for whether new accounts can be created; then manage in Settings → Admin → Site features |
|
||||
| `TTS_CACHE_MAX_SIZE_BYTES` | TTS caching | `268435456` (256 MB) | Tune in-memory TTS cache size |
|
||||
| `TTS_CACHE_TTL_MS` | TTS caching | `1800000` (30 min) | Tune in-memory TTS cache TTL |
|
||||
| `TTS_MAX_RETRIES` | TTS retry | `2` | Tune retry attempts for upstream 429/5xx |
|
||||
|
|
@ -38,6 +36,7 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o
|
|||
| `GITHUB_CLIENT_ID` | Auth/OAuth | unset | Set with `GITHUB_CLIENT_SECRET` to enable GitHub sign-in |
|
||||
| `GITHUB_CLIENT_SECRET` | Auth/OAuth | unset | Set with `GITHUB_CLIENT_ID` to enable GitHub sign-in |
|
||||
| `DISABLE_AUTH_RATE_LIMIT` | Rate limiting | `false` | Set `true` to disable auth-layer rate limiting |
|
||||
| `ADMIN_EMAILS` | Auth/Admin | empty | Comma-separated emails auto-promoted to admin (requires auth enabled) |
|
||||
| `POSTGRES_URL` | Database | unset (SQLite mode) | Set to switch metadata/auth DB to Postgres |
|
||||
| `USE_EMBEDDED_WEED_MINI` | Storage | `true` when unset | Set `false` to use external S3-compatible storage only |
|
||||
| `WEED_MINI_DIR` | Storage | `docstore/seaweedfs` | Override embedded SeaweedFS data directory |
|
||||
|
|
@ -60,6 +59,7 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o
|
|||
| `EMBEDDED_NATS_MONITOR_PORT` | Heavy compute backend | `8222` | Override embedded NATS monitor port |
|
||||
| `EMBEDDED_NATS_STORE_DIR` | Heavy compute backend | `docstore/nats/jetstream` | Override embedded JetStream storage directory |
|
||||
| `NATS_URL` | Heavy compute backend | `nats://127.0.0.1:4222` in embedded startup | Optional override for embedded startup or required on standalone worker service |
|
||||
| `COMPUTE_LOG_LEVEL` | Heavy compute backend | `info` | Compute worker log level |
|
||||
| `COMPUTE_JOB_CONCURRENCY` | Heavy compute backend | `1` | Worker-side shared compute concurrency cap |
|
||||
| `COMPUTE_WHISPER_TIMEOUT_MS` | Heavy compute backend | `30000` | Shared whisper alignment timeout budget (worker + worker client wait budget) |
|
||||
| `COMPUTE_PDF_TIMEOUT_MS` | Heavy compute backend | `300000` | Shared PDF idle-timeout budget (worker + worker client wait budget) |
|
||||
|
|
@ -67,9 +67,35 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o
|
|||
| `PDF_LAYOUT_MODEL_BASE_URL` | PDF layout model | PP-DocLayoutV3 ONNX base URL | Optional base URL override for `ensureModel()` |
|
||||
| `WHISPER_MODEL_BASE_URL` | Whisper ONNX model | onnx-community defaults | Optional base URL override for ONNX whisper-base_timestamped q4 downloads |
|
||||
| `FFMPEG_BIN` | Audio runtime | auto-detected (`ffmpeg-static`) | Override ffmpeg binary path |
|
||||
| `RUNTIME_SEED_*` runtime seeds | Legacy bootstrap seed | varies | Optional first-boot seeds for site features; then manage in Settings → Admin → Site features |
|
||||
| `RUNTIME_SEED_ENABLE_DOCX_CONVERSION` | Legacy bootstrap seed | `true` | Optional first-boot seed to enable/disable DOCX conversion UI |
|
||||
| `RUNTIME_SEED_ENABLE_DESTRUCTIVE_DELETE_ACTIONS` | Legacy bootstrap seed | `true` | Optional first-boot seed to show/hide destructive delete actions |
|
||||
| `RUNTIME_SEED_ENABLE_TTS_PROVIDERS_TAB` | Legacy bootstrap seed | `true` | Optional first-boot seed to show/hide user TTS providers tab |
|
||||
| `RUNTIME_SEED_CHANGELOG_FEED_URL` | Legacy bootstrap seed | `https://docs.openreader.richardr.dev/changelog/manifest.json` | Optional first-boot seed for changelog feed URL; then manage in Settings → Admin → Site features |
|
||||
| `RUNTIME_SEED_ENABLE_USER_SIGNUPS` | Legacy bootstrap seed | `true` | Optional first-boot seed for whether new accounts can be created; then manage in Settings → Admin → Site features |
|
||||
| `RUNTIME_SEED_RESTRICT_USER_API_KEYS` | Legacy bootstrap seed | runtime-dependent | Optional first-boot seed to restrict per-user BYOK |
|
||||
| `RUNTIME_SEED_DEFAULT_TTS_PROVIDER` | Legacy bootstrap seed | `custom-openai` | Optional first-boot seed for default TTS provider slug |
|
||||
| `RUNTIME_SEED_ENABLE_AUDIOBOOK_EXPORT` | Legacy bootstrap seed | `true` | Optional first-boot seed to enable audiobook export UI |
|
||||
|
||||
|
||||
|
||||
## Runtime Logging
|
||||
|
||||
### LOG_FORMAT
|
||||
|
||||
Controls log output format for server-side Pino loggers.
|
||||
|
||||
- Default: `pretty`
|
||||
- Allowed values: `pretty`, `json`
|
||||
- Applies to app server and compute worker
|
||||
- Recommended in production (Vercel + external worker): `json`
|
||||
|
||||
### LOG_LEVEL
|
||||
|
||||
App server log level.
|
||||
|
||||
- Default: `info`
|
||||
|
||||
## TTS Provider and Request Behavior
|
||||
|
||||
### API_BASE
|
||||
|
|
@ -242,6 +268,8 @@ Switches metadata/auth storage from SQLite to Postgres.
|
|||
- Set: Postgres mode
|
||||
- Related docs: [Database](../configure/database)
|
||||
|
||||
### Embedded SeaweedFS weed mini config
|
||||
|
||||
### USE_EMBEDDED_WEED_MINI
|
||||
|
||||
Controls embedded SeaweedFS startup.
|
||||
|
|
@ -264,6 +292,8 @@ Maximum seconds to wait for embedded SeaweedFS startup.
|
|||
- Default: `20`
|
||||
- Related docs: [Object / Blob Storage](../configure/object-blob-storage)
|
||||
|
||||
### S3 storage config
|
||||
|
||||
### S3_ACCESS_KEY_ID
|
||||
|
||||
Access key for S3-compatible storage.
|
||||
|
|
@ -410,6 +440,13 @@ NATS connection URL used by compute worker runtime.
|
|||
- For embedded startup, this is optional; startup supplies the default value.
|
||||
- Worker-side only in external mode: set on worker service env, not app/root `.env`.
|
||||
|
||||
### COMPUTE_LOG_LEVEL
|
||||
|
||||
Compute worker log level.
|
||||
|
||||
- Default: `info`
|
||||
- In standalone mode, set this on the worker service env.
|
||||
|
||||
### COMPUTE_JOB_CONCURRENCY
|
||||
|
||||
Worker-side shared compute concurrency cap.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ const eslintConfig = [
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/app/api/**/*.ts", "src/lib/server/**/*.ts"],
|
||||
rules: {
|
||||
"no-console": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@
|
|||
"openai": "^6.37.0",
|
||||
"pdfjs-dist": "4.8.69",
|
||||
"pg": "^8.20.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"react": "^19.2.6",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ importers:
|
|||
pg:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
pino:
|
||||
specifier: ^10.3.1
|
||||
version: 10.3.1
|
||||
pino-pretty:
|
||||
specifier: ^13.1.3
|
||||
version: 13.1.3
|
||||
react:
|
||||
specifier: ^19.2.6
|
||||
version: 19.2.6
|
||||
|
|
@ -4081,9 +4087,6 @@ packages:
|
|||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
||||
|
||||
pino-abstract-transport@3.0.0:
|
||||
resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
|
||||
|
||||
|
|
@ -4098,10 +4101,6 @@ packages:
|
|||
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
|
||||
hasBin: true
|
||||
|
||||
pino@9.14.0:
|
||||
resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==}
|
||||
hasBin: true
|
||||
|
||||
pirates@4.0.7:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -4680,9 +4679,6 @@ packages:
|
|||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
thread-stream@4.2.0:
|
||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
|
@ -7997,7 +7993,7 @@ snapshots:
|
|||
fast-json-stringify: 6.4.0
|
||||
find-my-way: 9.6.0
|
||||
light-my-request: 6.6.0
|
||||
pino: 9.14.0
|
||||
pino: 10.3.1
|
||||
process-warning: 5.0.0
|
||||
rfdc: 1.4.1
|
||||
secure-json-parse: 4.1.0
|
||||
|
|
@ -9133,10 +9129,6 @@ snapshots:
|
|||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
pino-abstract-transport@3.0.0:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
|
@ -9173,20 +9165,6 @@ snapshots:
|
|||
sonic-boom: 4.2.1
|
||||
thread-stream: 4.2.0
|
||||
|
||||
pino@9.14.0:
|
||||
dependencies:
|
||||
'@pinojs/redact': 0.4.0
|
||||
atomic-sleep: 1.0.0
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 2.0.0
|
||||
pino-std-serializers: 7.1.0
|
||||
process-warning: 5.0.0
|
||||
quick-format-unescaped: 4.0.4
|
||||
real-require: 0.2.0
|
||||
safe-stable-stringify: 2.5.0
|
||||
sonic-boom: 4.2.1
|
||||
thread-stream: 3.1.0
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
playwright-core@1.60.0: {}
|
||||
|
|
@ -9933,10 +9911,6 @@ snapshots:
|
|||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
thread-stream@3.1.0:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
thread-stream@4.2.0:
|
||||
dependencies:
|
||||
real-require: 1.0.0
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@ async function main() {
|
|||
}
|
||||
|
||||
const runtimeEnv = { ...process.env };
|
||||
runtimeEnv.LOG_FORMAT = withDefault(runtimeEnv.LOG_FORMAT, 'pretty');
|
||||
let weedProc = null;
|
||||
let weedExitPromise = Promise.resolve();
|
||||
let natsProc = null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { auth } from '@/lib/server/auth/auth';
|
|||
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { deleteUserStorageData } from '@/lib/server/user/data-cleanup';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export async function DELETE() {
|
||||
if (!isAuthEnabled() || !auth) {
|
||||
|
|
@ -28,7 +29,7 @@ export async function DELETE() {
|
|||
try {
|
||||
await deleteUserStorageData(session.user.id, testNamespace);
|
||||
} catch (error) {
|
||||
console.error('[account-delete] Failed to clean up namespaced user storage before deletion:', error);
|
||||
serverLogger.error({ err: error }, '[account-delete] Failed to clean up namespaced user storage before deletion:');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ export async function DELETE() {
|
|||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete account:', error);
|
||||
serverLogger.error({ err: error }, 'Failed to delete account:');
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete account' },
|
||||
{ status: 500 }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdminContext } from '@/lib/server/auth/admin';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
AdminProviderError,
|
||||
deleteAdminProvider,
|
||||
|
|
@ -41,7 +42,7 @@ export async function PUT(
|
|||
if (error instanceof AdminProviderError) {
|
||||
return NextResponse.json({ error: error.message }, { status: error.status });
|
||||
}
|
||||
console.error('[admin/providers/:id] update failed:', error);
|
||||
serverLogger.error({ err: error }, '[admin/providers/:id] update failed:');
|
||||
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +62,7 @@ export async function DELETE(
|
|||
if (error instanceof AdminProviderError) {
|
||||
return NextResponse.json({ error: error.message }, { status: error.status });
|
||||
}
|
||||
console.error('[admin/providers/:id] delete failed:', error);
|
||||
serverLogger.error({ err: error }, '[admin/providers/:id] delete failed:');
|
||||
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdminContext } from '@/lib/server/auth/admin';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
AdminProviderError,
|
||||
createAdminProvider,
|
||||
|
|
@ -46,7 +47,7 @@ export async function POST(req: NextRequest) {
|
|||
if (error instanceof AdminProviderError) {
|
||||
return NextResponse.json({ error: error.message }, { status: error.status });
|
||||
}
|
||||
console.error('[admin/providers] create failed:', error);
|
||||
serverLogger.error({ err: error }, '[admin/providers] create failed:');
|
||||
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { requireAuthContext } from '@/lib/server/auth/auth';
|
|||
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limit/rate-limiter';
|
||||
import { getClientIp } from '@/lib/server/rate-limit/request-ip';
|
||||
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/rate-limit/device-id';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
deleteAudiobookObject,
|
||||
getAudiobookObjectBuffer,
|
||||
|
|
@ -185,7 +186,7 @@ async function runFFmpeg(args: string[], signal?: AbortSignal): Promise<void> {
|
|||
}
|
||||
|
||||
ffmpeg.stderr.on('data', (data) => {
|
||||
console.error(`ffmpeg stderr: ${data}`);
|
||||
serverLogger.error(`ffmpeg stderr: ${data}`);
|
||||
});
|
||||
|
||||
ffmpeg.on('close', (code) => {
|
||||
|
|
@ -325,7 +326,7 @@ export async function POST(request: NextRequest) {
|
|||
fallbackProviderRef: runtimeConfig.defaultTtsProvider,
|
||||
});
|
||||
if (!existingResult.settings) {
|
||||
console.error('Invalid audiobook.meta.json settings payload', { bookId, storageUserId });
|
||||
serverLogger.error({ bookId, storageUserId }, 'Invalid audiobook.meta.json settings payload');
|
||||
return NextResponse.json({ error: 'Invalid audiobook metadata settings' }, { status: 500 });
|
||||
}
|
||||
normalizedExistingSettings = normalizeNativeSpeedForSettings(existingResult.settings);
|
||||
|
|
@ -399,11 +400,11 @@ export async function POST(request: NextRequest) {
|
|||
testNamespace,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist migrated audiobook metadata settings', {
|
||||
serverLogger.warn({
|
||||
bookId,
|
||||
storageUserId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}, 'Failed to persist migrated audiobook metadata settings');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -608,7 +609,7 @@ export async function POST(request: NextRequest) {
|
|||
request.signal,
|
||||
);
|
||||
} catch (copyError) {
|
||||
console.warn('Chapter remux failed; falling back to mp3 re-encode:', copyError);
|
||||
serverLogger.warn({ err: copyError }, 'Chapter remux failed; falling back to mp3 re-encode:');
|
||||
await runFFmpeg(
|
||||
chapterEncodeArgs(inputPath, chapterOutputTempPath, format, postSpeed, titleTag),
|
||||
request.signal,
|
||||
|
|
@ -728,7 +729,7 @@ export async function POST(request: NextRequest) {
|
|||
return response;
|
||||
}
|
||||
|
||||
console.error('Error processing audio chapter:', error);
|
||||
serverLogger.error({ err: error }, 'Error processing audio chapter:');
|
||||
const response = NextResponse.json({ error: 'Failed to process audio chapter' }, { status: 500 });
|
||||
attachDeviceIdCookie(response, deviceIdToSet, didCreateDeviceIdCookie);
|
||||
return response;
|
||||
|
|
@ -823,7 +824,7 @@ export async function GET(request: NextRequest) {
|
|||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error downloading chapter:', error);
|
||||
serverLogger.error({ err: error }, 'Error downloading chapter:');
|
||||
return NextResponse.json({ error: 'Failed to download chapter' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -891,7 +892,7 @@ export async function DELETE(request: NextRequest) {
|
|||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting chapter:', error);
|
||||
serverLogger.error({ err: error }, 'Error deleting chapter:');
|
||||
return NextResponse.json({ error: 'Failed to delete chapter' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { and, eq, inArray } from 'drizzle-orm';
|
|||
import { db } from '@/db';
|
||||
import { audiobooks, audiobookChapters } from '@/db/schema';
|
||||
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
audiobookPrefix,
|
||||
deleteAudiobookObject,
|
||||
|
|
@ -115,7 +116,7 @@ async function runFFmpeg(args: string[], signal?: AbortSignal): Promise<void> {
|
|||
}
|
||||
|
||||
ffmpeg.stderr.on('data', (data) => {
|
||||
console.error(`ffmpeg stderr: ${data}`);
|
||||
serverLogger.error(`ffmpeg stderr: ${data}`);
|
||||
});
|
||||
|
||||
ffmpeg.on('close', (code) => {
|
||||
|
|
@ -282,7 +283,7 @@ export async function GET(request: NextRequest) {
|
|||
request.signal,
|
||||
);
|
||||
} catch (copyError) {
|
||||
console.warn('MP3 concat copy failed; falling back to re-encode:', copyError);
|
||||
serverLogger.warn({ err: copyError }, 'MP3 concat copy failed; falling back to re-encode:');
|
||||
await runFFmpeg(
|
||||
['-f', 'concat', '-safe', '0', '-i', listPath, '-c:a', 'libmp3lame', '-b:a', '64k', outputPath],
|
||||
request.signal,
|
||||
|
|
@ -311,7 +312,7 @@ export async function GET(request: NextRequest) {
|
|||
request.signal,
|
||||
);
|
||||
} catch (copyError) {
|
||||
console.warn('M4B concat copy failed; falling back to re-encode:', copyError);
|
||||
serverLogger.warn({ err: copyError }, 'M4B concat copy failed; falling back to re-encode:');
|
||||
await runFFmpeg(
|
||||
[
|
||||
'-f',
|
||||
|
|
@ -360,7 +361,7 @@ export async function GET(request: NextRequest) {
|
|||
if ((error as Error)?.message === 'ABORTED' || request.signal.aborted) {
|
||||
return NextResponse.json({ error: 'cancelled' }, { status: 499 });
|
||||
}
|
||||
console.error('Error creating full audiobook:', error);
|
||||
serverLogger.error({ err: error }, 'Error creating full audiobook:');
|
||||
return NextResponse.json({ error: 'Failed to create full audiobook file' }, { status: 500 });
|
||||
} finally {
|
||||
if (workDir) await rm(workDir, { recursive: true, force: true }).catch(() => {});
|
||||
|
|
@ -408,7 +409,7 @@ export async function DELETE(request: NextRequest) {
|
|||
const deleted = await deleteAudiobookPrefix(audiobookPrefix(bookId, storageUserId, testNamespace)).catch(() => 0);
|
||||
return NextResponse.json({ success: true, existed: deleted > 0 });
|
||||
} catch (error) {
|
||||
console.error('Error resetting audiobook:', error);
|
||||
serverLogger.error({ err: error }, 'Error resetting audiobook:');
|
||||
return NextResponse.json({ error: 'Failed to reset audiobook' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/li
|
|||
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope';
|
||||
import type { AudiobookGenerationSettings } from '@/types/client';
|
||||
import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -155,7 +156,7 @@ export async function GET(request: NextRequest) {
|
|||
settings,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching chapters:', error);
|
||||
serverLogger.error({ err: error }, 'Error fetching chapters:');
|
||||
return NextResponse.json({ error: 'Failed to fetch chapters' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { normalizeParseStatus, parseDocumentParseState } from '@/lib/server/docu
|
|||
import { healStaleDocumentParseState } from '@/lib/server/documents/parse-state-healing';
|
||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { createRequestLogger, errorToLog, hashForLog } from '@/lib/server/logger';
|
||||
import type { PdfParseProgress, PdfParseStatus } from '@/types/parsed-pdf';
|
||||
import { parseSseEventId, parseSsePayload } from '@openreader/compute-core';
|
||||
import type { PdfLayoutJobResult, WorkerOperationEvent, WorkerOperationState } from '@openreader/compute-core/api-contracts';
|
||||
|
|
@ -134,6 +135,10 @@ async function syncFromDb(input: {
|
|||
}
|
||||
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
const { logger, requestId } = createRequestLogger({
|
||||
route: '/api/documents/[id]/parsed/events',
|
||||
request: req,
|
||||
});
|
||||
try {
|
||||
if (!isS3Configured()) return s3NotConfiguredResponse();
|
||||
|
||||
|
|
@ -153,6 +158,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
const testNamespace = getOpenReaderTestNamespace(req.headers);
|
||||
const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace);
|
||||
const storageUserId = authCtxOrRes.userId ?? unclaimedUserId;
|
||||
const storageUserIdHash = hashForLog(storageUserId);
|
||||
const allowedUserIds = authCtxOrRes.authEnabled ? [storageUserId, unclaimedUserId] : [unclaimedUserId];
|
||||
|
||||
const row = await loadPreferredRow({
|
||||
|
|
@ -261,9 +267,11 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
}
|
||||
}).catch((error) => {
|
||||
if (closed) return;
|
||||
console.warn('[parsed/events] db resync failed', {
|
||||
logger.warn({
|
||||
event: 'documents.parsed.events.db_resync_failed',
|
||||
documentId: id,
|
||||
storageUserId,
|
||||
storageUserIdHash,
|
||||
requestId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ error: String(error) })}\n\n`));
|
||||
|
|
@ -292,9 +300,10 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
if (!currentOpId) {
|
||||
if (!loggedMissingOpId) {
|
||||
loggedMissingOpId = true;
|
||||
console.warn('[parsed/events] missing worker opId while parse is non-terminal', {
|
||||
logger.warn({
|
||||
event: 'documents.parsed.events.missing_opid_non_terminal',
|
||||
documentId: id,
|
||||
storageUserId,
|
||||
storageUserIdHash,
|
||||
parseStatus: current.parseStatus,
|
||||
requestedOpId,
|
||||
});
|
||||
|
|
@ -331,7 +340,8 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
|
||||
if (!response.ok) {
|
||||
const detail = await response.text().catch(() => '');
|
||||
console.warn('[parsed/events] worker stream request failed', {
|
||||
logger.warn({
|
||||
event: 'documents.parsed.events.worker_stream_request_failed',
|
||||
documentId: id,
|
||||
opId: currentOpId,
|
||||
status: response.status,
|
||||
|
|
@ -341,7 +351,8 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
continue;
|
||||
}
|
||||
if (!response.body) {
|
||||
console.warn('[parsed/events] worker stream response missing body', {
|
||||
logger.warn({
|
||||
event: 'documents.parsed.events.worker_stream_missing_body',
|
||||
documentId: id,
|
||||
opId: currentOpId,
|
||||
});
|
||||
|
|
@ -441,9 +452,10 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
|
||||
void runWorkerProxy()
|
||||
.catch((error) => {
|
||||
console.error('[parsed/events] worker proxy crashed', {
|
||||
logger.error({
|
||||
event: 'documents.parsed.events.worker_proxy_crashed',
|
||||
documentId: id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: errorToLog(error),
|
||||
});
|
||||
if (!closed) {
|
||||
controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ error: String(error) })}\n\n`));
|
||||
|
|
@ -471,7 +483,10 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error streaming parsed PDF progress:', error);
|
||||
logger.error({
|
||||
event: 'documents.parsed.events.route_failed',
|
||||
error: errorToLog(error),
|
||||
});
|
||||
return NextResponse.json({ error: 'Failed to stream parsed PDF progress' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
import { healStaleDocumentParseState } from '@/lib/server/documents/parse-state-healing';
|
||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { createRequestLogger, errorToLog, hashForLog, type ServerLogger } from '@/lib/server/logger';
|
||||
import type { ParsedPdfDocument } from '@/types/parsed-pdf';
|
||||
import type { PdfLayoutJobResult, WorkerOperationState } from '@openreader/compute-core/api-contracts';
|
||||
|
||||
|
|
@ -92,6 +93,7 @@ async function finalizeFromWorkerState(input: {
|
|||
workerState: WorkerOperationState<PdfLayoutJobResult>;
|
||||
row: ParseRow;
|
||||
namespace: string | null;
|
||||
logger: ServerLogger;
|
||||
}): Promise<NextResponse> {
|
||||
const snapshot = snapshotFromWorkerState(input.workerState);
|
||||
|
||||
|
|
@ -159,9 +161,10 @@ async function finalizeFromWorkerState(input: {
|
|||
}
|
||||
|
||||
if (!hasAnyParsedBlocks(parsedDoc)) {
|
||||
console.warn('[documents/parsed] parsed doc has no blocks', {
|
||||
input.logger.warn({
|
||||
event: 'documents.parsed.no_blocks_in_output',
|
||||
documentId: input.row.id,
|
||||
userId: input.row.userId,
|
||||
userIdHash: hashForLog(input.row.userId),
|
||||
parsedJsonKey,
|
||||
});
|
||||
}
|
||||
|
|
@ -176,6 +179,10 @@ async function finalizeFromWorkerState(input: {
|
|||
}
|
||||
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
const { logger } = createRequestLogger({
|
||||
route: '/api/documents/[id]/parsed',
|
||||
request: req,
|
||||
});
|
||||
try {
|
||||
if (!isS3Configured()) return s3NotConfiguredResponse();
|
||||
|
||||
|
|
@ -208,11 +215,13 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
workerState,
|
||||
row,
|
||||
namespace: testNamespace,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
console.warn('[documents/parsed:get] requested opId unavailable', {
|
||||
logger.warn({
|
||||
event: 'documents.parsed.requested_op_unavailable',
|
||||
documentId: id,
|
||||
userId: row.userId,
|
||||
userIdHash: hashForLog(row.userId),
|
||||
opId: requestedOpId,
|
||||
});
|
||||
}
|
||||
|
|
@ -235,6 +244,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
workerState,
|
||||
row,
|
||||
namespace: testNamespace,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -273,9 +283,10 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
}
|
||||
|
||||
if (!hasAnyParsedBlocks(parsedDoc)) {
|
||||
console.warn('[documents/parsed] parsed doc has no blocks', {
|
||||
logger.warn({
|
||||
event: 'documents.parsed.no_blocks_from_blob',
|
||||
documentId: id,
|
||||
userId: row.userId,
|
||||
userIdHash: hashForLog(row.userId),
|
||||
parsedJsonKey: row.parsedJsonKey,
|
||||
});
|
||||
}
|
||||
|
|
@ -294,12 +305,19 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading parsed PDF:', error);
|
||||
logger.error({
|
||||
event: 'documents.parsed.get_failed',
|
||||
error: errorToLog(error),
|
||||
});
|
||||
return NextResponse.json({ error: 'Failed to read parsed PDF' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
const { logger } = createRequestLogger({
|
||||
route: '/api/documents/[id]/parsed',
|
||||
request: req,
|
||||
});
|
||||
try {
|
||||
if (!isS3Configured()) return s3NotConfiguredResponse();
|
||||
|
||||
|
|
@ -367,7 +385,10 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
opId: created.opId,
|
||||
}, { status: 202 });
|
||||
} catch (error) {
|
||||
console.error('Error forcing parsed PDF refresh:', error);
|
||||
logger.error({
|
||||
event: 'documents.parsed.force_refresh_failed',
|
||||
error: errorToLog(error),
|
||||
});
|
||||
return NextResponse.json({ error: 'Failed to force PDF refresh' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/li
|
|||
import { mergeDocumentSettings } from '@/lib/shared/document-settings';
|
||||
import { DEFAULT_DOCUMENT_SETTINGS, type DocumentSettings } from '@/types/document-settings';
|
||||
import { coerceTimestampMs, nowTimestampMs } from '@/lib/shared/timestamps';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -92,7 +93,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
hasStoredSettings: Boolean(row),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading document settings:', error);
|
||||
serverLogger.error({ err: error }, 'Error loading document settings:');
|
||||
return NextResponse.json({ error: 'Failed to load document settings' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +162,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
|
|||
applied: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating document settings:', error);
|
||||
serverLogger.error({ err: error }, 'Error updating document settings:');
|
||||
return NextResponse.json({ error: 'Failed to update document settings' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { contentTypeForName } from '@/lib/server/storage/library-mount';
|
|||
import { getDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
|
||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ export async function GET(req: NextRequest) {
|
|||
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.info('[blob-fallback] download proxy used', { id });
|
||||
serverLogger.info({ id }, '[blob-fallback] download proxy used');
|
||||
|
||||
const rows = (await db
|
||||
.select({ id: documents.id, userId: documents.userId, name: documents.name })
|
||||
|
|
@ -81,7 +82,7 @@ export async function GET(req: NextRequest) {
|
|||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading document content fallback:', error);
|
||||
serverLogger.error({ err: error }, 'Error loading document content fallback:');
|
||||
return NextResponse.json({ error: 'Failed to load document content' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { requireAuthContext } from '@/lib/server/auth/auth';
|
|||
import { isValidDocumentId, presignGet } from '@/lib/server/documents/blobstore';
|
||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -50,7 +51,7 @@ export async function GET(req: NextRequest) {
|
|||
const fallbackUrl = `/api/documents/blob/get/fallback?id=${encodeURIComponent(doc.id)}`;
|
||||
const directUrl = await presignGet(doc.id, testNamespace).catch(() => null);
|
||||
if (!directUrl) {
|
||||
console.warn('[blob-fallback] presign download unavailable, redirecting to proxy fallback', { id: doc.id });
|
||||
serverLogger.warn({ id: doc.id }, '[blob-fallback] presign download unavailable, redirecting to proxy fallback');
|
||||
return NextResponse.redirect(fallbackUrl, {
|
||||
status: 307,
|
||||
headers: { 'Cache-Control': 'no-store' },
|
||||
|
|
@ -62,7 +63,7 @@ export async function GET(req: NextRequest) {
|
|||
headers: { 'Cache-Control': 'no-store' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating document download signature:', error);
|
||||
serverLogger.error({ err: error }, 'Error creating document download signature:');
|
||||
return NextResponse.json({ error: 'Failed to prepare document download' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||
import { presignDocumentPreviewGet } from '@/lib/server/documents/previews-blobstore';
|
||||
import { ensureDocumentPreview } from '@/lib/server/documents/previews';
|
||||
import { validatePreviewRequest } from '../utils';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -50,7 +51,7 @@ export async function GET(req: NextRequest) {
|
|||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error ensuring document preview:', error);
|
||||
serverLogger.error({ err: error }, 'Error ensuring document preview:');
|
||||
return NextResponse.json({ error: 'Failed to ensure document preview' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { and, eq, inArray } from 'drizzle-orm';
|
|||
import { db } from '@/db';
|
||||
import { documents } from '@/db/schema';
|
||||
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
getDocumentRange,
|
||||
isMissingBlobError as isMissingDocumentBlobError,
|
||||
|
|
@ -65,10 +66,10 @@ export async function GET(req: NextRequest) {
|
|||
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.info('[blob-fallback] preview proxy used', {
|
||||
serverLogger.info({
|
||||
id,
|
||||
snippetRequested,
|
||||
});
|
||||
}, '[blob-fallback] preview proxy used');
|
||||
|
||||
const rows = (await db
|
||||
.select({
|
||||
|
|
@ -175,7 +176,7 @@ export async function GET(req: NextRequest) {
|
|||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading document preview fallback:', error);
|
||||
serverLogger.error({ err: error }, 'Error loading document preview fallback:');
|
||||
return NextResponse.json({ error: 'Failed to load document preview' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||
import { presignDocumentPreviewGet } from '@/lib/server/documents/previews-blobstore';
|
||||
import { ensureDocumentPreview } from '@/lib/server/documents/previews';
|
||||
import { validatePreviewRequest } from '../utils';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ export async function GET(req: NextRequest) {
|
|||
|
||||
const directUrl = await presignDocumentPreviewGet(doc.id, testNamespace).catch(() => null);
|
||||
if (!directUrl) {
|
||||
console.warn('[blob-fallback] presign preview unavailable, redirecting to proxy fallback', { id: doc.id });
|
||||
serverLogger.warn({ id: doc.id }, '[blob-fallback] presign preview unavailable, redirecting to proxy fallback');
|
||||
return NextResponse.redirect(fallbackUrl, {
|
||||
status: 307,
|
||||
headers: { 'Cache-Control': 'no-store' },
|
||||
|
|
@ -49,7 +50,7 @@ export async function GET(req: NextRequest) {
|
|||
headers: { 'Cache-Control': 'no-store' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating document preview signature:', error);
|
||||
serverLogger.error({ err: error }, 'Error creating document preview signature:');
|
||||
return NextResponse.json({ error: 'Failed to prepare document preview' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { requireAuthContext } from '@/lib/server/auth/auth';
|
|||
import { isValidDocumentId, putDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -42,15 +43,15 @@ export async function PUT(req: NextRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
console.info('[blob-fallback] upload proxy used', {
|
||||
serverLogger.info({
|
||||
id,
|
||||
contentType,
|
||||
bytes: body.byteLength,
|
||||
});
|
||||
}, '[blob-fallback] upload proxy used');
|
||||
|
||||
return NextResponse.json({ success: true, id });
|
||||
} catch (error) {
|
||||
console.error('Error proxy-uploading document blob:', error);
|
||||
serverLogger.error({ err: error }, 'Error proxy-uploading document blob:');
|
||||
return NextResponse.json({ error: 'Failed to upload document blob' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { requireAuthContext } from '@/lib/server/auth/auth';
|
|||
import { isValidDocumentId, presignPut } from '@/lib/server/documents/blobstore';
|
||||
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ export async function POST(req: NextRequest) {
|
|||
|
||||
return NextResponse.json({ uploads: signed });
|
||||
} catch (error) {
|
||||
console.error('Error creating document upload signatures:', error);
|
||||
serverLogger.error({ err: error }, 'Error creating document upload signatures:');
|
||||
return NextResponse.json({ error: 'Failed to presign uploads' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { stringifyDocumentParseState } from '@/lib/server/documents/parse-state'
|
|||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { putDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
const DOCSTORE_DIR = path.join(process.cwd(), 'docstore');
|
||||
const TEMP_DIR = path.join(DOCSTORE_DIR, 'tmp');
|
||||
|
|
@ -162,7 +163,7 @@ export async function POST(req: NextRequest) {
|
|||
},
|
||||
testNamespace,
|
||||
).catch((error) => {
|
||||
console.error(`Failed to enqueue preview for converted DOCX ${id}:`, error);
|
||||
serverLogger.error({ err: error }, `Failed to enqueue preview for converted DOCX ${id}:`);
|
||||
});
|
||||
|
||||
enqueueParsePdfJob({
|
||||
|
|
@ -184,7 +185,7 @@ export async function POST(req: NextRequest) {
|
|||
await rm(jobDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error converting/uploading DOCX:', error);
|
||||
serverLogger.error({ err: error }, 'Error converting/uploading DOCX:');
|
||||
return NextResponse.json({ error: 'Failed to convert document' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||
import { parseLibraryRoots } from '@/lib/server/storage/library-mount';
|
||||
import type { DocumentType } from '@/types/documents';
|
||||
import { auth } from '@/lib/server/auth/auth';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -111,7 +112,7 @@ export async function GET(req: NextRequest) {
|
|||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth:', error);
|
||||
serverLogger.error({ err: error }, 'Error checking auth:');
|
||||
return NextResponse.json({ error: 'Error checking auth' }, { status: 500 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { db } from '@/db';
|
|||
import { documents } from '@/db/schema';
|
||||
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||
import { safeDocumentName, toDocumentTypeFromName } from '@/lib/server/documents/utils';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
cleanupDocumentPreviewArtifacts,
|
||||
deleteDocumentPreviewRows,
|
||||
|
|
@ -172,7 +173,7 @@ export async function POST(req: NextRequest) {
|
|||
},
|
||||
testNamespace,
|
||||
).catch((error) => {
|
||||
console.error(`Failed to enqueue preview for document ${doc.id}:`, error);
|
||||
serverLogger.error({ err: error }, `Failed to enqueue preview for document ${doc.id}:`);
|
||||
});
|
||||
|
||||
if (doc.type === 'pdf') {
|
||||
|
|
@ -186,7 +187,7 @@ export async function POST(req: NextRequest) {
|
|||
|
||||
return NextResponse.json({ success: true, stored });
|
||||
} catch (error) {
|
||||
console.error('Error registering documents:', error);
|
||||
serverLogger.error({ err: error }, 'Error registering documents:');
|
||||
return NextResponse.json({ error: 'Failed to register documents' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -262,7 +263,7 @@ export async function GET(req: NextRequest) {
|
|||
|
||||
return NextResponse.json({ documents: results });
|
||||
} catch (error) {
|
||||
console.error('Error loading document metadata:', error);
|
||||
serverLogger.error({ err: error }, 'Error loading document metadata:');
|
||||
return NextResponse.json({ error: 'Failed to load documents' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -342,21 +343,21 @@ export async function DELETE(req: NextRequest) {
|
|||
await deleteDocumentBlob(id, testNamespace);
|
||||
} catch (error) {
|
||||
if (!isMissingBlobError(error)) {
|
||||
console.error(`[best-effort] Failed to delete blob for document ${id}, orphaned blob may need manual cleanup:`, error);
|
||||
serverLogger.error({ err: error }, `[best-effort] Failed to delete blob for document ${id}, orphaned blob may need manual cleanup:`);
|
||||
}
|
||||
}
|
||||
|
||||
await cleanupDocumentPreviewArtifacts(id, testNamespace).catch((error) => {
|
||||
console.error(`Failed to cleanup preview artifacts for document ${id}:`, error);
|
||||
serverLogger.error({ err: error }, `Failed to cleanup preview artifacts for document ${id}:`);
|
||||
});
|
||||
await deleteDocumentPreviewRows(id, testNamespace).catch((error) => {
|
||||
console.error(`Failed to cleanup preview rows for document ${id}:`, error);
|
||||
serverLogger.error({ err: error }, `Failed to cleanup preview rows for document ${id}:`);
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, deleted: deletedRows.length });
|
||||
} catch (error) {
|
||||
console.error('Error deleting documents:', error);
|
||||
serverLogger.error({ err: error }, 'Error deleting documents:');
|
||||
return NextResponse.json({ error: 'Failed to delete documents' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { isAuthEnabled } from '@/lib/server/auth/config';
|
|||
import { getClientIp } from '@/lib/server/rate-limit/request-ip';
|
||||
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/rate-limit/device-id';
|
||||
import { nextUtcMidnightTimestampMs } from '@/lib/shared/timestamps';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ export async function GET(req: NextRequest) {
|
|||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error getting rate limit status:', error);
|
||||
serverLogger.error({ err: error }, 'Error getting rate limit status:');
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get rate limit status' },
|
||||
{ status: 500 }
|
||||
|
|
|
|||
|
|
@ -7,10 +7,15 @@ import {
|
|||
ttsSegmentsS3NotConfiguredResponse,
|
||||
} from '@/lib/server/tts/segments-audio';
|
||||
import { getTtsSegmentAudioObjectStream } from '@/lib/server/tts/segments-blobstore';
|
||||
import { createRequestLogger, errorToLog } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { logger } = createRequestLogger({
|
||||
route: '/api/tts/segments/audio/fallback',
|
||||
request,
|
||||
});
|
||||
try {
|
||||
if (!isS3Configured()) return ttsSegmentsS3NotConfiguredResponse();
|
||||
|
||||
|
|
@ -45,7 +50,10 @@ export async function GET(request: NextRequest) {
|
|||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error serving TTS segment audio fallback:', error);
|
||||
logger.error({
|
||||
event: 'tts.segments.audio_fallback_failed',
|
||||
error: errorToLog(error),
|
||||
});
|
||||
return NextResponse.json({ error: 'Failed to load segment audio' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { resolveSegmentDocumentScope } from '@/lib/server/tts/segments-auth';
|
||||
import { clearTtsSegmentCache } from '@/lib/server/tts/segments-cache';
|
||||
import { createRequestLogger, errorToLog } from '@/lib/server/logger';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
|
@ -13,6 +14,10 @@ function parseBody(value: unknown): { documentId: string } | null {
|
|||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const { logger } = createRequestLogger({
|
||||
route: '/api/tts/segments/clear',
|
||||
request,
|
||||
});
|
||||
try {
|
||||
const parsed = parseBody(await request.json().catch(() => null));
|
||||
if (!parsed) {
|
||||
|
|
@ -34,7 +39,10 @@ export async function POST(request: NextRequest) {
|
|||
...cleared,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error clearing TTS segment cache:', error);
|
||||
logger.error({
|
||||
event: 'tts.segments.clear_failed',
|
||||
error: errorToLog(error),
|
||||
});
|
||||
return NextResponse.json({ error: 'Failed to clear TTS segment cache' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { ttsSegmentEntries, ttsSegmentVariants } from '@/db/schema';
|
||||
|
|
@ -35,6 +34,7 @@ import { userWhisperAlignJob } from '@/lib/server/jobs/user-whisper-align-job';
|
|||
import { getResolvedRuntimeConfig } from '@/lib/server/runtime-config';
|
||||
import { resolveTtsModelForProvider } from '@/lib/shared/tts-provider-policy';
|
||||
import { resolveSegmentAudioUrls } from '@/lib/server/tts/segment-audio-urls';
|
||||
import { createRequestLogger, errorToLog } from '@/lib/server/logger';
|
||||
import type {
|
||||
TTSSegmentInput,
|
||||
TTSSegmentManifestItem,
|
||||
|
|
@ -152,7 +152,10 @@ async function deleteEntryIfUnused(userId: string, segmentEntryId: string): Prom
|
|||
export async function POST(request: NextRequest) {
|
||||
let didCreateDeviceIdCookie = false;
|
||||
let deviceIdToSet: string | null = null;
|
||||
const requestId = randomUUID();
|
||||
const { logger, requestId } = createRequestLogger({
|
||||
route: '/api/tts/segments/ensure',
|
||||
request,
|
||||
});
|
||||
const requestStartedAt = Date.now();
|
||||
try {
|
||||
if (!isS3Configured()) return s3NotConfiguredResponse();
|
||||
|
|
@ -326,7 +329,8 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
for (const segment of normalized) {
|
||||
if (request.signal.aborted) {
|
||||
console.info('[tts-segments/ensure] request aborted; stopping remaining segment processing', {
|
||||
logger.info({
|
||||
event: 'tts.segments.ensure.request_aborted',
|
||||
requestId,
|
||||
documentId: parsed.documentId,
|
||||
completedSoFar: manifest.length,
|
||||
|
|
@ -412,9 +416,11 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
} catch (alignError) {
|
||||
const aborted = isAbortLikeError(alignError) || request.signal.aborted;
|
||||
const log = aborted ? console.info : console.warn;
|
||||
log('Whisper alignment still unavailable for completed segment; continuing without word highlights.', {
|
||||
const level = aborted ? 'info' : 'warn';
|
||||
logger[level]({
|
||||
event: 'tts.segments.ensure.self_heal_alignment_unavailable',
|
||||
requestId,
|
||||
documentId: parsed.documentId,
|
||||
segmentId: segment.segmentId,
|
||||
aborted,
|
||||
error: alignError instanceof Error ? alignError.message : String(alignError),
|
||||
|
|
@ -652,9 +658,11 @@ export async function POST(request: NextRequest) {
|
|||
stageTimings.whisperAlignMs = Date.now() - alignStartedAt;
|
||||
} catch (alignError) {
|
||||
const aborted = isAbortLikeError(alignError) || request.signal.aborted;
|
||||
const log = aborted ? console.info : console.warn;
|
||||
log('Whisper alignment unavailable for segment; continuing without word highlights.', {
|
||||
const level = aborted ? 'info' : 'warn';
|
||||
logger[level]({
|
||||
event: 'tts.segments.ensure.alignment_unavailable',
|
||||
requestId,
|
||||
documentId: parsed.documentId,
|
||||
segmentId: segment.segmentId,
|
||||
aborted,
|
||||
error: alignError instanceof Error ? alignError.message : String(alignError),
|
||||
|
|
@ -711,8 +719,9 @@ export async function POST(request: NextRequest) {
|
|||
: upstreamStatus && upstreamStatus >= 500
|
||||
? 'UPSTREAM_TTS_ERROR'
|
||||
: 'TTS_SEGMENT_GENERATION_FAILED';
|
||||
const segmentFailureLog = aborted ? console.info : console.error;
|
||||
segmentFailureLog('[tts-segments/ensure] segment failed', {
|
||||
const level = aborted ? 'info' : 'error';
|
||||
logger[level]({
|
||||
event: 'tts.segments.ensure.segment_failed',
|
||||
requestId,
|
||||
documentId: parsed.documentId,
|
||||
segmentId: segment.segmentId,
|
||||
|
|
@ -762,7 +771,8 @@ export async function POST(request: NextRequest) {
|
|||
});
|
||||
|
||||
if (aborted || request.signal.aborted) {
|
||||
console.info('[tts-segments/ensure] stopping remaining segments after abort', {
|
||||
logger.info({
|
||||
event: 'tts.segments.ensure.stop_after_abort',
|
||||
requestId,
|
||||
documentId: parsed.documentId,
|
||||
segmentId: segment.segmentId,
|
||||
|
|
@ -778,7 +788,8 @@ export async function POST(request: NextRequest) {
|
|||
const pendingCount = manifest.filter((s) => s.status === 'pending').length;
|
||||
const errorItems = manifest.filter((s) => s.status === 'error');
|
||||
if (errorItems.length > 0) {
|
||||
console.error('[tts-segments/ensure] partial result', {
|
||||
logger.error({
|
||||
event: 'tts.segments.ensure.partial_result',
|
||||
requestId,
|
||||
documentId: parsed.documentId,
|
||||
total: manifest.length,
|
||||
|
|
@ -803,7 +814,10 @@ export async function POST(request: NextRequest) {
|
|||
attachDeviceIdCookie(response, deviceIdToSet, didCreateDeviceIdCookie);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error ensuring TTS segments:', error);
|
||||
logger.error({
|
||||
event: 'tts.segments.ensure.route_failed',
|
||||
error: errorToLog(error),
|
||||
});
|
||||
const response = NextResponse.json({ error: 'Failed to ensure TTS segments' }, { status: 500 });
|
||||
attachDeviceIdCookie(response, deviceIdToSet, didCreateDeviceIdCookie);
|
||||
return response;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type {
|
|||
import { isTtsProviderType } from '@/lib/shared/tts-provider-catalog';
|
||||
import { resolveEffectiveProviderType } from '@/lib/shared/tts-provider-policy';
|
||||
import { resolveSegmentAudioUrls } from '@/lib/server/tts/segment-audio-urls';
|
||||
import { createRequestLogger, errorToLog } from '@/lib/server/logger';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
|
@ -178,6 +179,10 @@ function cursorFromGroupRow(row: ManifestGroupRow): TTSSegmentManifestCursor {
|
|||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { logger } = createRequestLogger({
|
||||
route: '/api/tts/segments/manifest',
|
||||
request,
|
||||
});
|
||||
try {
|
||||
const documentIdRaw = request.nextUrl.searchParams.get('documentId');
|
||||
const documentId = documentIdRaw?.trim().toLowerCase();
|
||||
|
|
@ -381,7 +386,10 @@ export async function GET(request: NextRequest) {
|
|||
};
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error listing TTS segments manifest:', error);
|
||||
logger.error({
|
||||
event: 'tts.segments.manifest.list_failed',
|
||||
error: errorToLog(error),
|
||||
});
|
||||
return NextResponse.json({ error: 'Failed to list TTS segments' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||
import { auth } from '@/lib/server/auth/auth';
|
||||
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||
import { listAdminProviders, toPublic } from '@/lib/server/admin/providers';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ export async function GET(req: NextRequest) {
|
|||
const visible = all.filter((p) => p.enabled).map(toPublic);
|
||||
return NextResponse.json({ providers: visible });
|
||||
} catch (error) {
|
||||
console.warn('[tts/shared-providers] list failed:', error);
|
||||
serverLogger.warn({ err: error }, '[tts/shared-providers] list failed:');
|
||||
return NextResponse.json({ providers: [] });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { defaultModelForProviderType, resolveTtsModelForProvider, resolveTtsProv
|
|||
import { resolveVoices } from '@/lib/server/tts/voice-resolution';
|
||||
import { resolveTtsCredentials } from '@/lib/server/admin/resolve-credentials';
|
||||
import { getResolvedRuntimeConfig } from '@/lib/server/runtime-config';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
|
|
@ -61,7 +62,7 @@ export async function GET(req: NextRequest) {
|
|||
});
|
||||
return NextResponse.json({ voices });
|
||||
} catch (error) {
|
||||
console.error('Error in voices endpoint:', error);
|
||||
serverLogger.error({ err: error }, 'Error in voices endpoint:');
|
||||
const providerRef = req.headers.get('x-tts-provider') || 'openai';
|
||||
const model = req.headers.get('x-tts-model') || 'tts-1';
|
||||
const provider = isBuiltInTtsProviderId(providerRef) ? providerRef : 'openai';
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { db } from '@/db';
|
|||
import { audiobooks, documents, userDocumentProgress, userPreferences } from '@/db/schema';
|
||||
import { count, eq, ne } from 'drizzle-orm';
|
||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
async function checkClaimMigrationReadiness(): Promise<NextResponse | null> {
|
||||
const [legacyRows] = await db
|
||||
|
|
@ -56,7 +57,7 @@ export async function GET(req: NextRequest) {
|
|||
const counts = await getClaimableCounts(unclaimedUserId);
|
||||
return NextResponse.json({ success: true, ...counts });
|
||||
} catch (error) {
|
||||
console.error('Error checking claimable data:', error);
|
||||
serverLogger.error({ err: error }, 'Error checking claimable data:');
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -83,7 +84,7 @@ export async function POST(req: NextRequest) {
|
|||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error claiming data:', error);
|
||||
serverLogger.error({ err: error }, 'Error claiming data:');
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getAudiobookObjectStream, listAudiobookObjects } from '@/lib/server/aud
|
|||
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
||||
import { nowTimestampMs } from '@/lib/shared/timestamps';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ export async function GET(req: NextRequest) {
|
|||
|
||||
archive.on('warning', (warning) => {
|
||||
if ((warning as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('User export warning:', warning);
|
||||
serverLogger.error({ err: warning }, 'User export warning:');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ export async function GET(req: NextRequest) {
|
|||
await archive.finalize();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export generation failed:', error);
|
||||
serverLogger.error({ err: error }, 'Export generation failed:');
|
||||
archive.abort();
|
||||
output.destroy(error instanceof Error ? error : new Error('Failed to generate export archive'));
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { userPreferences } from '@/db/schema';
|
|||
import { normalizeVersion, shouldOpenChangelogForVersionChange } from '@/lib/shared/changelog';
|
||||
import { nowTimestampMs } from '@/lib/shared/timestamps';
|
||||
import { resolveUserStateScope } from '@/lib/server/user/resolve-state-scope';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
deserializeUserPreferencesPayload,
|
||||
extractUserPreferencesMeta,
|
||||
|
|
@ -80,7 +81,7 @@ export async function POST(req: NextRequest) {
|
|||
lastSeenVersion,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking changelog version:', error);
|
||||
serverLogger.error({ err: error }, 'Error checking changelog version:');
|
||||
return NextResponse.json({ error: 'Failed to check changelog version' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { isTtsProviderType, type TtsProviderId } from '@/lib/shared/tts-provider
|
|||
import { listAdminProviders } from '@/lib/server/admin/providers';
|
||||
import { getResolvedRuntimeConfig } from '@/lib/server/runtime-config';
|
||||
import { normalizeLegacyProviderRef, resolveProviderDefaults } from '@/lib/shared/tts-provider-policy';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
deserializeUserPreferencesPayload,
|
||||
extractUserPreferencesMeta,
|
||||
|
|
@ -241,7 +242,7 @@ export async function GET(req: NextRequest) {
|
|||
hasStoredPreferences: Boolean(row),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading user preferences:', error);
|
||||
serverLogger.error({ err: error }, 'Error loading user preferences:');
|
||||
return NextResponse.json({ error: 'Failed to load user preferences' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -315,7 +316,7 @@ export async function PUT(req: NextRequest) {
|
|||
applied: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating user preferences:', error);
|
||||
serverLogger.error({ err: error }, 'Error updating user preferences:');
|
||||
return NextResponse.json({ error: 'Failed to update user preferences' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { ReaderType } from '@/types/user-state';
|
|||
import { isValidDocumentId } from '@/lib/server/documents/blobstore';
|
||||
import { resolveUserStateScope } from '@/lib/server/user/resolve-state-scope';
|
||||
import { coerceTimestampMs, nowTimestampMs } from '@/lib/shared/timestamps';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ export async function GET(req: NextRequest) {
|
|||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading user progress:', error);
|
||||
serverLogger.error({ err: error }, 'Error loading user progress:');
|
||||
return NextResponse.json({ error: 'Failed to load user progress' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +173,7 @@ export async function PUT(req: NextRequest) {
|
|||
applied: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating user progress:', error);
|
||||
serverLogger.error({ err: error }, 'Error updating user progress:');
|
||||
return NextResponse.json({ error: 'Failed to update user progress' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
// We only need the `user` table here. Better Auth manages its own schema files;
|
||||
// we import them lazily to avoid coupling this module to a single dialect.
|
||||
|
|
@ -60,7 +61,7 @@ export async function syncAdminFlag(
|
|||
await db.update(user).set({ isAdmin: shouldBeAdmin }).where(eq(user.id, userId));
|
||||
return shouldBeAdmin;
|
||||
} catch (error) {
|
||||
console.warn('[admin] Failed to sync isAdmin flag for user', userId, error);
|
||||
serverLogger.warn({ userId: userId, err: error }, '[admin] Failed to sync isAdmin flag for user');
|
||||
return currentIsAdmin;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { adminProviders, adminSettings } from '@/db/schema';
|
|||
import { encryptSecret, apiKeyLast4 } from '@/lib/server/crypto/secrets';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
RUNTIME_CONFIG_SCHEMA,
|
||||
seedRuntimeConfigFromEnv,
|
||||
|
|
@ -30,7 +31,7 @@ let seedPromise: Promise<void> | null = null;
|
|||
export async function ensureAdminSeed(): Promise<void> {
|
||||
if (!seedPromise) {
|
||||
seedPromise = runSeed().catch((error) => {
|
||||
console.warn('[admin-seed] failed:', error);
|
||||
serverLogger.warn({ err: error }, '[admin-seed] failed:');
|
||||
// Reset so a subsequent call can retry (e.g. once migrations run).
|
||||
seedPromise = null;
|
||||
throw error;
|
||||
|
|
@ -58,7 +59,7 @@ async function seedDefaultAdminProvider(): Promise<void> {
|
|||
try {
|
||||
existing = await db.select({ id: adminProviders.id }).from(adminProviders).limit(1);
|
||||
} catch (error) {
|
||||
console.warn('[admin-seed] could not check admin_providers (table missing?)', error);
|
||||
serverLogger.warn({ err: error }, '[admin-seed] could not check admin_providers (table missing?)');
|
||||
return;
|
||||
}
|
||||
if (existing.length > 0) return;
|
||||
|
|
@ -85,17 +86,14 @@ async function seedDefaultAdminProvider(): Promise<void> {
|
|||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoNothing({ target: adminSettings.key });
|
||||
console.warn(
|
||||
serverLogger.warn(
|
||||
'[admin-seed] API_KEY present but AUTH_SECRET missing; defaulting restrictUserApiKeys=false so BYOK remains available',
|
||||
);
|
||||
} catch (fallbackError) {
|
||||
console.warn(
|
||||
'[admin-seed] failed to write restrictUserApiKeys fallback after encryption failure',
|
||||
fallbackError,
|
||||
);
|
||||
serverLogger.warn({ err: fallbackError }, '[admin-seed] failed to write restrictUserApiKeys fallback after encryption failure');
|
||||
}
|
||||
}
|
||||
console.warn('[admin-seed] failed to encrypt default provider API key', error);
|
||||
serverLogger.warn({ err: error }, '[admin-seed] failed to encrypt default provider API key');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -114,9 +112,9 @@ async function seedDefaultAdminProvider(): Promise<void> {
|
|||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
console.log('[admin-seed] created default-openai admin provider from env');
|
||||
serverLogger.info('[admin-seed] created default-openai admin provider from env');
|
||||
} catch (error) {
|
||||
console.warn('[admin-seed] failed to insert default-openai provider', error);
|
||||
serverLogger.warn({ err: error }, '[admin-seed] failed to insert default-openai provider');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +138,7 @@ async function cleanupLegacyDefaultTtsProviderSeedRow(): Promise<void> {
|
|||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[admin-seed] failed to cleanup legacy defaultTtsProvider seed row', error);
|
||||
serverLogger.warn({ err: error }, '[admin-seed] failed to cleanup legacy defaultTtsProvider seed row');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +148,6 @@ async function cleanupLegacyDefaultTtsModelRows(): Promise<void> {
|
|||
.delete(adminSettings)
|
||||
.where(eq(adminSettings.key, 'defaultTtsModel'));
|
||||
} catch (error) {
|
||||
console.warn('[admin-seed] failed to cleanup legacy defaultTtsModel rows', error);
|
||||
serverLogger.warn({ err: error }, '[admin-seed] failed to cleanup legacy defaultTtsModel rows');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm';
|
|||
import { db } from '@/db';
|
||||
import { adminProviders, adminSettings } from '@/db/schema';
|
||||
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
/**
|
||||
* Runtime config: site-wide settings that used to live in build-time env vars.
|
||||
|
|
@ -113,7 +114,7 @@ async function resolveImplicitDefaultTtsProvider(): Promise<string | undefined>
|
|||
.limit(1);
|
||||
return rows[0]?.slug;
|
||||
} catch (error) {
|
||||
console.warn('[runtime-config] implicit defaultTtsProvider lookup failed:', error);
|
||||
serverLogger.warn({ err: error }, '[runtime-config] implicit defaultTtsProvider lookup failed:');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +142,7 @@ async function readAllRows(): Promise<Map<string, { value: unknown; source: stri
|
|||
}
|
||||
return out;
|
||||
} catch (error) {
|
||||
console.warn('[runtime-config] read failed (table may not exist yet):', error);
|
||||
serverLogger.warn({ err: error }, '[runtime-config] read failed (table may not exist yet):');
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
|
@ -297,7 +298,7 @@ export async function seedRuntimeConfigFromEnv(): Promise<{ seeded: RuntimeConfi
|
|||
.onConflictDoNothing({ target: adminSettings.key });
|
||||
seeded.push(key);
|
||||
} catch (error) {
|
||||
console.warn('[runtime-config] seed failed for', key, error);
|
||||
serverLogger.warn({ key: key, err: error }, '[runtime-config] seed failed for');
|
||||
}
|
||||
}
|
||||
return { seeded };
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getResolvedRuntimeConfig } from '@/lib/server/runtime-config';
|
|||
import { assertUserSignupAllowed } from '@/lib/server/auth/signup-policy';
|
||||
import * as authSchemaSqlite from "@/db/schema_auth_sqlite";
|
||||
import * as authSchemaPostgres from "@/db/schema_auth_postgres";
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
// Heavy modules (S3 SDK, blobstore, rate-limiter, claim-data) are loaded
|
||||
// lazily via dynamic import() inside the beforeDelete / onLinkAccount
|
||||
|
|
@ -71,7 +72,7 @@ const createAuth = () => betterAuth({
|
|||
requireEmailVerification: false, // Set to true in production
|
||||
async sendResetPassword(data) {
|
||||
// Send an email to the user with a link to reset their password
|
||||
console.log("Password reset requested for:", data.user.email);
|
||||
serverLogger.info({ err: data.user.email }, "Password reset requested for:");
|
||||
},
|
||||
},
|
||||
user: {
|
||||
|
|
@ -90,7 +91,7 @@ const createAuth = () => betterAuth({
|
|||
const { deleteUserStorageData } = await import('@/lib/server/user/data-cleanup');
|
||||
await deleteUserStorageData(user.id, null);
|
||||
} catch (error) {
|
||||
console.error('[auth] Failed to clean up user storage before deletion:', error);
|
||||
serverLogger.error({ err: error }, '[auth] Failed to clean up user storage before deletion:');
|
||||
// Don't throw – allow the user deletion to proceed even if S3 cleanup fails.
|
||||
// Orphaned blobs are preferable to a blocked account deletion.
|
||||
}
|
||||
|
|
@ -145,11 +146,11 @@ const createAuth = () => betterAuth({
|
|||
onLinkAccount: async ({ anonymousUser, newUser }) => {
|
||||
try {
|
||||
// Log when anonymous user links to a real account
|
||||
console.log("Anonymous user linked to account:", {
|
||||
serverLogger.info({
|
||||
anonymousUserId: anonymousUser.user.id,
|
||||
newUserId: newUser.user.id,
|
||||
newUserEmail: newUser.user.email,
|
||||
});
|
||||
}, "Anonymous user linked to account:");
|
||||
|
||||
// Lazy-load heavy modules only when account linking actually happens
|
||||
const [{ rateLimiter }, claimData] = await Promise.all([
|
||||
|
|
@ -160,9 +161,9 @@ const createAuth = () => betterAuth({
|
|||
// Transfer rate limiting data (TTS char counts) from anonymous user to authenticated user
|
||||
try {
|
||||
await rateLimiter.transferAnonymousUsage(anonymousUser.user.id, newUser.user.id);
|
||||
console.log(`Successfully transferred rate limit data from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
serverLogger.info(`Successfully transferred rate limit data from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
} catch (error) {
|
||||
console.error("Error transferring rate limit data during account linking:", error);
|
||||
serverLogger.error({ err: error }, "Error transferring rate limit data during account linking:");
|
||||
// Don't throw here to prevent blocking the account linking process
|
||||
}
|
||||
|
||||
|
|
@ -170,10 +171,10 @@ const createAuth = () => betterAuth({
|
|||
try {
|
||||
const transferred = await claimData.transferUserAudiobooks(anonymousUser.user.id, newUser.user.id);
|
||||
if (transferred > 0) {
|
||||
console.log(`Successfully transferred ${transferred} audiobook(s) from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
serverLogger.info(`Successfully transferred ${transferred} audiobook(s) from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error transferring audiobooks during account linking:", error);
|
||||
serverLogger.error({ err: error }, "Error transferring audiobooks during account linking:");
|
||||
// Don't throw here to prevent blocking the account linking process
|
||||
}
|
||||
|
||||
|
|
@ -181,10 +182,10 @@ const createAuth = () => betterAuth({
|
|||
try {
|
||||
const transferred = await claimData.transferUserDocuments(anonymousUser.user.id, newUser.user.id);
|
||||
if (transferred > 0) {
|
||||
console.log(`Successfully transferred ${transferred} document(s) from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
serverLogger.info(`Successfully transferred ${transferred} document(s) from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error transferring documents during account linking:", error);
|
||||
serverLogger.error({ err: error }, "Error transferring documents during account linking:");
|
||||
// Don't throw here to prevent blocking the account linking process
|
||||
}
|
||||
|
||||
|
|
@ -192,10 +193,10 @@ const createAuth = () => betterAuth({
|
|||
try {
|
||||
const transferred = await claimData.transferUserPreferences(anonymousUser.user.id, newUser.user.id);
|
||||
if (transferred > 0) {
|
||||
console.log(`Successfully transferred preferences from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
serverLogger.info(`Successfully transferred preferences from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error transferring preferences during account linking:", error);
|
||||
serverLogger.error({ err: error }, "Error transferring preferences during account linking:");
|
||||
// Don't throw here to prevent blocking the account linking process
|
||||
}
|
||||
|
||||
|
|
@ -203,14 +204,14 @@ const createAuth = () => betterAuth({
|
|||
try {
|
||||
const transferred = await claimData.transferUserProgress(anonymousUser.user.id, newUser.user.id);
|
||||
if (transferred > 0) {
|
||||
console.log(`Successfully transferred ${transferred} progress row(s) from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
serverLogger.info(`Successfully transferred ${transferred} progress row(s) from anonymous user ${anonymousUser.user.id} to user ${newUser.user.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error transferring reading progress during account linking:", error);
|
||||
serverLogger.error({ err: error }, "Error transferring reading progress during account linking:");
|
||||
// Don't throw here to prevent blocking the account linking process
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in onLinkAccount callback:", error);
|
||||
serverLogger.error({ err: error }, "Error in onLinkAccount callback:");
|
||||
// Don't throw here to prevent blocking the account linking process
|
||||
}
|
||||
// Note: Anonymous user will be automatically deleted after this callback completes
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { getWorkerClientConfigFromEnv } from '@/lib/server/compute/worker';
|
||||
import type { WorkerOperationState } from '@openreader/compute-core/api-contracts';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
const WORKER_OP_REQUEST_TIMEOUT_MS = 2_500;
|
||||
|
||||
|
|
@ -13,10 +14,10 @@ export async function fetchWorkerOperationState<Result>(
|
|||
try {
|
||||
cfg = getWorkerClientConfigFromEnv();
|
||||
} catch (error) {
|
||||
console.warn('[worker-op-state] worker client env missing/invalid', {
|
||||
serverLogger.warn({
|
||||
opId: normalized,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}, '[worker-op-state] worker client env missing/invalid');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -36,26 +37,26 @@ export async function fetchWorkerOperationState<Result>(
|
|||
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => '');
|
||||
console.warn('[worker-op-state] worker op request failed', {
|
||||
serverLogger.warn({
|
||||
opId: normalized,
|
||||
status: res.status,
|
||||
detail,
|
||||
});
|
||||
}, '[worker-op-state] worker op request failed');
|
||||
return null;
|
||||
}
|
||||
const parsed = await res.json() as WorkerOperationState<Result>;
|
||||
if (!parsed || typeof parsed !== 'object' || parsed.opId !== normalized) {
|
||||
console.warn('[worker-op-state] worker op response invalid', {
|
||||
serverLogger.warn({
|
||||
opId: normalized,
|
||||
});
|
||||
}, '[worker-op-state] worker op response invalid');
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.warn('[worker-op-state] worker op request threw', {
|
||||
serverLogger.warn({
|
||||
opId: normalized,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}, '[worker-op-state] worker op request threw');
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|||
import type { ComputeBackend, PdfLayoutInput, WhisperAlignInput, WhisperAlignResult } from '@/lib/server/compute/types';
|
||||
import { parseSseEventId, parseSsePayload } from '@openreader/compute-core';
|
||||
import { getWorkerClientWaitTimeoutMs } from '@openreader/compute-core';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import type {
|
||||
PdfLayoutJobRequest,
|
||||
PdfLayoutJobResult,
|
||||
|
|
@ -70,14 +71,14 @@ function logWorker(level: WorkerLogLevel, event: string, fields: Record<string,
|
|||
};
|
||||
const line = `${LOG_PREFIX} ${JSON.stringify(payload)}`;
|
||||
if (level === 'error') {
|
||||
console.error(line);
|
||||
serverLogger.error(line);
|
||||
return;
|
||||
}
|
||||
if (level === 'warn') {
|
||||
console.warn(line);
|
||||
serverLogger.warn(line);
|
||||
return;
|
||||
}
|
||||
console.info(line);
|
||||
serverLogger.info(line);
|
||||
}
|
||||
|
||||
function opSummary(value: unknown): Record<string, unknown> {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { randomUUID } from 'crypto';
|
|||
import { and, eq, inArray, lt, or, sql } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { documentPreviews } from '@/db/schema';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
DOCUMENT_PREVIEW_CONTENT_TYPE,
|
||||
DOCUMENT_PREVIEW_VARIANT,
|
||||
|
|
@ -374,7 +375,7 @@ export async function ensureDocumentPreview(doc: PreviewSourceDocument, namespac
|
|||
eTag: head.eTag,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[document-previews] Preview generation failed for ${doc.id} (type=${doc.type}):`, error);
|
||||
serverLogger.error({ err: error }, `[document-previews] Preview generation failed for ${doc.id} (type=${doc.type}):`);
|
||||
await markPreviewFailed(doc.id, namespaceKey, error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { and, eq, inArray, isNull } from 'drizzle-orm';
|
|||
import { db } from '@/db';
|
||||
import { documents } from '@/db/schema';
|
||||
import { documentKey, putParsedDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
parseDocumentParseState,
|
||||
stringifyDocumentParseState,
|
||||
|
|
@ -348,18 +349,18 @@ export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void>
|
|||
readerType: 'pdf',
|
||||
}).then((cleared) => {
|
||||
if (cleared.warning) {
|
||||
console.warn('[parsePdfJob] cache invalidation warning', {
|
||||
serverLogger.warn({
|
||||
documentId: input.documentId,
|
||||
userId,
|
||||
warning: cleared.warning,
|
||||
});
|
||||
}, '[parsePdfJob] cache invalidation warning');
|
||||
}
|
||||
}).catch((cacheError) => {
|
||||
console.warn('[parsePdfJob] cache invalidation failed', {
|
||||
serverLogger.warn({
|
||||
documentId: input.documentId,
|
||||
userId,
|
||||
error: cacheError instanceof Error ? cacheError.message : String(cacheError),
|
||||
});
|
||||
}, '[parsePdfJob] cache invalidation failed');
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -383,19 +384,19 @@ export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void>
|
|||
parseState: stringifyDocumentParseState(failedState),
|
||||
});
|
||||
} catch (statusError) {
|
||||
console.error('[parsePdfJob] failed to write parse status', {
|
||||
serverLogger.error({
|
||||
documentId: input.documentId,
|
||||
parseStatus,
|
||||
error: statusError instanceof Error ? statusError.message : String(statusError),
|
||||
});
|
||||
}, '[parsePdfJob] failed to write parse status');
|
||||
}
|
||||
console.error('[parsePdfJob] failed', {
|
||||
serverLogger.error({
|
||||
documentId: input.documentId,
|
||||
parseStatus,
|
||||
error: message,
|
||||
...(stack ? { stack } : {}),
|
||||
...(cause ? { cause: String(cause) } : {}),
|
||||
});
|
||||
}, '[parsePdfJob] failed');
|
||||
} finally {
|
||||
running.delete(key);
|
||||
}
|
||||
|
|
@ -405,6 +406,6 @@ export function enqueueParsePdfJob(input: UserPdfLayoutJobRequest): void {
|
|||
Promise.resolve()
|
||||
.then(() => parsePdfJob(input))
|
||||
.catch((error) => {
|
||||
console.error('[parsePdfJob] uncaught error', error);
|
||||
serverLogger.error({ err: error }, '[parsePdfJob] uncaught error');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
78
src/lib/server/logger.ts
Normal file
78
src/lib/server/logger.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import pino, { type Logger } from 'pino';
|
||||
import pinoPretty from 'pino-pretty';
|
||||
|
||||
export type ServerLogger = Logger;
|
||||
|
||||
const LOG_FORMAT = process.env.LOG_FORMAT?.trim().toLowerCase() || 'pretty';
|
||||
const LOG_LEVEL = process.env.LOG_LEVEL?.trim() || 'info';
|
||||
|
||||
function buildLoggerConfig(): pino.LoggerOptions {
|
||||
return {
|
||||
level: LOG_LEVEL,
|
||||
base: undefined,
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
formatters: {
|
||||
level(label) {
|
||||
return { level: label };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createServerLogger(): ServerLogger {
|
||||
const config = buildLoggerConfig();
|
||||
if (LOG_FORMAT === 'json') {
|
||||
return pino(config);
|
||||
}
|
||||
|
||||
const prettyStream = pinoPretty({
|
||||
colorize: true,
|
||||
translateTime: 'SYS:standard',
|
||||
ignore: 'pid,hostname',
|
||||
});
|
||||
return pino(config, prettyStream);
|
||||
}
|
||||
|
||||
export const serverLogger: ServerLogger = createServerLogger();
|
||||
|
||||
export function errorToLog(error: unknown): Record<string, unknown> {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
return { message: String(error) };
|
||||
}
|
||||
|
||||
export function hashForLog(value: string | null | undefined): string | null {
|
||||
const normalized = typeof value === 'string' ? value.trim() : '';
|
||||
if (!normalized) return null;
|
||||
return createHash('sha256').update(normalized).digest('hex').slice(0, 12);
|
||||
}
|
||||
|
||||
export function getRequestId(request: NextRequest): string {
|
||||
const fromHeader = request.headers.get('x-request-id')
|
||||
|| request.headers.get('x-vercel-id');
|
||||
const normalized = fromHeader?.trim();
|
||||
return normalized || randomUUID();
|
||||
}
|
||||
|
||||
export function createRequestLogger(input: {
|
||||
route: string;
|
||||
request: NextRequest;
|
||||
requestId?: string;
|
||||
fields?: Record<string, unknown>;
|
||||
}): { logger: ServerLogger; requestId: string } {
|
||||
const requestId = input.requestId || getRequestId(input.request);
|
||||
const logger = serverLogger.child({
|
||||
route: input.route,
|
||||
method: input.request.method,
|
||||
requestId,
|
||||
...(input.fields || {}),
|
||||
});
|
||||
return { logger, requestId };
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { userTtsChars } from '@/db/schema';
|
|||
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||
import { eq, and, lt, sql } from 'drizzle-orm';
|
||||
import { nextUtcMidnightTimestampMs, nowTimestampMs } from '@/lib/shared/timestamps';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
function readPositiveIntEnv(name: string, fallback: number): number {
|
||||
const raw = process.env[name];
|
||||
|
|
@ -10,7 +11,7 @@ function readPositiveIntEnv(name: string, fallback: number): number {
|
|||
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
console.warn(`[rate-limiter] Invalid ${name}=${raw}; using default ${fallback}`);
|
||||
serverLogger.warn(`[rate-limiter] Invalid ${name}=${raw}; using default ${fallback}`);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { db } from '@/db';
|
|||
import { ttsSegmentEntries, ttsSegmentVariants } from '@/db/schema';
|
||||
import { deleteTtsSegmentAudioObjects } from '@/lib/server/tts/segments-blobstore';
|
||||
import type { ReaderType } from '@/types/user-state';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
type ClearTtsSegmentCacheInput = {
|
||||
userId: string;
|
||||
|
|
@ -65,11 +66,11 @@ export async function clearTtsSegmentCache(
|
|||
}
|
||||
} catch (error) {
|
||||
warning = error instanceof Error ? error.message : 'Failed deleting some audio objects';
|
||||
console.warn('Failed clearing some TTS segment audio objects:', {
|
||||
serverLogger.warn({
|
||||
documentId: input.documentId,
|
||||
userId: input.userId,
|
||||
error: warning,
|
||||
});
|
||||
}, 'Failed clearing some TTS segment audio objects:');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { LRUCache } from 'lru-cache';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
import {
|
||||
resolveProviderModels,
|
||||
type ReplicateVoiceInputKey,
|
||||
|
|
@ -192,7 +193,7 @@ async function fetchReplicateOpenApiSchema(apiKey: string, model: string): Promi
|
|||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
console.error('Error fetching Replicate model schema:', error);
|
||||
serverLogger.error({ err: error }, 'Error fetching Replicate model schema:');
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
|
@ -283,7 +284,7 @@ async function fetchDeepinfraVoices(apiKey: string): Promise<string[]> {
|
|||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
return [];
|
||||
}
|
||||
console.error('Error fetching Deepinfra voices:', error);
|
||||
serverLogger.error({ err: error }, 'Error fetching Deepinfra voices:');
|
||||
return [];
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
|
@ -315,7 +316,7 @@ async function fetchCustomOpenAiVoices(baseUrl: string, apiKey: string): Promise
|
|||
? data.voices
|
||||
: null;
|
||||
} catch {
|
||||
console.log('Custom endpoint does not support voices, using defaults');
|
||||
serverLogger.info('Custom endpoint does not support voices, using defaults');
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { deleteDocumentPreviewArtifacts } from '@/lib/server/documents/previews-
|
|||
import { deleteDocumentPreviewRows } from '@/lib/server/documents/previews';
|
||||
import { audiobookPrefix, deleteAudiobookPrefix } from '@/lib/server/audiobooks/blobstore';
|
||||
import { deleteTtsSegmentPrefix } from '@/lib/server/tts/segments-blobstore';
|
||||
import { serverLogger } from '@/lib/server/logger';
|
||||
|
||||
type DocumentRow = { id: string };
|
||||
type AudiobookRow = { id: string };
|
||||
|
|
@ -49,13 +50,13 @@ export async function deleteUserStorageData(
|
|||
await deleteDocumentBlob(doc.id, namespace);
|
||||
docsDeleted++;
|
||||
} catch (error) {
|
||||
console.error(`[user-data-cleanup] Failed to delete document blob ${doc.id}:`, error);
|
||||
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete document blob ${doc.id}:`);
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteDocumentPreviewArtifacts(doc.id, namespace);
|
||||
} catch (error) {
|
||||
console.error(`[user-data-cleanup] Failed to delete preview for ${doc.id}:`, error);
|
||||
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete preview for ${doc.id}:`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ export async function deleteUserStorageData(
|
|||
try {
|
||||
await deleteDocumentPreviewRows(doc.id, namespace);
|
||||
} catch (error) {
|
||||
console.error(`[user-data-cleanup] Failed to delete preview rows for ${doc.id}:`, error);
|
||||
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete preview rows for ${doc.id}:`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ export async function deleteUserStorageData(
|
|||
await deleteAudiobookPrefix(prefix);
|
||||
booksDeleted++;
|
||||
} catch (error) {
|
||||
console.error(`[user-data-cleanup] Failed to delete audiobook blobs ${book.id}:`, error);
|
||||
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete audiobook blobs ${book.id}:`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,12 +98,12 @@ export async function deleteUserStorageData(
|
|||
segmentsDeleted += await deleteTtsSegmentPrefix(ttsPrefixV1);
|
||||
segmentsDeleted += await deleteTtsSegmentPrefix(ttsPrefixV2);
|
||||
} catch (error) {
|
||||
console.error(`[user-data-cleanup] Failed to delete TTS segment blobs for user ${userId}:`, error);
|
||||
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete TTS segment blobs for user ${userId}:`);
|
||||
}
|
||||
}
|
||||
|
||||
if (docsDeleted > 0 || booksDeleted > 0 || segmentsDeleted > 0) {
|
||||
console.log(
|
||||
serverLogger.info(
|
||||
`[user-data-cleanup] Cleaned up S3 data for user ${userId}: ` +
|
||||
`${docsDeleted}/${userDocs.length} document(s), ` +
|
||||
`${booksDeleted}/${userBooks.length} audiobook(s), ` +
|
||||
|
|
|
|||
Loading…
Reference in a new issue