mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 00:25:38 -04:00
Add LOG_LEVEL logger helper with createLogger() utility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user