feat: implement debug mode with enhanced logging and filtering capabilities

This commit is contained in:
Julien Froidefond
2025-10-07 21:08:20 +02:00
parent 760bd14aa7
commit df6a30b226
9 changed files with 420 additions and 60 deletions

View File

@@ -1,11 +1,11 @@
import type { AuthConfig } from "@/types/auth";
import { getServerCacheService } from "./server-cache.service";
import { ConfigDBService } from "./config-db.service";
import { DebugService } from "./debug.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { KomgaConfig } from "@/types/komga";
import type { ServerCacheService } from "./server-cache.service";
import { fetchWithCacheDetection } from "../utils/fetch-with-cache-detection";
// Types de cache disponibles
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
@@ -86,7 +86,6 @@ export abstract class BaseApiService {
headersOptions = {},
options: KomgaRequestInit = {}
): Promise<T> {
const startTime = performance.now();
const config: AuthConfig = await this.getKomgaConfig();
const { path, params } = urlBuilder;
const url = this.buildUrl(config, path, params);
@@ -99,16 +98,7 @@ export abstract class BaseApiService {
}
try {
const response = await fetch(url, { headers, ...options });
const endTime = performance.now();
// Log la requête
await DebugService.logRequest({
url: path,
startTime,
endTime,
fromCache: false,
});
const response = await fetchWithCacheDetection(url, { headers, ...options });
if (!response.ok) {
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
@@ -117,18 +107,8 @@ export abstract class BaseApiService {
});
}
return options.isImage ? response : response.json();
return options.isImage ? (response as T) : response.json();
} catch (error) {
const endTime = performance.now();
// Log aussi les erreurs
await DebugService.logRequest({
url: path,
startTime,
endTime,
fromCache: false,
});
throw error;
}
}

View File

@@ -25,6 +25,8 @@ export interface RequestTiming {
}
export class DebugService {
private static writeQueues = new Map<string, Promise<void>>();
private static async getCurrentUserId(): Promise<string> {
const user = await AuthServerService.getCurrentUser();
if (!user) {
@@ -60,13 +62,116 @@ export class DebugService {
const content = await fs.readFile(filePath, "utf-8");
return JSON.parse(content);
} catch {
return [];
// Essayer de lire un fichier de sauvegarde
try {
const backupPath = filePath + '.backup';
const backupContent = await fs.readFile(backupPath, "utf-8");
return JSON.parse(backupContent);
} catch {
return [];
}
}
}
private static async writeLogs(filePath: string, logs: RequestTiming[]): Promise<void> {
const trimmedLogs = logs.slice(-99);
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2));
// Obtenir la queue existante ou créer une nouvelle
const existingQueue = this.writeQueues.get(filePath);
// Créer une nouvelle promesse qui attend la queue précédente
const newQueue = existingQueue
? existingQueue.then(() => this.performAppend(filePath, logs))
: this.performAppend(filePath, logs);
// Mettre à jour la queue
this.writeQueues.set(filePath, newQueue);
try {
await newQueue;
} finally {
// Nettoyer la queue si c'est la dernière opération
if (this.writeQueues.get(filePath) === newQueue) {
this.writeQueues.delete(filePath);
}
}
}
private static async performAppend(filePath: string, logs: RequestTiming[]): Promise<void> {
try {
// Lire le fichier existant
const existingLogs = await this.readLogs(filePath);
// Fusionner avec les nouveaux logs
const allLogs = [...existingLogs, ...logs];
// Garder seulement les 1000 derniers logs
const trimmedLogs = allLogs.slice(-1000);
// Créer une sauvegarde avant d'écrire
try {
await fs.copyFile(filePath, filePath + '.backup');
} catch {
// Ignorer si le fichier n'existe pas encore
}
// Écrire le fichier complet (c'est nécessaire pour maintenir l'ordre chronologique)
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
} catch (error) {
console.error(`Erreur lors de l'écriture des logs pour ${filePath}:`, error);
// Ne pas relancer l'erreur pour éviter de casser l'application
}
}
private static async appendLog(filePath: string, log: RequestTiming): Promise<void> {
// Obtenir la queue existante ou créer une nouvelle
const existingQueue = this.writeQueues.get(filePath);
// Créer une nouvelle promesse qui attend la queue précédente
const newQueue = existingQueue
? existingQueue.then(() => this.performSingleAppend(filePath, log))
: this.performSingleAppend(filePath, log);
// Mettre à jour la queue
this.writeQueues.set(filePath, newQueue);
try {
await newQueue;
} finally {
// Nettoyer la queue si c'est la dernière opération
if (this.writeQueues.get(filePath) === newQueue) {
this.writeQueues.delete(filePath);
}
}
}
private static async performSingleAppend(filePath: string, log: RequestTiming): Promise<void> {
try {
// Lire le fichier existant
const existingLogs = await this.readLogs(filePath);
// Vérifier les doublons avec des tolérances différentes selon le type
const isPageRender = log.pageRender !== undefined;
const timeTolerance = isPageRender ? 500 : 50; // 500ms pour les rendus, 50ms pour les requêtes
const exists = existingLogs.some(existingLog =>
existingLog.url === log.url &&
Math.abs(existingLog.duration - log.duration) < 10 && // Durée similaire (10ms de tolérance)
Math.abs(new Date(existingLog.timestamp).getTime() - new Date(log.timestamp).getTime()) < timeTolerance
);
if (!exists) {
// Ajouter le nouveau log
const allLogs = [...existingLogs, log];
// Garder seulement les 1000 derniers logs
const trimmedLogs = allLogs.slice(-1000);
// Écrire le fichier complet avec gestion d'erreur
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
}
} catch (error) {
console.error(`Erreur lors de l'écriture du log pour ${filePath}:`, error);
// Ne pas relancer l'erreur pour éviter de casser l'application
}
}
private static createTiming(
@@ -95,7 +200,6 @@ export class DebugService {
await this.ensureDebugDir();
const filePath = this.getLogFilePath(userId);
const logs = await this.readLogs(filePath);
const newTiming = this.createTiming(
timing.url,
timing.startTime,
@@ -108,7 +212,8 @@ export class DebugService {
}
);
await this.writeLogs(filePath, [...logs, newTiming]);
// Utiliser un système d'append atomique
await this.appendLog(filePath, newTiming);
} catch (error) {
console.error("Erreur lors de l'enregistrement du log:", error);
}
@@ -143,13 +248,13 @@ export class DebugService {
await this.ensureDebugDir();
const filePath = this.getLogFilePath(userId);
const logs = await this.readLogs(filePath);
const now = performance.now();
const newTiming = this.createTiming(`Page Render: ${page}`, now - duration, now, false, {
pageRender: { page, duration },
});
await this.writeLogs(filePath, [...logs, newTiming]);
// Utiliser le même système d'append atomique
await this.appendLog(filePath, newTiming);
} catch (error) {
console.error("Erreur lors de l'enregistrement du log de rendu:", error);
}

View File

@@ -400,9 +400,9 @@ class ServerCacheService {
if (cached !== null) {
const endTime = performance.now();
// Log la requête avec l'indication du cache
// Log la requête avec l'indication du cache (URL plus claire)
await DebugService.logRequest({
url: cacheKey,
url: `[CACHE] ${key}`,
startTime,
endTime,
fromCache: true,

View File

@@ -0,0 +1,57 @@
// Wrapper pour détecter le cache du navigateur
export async function fetchWithCacheDetection(url: string, options: RequestInit = {}) {
const startTime = performance.now();
try {
const response = await fetch(url, options);
const endTime = performance.now();
// Détecter si la réponse vient du cache du navigateur
const fromBrowserCache = response.headers.get('x-cache') === 'HIT' ||
response.headers.get('cf-cache-status') === 'HIT' ||
(endTime - startTime) < 5; // Si très rapide, probablement du cache
// Logger la requête seulement si ce n'est pas une requête de debug
// Note: La vérification du mode debug se fait côté serveur dans DebugService
if (!url.includes('/api/debug')) {
try {
await fetch("/api/debug", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: url,
startTime,
endTime,
fromCache: fromBrowserCache,
}),
});
} catch {
// Ignorer les erreurs de logging
}
}
return response;
} catch (error) {
const endTime = performance.now();
// Logger aussi les erreurs seulement si ce n'est pas une requête de debug
if (!url.includes('/api/debug')) {
try {
await fetch("/api/debug", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: url,
startTime,
endTime,
fromCache: false,
}),
});
} catch {
// Ignorer les erreurs de logging
}
}
throw error;
}
}