feat: add code queue services and baidu netdisk
This commit is contained in:
@@ -68,6 +68,12 @@ export interface DockerContainerSummary {
|
||||
runningFor: string;
|
||||
size: string;
|
||||
networks: string;
|
||||
restartPolicy?: string;
|
||||
restartPolicyMaximumRetryCount?: number;
|
||||
pidMode?: string;
|
||||
composeProject?: string;
|
||||
composeService?: string;
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DockerImageSummary {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
|
||||
export const DEFAULT_LOG_RETENTION_BYTES = 1024 * 1024 * 1024;
|
||||
|
||||
export interface HourlyJsonlWriter {
|
||||
appendLine(line: string, at?: Date): void;
|
||||
appendJson(value: unknown, at?: Date): void;
|
||||
currentPath(at?: Date): string;
|
||||
prune(at?: Date): void;
|
||||
}
|
||||
|
||||
interface HourlyJsonlWriterOptions {
|
||||
baseLogFile: string;
|
||||
service: string;
|
||||
maxBytes?: number;
|
||||
}
|
||||
|
||||
interface FileEntry {
|
||||
path: string;
|
||||
size: number;
|
||||
mtimeMs: number;
|
||||
}
|
||||
|
||||
function pad(value: number): string {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function localParts(date: Date): { day: string; hour: string } {
|
||||
return {
|
||||
day: `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`,
|
||||
hour: pad(date.getHours()),
|
||||
};
|
||||
}
|
||||
|
||||
function safeServiceName(service: string): string {
|
||||
return service.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "service";
|
||||
}
|
||||
|
||||
function parseBase(baseLogFile: string, service: string): { rootDir: string; prefix: string; suffix: string } {
|
||||
const suffix = `_${service}.jsonl`;
|
||||
const dir = dirname(baseLogFile);
|
||||
const dirName = basename(dir);
|
||||
const rootDir = /^\d{8}$/u.test(dirName) ? dirname(dir) : dir;
|
||||
const fileName = basename(baseLogFile);
|
||||
const stem = fileName.replace(/\.jsonl$/u, "");
|
||||
const ownPrefix = fileName.endsWith(suffix) ? fileName.slice(0, -suffix.length) : "";
|
||||
const startStampPrefix = stem.match(/^(\d{8}_\d{6})(?:_\d{8}_\d{2})?_/u)?.[1] ?? "";
|
||||
const genericPrefix = stem.includes("_") ? stem.slice(0, stem.lastIndexOf("_")) : stem;
|
||||
const prefix = ownPrefix || startStampPrefix || genericPrefix;
|
||||
return { rootDir, prefix: prefix || "unidesk", suffix };
|
||||
}
|
||||
|
||||
function collectFiles(root: string, suffix: string): FileEntry[] {
|
||||
if (!existsSync(root)) return [];
|
||||
const result: FileEntry[] = [];
|
||||
const scan = (dir: string): void => {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
scan(path);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile() || !entry.name.endsWith(suffix)) continue;
|
||||
try {
|
||||
const stat = statSync(path);
|
||||
result.push({ path, size: stat.size, mtimeMs: stat.mtimeMs });
|
||||
} catch {
|
||||
// Ignore files that disappear while retention is scanning.
|
||||
}
|
||||
}
|
||||
};
|
||||
scan(root);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function logRetentionBytesFromEnv(name: string, fallback = DEFAULT_LOG_RETENTION_BYTES): number {
|
||||
const raw = process.env[name]?.trim();
|
||||
if (!raw) return fallback;
|
||||
const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|k|kb|kib|m|mb|mib|g|gb|gib)?$/iu);
|
||||
if (!match) return fallback;
|
||||
const value = Number(match[1]);
|
||||
const unit = (match[2] ?? "b").toLowerCase();
|
||||
const multiplier =
|
||||
unit === "g" || unit === "gb" || unit === "gib" ? 1024 * 1024 * 1024
|
||||
: unit === "m" || unit === "mb" || unit === "mib" ? 1024 * 1024
|
||||
: unit === "k" || unit === "kb" || unit === "kib" ? 1024
|
||||
: 1;
|
||||
const bytes = value * multiplier;
|
||||
return Number.isFinite(bytes) && bytes > 0 ? Math.floor(bytes) : fallback;
|
||||
}
|
||||
|
||||
export function logRetentionEnvNameForService(service: string): string {
|
||||
const normalized = safeServiceName(service).toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
||||
return `UNIDESK_${normalized}_LOG_MAX_BYTES`;
|
||||
}
|
||||
|
||||
export function logRetentionBytesForService(service: string, extraEnvNames: string[] = [], fallback = DEFAULT_LOG_RETENTION_BYTES): number {
|
||||
const globalMaxBytes = logRetentionBytesFromEnv("UNIDESK_LOG_RETENTION_BYTES", fallback);
|
||||
for (const name of [logRetentionEnvNameForService(service), ...extraEnvNames]) {
|
||||
if (process.env[name]?.trim()) return logRetentionBytesFromEnv(name, globalMaxBytes);
|
||||
}
|
||||
return globalMaxBytes;
|
||||
}
|
||||
|
||||
export function createHourlyJsonlWriter(options: HourlyJsonlWriterOptions): HourlyJsonlWriter {
|
||||
const service = safeServiceName(options.service);
|
||||
const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_LOG_RETENTION_BYTES));
|
||||
const { rootDir, prefix, suffix } = parseBase(options.baseLogFile, service);
|
||||
let lastPruneAt = 0;
|
||||
|
||||
const currentPath = (at = new Date()): string => {
|
||||
const parts = localParts(at);
|
||||
return join(rootDir, parts.day, `${prefix}_${parts.day}_${parts.hour}${suffix}`);
|
||||
};
|
||||
|
||||
const prune = (at = new Date()): void => {
|
||||
const activePath = currentPath(at);
|
||||
const files = collectFiles(rootDir, suffix).sort((left, right) => {
|
||||
if (left.path === activePath) return 1;
|
||||
if (right.path === activePath) return -1;
|
||||
return left.path.localeCompare(right.path) || left.mtimeMs - right.mtimeMs;
|
||||
});
|
||||
let total = files.reduce((sum, file) => sum + file.size, 0);
|
||||
for (const file of files) {
|
||||
if (total <= maxBytes) break;
|
||||
if (file.path === activePath) continue;
|
||||
try {
|
||||
unlinkSync(file.path);
|
||||
total -= file.size;
|
||||
} catch {
|
||||
// Best-effort retention must never break service logging.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const maybePrune = (at: Date): void => {
|
||||
const now = Date.now();
|
||||
if (now - lastPruneAt < 60_000) return;
|
||||
lastPruneAt = now;
|
||||
prune(at);
|
||||
};
|
||||
|
||||
const appendLine = (line: string, at = new Date()): void => {
|
||||
const path = currentPath(at);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
appendFileSync(path, line.endsWith("\n") ? line : `${line}\n`, "utf8");
|
||||
maybePrune(at);
|
||||
};
|
||||
|
||||
return {
|
||||
appendLine,
|
||||
appendJson(value: unknown, at = new Date()): void {
|
||||
appendLine(JSON.stringify(value), at);
|
||||
},
|
||||
currentPath,
|
||||
prune,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user