mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 18:15:40 +00:00
fix(logging): address review feedback on cleanup and log reader
This commit is contained in:
1 parent
382aa04ca0
commit
42481c0bb9
7 files changed
+410
-34
No files matched your search
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Cleanup Command Handler
|
||||
*
|
||||
* Removes old CLIProxy logs to free up disk space.
|
||||
* Removes old CCS and CLIProxy logs to free up disk space.
|
||||
* Supports both main logs and error request logs with age-based filtering.
|
||||
* Logs can accumulate to several GB without user awareness.
|
||||
*/
|
||||
@@ -32,23 +32,20 @@ function formatBytes(bytes: number): string {
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
||||
}
|
||||
|
||||
/** Calculate total size of a directory */
|
||||
/** Calculate total size of regular top-level files in a directory */
|
||||
function getDirSize(dirPath: string): number {
|
||||
if (!fs.existsSync(dirPath)) return 0;
|
||||
|
||||
let totalSize = 0;
|
||||
const files = fs.readdirSync(dirPath);
|
||||
const entries = fs.readdirSync(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
for (const entry of entries) {
|
||||
const filePath = path.join(dirPath, entry);
|
||||
try {
|
||||
const stats = fs.lstatSync(filePath); // Use lstat to detect symlinks
|
||||
const stats = fs.lstatSync(filePath);
|
||||
if (stats.isFile() && !stats.isSymbolicLink()) {
|
||||
totalSize += stats.size;
|
||||
} else if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
||||
totalSize += getDirSize(filePath);
|
||||
}
|
||||
// Skip symlinks for safety
|
||||
} catch {
|
||||
// File may have been deleted between readdir and stat - skip
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export const ROOT_COMMAND_CATALOG: readonly RootCommandEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'cleanup',
|
||||
summary: 'Remove old CLIProxy logs',
|
||||
summary: 'Remove old CCS and CLIProxy logs',
|
||||
group: 'operations',
|
||||
aliases: ['--cleanup'],
|
||||
visibility: 'public',
|
||||
|
||||
@@ -8,6 +8,15 @@ import {
|
||||
type ReadLogEntriesOptions,
|
||||
} from './log-types';
|
||||
|
||||
type CurrentLogCache = {
|
||||
entries: LogEntry[];
|
||||
mtimeNs: bigint;
|
||||
path: string;
|
||||
size: bigint;
|
||||
} | null;
|
||||
|
||||
let currentLogCache: CurrentLogCache = null;
|
||||
|
||||
function parseLogLine(line: string): LogEntry | null {
|
||||
try {
|
||||
return JSON.parse(line) as LogEntry;
|
||||
@@ -19,16 +28,62 @@ function parseLogLine(line: string): LogEntry | null {
|
||||
function readCurrentFileEntries(): LogEntry[] {
|
||||
const currentLogPath = getCurrentLogPath();
|
||||
if (!fs.existsSync(currentLogPath)) {
|
||||
currentLogCache = null;
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
const stats = fs.statSync(currentLogPath, { bigint: true });
|
||||
if (
|
||||
currentLogCache &&
|
||||
currentLogCache.path === currentLogPath &&
|
||||
currentLogCache.mtimeNs === stats.mtimeNs &&
|
||||
currentLogCache.size === stats.size
|
||||
) {
|
||||
return [...currentLogCache.entries];
|
||||
}
|
||||
|
||||
const entries = fs
|
||||
.readFileSync(currentLogPath, 'utf8')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map(parseLogLine)
|
||||
.filter((entry): entry is LogEntry => entry !== null);
|
||||
|
||||
currentLogCache = {
|
||||
entries,
|
||||
mtimeNs: stats.mtimeNs,
|
||||
path: currentLogPath,
|
||||
size: stats.size,
|
||||
};
|
||||
|
||||
return [...entries];
|
||||
}
|
||||
|
||||
function matchesLogQuery(entry: LogEntry, options: ReadLogEntriesOptions): boolean {
|
||||
if (options.source && entry.source !== options.source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.level && entry.level !== options.level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options.search) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const search = options.search.toLowerCase();
|
||||
return (
|
||||
entry.message.toLowerCase().includes(search) ||
|
||||
entry.event.toLowerCase().includes(search) ||
|
||||
entry.source.toLowerCase().includes(search) ||
|
||||
String(entry.processId).toLowerCase().includes(search) ||
|
||||
entry.runId.toLowerCase().includes(search) ||
|
||||
JSON.stringify(entry.context || {})
|
||||
.toLowerCase()
|
||||
.includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
function dedupeEntries(entries: LogEntry[]): LogEntry[] {
|
||||
@@ -40,28 +95,12 @@ function dedupeEntries(entries: LogEntry[]): LogEntry[] {
|
||||
}
|
||||
|
||||
export function readLogEntries(options: ReadLogEntriesOptions = {}): LogEntry[] {
|
||||
const limit = options.limit ?? 200;
|
||||
const entries = dedupeEntries([...readCurrentFileEntries(), ...getRecentLogEntries()])
|
||||
.filter((entry) => (options.source ? entry.source === options.source : true))
|
||||
.filter((entry) => (options.level ? entry.level === options.level : true))
|
||||
.filter((entry) => {
|
||||
if (!options.search) {
|
||||
return true;
|
||||
}
|
||||
const search = options.search.toLowerCase();
|
||||
return (
|
||||
entry.message.toLowerCase().includes(search) ||
|
||||
entry.event.toLowerCase().includes(search) ||
|
||||
entry.source.toLowerCase().includes(search) ||
|
||||
String(entry.processId).toLowerCase().includes(search) ||
|
||||
entry.runId.toLowerCase().includes(search) ||
|
||||
JSON.stringify(entry.context || {})
|
||||
.toLowerCase()
|
||||
.includes(search)
|
||||
);
|
||||
})
|
||||
.filter((entry) => matchesLogQuery(entry, options))
|
||||
.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
||||
|
||||
return entries.slice(0, options.limit ?? 200);
|
||||
return entries.slice(0, limit);
|
||||
}
|
||||
|
||||
export function readLogSourceSummaries(): LogSourceSummary[] {
|
||||
|
||||
Reference in new issue
Block a user