UNPKG

@hacksaw/hono-google-cloud-logging

Version:

Google Cloud Logging Middleware for Hono

149 lines (148 loc) 5.64 kB
import { createMiddleware } from "hono/factory"; import { Logging } from "@google-cloud/logging"; /** * Creates a Hono middleware for Google Cloud Logging * * @param loggerConfigs Array of logger configurations * @param options Global options for all loggers * @returns Hono middleware handler */ export const logger = (loggerConfigs, options = {}) => { // Default options const { logRequestBody = false, logResponseBody = false, maxBodySize = 10240, // 10KB severity = "DEFAULT", dev = process.env.NODE_ENV === "development", } = options; // Initialize Google Cloud Logging only if not in dev mode let loggers = []; if (!dev) { const logging = new Logging({ projectId: options.projectId, }); // Create Log instances based on configurations loggers = []; for (const config of loggerConfigs) { const log = logging.log(config.name); loggers.push({ log, config, }); } } return createMiddleware(async (c, next) => { if (!c || !c.req) { console.error("Logger middleware received invalid context"); return next(); } const path = c.req.path; const method = c.req.method; const startTime = Date.now(); // Clone the request for logging purposes const requestClone = c.req.raw.clone(); let requestBody; if (logRequestBody) { try { const bodyText = await requestClone.text(); requestBody = bodyText.length > maxBodySize ? `${bodyText.substring(0, maxBodySize)}... [truncated]` : bodyText; } catch (error) { // Ignore errors reading request body } } // Execute the request await next(); const endTime = Date.now(); const duration = endTime - startTime; // Get response info if (!c.res) throw new Error("No response found in Hono context"); const status = c.res.status; // Capture headers const responseHeaders = {}; for (const [key, value] of c.res.headers.entries()) { responseHeaders[key] = value; } // Capture response body if enabled let responseBody; if (logResponseBody) { try { const resClone = c.res.clone(); const bodyText = await resClone.text(); responseBody = bodyText.length > maxBodySize ? `${bodyText.substring(0, maxBodySize)}... [truncated]` : bodyText; } catch (error) { // Ignore errors reading response body } } // Prepare log entry data const logData = { httpRequest: { requestMethod: method, requestUrl: c.req.url, status: status, userAgent: c.req.header("user-agent"), remoteIp: c.req.header("x-forwarded-for") || "unknown", latency: { seconds: Math.floor(duration / 1000), nanos: (duration % 1000) * 1000000, }, }, requestHeaders: Object.fromEntries(c.req.raw.headers.entries()), responseHeaders, duration: `${duration}ms`, }; // Add request body if available if (requestBody !== undefined) { logData.requestBody = requestBody; } // Add response body if available if (responseBody !== undefined) { logData.responseBody = responseBody; } if (dev) { // In development mode, log to console const timestamp = new Date().toISOString(); const statusColor = status >= 500 ? "\x1b[31m" // Red for 5xx : status >= 400 ? "\x1b[33m" // Yellow for 4xx : status >= 300 ? "\x1b[36m" // Cyan for 3xx : "\x1b[32m"; // Green for 2xx/1xx const resetColor = "\x1b[0m"; console.log(`[${timestamp}] ${method} ${path} ${statusColor}${status}${resetColor} ${duration}ms`); // Log request details in dev mode if (logRequestBody && requestBody) { console.log(`Request Body: ${requestBody}`); } if (logResponseBody && responseBody) { console.log(`Response Body: ${responseBody}`); } // Log additional details at debug level if (severity === "DEBUG") { console.log(`Request Headers: ${JSON.stringify(logData.requestHeaders, null, 2)}`); console.log(`Response Headers: ${JSON.stringify(logData.responseHeaders, null, 2)}`); } } else { // Log to each configured logger that matches the filter for (const { log, config } of loggers) { // Skip if filter exists and returns false if (config.filter && !config.filter(path)) { continue; } const metadata = { severity, resource: config.resource, labels: config.labels, }; // Write log entry log.write(log.entry(metadata, logData)); } } }); };