feat: add logging enhancements by integrating pino and pino-pretty for improved error tracking and debugging across the application

This commit is contained in:
Julien Froidefond
2025-10-26 06:15:47 +01:00
parent 7cc72dc13d
commit 52350a43d9
84 changed files with 455 additions and 177 deletions

View File

@@ -10,6 +10,7 @@ import { RequestMonitorService } from "./request-monitor.service";
import { RequestQueueService } from "./request-queue.service";
import { CircuitBreakerService } from "./circuit-breaker.service";
import { PreferencesService } from "./preferences.service";
import logger from "@/lib/logger";
export type { CacheType };
@@ -42,14 +43,14 @@ export abstract class BaseApiService {
const preferences = await PreferencesService.getPreferences();
return preferences.komgaMaxConcurrentRequests;
} catch (error) {
console.error('Failed to get preferences for request queue:', error);
logger.error({ err: error }, 'Failed to get preferences for request queue');
return 5; // Valeur par défaut
}
});
this.requestQueueInitialized = true;
} catch (error) {
console.error('Failed to initialize request queue:', error);
logger.error({ err: error }, 'Failed to initialize request queue');
}
}
@@ -68,7 +69,7 @@ export abstract class BaseApiService {
const preferences = await PreferencesService.getPreferences();
return preferences.circuitBreakerConfig;
} catch (error) {
console.error('Failed to get preferences for circuit breaker:', error);
logger.error({ err: error }, 'Failed to get preferences for circuit breaker');
return {
threshold: 5,
timeout: 30000,
@@ -79,7 +80,7 @@ export abstract class BaseApiService {
this.circuitBreakerInitialized = true;
} catch (error) {
console.error('Failed to initialize circuit breaker:', error);
logger.error({ err: error }, 'Failed to initialize circuit breaker');
}
}
@@ -103,7 +104,7 @@ export abstract class BaseApiService {
if (error instanceof AppError && error.code === ERROR_CODES.KOMGA.MISSING_CONFIG) {
throw error;
}
console.error("Erreur lors de la récupération de la configuration:", error);
logger.error({ err: error }, "Erreur lors de la récupération de la configuration");
throw new AppError(ERROR_CODES.KOMGA.MISSING_CONFIG, {}, error);
}
}
@@ -199,7 +200,7 @@ export abstract class BaseApiService {
} catch (fetchError: any) {
// Gestion spécifique des erreurs DNS
if (fetchError?.cause?.code === 'EAI_AGAIN' || fetchError?.code === 'EAI_AGAIN') {
console.error(`DNS resolution failed for ${url}. Retrying with different DNS settings...`);
logger.error(`DNS resolution failed for ${url}. Retrying with different DNS settings...`);
// Retry avec des paramètres DNS différents
return await fetch(url, {
@@ -218,8 +219,7 @@ export abstract class BaseApiService {
// Retry automatique sur timeout de connexion (cold start)
if (fetchError?.cause?.code === 'UND_ERR_CONNECT_TIMEOUT') {
// eslint-disable-next-line no-console
console.log(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
logger.info(`⏱️ Connection timeout for ${url}. Retrying once (cold start)...`);
return await fetch(url, {
headers,

View File

@@ -8,6 +8,7 @@ import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import { SeriesService } from "./series.service";
import type { Series } from "@/types/series";
import logger from "@/lib/logger";
export class BookService extends BaseApiService {
private static async getImageCacheMaxAge(): Promise<number> {
@@ -16,7 +17,7 @@ export class BookService extends BaseApiService {
const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000;
return maxAge;
} catch (error) {
console.error('[ImageCache] Error fetching TTL config:', error);
logger.error({ err: error }, '[ImageCache] Error fetching TTL config');
return 2592000; // 30 jours par défaut en cas d'erreur
}
}

View File

@@ -3,6 +3,7 @@
* Évite l'effet avalanche en coupant les requêtes vers un service défaillant
*/
import type { CircuitBreakerConfig } from "@/types/preferences";
import logger from "@/lib/logger";
interface CircuitBreakerState {
state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
@@ -47,7 +48,7 @@ class CircuitBreaker {
resetTimeout: prefConfig.resetTimeout ?? 60000,
};
} catch (error) {
console.error('Error getting circuit breaker config from preferences:', error);
logger.error({ err: error }, 'Error getting circuit breaker config from preferences');
return this.config;
}
}
@@ -78,8 +79,7 @@ class CircuitBreaker {
if (this.state.state === 'HALF_OPEN') {
this.state.failureCount = 0;
this.state.state = 'CLOSED';
// eslint-disable-next-line no-console
console.log('[CIRCUIT-BREAKER] ✅ Circuit closed - Komga recovered');
logger.info('[CIRCUIT-BREAKER] ✅ Circuit closed - Komga recovered');
}
}
@@ -90,7 +90,7 @@ class CircuitBreaker {
if (this.state.failureCount >= config.failureThreshold) {
this.state.state = 'OPEN';
this.state.nextAttemptTime = Date.now() + config.resetTimeout;
console.warn(`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures, reset in ${config.resetTimeout}ms)`);
logger.warn(`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures, reset in ${config.resetTimeout}ms)`);
}
}
@@ -105,8 +105,7 @@ class CircuitBreaker {
lastFailureTime: 0,
nextAttemptTime: 0,
};
// eslint-disable-next-line no-console
console.log('[CIRCUIT-BREAKER] 🔄 Circuit reset');
logger.info('[CIRCUIT-BREAKER] 🔄 Circuit reset');
}
}

View File

@@ -3,6 +3,7 @@ import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { User } from "@/types/komga";
import logger from "@/lib/logger";
export class FavoriteService {
private static readonly FAVORITES_CHANGE_EVENT = "favoritesChanged";
@@ -38,7 +39,7 @@ export class FavoriteService {
});
return !!favorite;
} catch (error) {
console.error("Erreur lors de la vérification du favori:", error);
logger.error({ err: error, seriesId }, "Erreur lors de la vérification du favori");
return false;
}
}

View File

@@ -1,6 +1,7 @@
import { BaseApiService } from "./base-api.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import logger from "@/lib/logger";
export interface ImageResponse {
buffer: Buffer;
@@ -23,7 +24,7 @@ export class ImageService extends BaseApiService {
contentType,
};
} catch (error) {
console.error("Erreur lors de la récupération de l'image:", error);
logger.error({ err: error }, "Erreur lors de la récupération de l'image");
throw new AppError(ERROR_CODES.IMAGE.FETCH_ERROR, {}, error);
}
}

View File

@@ -2,6 +2,8 @@
* Service de monitoring des requêtes concurrentes vers Komga
* Permet de tracker le nombre de requêtes actives et d'alerter en cas de charge élevée
*/
import logger from "@/lib/logger";
class RequestMonitor {
private activeRequests = 0;
private readonly thresholds = {
@@ -29,12 +31,11 @@ class RequestMonitor {
const count = this.activeRequests;
if (count >= this.thresholds.critical) {
console.warn(`[REQUEST-MONITOR] 🔴 CRITICAL concurrency: ${count} active requests`);
logger.warn(`[REQUEST-MONITOR] 🔴 CRITICAL concurrency: ${count} active requests`);
} else if (count >= this.thresholds.high) {
console.warn(`[REQUEST-MONITOR] ⚠️ HIGH concurrency: ${count} active requests`);
logger.warn(`[REQUEST-MONITOR] ⚠️ HIGH concurrency: ${count} active requests`);
} else if (count >= this.thresholds.warning) {
// eslint-disable-next-line no-console
console.log(`[REQUEST-MONITOR] ⚡ Warning concurrency: ${count} active requests`);
logger.info(`[REQUEST-MONITOR] ⚡ Warning concurrency: ${count} active requests`);
}
}
}

View File

@@ -2,6 +2,7 @@
* Service de gestion de queue pour limiter les requêtes concurrentes vers Komga
* Évite de surcharger Komga avec trop de requêtes simultanées
*/
import logger from "@/lib/logger";
interface QueuedRequest<T> {
execute: () => Promise<T>;
@@ -35,7 +36,7 @@ class RequestQueue {
try {
return await this.getMaxConcurrent();
} catch (error) {
console.error('Error getting maxConcurrent from preferences, using default:', error);
logger.error({ err: error }, 'Error getting maxConcurrent from preferences, using default');
return this.maxConcurrent;
}
}

View File

@@ -11,6 +11,7 @@ import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { UserPreferences } from "@/types/preferences";
import type { ServerCacheService } from "./server-cache.service";
import logger from "@/lib/logger";
export class SeriesService extends BaseApiService {
private static async getImageCacheMaxAge(): Promise<number> {
@@ -19,7 +20,7 @@ export class SeriesService extends BaseApiService {
const maxAge = ttlConfig?.imageCacheMaxAge ?? 2592000;
return maxAge;
} catch (error) {
console.error('[ImageCache] Error fetching TTL config:', error);
logger.error({ err: error }, '[ImageCache] Error fetching TTL config');
return 2592000; // 30 jours par défaut en cas d'erreur
}
}
@@ -181,7 +182,7 @@ export class SeriesService extends BaseApiService {
"SERIES"
);
} catch (error) {
console.error("Erreur lors de la récupération du premier livre:", error);
logger.error({ err: error }, "Erreur lors de la récupération du premier livre");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}

View File

@@ -2,6 +2,7 @@ import fs from "fs";
import path from "path";
import { PreferencesService } from "./preferences.service";
import { getCurrentUser } from "../auth-utils";
import logger from "@/lib/logger";
export type CacheMode = "file" | "memory";
@@ -52,7 +53,7 @@ class ServerCacheService {
const preferences = await PreferencesService.getPreferences();
this.setCacheMode(preferences.cacheMode);
} catch (error) {
console.error("Error initializing cache mode from preferences:", error);
logger.error({ err: error }, "Error initializing cache mode from preferences");
// Keep default memory mode if preferences can't be loaded
}
}
@@ -93,7 +94,7 @@ class ServerCacheService {
try {
fs.rmdirSync(itemPath);
} catch (error) {
console.error(`Could not remove directory ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not remove directory ${itemPath}`);
isEmpty = false;
}
} else {
@@ -109,12 +110,12 @@ class ServerCacheService {
isEmpty = false;
}
} catch (error) {
console.error(`Could not parse file ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not parse file ${itemPath}`);
// Si le fichier est corrompu, on le supprime
try {
fs.unlinkSync(itemPath);
} catch (error) {
console.error(`Could not remove file ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not remove file ${itemPath}`);
isEmpty = false;
}
}
@@ -122,7 +123,7 @@ class ServerCacheService {
isEmpty = false;
}
} catch (error) {
console.error(`Could not access ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`);
// En cas d'erreur sur le fichier/dossier, on continue
isEmpty = false;
continue;
@@ -197,12 +198,12 @@ class ServerCacheService {
this.memoryCache.set(key, cached);
}
} catch (error) {
console.error(`Could not parse file ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not parse file ${itemPath}`);
// Ignore les fichiers corrompus
}
}
} catch (error) {
console.error(`Could not access ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`);
// Ignore les erreurs d'accès
}
}
@@ -221,7 +222,7 @@ class ServerCacheService {
}
fs.writeFileSync(filePath, JSON.stringify(value), "utf-8");
} catch (error) {
console.error(`Could not write cache file ${filePath}:`, error);
logger.error({ err: error, path: filePath }, `Could not write cache file ${filePath}`);
}
}
@@ -246,7 +247,7 @@ class ServerCacheService {
}
fs.writeFileSync(filePath, JSON.stringify(cacheData), "utf-8");
} catch (error) {
console.error(`Error writing cache file ${filePath}:`, error);
logger.error({ err: error, path: filePath }, `Error writing cache file ${filePath}`);
}
}
}
@@ -283,7 +284,7 @@ class ServerCacheService {
fs.unlinkSync(filePath);
return null;
} catch (error) {
console.error(`Error reading cache file ${filePath}:`, error);
logger.error({ err: error, path: filePath }, `Error reading cache file ${filePath}`);
return null;
}
}
@@ -317,7 +318,7 @@ class ServerCacheService {
isStale: cached.expiry <= Date.now(),
};
} catch (error) {
console.error(`Error reading cache file ${filePath}:`, error);
logger.error({ err: error, path: filePath }, `Error reading cache file ${filePath}`);
return null;
}
}
@@ -392,17 +393,17 @@ class ServerCacheService {
try {
fs.rmdirSync(itemPath);
} catch (error) {
console.error(`Could not remove directory ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not remove directory ${itemPath}`);
}
} else {
try {
fs.unlinkSync(itemPath);
} catch (error) {
console.error(`Could not remove file ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not remove file ${itemPath}`);
}
}
} catch (error) {
console.error(`Error accessing ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Error accessing ${itemPath}`);
}
}
};
@@ -410,7 +411,7 @@ class ServerCacheService {
try {
removeDirectory(this.cacheDir);
} catch (error) {
console.error("Error clearing cache:", error);
logger.error({ err: error }, "Error clearing cache");
}
}
@@ -443,8 +444,7 @@ class ServerCacheService {
if (process.env.CACHE_DEBUG === 'true') {
const icon = isStale ? '⚠️' : '✅';
const status = isStale ? 'STALE' : 'HIT';
// eslint-disable-next-line no-console
console.log(`${icon} [CACHE ${status}] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
logger.debug(`${icon} [CACHE ${status}] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
}
// Si le cache est expiré, revalider en background sans bloquer la réponse
@@ -458,8 +458,7 @@ class ServerCacheService {
// Pas de cache du tout, fetch normalement
if (process.env.CACHE_DEBUG === 'true') {
// eslint-disable-next-line no-console
console.log(`❌ [CACHE MISS] ${key} | ${type}`);
logger.debug(`❌ [CACHE MISS] ${key} | ${type}`);
}
try {
@@ -468,8 +467,7 @@ class ServerCacheService {
const endTime = performance.now();
if (process.env.CACHE_DEBUG === 'true') {
// eslint-disable-next-line no-console
console.log(`💾 [CACHE SET] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
logger.debug(`💾 [CACHE SET] ${key} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
}
return data;
@@ -494,11 +492,10 @@ class ServerCacheService {
if (process.env.CACHE_DEBUG === 'true') {
const endTime = performance.now();
// eslint-disable-next-line no-console
console.log(`🔄 [CACHE REVALIDATE] ${debugKey} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
logger.debug(`🔄 [CACHE REVALIDATE] ${debugKey} | ${type} | ${(endTime - startTime).toFixed(2)}ms`);
}
} catch (error) {
console.error(`🔴 [CACHE REVALIDATE ERROR] ${debugKey}:`, error);
logger.error({ err: error, key: debugKey }, `🔴 [CACHE REVALIDATE ERROR] ${debugKey}`);
// Ne pas relancer l'erreur car c'est en background
}
}
@@ -582,7 +579,7 @@ class ServerCacheService {
itemCount++;
}
} catch (error) {
console.error(`Could not access ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`);
}
}
};
@@ -648,11 +645,11 @@ class ServerCacheService {
isExpired: cached.expiry <= Date.now(),
});
} catch (error) {
console.error(`Could not parse file ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not parse file ${itemPath}`);
}
}
} catch (error) {
console.error(`Could not access ${itemPath}:`, error);
logger.error({ err: error, path: itemPath }, `Could not access ${itemPath}`);
}
}
};

View File

@@ -3,6 +3,7 @@ import type { AuthConfig } from "@/types/auth";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { KomgaLibrary } from "@/types/komga";
import logger from "@/lib/logger";
export class TestService extends BaseApiService {
static async testConnection(config: AuthConfig): Promise<{ libraries: KomgaLibrary[] }> {
@@ -20,7 +21,7 @@ export class TestService extends BaseApiService {
const libraries = await response.json();
return { libraries };
} catch (error) {
console.error("Erreur lors du test de connexion:", error);
logger.error({ err: error }, "Erreur lors du test de connexion");
if (error instanceof AppError) {
throw error;
}