example-remote-server/src/utils/logger.ts
Jerome f4aed937f0
Replace console.log with structured logging for Cloud Run
- Add structured logger utility with Cloud Run trace context support
- Uses AsyncLocalStorage for request context propagation
- Replace all console.log/error/warn statements with structured logger
- Add logging middleware to capture request/response metadata
- Support Google Cloud Logging severity levels and trace correlation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 10:55:57 +01:00

172 lines
No EOL
4.4 KiB
TypeScript

import { AsyncLocalStorage } from 'async_hooks';
import { Request } from 'express';
// Severity levels as per Google Cloud Logging
export enum LogSeverity {
DEFAULT = 'DEFAULT',
DEBUG = 'DEBUG',
INFO = 'INFO',
NOTICE = 'NOTICE',
WARNING = 'WARNING',
ERROR = 'ERROR',
CRITICAL = 'CRITICAL',
ALERT = 'ALERT',
EMERGENCY = 'EMERGENCY'
}
interface LogContext {
trace?: string;
spanId?: string;
[key: string]: any;
}
interface StructuredLogEntry {
severity: LogSeverity;
message: string;
timestamp: string;
'logging.googleapis.com/trace'?: string;
'logging.googleapis.com/spanId'?: string;
[key: string]: any;
}
class StructuredLogger {
private asyncLocalStorage = new AsyncLocalStorage<LogContext>();
private projectId: string | undefined;
constructor() {
// Get project ID from environment or metadata server
this.projectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCP_PROJECT;
}
/**
* Run a function with a specific logging context
*/
runWithContext<T>(context: LogContext, fn: () => T): T {
return this.asyncLocalStorage.run(context, fn);
}
/**
* Extract trace context from Cloud Run request
*/
extractTraceContext(req: Request): LogContext {
const context: LogContext = {};
const traceHeader = req.header('X-Cloud-Trace-Context');
if (traceHeader && this.projectId) {
const [trace, spanId] = traceHeader.split('/');
context.trace = `projects/${this.projectId}/traces/${trace}`;
if (spanId) {
context.spanId = spanId.split(';')[0]; // Remove any trace flags
}
}
// Add other useful request context
context.requestId = req.header('X-Request-Id');
context.userAgent = req.header('User-Agent');
context.method = req.method;
context.path = req.path;
return context;
}
/**
* Create Express middleware for request context
*/
middleware() {
return (req: Request, res: any, next: any) => {
const context = this.extractTraceContext(req);
this.runWithContext(context, () => {
next();
});
};
}
/**
* Log a structured message
*/
private log(severity: LogSeverity, message: string, metadata?: Record<string, any>) {
const context = this.asyncLocalStorage.getStore() || {};
const entry: StructuredLogEntry = {
severity,
message,
timestamp: new Date().toISOString(),
...metadata
};
// Add trace context if available
if (context.trace) {
entry['logging.googleapis.com/trace'] = context.trace;
}
if (context.spanId) {
entry['logging.googleapis.com/spanId'] = context.spanId;
}
// Add any other context fields
Object.keys(context).forEach(key => {
if (key !== 'trace' && key !== 'spanId') {
entry[`context.${key}`] = context[key];
}
});
// Output as JSON for Cloud Logging
console.log(JSON.stringify(entry));
}
// Convenience methods for different severity levels
debug(message: string, metadata?: Record<string, any>) {
this.log(LogSeverity.DEBUG, message, metadata);
}
info(message: string, metadata?: Record<string, any>) {
this.log(LogSeverity.INFO, message, metadata);
}
notice(message: string, metadata?: Record<string, any>) {
this.log(LogSeverity.NOTICE, message, metadata);
}
warning(message: string, metadata?: Record<string, any>) {
this.log(LogSeverity.WARNING, message, metadata);
}
error(message: string, error?: Error, metadata?: Record<string, any>) {
const errorMetadata = {
...metadata,
error: error ? {
name: error.name,
message: error.message,
stack: error.stack
} : undefined
};
this.log(LogSeverity.ERROR, message, errorMetadata);
}
critical(message: string, metadata?: Record<string, any>) {
this.log(LogSeverity.CRITICAL, message, metadata);
}
alert(message: string, metadata?: Record<string, any>) {
this.log(LogSeverity.ALERT, message, metadata);
}
emergency(message: string, metadata?: Record<string, any>) {
this.log(LogSeverity.EMERGENCY, message, metadata);
}
/**
* Add additional context to the current async context
*/
addContext(context: LogContext) {
const currentContext = this.asyncLocalStorage.getStore();
if (currentContext) {
Object.assign(currentContext, context);
}
}
}
// Export singleton instance
export const logger = new StructuredLogger();
// Re-export for convenience
export { LogContext, StructuredLogEntry };