feat(loop): loop logları kalıcı hale getir
Loop loglarının dosya sistemine kaydedilmesi, okunması ve arşivlenmesi için yeni storage modülü eklendi. Loglar artık oturumlar arasında korunur. UI tarafında mobil menü iyileştirmeleri ve log paneli güncellemeleri yapıldı.
This commit is contained in:
@@ -25,6 +25,8 @@ export const config = {
|
||||
dataDir: "/app/data",
|
||||
dbPath: "/app/data/db.json",
|
||||
logsPath: "/app/data/logs.json",
|
||||
loopLogsDir: "/app/data/loop-logs",
|
||||
loopLogsArchiveDir: "/app/data/loop-logs-archive",
|
||||
torrentArchiveDir: "/app/data/torrents",
|
||||
webPublicDir: path.resolve("/app/apps/server/public"),
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readDb, writeDb } from "../storage/jsondb";
|
||||
import { nowIso } from "../utils/time";
|
||||
import { emitJobLog, emitJobMetrics } from "../realtime/emitter";
|
||||
import { appendAuditLog } from "../utils/logger";
|
||||
import { appendLoopLog } from "../storage/loopLogs";
|
||||
|
||||
const peerErrorThrottle = new Map<string, number>();
|
||||
|
||||
@@ -30,6 +31,12 @@ export const startEnforcementWorker = (intervalMs: number) => {
|
||||
message: "Peer listing unsupported; enforcement disabled",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
await appendLoopLog({
|
||||
jobId: job.id,
|
||||
level: "WARN",
|
||||
message: "Peer listing unsupported; enforcement disabled",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let peersResponse;
|
||||
@@ -46,6 +53,12 @@ export const startEnforcementWorker = (intervalMs: number) => {
|
||||
message: "Peer listesi desteklenmiyor; enforcement devre dışı.",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
await appendLoopLog({
|
||||
jobId: job.id,
|
||||
level: "WARN",
|
||||
message: "Peer listesi desteklenmiyor; enforcement devre dışı.",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
peerErrorThrottle.set(job.id, Date.now());
|
||||
}
|
||||
continue;
|
||||
@@ -77,6 +90,12 @@ export const startEnforcementWorker = (intervalMs: number) => {
|
||||
message: `Banned ${banned.length} peers`,
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
await appendLoopLog({
|
||||
jobId: job.id,
|
||||
level: "WARN",
|
||||
message: `Banned ${banned.length} peers`,
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
await appendAuditLog({
|
||||
level: "WARN",
|
||||
event: "PEER_BANNED",
|
||||
@@ -91,6 +110,12 @@ export const startEnforcementWorker = (intervalMs: number) => {
|
||||
message: "Peer ban unsupported; warn-only enforcement",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
await appendLoopLog({
|
||||
jobId: job.id,
|
||||
level: "WARN",
|
||||
message: "Peer ban unsupported; warn-only enforcement",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
}
|
||||
|
||||
if (!allowIpConnected) {
|
||||
@@ -100,6 +125,12 @@ export const startEnforcementWorker = (intervalMs: number) => {
|
||||
message: "Allowed IP not connected",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
await appendLoopLog({
|
||||
jobId: job.id,
|
||||
level: "WARN",
|
||||
message: "Allowed IP not connected",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
}
|
||||
|
||||
job.updatedAt = nowIso();
|
||||
@@ -111,6 +142,12 @@ export const startEnforcementWorker = (intervalMs: number) => {
|
||||
message: "Enforcement error; continuing.",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
await appendLoopLog({
|
||||
jobId: job.id,
|
||||
level: "ERROR",
|
||||
message: "Enforcement error; continuing.",
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ import { LoopJob } from "../types";
|
||||
import { nowIso } from "../utils/time";
|
||||
import { appendAuditLog, logger } from "../utils/logger";
|
||||
import { emitJobLog, emitJobMetrics } from "../realtime/emitter";
|
||||
import { appendLoopLog, archiveLoopLogs } from "../storage/loopLogs";
|
||||
import { config } from "../config";
|
||||
|
||||
const logJob = async (jobId: string, level: "INFO" | "WARN" | "ERROR", message: string, event?: string) => {
|
||||
const createdAt = nowIso();
|
||||
emitJobLog({ jobId, level, message, createdAt });
|
||||
await appendLoopLog({ jobId, level, message, createdAt });
|
||||
if (event) {
|
||||
await appendAuditLog({ level, event: event as any, message });
|
||||
}
|
||||
@@ -78,6 +80,7 @@ export const stopLoopJob = async (jobId: string) => {
|
||||
job.updatedAt = nowIso();
|
||||
await writeDb(db);
|
||||
await logJob(job.id, "WARN", "Loop job stopped by user");
|
||||
await archiveLoopLogs(job.id, job.torrentHash);
|
||||
return job;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getArchiveStatus } from "../torrent/torrent.archive";
|
||||
import { config } from "../config";
|
||||
import { setArchiveStatus } from "../torrent/torrent.archive";
|
||||
import { nowIso } from "../utils/time";
|
||||
import { readLoopLogs } from "../storage/loopLogs";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -145,7 +146,9 @@ router.get("/job/:jobId", async (req, res) => {
|
||||
});
|
||||
|
||||
router.get("/logs/:jobId", async (req, res) => {
|
||||
res.json({ jobId: req.params.jobId, logs: [] });
|
||||
const { jobId } = req.params;
|
||||
const logs = await readLoopLogs(jobId);
|
||||
res.json({ jobId, logs });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
89
apps/server/src/storage/loopLogs.ts
Normal file
89
apps/server/src/storage/loopLogs.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { config } from "../config";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Mutex } from "./mutex";
|
||||
|
||||
export type LoopLogEntry = {
|
||||
jobId: string;
|
||||
level: "INFO" | "WARN" | "ERROR";
|
||||
message: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const loopLogMutex = new Mutex();
|
||||
|
||||
const getLoopLogPath = (jobId: string) =>
|
||||
path.join(config.loopLogsDir, `${jobId}.json`);
|
||||
|
||||
const getArchivePath = (jobId: string, torrentHash: string | undefined) => {
|
||||
const safeTime = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const suffix = torrentHash ? `-${torrentHash}` : "";
|
||||
return path.join(config.loopLogsArchiveDir, `${jobId}${suffix}-${safeTime}.json`);
|
||||
};
|
||||
|
||||
export const appendLoopLog = async (entry: LoopLogEntry) => {
|
||||
try {
|
||||
await loopLogMutex.run(async () => {
|
||||
const filePath = getLoopLogPath(entry.jobId);
|
||||
let existing: LoopLogEntry[] = [];
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
existing = JSON.parse(content) as LoopLogEntry[];
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const next = [...existing, entry].slice(-2000);
|
||||
await fs.writeFile(filePath, JSON.stringify(next, null, 2), "utf-8");
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Failed to append loop log");
|
||||
}
|
||||
};
|
||||
|
||||
export const readLoopLogs = async (jobId: string) => {
|
||||
const filePath = getLoopLogPath(jobId);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return JSON.parse(content) as LoopLogEntry[];
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(config.loopLogsArchiveDir);
|
||||
const matches = entries
|
||||
.filter((name) => name.startsWith(jobId))
|
||||
.map((name) => path.join(config.loopLogsArchiveDir, name));
|
||||
if (!matches.length) {
|
||||
return [] as LoopLogEntry[];
|
||||
}
|
||||
const latest = matches.sort().at(-1) as string;
|
||||
const content = await fs.readFile(latest, "utf-8");
|
||||
return JSON.parse(content) as LoopLogEntry[];
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return [] as LoopLogEntry[];
|
||||
};
|
||||
|
||||
export const archiveLoopLogs = async (jobId: string, torrentHash?: string) => {
|
||||
const filePath = getLoopLogPath(jobId);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const target = getArchivePath(jobId, torrentHash);
|
||||
await fs.rename(filePath, target);
|
||||
return target;
|
||||
};
|
||||
@@ -4,4 +4,6 @@ import { config } from "../config"
|
||||
export const ensureDataPaths = async () => {
|
||||
await fs.mkdir(config.dataDir, { recursive: true });
|
||||
await fs.mkdir(config.torrentArchiveDir, { recursive: true });
|
||||
await fs.mkdir(config.loopLogsDir, { recursive: true });
|
||||
await fs.mkdir(config.loopLogsArchiveDir, { recursive: true });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user