diff --git a/src/middlewares/log/index.ts b/src/middlewares/log/index.ts index 32b413912..d55105277 100644 --- a/src/middlewares/log/index.ts +++ b/src/middlewares/log/index.ts @@ -4,6 +4,13 @@ import { getRuntimeKey } from 'hono/adapter'; let logId = 0; const MAX_RESPONSE_LENGTH = 100000; +// Log level control via environment variable +// Set LOG_LEVEL=verbose for detailed console logs +// Set LOG_LEVEL=minimal for basic logs only +// Set LOG_LEVEL=silent to disable console logs +// Default to verbose logging if not set or set to an invalid value +const LOG_LEVEL = process.env.LOG_LEVEL || 'verbose'; + // Map to store all connected log clients const logClients: Map = new Map(); @@ -56,33 +63,144 @@ async function processLog(c: Context, start: number) { return; } + // Capture the final response body sent to the client + // Note: requestOptionsArray is ordered chronologically (first attempt at [0], last at [-1]) + // The last element contains the final successful (or failed) response + const lastAttemptIndex = requestOptionsArray.length - 1; + let finalClientResponse = null; try { - const response = requestOptionsArray[0].finalUntransformedRequest.body - .stream + finalClientResponse = requestOptionsArray[lastAttemptIndex] + .finalUntransformedRequest.body.stream ? { message: 'The response was a stream.' } : await c.res.clone().json(); - const responseString = JSON.stringify(response); + const responseString = JSON.stringify(finalClientResponse); if (responseString.length > MAX_RESPONSE_LENGTH) { - requestOptionsArray[0].response = + requestOptionsArray[lastAttemptIndex].response = responseString.substring(0, MAX_RESPONSE_LENGTH) + '...'; } else { - requestOptionsArray[0].response = response; + requestOptionsArray[lastAttemptIndex].response = finalClientResponse; } } catch (error) { console.error('Error processing log:', error); } - await broadcastLog( - JSON.stringify({ - time: new Date().toLocaleString(), - method: c.req.method, - endpoint: c.req.url.split(':8787')[1], - status: c.res.status, - duration: ms, - requestOptions: requestOptionsArray, - }) - ); + const now = new Date(); + const timestamp = now.toLocaleString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short', + }); + + // Extract the endpoint path from the URL + const url = new URL(c.req.url); + const endpoint = url.pathname + url.search; + + const logData = { + time: timestamp, + method: c.req.method, + endpoint: endpoint, + status: c.res.status, + duration: ms, + requestOptions: requestOptionsArray, + }; + + // Log to console for STDOUT visibility based on LOG_LEVEL + if (LOG_LEVEL !== 'silent') { + if (LOG_LEVEL === 'minimal') { + // Minimal logging: just method, endpoint, status, duration + console.log( + `[${logData.time}] ${logData.method} ${logData.endpoint} - ${logData.status} (${ms}ms)` + ); + } else { + // Verbose logging: full details (default) + console.log('\n' + '='.repeat(80)); + console.log(`[${logData.time}] ${logData.method} ${logData.endpoint}`); + console.log(`Status: ${logData.status} | Duration: ${ms}ms`); + console.log('-'.repeat(80)); + + // Log incoming client request (Client -> Gateway) + console.log('\nINCOMING REQUEST (Client -> Gateway):'); + console.log('\nClient Headers:'); + const headers: Record = {}; + c.req.raw.headers.forEach((value, key) => { + headers[key] = value; + }); + console.log(JSON.stringify(headers, null, 2)); + + // Log original request body if available + // Note: Client request body is the same across all attempts, so we can use any element + if (requestOptionsArray[0]?.finalUntransformedRequest?.body) { + console.log('\nClient Request Body:'); + console.log( + JSON.stringify( + requestOptionsArray[0].finalUntransformedRequest.body, + null, + 2 + ) + ); + } + + console.log('\n' + '-'.repeat(80)); + + // Log all attempts (useful for retries, fallbacks, load balancing) + console.log('\nOUTGOING REQUESTS (Gateway -> Provider):'); + requestOptionsArray.forEach((option: any, index: number) => { + if (requestOptionsArray.length > 1) { + console.log( + `\n--- Attempt ${index + 1} of ${requestOptionsArray.length} ---` + ); + } + + // Log provider and model info + if (option.providerOptions) { + console.log('Provider:', option.providerOptions?.provider || 'N/A'); + console.log( + 'Request URL:', + option.providerOptions?.requestURL || 'N/A' + ); + } + + // Log request parameters + if (option.requestParams) { + console.log('\nRequest Parameters:'); + console.log(JSON.stringify(option.requestParams, null, 2)); + } + + // Log response from provider + if (option.response) { + console.log('\nProvider Response:'); + console.log(JSON.stringify(option.response, null, 2)); + } + }); + + // Log final response back to client (Gateway -> Client) + console.log('\n' + '-'.repeat(80)); + console.log('\nOUTGOING RESPONSE (Gateway -> Client):'); + console.log(`\nStatus: ${c.res.status}`); + console.log('\nResponse Headers:'); + const responseHeaders: Record = {}; + c.res.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + console.log(JSON.stringify(responseHeaders, null, 2)); + + // Log the actual response body sent to client + if (finalClientResponse) { + console.log('\nResponse Body:'); + console.log(JSON.stringify(finalClientResponse, null, 2)); + } + + console.log('\n' + '='.repeat(80) + '\n'); + } + } + + // Broadcast to SSE clients + await broadcastLog(JSON.stringify(logData)); } export const logger = () => {