/** * Copyright (c) 2020 TypeFox GmbH. All rights reserved. * Licensed under the GNU Affero General Public License (AGPL). * See License-AGPL.txt in the project root for license information. */ const inspect: (object: any) => string = require('util').inspect; // undefined in frontend let jsonLogging: boolean = false; let component: string | undefined; let version: string | undefined; export interface LogContext { instanceId?: string; sessionId?: string; userId?: string; workspaceId?: string; }; export interface LogPayload { // placeholder to indicate that only dictionary-style objects should be passed as payload }; export namespace log { export function error(context: LogContext, message: string, error: Error, payload: LogPayload): void; export function error(context: LogContext, message: string, error: Error): void; export function error(context: LogContext, message: string, payload: LogPayload): void; export function error(context: LogContext, message: string): void; export function error(context: LogContext, error: Error, payload: LogPayload): void; export function error(context: LogContext, error: Error): void; export function error(message: string, error: Error, payload: LogPayload): void; export function error(message: string, error: Error): void; export function error(message: string, payload: LogPayload): void; export function error(message: string): void; export function error(error: Error, payload: LogPayload): void; export function error(error: Error): void; export function error(...args: any[]): void { errorLog(false, args); } export function warn(context: LogContext, message: string, error: Error, payload: LogPayload): void; export function warn(context: LogContext, message: string, error: Error): void; export function warn(context: LogContext, message: string, payload: LogPayload): void; export function warn(context: LogContext, message: string): void; export function warn(context: LogContext, error: Error, payload: LogPayload): void; export function warn(context: LogContext, error: Error): void; export function warn(message: string, error: Error, payload: LogPayload): void; export function warn(message: string, error: Error): void; export function warn(message: string, payload: LogPayload): void; export function warn(message: string): void; export function warn(error: Error, payload: LogPayload): void; export function warn(error: Error): void; export function warn(...args: any[]): void { warnLog(false, args); } export function info(context: LogContext, message: string, error: Error, payload: LogPayload): void; export function info(context: LogContext, message: string, error: Error): void; export function info(context: LogContext, message: string, payload: LogPayload): void; export function info(context: LogContext, message: string): void; export function info(context: LogContext, error: Error, payload: LogPayload): void; export function info(context: LogContext, error: Error): void; export function info(message: string, error: Error, payload: LogPayload): void; export function info(message: string, error: Error): void; export function info(message: string, payload: LogPayload): void; export function info(message: string): void; export function info(error: Error, payload: LogPayload): void; export function info(error: Error): void; export function info(...args: any[]): void { infoLog(false, args); } export function debug(context: LogContext, message: string, error: Error, payload: LogPayload): void; export function debug(context: LogContext, message: string, error: Error): void; export function debug(context: LogContext, message: string, payload: LogPayload): void; export function debug(context: LogContext, message: string): void; export function debug(context: LogContext, error: Error, payload: LogPayload): void; export function debug(context: LogContext, error: Error): void; export function debug(message: string, error: Error, payload: LogPayload): void; export function debug(message: string, error: Error): void; export function debug(message: string, payload: LogPayload): void; export function debug(message: string): void; export function debug(error: Error, payload: LogPayload): void; export function debug(error: Error): void; export function debug(...args: any[]): void { debugLog(false, args); } /** * Do not use in frontend. */ export function enableJSONLogging(componentArg: string, versionArg: string | undefined): void { component = componentArg; version = versionArg; jsonLogging = true; console.error = function (...args: any[]): void { errorLog(true, args); } console.warn = function (...args: any[]): void { warnLog(true, args); } console.info = function (...args: any[]): void { infoLog(true, args); } console.debug = function (...args: any[]): void { debugLog(true, args); } console.log = console.info; // FIXME wrap also other console methods (e.g. trace()) } export function resetToDefaultLogging(): void { jsonLogging = false; console.log = logConsoleLog; console.error = errorConsoleLog; console.warn = warnConsoleLog; console.info = infoConsoleLog; console.debug = debugConsoleLog; } } function errorLog(calledViaConsole: boolean, args: any[]): void { doLog(calledViaConsole, errorConsoleLog, 'ERROR', args); } function warnLog(calledViaConsole: boolean, args: any[]): void { doLog(calledViaConsole, warnConsoleLog, 'WARNING', args); } function infoLog(calledViaConsole: boolean, args: any[]): void { doLog(calledViaConsole, infoConsoleLog, 'INFO', args); } function debugLog(calledViaConsole: boolean, args: any[]): void { doLog(calledViaConsole, debugConsoleLog, 'DEBUG', args); } // Source: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity type GoogleLogSeverity = 'EMERGENCY' | 'ALERT' | 'CRITICAL' | 'ERROR' | 'WARNING' | 'INFO' | 'DEBUG'; namespace GoogleLogSeverity { export const isGreaterOrEqualThanWarning = (severity: GoogleLogSeverity) => { switch (severity) { case 'INFO': case 'DEBUG': return false; default: return true; } }; } function doLog(calledViaConsole: boolean, consoleLog: ConsoleLog, severity: GoogleLogSeverity, args: any[]): void { if (!jsonLogging) { consoleLog(...args); return; } if (args.length == 0) { // console.xyz() return; } let context: LogContext | undefined; let message: string | undefined; let error: Error | undefined; let payloadArgs: any[]; if (args[0] instanceof Error) { // console.xyz(Error, ...any) / log.xyz(Error) / log.xyz(Error, LogPayload) error = args[0]; payloadArgs = args.slice(1); } else if (typeof args[0] === 'string') { message = args[0]; if (args.length < 2 || !(args[1] instanceof Error)) { // console.xyz(string) / console.xyz(string, !Error, ...any) / log.xyz(string) / log.xyz(string, LogPayload) payloadArgs = args.slice(1); } else { // console.xyz(string, Error, ...any) / log.xyz(string, Error) / log.xyz(string, Error, LogPayload) error = args[1]; payloadArgs = args.slice(2); } } else if (calledViaConsole || args.length < 2) { // console.xyz(!string & !Error, ...any) / wrong call of log.xyz (can happen when juggling with 'any' // or when passing, by mistake, log.xyz instead of console.xyz to third-party code as a callback (*)) payloadArgs = args; } else { context = args[0]; if (args[1] instanceof Error) { // log.xyz(LogContext, Error) / log.xyz(LogContext, Error, LogPayload) error = args[1]; payloadArgs = args.slice(2); } else if (typeof args[1] === 'string') { message = args[1]; if (args.length < 3 || !(args[2] instanceof Error)) { // log.xyz(LogContext, string) / log.xyz(LogContext, string, LogPayload) payloadArgs = args.slice(2); } else { // log.xyz(LogContext, string, Error) / log.xyz(LogContext, string, Error, LogPayload) error = args[2]; payloadArgs = args.slice(3); } } else { // wrong call of log.xyz (see (*) above) context = undefined; payloadArgs = args; } } const logItem: string | undefined = makeLogItem(severity, context, message, error, payloadArgs, calledViaConsole); if (logItem !== undefined) { consoleLog(logItem); } } function makeLogItem(severity: GoogleLogSeverity, context: LogContext | undefined, message: string | undefined, error: Error | undefined, payloadArgs: any[], calledViaConsole: boolean): string | undefined { if (context !== undefined && Object.keys(context).length == 0) { context = undefined; } let reportedErrorEvent: {} = {}; if (GoogleLogSeverity.isGreaterOrEqualThanWarning(severity)) { reportedErrorEvent = makeReportedErrorEvent(error); } const payload: any = payloadArgs.length == 0 ? undefined : payloadArgs.length == 1 ? payloadArgs[0] : payloadArgs; const logItem: any = { // undefined fields get eliminated in JSON.stringify() ...reportedErrorEvent, component, severity, time: new Date().toISOString(), environment: process.env.KUBE_STAGE, region: process.env.GITPOD_REGION, context, message, error, payload, loggedViaConsole: calledViaConsole ? true : undefined }; let result: string = stringifyLogItem(logItem); if (result.length > maxAllowedLogItemLength && payload !== undefined) { delete logItem.payload; logItem.payloadStub = `Payload stripped as log item was longer than ${maxAllowedLogItemLength} characters`; result = stringifyLogItem(logItem); if (result.length <= maxAllowedLogItemLength) { log.warn('Log item too large, stripping payload', { logItemStub: makeLogItemStub(logItem) }); } } if (result.length > maxAllowedLogItemLength) { log.error('Log item too large w/o payload, discarding', { logItemStub: makeLogItemStub(logItem) }); return undefined; } return result; } // See https://cloud.google.com/error-reporting/docs/formatting-error-messages // and https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#ReportedErrorEvent function makeReportedErrorEvent(error: Error | undefined) { const result: any = { // Serves as marker only "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent", // This is useful for filtering in the UI "serviceContext": { "service": component || "", "version": version || "", } }; if (error) { // According to: https://cloud.google.com/error-reporting/docs/formatting-error-messages#json_representation const stackTrace = error.stack; if (stackTrace) { result.stack_trace = stackTrace; } } return result; } function makeLogItemStub(logItem: any): any { const result: any = { component: logItem.component, severity: logItem.severity, time: logItem.time, environment: logItem.environment, region: logItem.region }; if (typeof (logItem.message) === 'string') { if (logItem.message.length <= maxMessageStubLength) { result.message = logItem.message; } else { result.messageStub = logItem.message.substring(0, maxMessageStubLength) + " ... (too long, truncated)"; } } if (logItem.error instanceof Error) { if (logItem.error.stack.length <= maxErrorStubLength) { result.error = logItem.error.stack; } else { result.errorStub = logItem.error.stack.substring(0, maxErrorStubLength) + " ... (too long, truncated)"; } } return result; } function stringifyLogItem(logItem: any): string { try { return jsonStringifyWithErrors(logItem); } catch (err) { if (err instanceof TypeError && logItem.payload !== undefined) { // payload contains circular references: save it as a string in the form console.xyz() would print logItem.payload = inspect(logItem.payload); return jsonStringifyWithErrors(logItem); } throw err; } } /** * Jsonifies Errors properly, not as {} only. */ function jsonStringifyWithErrors(value: any): string { return JSON.stringify(value, (key: string, value: any): any => { return value instanceof Error ? value.stack : value }); } type ConsoleLog = (message?: any, ...optionalArgs: any[]) => void; // signature of console.xyz const logConsoleLog: ConsoleLog = console.log; const errorConsoleLog: ConsoleLog = console.error; const warnConsoleLog: ConsoleLog = console.warn; const infoConsoleLog: ConsoleLog = console.info; const debugConsoleLog: ConsoleLog = console.debug; // according to https://cloud.google.com/logging/quotas#logging_usage_limits, the log item must fit in 100 KB (internal data // size; its relation to the stringified JSON's size is unknown), so let's have a sufficient safe margin const maxAllowedLogItemLength: number = 32 * 1024; const maxMessageStubLength: number = 1024; const maxErrorStubLength: number = 4096;