mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-05 02:18:57 +00:00
feat(dashboard): add error log viewer for CLIProxy diagnostics
Add ErrorLogsMonitor component to Home page that displays CLIProxyAPI error logs when requests fail. Users can now diagnose why success rates drop by viewing detailed error log contents. Backend: - Add fetchCliproxyErrorLogs/fetchCliproxyErrorLogContent in stats-fetcher - Add GET /api/cliproxy/error-logs and /api/cliproxy/error-logs/:name routes - Include path traversal protection for filename validation Frontend: - Add CliproxyErrorLog type and errorLogs API methods - Add useCliproxyErrorLogs/useCliproxyErrorLogContent hooks - Create ErrorLogsMonitor component with expandable log viewer - Integrate into Home page below AuthMonitor Closes #132
This commit is contained in:
@@ -271,6 +271,90 @@ export async function fetchCliproxyModels(
|
||||
}
|
||||
}
|
||||
|
||||
/** Error log file metadata from CLIProxyAPI */
|
||||
export interface CliproxyErrorLog {
|
||||
/** Filename (e.g., "error-v1-chat-completions-2025-01-15T10-30-00.log") */
|
||||
name: string;
|
||||
/** File size in bytes */
|
||||
size: number;
|
||||
/** Last modified timestamp (Unix seconds) */
|
||||
modified: number;
|
||||
}
|
||||
|
||||
/** Response from /v0/management/request-error-logs endpoint */
|
||||
interface ErrorLogsApiResponse {
|
||||
files: CliproxyErrorLog[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch error log file list from CLIProxyAPI management API
|
||||
* @param port CLIProxyAPI port (default: 8317)
|
||||
* @returns Array of error log metadata or null if unavailable
|
||||
*/
|
||||
export async function fetchCliproxyErrorLogs(
|
||||
port: number = CLIPROXY_DEFAULT_PORT
|
||||
): Promise<CliproxyErrorLog[] | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${port}/v0/management/request-error-logs`, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${CCS_CONTROL_PANEL_SECRET}`,
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ErrorLogsApiResponse;
|
||||
return data.files ?? [];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch error log file content from CLIProxyAPI management API
|
||||
* @param name Error log filename
|
||||
* @param port CLIProxyAPI port (default: 8317)
|
||||
* @returns Log file content as string or null if unavailable
|
||||
*/
|
||||
export async function fetchCliproxyErrorLogContent(
|
||||
name: string,
|
||||
port: number = CLIPROXY_DEFAULT_PORT
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${port}/v0/management/request-error-logs/${encodeURIComponent(name)}`,
|
||||
{
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
Authorization: `Bearer ${CCS_CONTROL_PANEL_SECRET}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CLIProxyAPI is running and responsive
|
||||
* @param port CLIProxyAPI port (default: 8317)
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
fetchCliproxyStats,
|
||||
fetchCliproxyModels,
|
||||
isCliproxyRunning,
|
||||
fetchCliproxyErrorLogs,
|
||||
fetchCliproxyErrorLogContent,
|
||||
} from '../cliproxy/stats-fetcher';
|
||||
import {
|
||||
listOpenAICompatProviders,
|
||||
@@ -1374,6 +1376,77 @@ apiRoutes.get('/cliproxy/models', async (_req: Request, res: Response): Promise<
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Error Logs ====================
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/error-logs - Get list of error log files
|
||||
* Returns: { files: CliproxyErrorLog[] } or error if proxy not running
|
||||
*/
|
||||
apiRoutes.get('/cliproxy/error-logs', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const running = await isCliproxyRunning();
|
||||
if (!running) {
|
||||
res.status(503).json({
|
||||
error: 'CLIProxyAPI not running',
|
||||
message: 'Start a CLIProxy session to view error logs',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await fetchCliproxyErrorLogs();
|
||||
if (files === null) {
|
||||
res.status(503).json({
|
||||
error: 'Error logs unavailable',
|
||||
message: 'CLIProxyAPI is running but error logs endpoint not responding',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ files });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/error-logs/:name - Get content of a specific error log
|
||||
* Returns: plain text log content
|
||||
*/
|
||||
apiRoutes.get('/cliproxy/error-logs/:name', async (req: Request, res: Response): Promise<void> => {
|
||||
const { name } = req.params;
|
||||
|
||||
// Validate filename format and prevent path traversal
|
||||
if (
|
||||
!name ||
|
||||
!name.startsWith('error-') ||
|
||||
!name.endsWith('.log') ||
|
||||
name.includes('..') ||
|
||||
name.includes('/') ||
|
||||
name.includes('\\')
|
||||
) {
|
||||
res.status(400).json({ error: 'Invalid error log filename' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const running = await isCliproxyRunning();
|
||||
if (!running) {
|
||||
res.status(503).json({ error: 'CLIProxyAPI not running' });
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await fetchCliproxyErrorLogContent(name);
|
||||
if (content === null) {
|
||||
res.status(404).json({ error: 'Error log not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.type('text/plain').send(content);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// OpenAI Compatibility Layer Routes
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user