From 9410efac7a099cb46a88ae00d9bfb06e530a07c6 Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Sat, 4 Apr 2026 08:19:49 -0400 Subject: [PATCH] fix: properly set child log levels on level change from UI --- server/src/api/debugApi.ts | 20 +++++++++++- server/src/util/logging/LoggerFactory.ts | 22 +++++-------- server/src/util/logging/LoggerWrapper.ts | 40 +++++++++++++----------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/server/src/api/debugApi.ts b/server/src/api/debugApi.ts index 978b60c8..e8cadc91 100644 --- a/server/src/api/debugApi.ts +++ b/server/src/api/debugApi.ts @@ -6,7 +6,8 @@ import { SavePlexProgramExternalIdsTask } from '@/tasks/plex/SavePlexProgramExte import { DateTimeRange } from '@/types/DateTimeRange.js'; import { OpenDateTimeRange } from '@/types/OpenDateTimeRange.js'; import type { RouterPluginAsyncCallback } from '@/types/serverType.js'; -import { tag } from '@tunarr/types'; +import { LoggerFactory } from '@/util/logging/LoggerFactory.js'; +import { LogLevels, tag } from '@tunarr/types'; import { ChannelLineupQuery } from '@tunarr/types/api'; import { ChannelLineupSchema } from '@tunarr/types/schemas'; import dayjs from 'dayjs'; @@ -44,6 +45,23 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => { return res.send(getHeapStatistics()); }); + fastify.get( + '/debug/log', + { + schema: { + querystring: z.object({ + level: z.enum(LogLevels).default('debug'), + log: z.string().optional(), + }), + }, + }, + async (req, res) => { + const logger = LoggerFactory.root; + logger[req.query.level](req.query.log ?? 'Test log'); + return res.send('ok'); + }, + ); + fastify.get( '/debug/helpers/playing_at', { diff --git a/server/src/util/logging/LoggerFactory.ts b/server/src/util/logging/LoggerFactory.ts index 0b849896..36dfde82 100644 --- a/server/src/util/logging/LoggerFactory.ts +++ b/server/src/util/logging/LoggerFactory.ts @@ -31,11 +31,6 @@ import type { SerializedLogger } from './LoggerWrapper.ts'; import { RootLoggerWrapper } from './LoggerWrapper.ts'; import { RollingLogDestination } from './RollingDestination.ts'; -export const LogConfigEnvVars = { - level: 'LOG_LEVEL', - directory: 'LOG_DIRECTORY', -} as const; - export function getEnvironmentLogLevel(envVar?: string): Maybe { const envLevel = trim( toLower(process.env[envVar ?? TUNARR_ENV_VARS.LOG_LEVEL_ENV_VAR]), @@ -108,12 +103,12 @@ export const LogCategories = ['streaming', 'scheduling'] as const; export type LogCategory = TupleToUnion; class LoggerFactoryImpl { - private settingsDB: SettingsDB; + private settingsDB?: SettingsDB; // private rootLogger: PinoLogger; private rootLogger!: RootLoggerWrapper; private initialized = false; private children: Record> = {}; - private currentStreams: MultiStreamRes; + private currentStreams?: MultiStreamRes; private roller?: RollingLogDestination; constructor() { @@ -136,7 +131,7 @@ class LoggerFactoryImpl { } const currentSettings = - this.settingsDB.systemSettings().logging.logRollConfig; + this.settingsDB?.systemSettings().logging.logRollConfig; const { level: newLevel } = this.logLevel; const perCategoryLogLevel = this.perCategoryLogLevel; @@ -272,12 +267,11 @@ class LoggerFactoryImpl { return; } - // Reset the level of the root logger and all children - // We do this by setting the level on the instance directly - // but then for multistream to work, we have to manually reset the streams - // by cloning them with new levels. - this.rootLogger.level = newLevel; - this.rootLogger.updateStreams(this.createLogStreams(newLevel)); + // Reset the level of the root logger and all children. + // We set the level on every logger instance directly because pino children + // snapshot the parent's level at creation time and don't follow changes. + // For multistream to work, we also have to manually reset the streams. + this.rootLogger.updateLevel(newLevel, this.createLogStreams(newLevel)); } private createStreams(logLevel: LogLevels): StreamEntry[] { diff --git a/server/src/util/logging/LoggerWrapper.ts b/server/src/util/logging/LoggerWrapper.ts index 1a69e1e5..bf421cfd 100644 --- a/server/src/util/logging/LoggerWrapper.ts +++ b/server/src/util/logging/LoggerWrapper.ts @@ -25,6 +25,7 @@ interface ILoggerWrapper { args: GetChildLoggerArgs, opts?: ChildLoggerOptions, ): ILoggerWrapper; + updateLevel(level: LogLevels, streams: MultiStreamRes): void; updateStreams(streams: MultiStreamRes): void; logger: Logger; traverseHierarchy(): Generator; @@ -32,7 +33,7 @@ interface ILoggerWrapper { } abstract class BaseLoggerWrapper implements ILoggerWrapper { - protected children: Record> = {}; + protected children: Record = {}; constructor(protected wrappedLogger: Logger) {} @@ -41,14 +42,20 @@ abstract class BaseLoggerWrapper implements ILoggerWrapper { opts?: ChildLoggerOptions, ): ILoggerWrapper; + updateLevel(level: LogLevels, streams: MultiStreamRes) { + this.wrappedLogger.level = level; + Object.assign(this.wrappedLogger[symbols.streamSym], streams); + + for (const child of Object.values(this.children)) { + child.updateLevel(level, streams); + } + } + updateStreams(streams: MultiStreamRes) { Object.assign(this.wrappedLogger[symbols.streamSym], streams); - for (const childRef of Object.values(this.children)) { - const child = childRef.deref(); - if (child) { - child.updateStreams(streams); - } + for (const child of Object.values(this.children)) { + child.updateStreams(streams); } } @@ -67,8 +74,8 @@ abstract class BaseLoggerWrapper implements ILoggerWrapper { } *traverseHierarchy() { - for (const [loggerName, ref] of Object.entries(this.children)) { - const child = ref.deref(); + for (const [loggerName, child] of Object.entries(this.children)) { + // const child = ref.deref(); if (!child) { continue; } @@ -110,7 +117,7 @@ export class RootLoggerWrapper extends BaseLoggerWrapper { { level: initialLogSettings?.categoryLogLevel?.[category] }, ); const wrapped = new LoggerWrapper(categoryLogger); - this.children[`category:${category}`] = new WeakRef(wrapped); + this.children[`category:${category}`] = wrapped; this.loggerByCategory.set(category, wrapped); } } @@ -121,7 +128,7 @@ export class RootLoggerWrapper extends BaseLoggerWrapper { ): ILoggerWrapper { const { caller, className, category, ...rest } = args; - const ref = this.children[className]?.deref(); + const ref = this.children[className]; //?.deref(); if (ref) { return ref; } @@ -145,15 +152,11 @@ export class RootLoggerWrapper extends BaseLoggerWrapper { } else { const newLogger = this.wrappedLogger.child(childOpts, opts); const wrapped = new LoggerWrapper(newLogger); - this.children[className] = new WeakRef(wrapped); + this.children[className] = wrapped; return wrapped; } } - set level(newLevel: LogLevels) { - this.wrappedLogger.level = newLevel; - } - updateCategoryLevel( newLevel: LogLevels, category: LogCategory, @@ -164,8 +167,7 @@ export class RootLoggerWrapper extends BaseLoggerWrapper { return; } - rootCategoryLogger.logger.level = newLevel; - rootCategoryLogger.updateStreams(newStreamFn()); + rootCategoryLogger.updateLevel(newLevel, newStreamFn()); } } @@ -189,7 +191,7 @@ export class LoggerWrapper extends BaseLoggerWrapper { ): ILoggerWrapper { const { caller, className, ...rest } = args; - const ref = this.children[className]?.deref(); + const ref = this.children[className]; //?.deref(); if (ref) { return ref; } @@ -208,7 +210,7 @@ export class LoggerWrapper extends BaseLoggerWrapper { const newChild = new LoggerWrapper( this.wrappedLogger.child(childOpts, opts), ); - this.children[className] = new WeakRef(newChild); + this.children[className] = newChild; return newChild; } }