mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-07 13:08:54 -07:00
fix(logs): use local date for log filenames and expand export redaction (#131)
This commit is contained in:
@@ -12,6 +12,45 @@ import {
|
||||
resolveDefaultLogFilePath,
|
||||
} from './log-files';
|
||||
|
||||
function withTimeZone<T>(timeZone: string, run: () => T): T {
|
||||
const previous = process.env.TZ;
|
||||
process.env.TZ = timeZone;
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.TZ;
|
||||
} else {
|
||||
process.env.TZ = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function withMockedNow<T>(isoDate: string, run: () => T): T {
|
||||
const RealDate = Date;
|
||||
const fixedMs = RealDate.parse(isoDate);
|
||||
class FixedDate extends RealDate {
|
||||
constructor(value?: string | number | Date) {
|
||||
if (arguments.length === 0) {
|
||||
super(fixedMs);
|
||||
} else {
|
||||
super(value as string);
|
||||
}
|
||||
}
|
||||
|
||||
static override now(): number {
|
||||
return fixedMs;
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.Date = FixedDate as DateConstructor;
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
globalThis.Date = RealDate;
|
||||
}
|
||||
}
|
||||
|
||||
test('resolveDefaultLogFilePath uses app prefix by default', () => {
|
||||
const now = new Date('2026-03-22T12:00:00.000Z');
|
||||
const resolved = resolveDefaultLogFilePath('app', {
|
||||
@@ -26,6 +65,36 @@ test('resolveDefaultLogFilePath uses app prefix by default', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveDefaultLogFilePath uses the local date when UTC has advanced', () => {
|
||||
withTimeZone('America/Los_Angeles', () => {
|
||||
const now = new Date('2026-05-26T02:00:00.000Z');
|
||||
assert.equal(now.toISOString().slice(0, 10), '2026-05-26');
|
||||
|
||||
const resolved = resolveDefaultLogFilePath('app', {
|
||||
platform: 'linux',
|
||||
homeDir: '/home/tester',
|
||||
now,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
resolved,
|
||||
path.join('/home/tester', '.config', 'SubMiner', 'logs', 'app-2026-05-25.log'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveDefaultLogFilePath rejects invalid dates', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveDefaultLogFilePath('app', {
|
||||
platform: 'linux',
|
||||
homeDir: '/home/tester',
|
||||
now: new Date('invalid'),
|
||||
}),
|
||||
/Invalid time value/,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveDefaultLogFilePath uses daily filenames for mpv logs', () => {
|
||||
const now = new Date('2026-03-22T12:00:00.000Z');
|
||||
const resolved = resolveDefaultLogFilePath('mpv', {
|
||||
@@ -91,6 +160,34 @@ test('pruneLogFiles removes logs older than retention window', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('pruneLogDirectoryForPath deduplicates by local day across UTC midnight', () => {
|
||||
withTimeZone('America/Los_Angeles', () => {
|
||||
const logsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-log-prune-local-day-'));
|
||||
const logPath = path.join(logsDir, 'app-2026-05-25.log');
|
||||
const firstStalePath = path.join(logsDir, 'app-first-stale.log');
|
||||
const secondStalePath = path.join(logsDir, 'app-second-stale.log');
|
||||
const staleDate = new Date('2026-05-01T12:00:00.000Z');
|
||||
|
||||
try {
|
||||
fs.writeFileSync(firstStalePath, 'stale\n', 'utf8');
|
||||
fs.utimesSync(firstStalePath, staleDate, staleDate);
|
||||
|
||||
withMockedNow('2026-05-25T23:30:00.000Z', () => pruneLogDirectoryForPath(logPath, 7));
|
||||
|
||||
assert.equal(fs.existsSync(firstStalePath), false);
|
||||
|
||||
fs.writeFileSync(secondStalePath, 'stale\n', 'utf8');
|
||||
fs.utimesSync(secondStalePath, staleDate, staleDate);
|
||||
|
||||
withMockedNow('2026-05-26T00:30:00.000Z', () => pruneLogDirectoryForPath(logPath, 7));
|
||||
|
||||
assert.equal(fs.existsSync(secondStalePath), true);
|
||||
} finally {
|
||||
fs.rmSync(logsDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('appendLogLine trims oversized logs to newest bytes', () => {
|
||||
const logsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-log-trim-'));
|
||||
const logPath = path.join(logsDir, 'app.log');
|
||||
|
||||
+13
-2
@@ -36,6 +36,17 @@ function floorDiv(left: number, right: number): number {
|
||||
return Math.floor(left / right);
|
||||
}
|
||||
|
||||
function padDatePart(value: number): string {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
export function localDateKey(date: Date): string {
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
throw new RangeError('Invalid time value');
|
||||
}
|
||||
return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`;
|
||||
}
|
||||
|
||||
function daysFromCivil(year: number, month: number, day: number): bigint {
|
||||
const adjustedYear = year - (month <= 2 ? 1 : 0);
|
||||
const era = floorDiv(adjustedYear >= 0 ? adjustedYear : adjustedYear - 399, 400);
|
||||
@@ -78,7 +89,7 @@ export function resolveDefaultLogFilePath(
|
||||
},
|
||||
): string {
|
||||
const now = options?.now ?? new Date();
|
||||
const suffix = now.toISOString().slice(0, 10);
|
||||
const suffix = localDateKey(now);
|
||||
return path.join(resolveLogBaseDir(options), 'logs', `${kind}-${suffix}.log`);
|
||||
}
|
||||
|
||||
@@ -167,7 +178,7 @@ export function pruneLogFiles(
|
||||
|
||||
function maybePruneLogDirectory(logPath: string, retentionDays: number): void {
|
||||
const logsDir = path.dirname(logPath);
|
||||
const key = `${logsDir}:${new Date().toISOString().slice(0, 10)}:${retentionDays}`;
|
||||
const key = `${logsDir}:${localDateKey(new Date())}:${retentionDays}`;
|
||||
if (prunedDirectories.has(key)) return;
|
||||
pruneLogFiles(logsDir, { retentionDays });
|
||||
prunedDirectories.add(key);
|
||||
|
||||
Reference in New Issue
Block a user