Add LOG_LEVEL logger helper with createLogger() utility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 19:25:44 -05:00
co-authored by Claude Opus 4.6
parent 8d4628bb94
commit eb836eb2c7
10 changed files with 84 additions and 18 deletions
+4
View File
@@ -27,6 +27,10 @@ BETTER_AUTH_URL=http://localhost:3000
# OIDC_AUTO_REGISTER=true # Auto-create users on first OIDC login (default: true)
# DISABLE_PASSWORD_LOGIN=false # Set to "true" to hide email/password form when OIDC is configured
# ─── Logging ──────────────────────────────────────────────────────────
# Log verbosity: error, warn, info, debug (default: info)
# LOG_LEVEL=info
# ─── Image Caching ─────────────────────────────────────────────────────
# Set IMAGE_CACHE_ENABLED to "false" to use TMDB CDN directly (default: enabled)
# IMAGE_CACHE_ENABLED=true
+5 -1
View File
@@ -1,13 +1,17 @@
import { sql } from "drizzle-orm";
import { NextResponse } from "next/server";
import { db } from "@/lib/db/client";
import { createLogger } from "@/lib/logger";
const log = createLogger("health");
export async function GET() {
try {
db.run(sql`SELECT 1`);
return NextResponse.json({ status: "healthy" }, { status: 200 });
} catch {
} catch (err) {
log.error("Health check failed:", err);
return NextResponse.json({ status: "unhealthy" }, { status: 503 });
}
}
+5 -1
View File
@@ -3,6 +3,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { db } from "@/lib/db/client";
import { webhookConnections } from "@/lib/db/schema";
import { createLogger } from "@/lib/logger";
import type { WebhookEvent } from "@/lib/services/webhooks";
import {
parseJellyfinPayload,
@@ -10,6 +11,8 @@ import {
processWebhook,
} from "@/lib/services/webhooks";
const log = createLogger("webhooks");
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ token: string }> },
@@ -49,8 +52,9 @@ export async function POST(
connection.provider,
event,
);
} catch {
} catch (err) {
// Swallow errors — never return non-200 to media servers
log.debug("Webhook processing failed:", err);
}
return NextResponse.json({ ok: true });
+6 -3
View File
@@ -4,6 +4,9 @@ export async function onRequestError() {
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { createLogger } = await import("@/lib/logger");
const log = createLogger("server");
// Ensure image cache directories exist (all environments)
const { ensureImageDirs, imageCacheEnabled } = await import(
"@/lib/services/image-cache"
@@ -24,11 +27,11 @@ export async function register() {
const { closeDatabase } = await import("@/lib/db/client");
const shutdown = () => {
console.log("[shutdown] Stopping scheduler...");
log.info("Stopping scheduler...");
stopJobs();
console.log("[shutdown] Closing database...");
log.info("Closing database...");
closeDatabase();
console.log("[shutdown] Clean shutdown complete");
log.info("Clean shutdown complete");
process.exit(0);
};
+12
View File
@@ -9,6 +9,7 @@ import {
isPasswordLoginDisabled,
} from "@/lib/config";
import { db } from "@/lib/db/client";
import { createLogger } from "@/lib/logger";
import {
getUserCount,
isRegistrationOpen,
@@ -36,7 +37,18 @@ const oidcPlugin = isOidcConfigured()
]
: [];
const authLog = createLogger("auth");
export const auth = betterAuth({
logger: {
// Suppress unset secret/low entropy warnings during build
disabled: process.env.NEXT_PHASE === "phase-production-build",
level: "debug",
log: (level, message, ...args) => {
const fn = authLog[level as keyof typeof authLog];
if (fn) fn(message, ...args);
},
},
database: drizzleAdapter(db, {
provider: "sqlite",
}),
+7 -4
View File
@@ -7,6 +7,7 @@ import {
titles,
userTitleStatus,
} from "@/lib/db/schema";
import { createLogger } from "@/lib/logger";
import { refreshAvailability } from "@/lib/services/availability";
import {
cacheEpisodeStills,
@@ -21,6 +22,8 @@ import {
} from "@/lib/services/metadata";
import { getTvDetails } from "@/lib/tmdb/client";
const log = createLogger("cron");
const DAY = 24 * 60 * 60 * 1000;
const RATE_LIMIT_MS = 300;
@@ -42,13 +45,13 @@ function schedule(name: string, cron: string, handler: () => Promise<void>) {
name,
protect: true,
catch: (err: unknown) => {
console.error(`[scheduler] Job ${name} failed:`, err);
log.error(`Job ${name} failed:`, err);
},
},
async () => {
console.log(`[scheduler] Running job: ${name}`);
log.info(`Running job: ${name}`);
await handler();
console.log(`[scheduler] Completed job: ${name}`);
log.info(`Completed job: ${name}`);
},
),
);
@@ -213,7 +216,7 @@ export function startJobs() {
schedule("refreshTvChildren", "30 */12 * * *", refreshTvChildrenJob);
schedule("cacheImages", "0 1,13 * * *", cacheImagesJob);
console.log(`[scheduler] Started ${jobs.size} jobs`);
log.info(`Started ${jobs.size} jobs`);
}
export function stopJobs() {
+5 -2
View File
@@ -1,8 +1,11 @@
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import { createLogger } from "@/lib/logger";
import { db } from "./client";
const log = createLogger("db");
export function runMigrations() {
console.log("[migrate] Running database migrations...");
log.info("Running database migrations...");
migrate(db, { migrationsFolder: "./drizzle" });
console.log("[migrate] Database migrations complete");
log.info("Database migrations complete");
}
+27
View File
@@ -0,0 +1,27 @@
const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 } as const;
type Level = keyof typeof LEVELS;
const currentLevel: Level =
(process.env.LOG_LEVEL as Level) in LEVELS
? (process.env.LOG_LEVEL as Level)
: "info";
export function createLogger(prefix: string) {
const fmt = (msg: string) => `[${prefix}] ${msg}`;
return {
error(msg: string, ...args: unknown[]) {
if (LEVELS[currentLevel] >= LEVELS.error)
console.error(fmt(msg), ...args);
},
warn(msg: string, ...args: unknown[]) {
if (LEVELS[currentLevel] >= LEVELS.warn) console.warn(fmt(msg), ...args);
},
info(msg: string, ...args: unknown[]) {
if (LEVELS[currentLevel] >= LEVELS.info) console.log(fmt(msg), ...args);
},
debug(msg: string, ...args: unknown[]) {
if (LEVELS[currentLevel] >= LEVELS.debug)
console.debug(fmt(msg), ...args);
},
};
}
+4 -1
View File
@@ -3,6 +3,7 @@ import { eq } from "drizzle-orm";
import { Vibrant } from "node-vibrant/node";
import { db } from "@/lib/db/client";
import { titles } from "@/lib/db/schema";
import { createLogger } from "@/lib/logger";
import {
downloadAndCacheImage,
getLocalImagePath,
@@ -10,6 +11,8 @@ import {
isImageCached,
} from "@/lib/services/image-cache";
const log = createLogger("colors");
export interface ColorPalette {
vibrant: string | null;
darkVibrant: string | null;
@@ -58,7 +61,7 @@ export async function extractAndStoreColors(
return colors;
} catch (err) {
console.error(`Failed to extract colors for title ${titleId}:`, err);
log.error(`Failed to extract colors for title ${titleId}:`, err);
return null;
}
}
+9 -6
View File
@@ -7,6 +7,7 @@ import {
titleRecommendations,
titles,
} from "@/lib/db/schema";
import { createLogger } from "@/lib/logger";
import {
getMovieDetails,
getRecommendations,
@@ -29,6 +30,8 @@ import {
imageCacheEnabled,
} from "./image-cache";
const log = createLogger("metadata");
export async function importTitle(
tmdbId: number,
type: "movie" | "tv",
@@ -277,10 +280,10 @@ export async function refreshTvChildren(
})
.run();
}
} catch {
} catch (err) {
// Skip this season and continue with the rest — partial data is
// better than aborting entirely. The next refresh cycle will retry.
console.error(`Failed to fetch season ${sn} for TMDB ${tmdbId}`);
log.error(`Failed to fetch season ${sn} for TMDB ${tmdbId}:`, err);
}
}
}
@@ -457,8 +460,8 @@ export async function getTitleWithChildren(id: string): Promise<{
.run();
await refreshTvChildren(id, title.tmdbId, show.number_of_seasons);
title = db.select().from(titles).where(eq(titles.id, id)).get() ?? title;
} catch {
// Continue with whatever data we have
} catch (err) {
log.debug(`Failed to hydrate shell TV title ${id}:`, err);
}
}
@@ -483,8 +486,8 @@ export async function getTitleWithChildren(id: string): Promise<{
.where(eq(titles.id, id))
.run();
title = db.select().from(titles).where(eq(titles.id, id)).get() ?? title;
} catch {
// Continue with whatever data we have
} catch (err) {
log.debug(`Failed to hydrate shell movie title ${id}:`, err);
}
}