@hacksaw/hono-google-cloud-logging
Version:
Google Cloud Logging Middleware for Hono
149 lines (148 loc) • 5.64 kB
JavaScript
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));
}
}
});
};