Path: blob/main/components/gitpod-protocol/src/util/logging.ts
2500 views
/**1* Copyright (c) 2020 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { scrubber } from "./scrubbing";78const inspect: (object: unknown) => string = require("util").inspect; // undefined in frontend910const plainLogging: boolean = false; // set to true during development to get non JSON output11let jsonLogging: boolean = false;12let component: string | undefined;13let version: string | undefined;1415export interface LogContext {16organizationId?: string;17sessionId?: string;18userId?: string;19subjectId?: string;20workspaceId?: string;21instanceId?: string;22}2324/**25* allows to globally augment the log context, default is an identity function26*/27let logContextAugmenter: LogContext.Augmenter = (context) => context;2829export namespace LogContext {30export type Augmenter = (context: LogContext | undefined) => LogContext | undefined;31export function setAugmenter(augmenter: Augmenter): void {32logContextAugmenter = augmenter;33}3435/**36* @deprecated create LogContext directly37*/38export function from(params: { userId?: string; user?: any; request?: any }) {39return <LogContext>{40sessionId: params.request?.requestID,41userId: params.userId || params.user?.id,42};43}44}4546let logItemHook: LogHook.Hook | undefined = undefined;47export namespace LogHook {48export type Hook = (item: LogItem) => void;49export function setHook(hook: Hook): void {50logItemHook = hook;51}52}5354export interface LogPayload {55// placeholder to indicate that only dictionary-style objects should be passed as payload56}5758export namespace log {59export function error(context: LogContext, message: string, error: unknown, payload: LogPayload): void;60export function error(context: LogContext, message: string, error: unknown): void;61export function error(context: LogContext, message: string, payload: LogPayload): void;62export function error(context: LogContext, message: string): void;63export function error(context: LogContext, error: unknown, payload: LogPayload): void;64export function error(context: LogContext, error: unknown): void;65export function error(message: string, error: unknown, payload: LogPayload): void;66export function error(message: string, error: unknown): void;67export function error(message: string, payload: LogPayload): void;68export function error(message: string): void;69export function error(error: unknown, payload: LogPayload): void;70export function error(error: unknown): void;71export function error(...args: unknown[]): void {72errorLog(false, args);73}7475export function warn(context: LogContext, message: string, error: unknown, payload: LogPayload): void;76export function warn(context: LogContext, message: string, error: unknown): void;77export function warn(context: LogContext, message: string, payload: LogPayload): void;78export function warn(context: LogContext, message: string): void;79export function warn(context: LogContext, error: unknown, payload: LogPayload): void;80export function warn(context: LogContext, error: unknown): void;81export function warn(message: string, error: unknown, payload: LogPayload): void;82export function warn(message: string, error: unknown): void;83export function warn(message: string, payload: LogPayload): void;84export function warn(message: string): void;85export function warn(error: unknown, payload: LogPayload): void;86export function warn(error: unknown): void;87export function warn(...args: unknown[]): void {88warnLog(false, args);89}9091export function info(context: LogContext, message: string, error: unknown, payload: LogPayload): void;92export function info(context: LogContext, message: string, error: unknown): void;93export function info(context: LogContext, message: string, payload: LogPayload): void;94export function info(context: LogContext, message: string): void;95export function info(context: LogContext, error: unknown, payload: LogPayload): void;96export function info(context: LogContext, error: unknown): void;97export function info(message: string, error: unknown, payload: LogPayload): void;98export function info(message: string, error: unknown): void;99export function info(message: string, payload: LogPayload): void;100export function info(message: string): void;101export function info(error: unknown, payload: LogPayload): void;102export function info(error: unknown): void;103export function info(...args: unknown[]): void {104infoLog(false, args);105}106107export function debug(context: LogContext, message: string, error: unknown, payload: LogPayload): void;108export function debug(context: LogContext, message: string, error: unknown): void;109export function debug(context: LogContext, message: string, payload: LogPayload): void;110export function debug(context: LogContext, message: string): void;111export function debug(context: LogContext, error: unknown, payload: LogPayload): void;112export function debug(context: LogContext, error: unknown): void;113export function debug(message: string, error: unknown, payload: LogPayload): void;114export function debug(message: string, error: unknown): void;115export function debug(message: string, payload: LogPayload): void;116export function debug(message: string): void;117export function debug(error: unknown, payload: LogPayload): void;118export function debug(error: unknown): void;119export function debug(...args: unknown[]): void {120debugLog(false, args);121}122123/**124* Do not use in frontend.125*/126export function enableJSONLogging(127componentArg: string,128versionArg: string | undefined,129logLevel?: LogrusLogLevel,130): void {131component = componentArg;132version = versionArg;133134setLogLevel(logLevel);135}136137export function setLogLevel(logLevel: LogrusLogLevel | undefined) {138jsonLogging = true;139140console.error = function (...args: unknown[]): void {141errorLog(true, args);142};143console.warn = function (...args: unknown[]): void {144warnLog(true, args);145};146console.info = function (...args: unknown[]): void {147infoLog(true, args);148};149console.debug = function (...args: unknown[]): void {150debugLog(true, args);151};152153console.log = console.info;154// FIXME wrap also other console methods (e.g. trace())155156// set/unset log functions based on loglevel so we only have to evaluate once, not every call157const noop = () => {};158const setLog = (logFunc: DoLogFunction, funcLevel: LogrusLogLevel): DoLogFunction => {159return LogrusLogLevel.isGreatherOrEqual(funcLevel, logLevel) ? logFunc : noop;160};161162errorLog = setLog(doErrorLog, "error");163warnLog = setLog(doWarnLog, "warning");164infoLog = setLog(doInfoLog, "info");165debugLog = setLog(doDebugLog, "debug");166}167168export function resetToDefaultLogging(): void {169jsonLogging = false;170171console.log = logConsoleLog;172console.error = errorConsoleLog;173console.warn = warnConsoleLog;174console.info = infoConsoleLog;175console.debug = debugConsoleLog;176}177178export function setVersion(versionArg: string) {179version = versionArg;180}181}182183type DoLogFunction = (calledViaConsole: boolean, args: unknown[]) => void;184185let errorLog = doErrorLog;186function doErrorLog(calledViaConsole: boolean, args: unknown[]): void {187doLog(calledViaConsole, errorConsoleLog, "ERROR", args);188}189190let warnLog = doWarnLog;191function doWarnLog(calledViaConsole: boolean, args: unknown[]): void {192doLog(calledViaConsole, warnConsoleLog, "WARNING", args);193}194195let infoLog = doInfoLog;196function doInfoLog(calledViaConsole: boolean, args: unknown[]): void {197doLog(calledViaConsole, infoConsoleLog, "INFO", args);198}199200let debugLog = doDebugLog;201function doDebugLog(calledViaConsole: boolean, args: unknown[]): void {202doLog(calledViaConsole, debugConsoleLog, "DEBUG", args);203}204205// Ref: https://github.com/sirupsen/logrus#level-logging206export type LogrusLogLevel = keyof typeof LogrusLogLevels;207export const LogrusLogLevels = {208trace: true,209debug: true,210info: true,211warning: true,212error: true,213fatal: true,214panic: true,215};216export namespace LogrusLogLevel {217export function isGreatherOrEqual(lvl: LogrusLogLevel | undefined, ref: LogrusLogLevel | undefined): boolean {218if (lvl === undefined) {219return false;220}221if (ref === undefined) {222return true;223}224return getLevelArity(lvl) >= getLevelArity(ref);225}226function getLevelArity(lvl: LogrusLogLevel): number {227return Object.keys(LogrusLogLevels).findIndex((l) => l === lvl);228}229export function getFromEnv(): LogrusLogLevel | undefined {230const lvlStr = process.env.LOG_LEVEL;231if (!lvlStr) {232return undefined;233}234const lvl = lvlStr as LogrusLogLevel;235const exists = LogrusLogLevels[lvl];236if (!exists) {237return undefined;238}239return lvl;240}241}242243// Source: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity244type GoogleLogSeverity = "EMERGENCY" | "ALERT" | "CRITICAL" | "ERROR" | "WARNING" | "INFO" | "DEBUG";245namespace GoogleLogSeverity {246export const isGreaterOrEqualThanWarning = (severity: GoogleLogSeverity) => {247switch (severity) {248case "INFO":249case "DEBUG":250return false;251default:252return true;253}254};255}256257function doLog(calledViaConsole: boolean, consoleLog: ConsoleLog, severity: GoogleLogSeverity, args: unknown[]): void {258if (!jsonLogging) {259// eslint-disable-next-line @typescript-eslint/no-unsafe-argument260consoleLog(...args);261return;262}263264if (args.length == 0) {265// console.xyz()266return;267}268269let context: LogContext | undefined;270let message: string | undefined;271let error: Error | undefined;272let payloadArgs: unknown[];273274if (args[0] instanceof Error) {275// console.xyz(Error, ...any) / log.xyz(Error) / log.xyz(Error, LogPayload)276error = args[0];277payloadArgs = args.slice(1);278} else if (typeof args[0] === "string") {279message = args[0];280if (args.length < 2 || !(args[1] instanceof Error)) {281// console.xyz(string) / console.xyz(string, !Error, ...any) / log.xyz(string) / log.xyz(string, LogPayload)282payloadArgs = args.slice(1);283} else {284// console.xyz(string, Error, ...any) / log.xyz(string, Error) / log.xyz(string, Error, LogPayload)285error = args[1];286payloadArgs = args.slice(2);287}288} else if (calledViaConsole || args.length < 2) {289// console.xyz(!string & !Error, ...any) / wrong call of log.xyz (can happen when juggling with 'any'290// or when passing, by mistake, log.xyz instead of console.xyz to third-party code as a callback (*))291payloadArgs = args;292} else {293context = args[0] instanceof Object ? args[0] : undefined;294if (args[1] instanceof Error) {295// log.xyz(LogContext, Error) / log.xyz(LogContext, Error, LogPayload)296error = args[1];297payloadArgs = args.slice(2);298} else if (typeof args[1] === "string") {299message = args[1];300if (args.length < 3 || !(args[2] instanceof Error)) {301// log.xyz(LogContext, string) / log.xyz(LogContext, string, LogPayload)302payloadArgs = args.slice(2);303} else {304// log.xyz(LogContext, string, Error) / log.xyz(LogContext, string, Error, LogPayload)305error = args[2];306payloadArgs = args.slice(3);307}308} else {309// wrong call of log.xyz (see (*) above)310context = undefined;311payloadArgs = args;312}313}314315const logItem: string | undefined = makeLogItem(severity, context, message, error, payloadArgs, calledViaConsole);316if (logItem !== undefined) {317consoleLog(logItem);318}319}320321function makeLogItem(322severity: GoogleLogSeverity,323context: LogContext | undefined,324message: string | undefined,325error: Error | undefined,326payloadArgs: unknown[],327calledViaConsole: boolean,328): string | undefined {329if (context !== undefined && Object.keys(context).length == 0) {330context = undefined;331}332context = logContextAugmenter(context);333context = scrubPayload(context, plainLogging);334335let reportedErrorEvent: {} = {};336if (GoogleLogSeverity.isGreaterOrEqualThanWarning(severity)) {337reportedErrorEvent = makeReportedErrorEvent(error);338}339340payloadArgs = payloadArgs.map((arg) => scrubPayload(arg, plainLogging));341const payload: unknown =342payloadArgs.length == 0 ? undefined : payloadArgs.length == 1 ? payloadArgs[0] : payloadArgs;343const logItem: LogItem = {344// undefined fields get eliminated in JSON.stringify()345...reportedErrorEvent,346component,347severity,348time: new Date().toISOString(),349context,350message,351error,352payload,353loggedViaConsole: calledViaConsole ? true : undefined,354};355if (logItemHook) {356try {357logItemHook(logItem);358} catch (err) {}359}360if (plainLogging) {361return `[${logItem.severity}] [${logItem.component}] ${logItem.message}362${JSON.stringify(payload || "", undefined, " ")}363${error || ""}364`.trim();365}366let result: string = stringifyLogItem(logItem);367368if (result.length > maxAllowedLogItemLength && payload !== undefined) {369delete logItem.payload;370(<any>(371logItem372)).payloadStub = `Payload stripped as log item was longer than ${maxAllowedLogItemLength} characters`;373374result = stringifyLogItem(logItem);375376if (result.length <= maxAllowedLogItemLength) {377log.warn("Log item too large, stripping payload", { logItemStub: makeLogItemStub(logItem) });378}379}380if (result.length > maxAllowedLogItemLength) {381log.error("Log item too large w/o payload, discarding", { logItemStub: makeLogItemStub(logItem) });382return undefined;383}384385return result;386}387388function scrubPayload<T>(payload: T, plainLogging: boolean): T {389if (plainLogging) {390return payload;391}392return scrubber.scrub(payload, false);393}394395// See https://cloud.google.com/error-reporting/docs/formatting-error-messages396// and https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#ReportedErrorEvent397function makeReportedErrorEvent(error: Error | undefined): {} {398const result = {399// Serves as marker only400"@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent",401// This is useful for filtering in the UI402serviceContext: {403service: component || "<ts-not-set>",404version: version || "<ts-not-set>",405},406407// According to: https://cloud.google.com/error-reporting/docs/formatting-error-messages#json_representation408stack_trace: error?.stack,409};410411return result;412}413414type LogItem = {415component?: string;416severity: string;417time?: string;418context?: LogContext;419environment?: string;420region?: string;421message?: string;422messageStub?: string;423errorStub?: string;424error?: unknown;425payload?: unknown;426loggedViaConsole?: boolean;427};428429function makeLogItemStub(logItem: LogItem): LogItem {430const result = <LogItem>{431component: logItem.component,432severity: logItem.severity,433time: logItem.time,434environment: logItem.environment,435region: logItem.region,436};437if (typeof logItem.message === "string") {438if (logItem.message.length <= maxMessageStubLength) {439result.message = logItem.message;440} else {441result.messageStub = logItem.message.substring(0, maxMessageStubLength) + " ... (too long, truncated)";442}443}444if (logItem.error instanceof Error && logItem.error.stack) {445if (logItem.error.stack.length <= maxErrorStubLength) {446result.error = logItem.error.stack;447} else {448result.errorStub = logItem.error.stack.substring(0, maxErrorStubLength) + " ... (too long, truncated)";449}450}451return result;452}453454function stringifyLogItem(logItem: LogItem): string {455try {456return jsonStringifyWithErrors(logItem);457} catch (err) {458if (err instanceof TypeError && logItem.payload !== undefined) {459// payload contains circular references: save it as a string in the form console.xyz() would print460logItem.payload = inspect(logItem.payload);461return jsonStringifyWithErrors(logItem);462}463throw err;464}465}466467/**468* Jsonifies Errors properly, not as {} only.469*/470function jsonStringifyWithErrors(value: unknown): string {471return JSON.stringify(value, (_, value) => {472return value instanceof Error ? value.stack : value;473});474}475476type ConsoleLog = (message?: unknown, ...optionalArgs: unknown[]) => void; // signature of console.xyz477const logConsoleLog: ConsoleLog = console.log;478const errorConsoleLog: ConsoleLog = console.error;479const warnConsoleLog: ConsoleLog = console.warn;480const infoConsoleLog: ConsoleLog = console.info;481const debugConsoleLog: ConsoleLog = console.debug;482483// according to https://cloud.google.com/logging/quotas#logging_usage_limits, the log item must fit in 100 KB (internal data484// size; its relation to the stringified JSON's size is unknown), so let's have a sufficient safe margin485const maxAllowedLogItemLength: number = 32 * 1024;486const maxMessageStubLength: number = 1024;487const maxErrorStubLength: number = 4096;488489490