Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-protocol/src/util/logging.ts
2500 views
1
/**
2
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { scrubber } from "./scrubbing";
8
9
const inspect: (object: unknown) => string = require("util").inspect; // undefined in frontend
10
11
const plainLogging: boolean = false; // set to true during development to get non JSON output
12
let jsonLogging: boolean = false;
13
let component: string | undefined;
14
let version: string | undefined;
15
16
export interface LogContext {
17
organizationId?: string;
18
sessionId?: string;
19
userId?: string;
20
subjectId?: string;
21
workspaceId?: string;
22
instanceId?: string;
23
}
24
25
/**
26
* allows to globally augment the log context, default is an identity function
27
*/
28
let logContextAugmenter: LogContext.Augmenter = (context) => context;
29
30
export namespace LogContext {
31
export type Augmenter = (context: LogContext | undefined) => LogContext | undefined;
32
export function setAugmenter(augmenter: Augmenter): void {
33
logContextAugmenter = augmenter;
34
}
35
36
/**
37
* @deprecated create LogContext directly
38
*/
39
export function from(params: { userId?: string; user?: any; request?: any }) {
40
return <LogContext>{
41
sessionId: params.request?.requestID,
42
userId: params.userId || params.user?.id,
43
};
44
}
45
}
46
47
let logItemHook: LogHook.Hook | undefined = undefined;
48
export namespace LogHook {
49
export type Hook = (item: LogItem) => void;
50
export function setHook(hook: Hook): void {
51
logItemHook = hook;
52
}
53
}
54
55
export interface LogPayload {
56
// placeholder to indicate that only dictionary-style objects should be passed as payload
57
}
58
59
export namespace log {
60
export function error(context: LogContext, message: string, error: unknown, payload: LogPayload): void;
61
export function error(context: LogContext, message: string, error: unknown): void;
62
export function error(context: LogContext, message: string, payload: LogPayload): void;
63
export function error(context: LogContext, message: string): void;
64
export function error(context: LogContext, error: unknown, payload: LogPayload): void;
65
export function error(context: LogContext, error: unknown): void;
66
export function error(message: string, error: unknown, payload: LogPayload): void;
67
export function error(message: string, error: unknown): void;
68
export function error(message: string, payload: LogPayload): void;
69
export function error(message: string): void;
70
export function error(error: unknown, payload: LogPayload): void;
71
export function error(error: unknown): void;
72
export function error(...args: unknown[]): void {
73
errorLog(false, args);
74
}
75
76
export function warn(context: LogContext, message: string, error: unknown, payload: LogPayload): void;
77
export function warn(context: LogContext, message: string, error: unknown): void;
78
export function warn(context: LogContext, message: string, payload: LogPayload): void;
79
export function warn(context: LogContext, message: string): void;
80
export function warn(context: LogContext, error: unknown, payload: LogPayload): void;
81
export function warn(context: LogContext, error: unknown): void;
82
export function warn(message: string, error: unknown, payload: LogPayload): void;
83
export function warn(message: string, error: unknown): void;
84
export function warn(message: string, payload: LogPayload): void;
85
export function warn(message: string): void;
86
export function warn(error: unknown, payload: LogPayload): void;
87
export function warn(error: unknown): void;
88
export function warn(...args: unknown[]): void {
89
warnLog(false, args);
90
}
91
92
export function info(context: LogContext, message: string, error: unknown, payload: LogPayload): void;
93
export function info(context: LogContext, message: string, error: unknown): void;
94
export function info(context: LogContext, message: string, payload: LogPayload): void;
95
export function info(context: LogContext, message: string): void;
96
export function info(context: LogContext, error: unknown, payload: LogPayload): void;
97
export function info(context: LogContext, error: unknown): void;
98
export function info(message: string, error: unknown, payload: LogPayload): void;
99
export function info(message: string, error: unknown): void;
100
export function info(message: string, payload: LogPayload): void;
101
export function info(message: string): void;
102
export function info(error: unknown, payload: LogPayload): void;
103
export function info(error: unknown): void;
104
export function info(...args: unknown[]): void {
105
infoLog(false, args);
106
}
107
108
export function debug(context: LogContext, message: string, error: unknown, payload: LogPayload): void;
109
export function debug(context: LogContext, message: string, error: unknown): void;
110
export function debug(context: LogContext, message: string, payload: LogPayload): void;
111
export function debug(context: LogContext, message: string): void;
112
export function debug(context: LogContext, error: unknown, payload: LogPayload): void;
113
export function debug(context: LogContext, error: unknown): void;
114
export function debug(message: string, error: unknown, payload: LogPayload): void;
115
export function debug(message: string, error: unknown): void;
116
export function debug(message: string, payload: LogPayload): void;
117
export function debug(message: string): void;
118
export function debug(error: unknown, payload: LogPayload): void;
119
export function debug(error: unknown): void;
120
export function debug(...args: unknown[]): void {
121
debugLog(false, args);
122
}
123
124
/**
125
* Do not use in frontend.
126
*/
127
export function enableJSONLogging(
128
componentArg: string,
129
versionArg: string | undefined,
130
logLevel?: LogrusLogLevel,
131
): void {
132
component = componentArg;
133
version = versionArg;
134
135
setLogLevel(logLevel);
136
}
137
138
export function setLogLevel(logLevel: LogrusLogLevel | undefined) {
139
jsonLogging = true;
140
141
console.error = function (...args: unknown[]): void {
142
errorLog(true, args);
143
};
144
console.warn = function (...args: unknown[]): void {
145
warnLog(true, args);
146
};
147
console.info = function (...args: unknown[]): void {
148
infoLog(true, args);
149
};
150
console.debug = function (...args: unknown[]): void {
151
debugLog(true, args);
152
};
153
154
console.log = console.info;
155
// FIXME wrap also other console methods (e.g. trace())
156
157
// set/unset log functions based on loglevel so we only have to evaluate once, not every call
158
const noop = () => {};
159
const setLog = (logFunc: DoLogFunction, funcLevel: LogrusLogLevel): DoLogFunction => {
160
return LogrusLogLevel.isGreatherOrEqual(funcLevel, logLevel) ? logFunc : noop;
161
};
162
163
errorLog = setLog(doErrorLog, "error");
164
warnLog = setLog(doWarnLog, "warning");
165
infoLog = setLog(doInfoLog, "info");
166
debugLog = setLog(doDebugLog, "debug");
167
}
168
169
export function resetToDefaultLogging(): void {
170
jsonLogging = false;
171
172
console.log = logConsoleLog;
173
console.error = errorConsoleLog;
174
console.warn = warnConsoleLog;
175
console.info = infoConsoleLog;
176
console.debug = debugConsoleLog;
177
}
178
179
export function setVersion(versionArg: string) {
180
version = versionArg;
181
}
182
}
183
184
type DoLogFunction = (calledViaConsole: boolean, args: unknown[]) => void;
185
186
let errorLog = doErrorLog;
187
function doErrorLog(calledViaConsole: boolean, args: unknown[]): void {
188
doLog(calledViaConsole, errorConsoleLog, "ERROR", args);
189
}
190
191
let warnLog = doWarnLog;
192
function doWarnLog(calledViaConsole: boolean, args: unknown[]): void {
193
doLog(calledViaConsole, warnConsoleLog, "WARNING", args);
194
}
195
196
let infoLog = doInfoLog;
197
function doInfoLog(calledViaConsole: boolean, args: unknown[]): void {
198
doLog(calledViaConsole, infoConsoleLog, "INFO", args);
199
}
200
201
let debugLog = doDebugLog;
202
function doDebugLog(calledViaConsole: boolean, args: unknown[]): void {
203
doLog(calledViaConsole, debugConsoleLog, "DEBUG", args);
204
}
205
206
// Ref: https://github.com/sirupsen/logrus#level-logging
207
export type LogrusLogLevel = keyof typeof LogrusLogLevels;
208
export const LogrusLogLevels = {
209
trace: true,
210
debug: true,
211
info: true,
212
warning: true,
213
error: true,
214
fatal: true,
215
panic: true,
216
};
217
export namespace LogrusLogLevel {
218
export function isGreatherOrEqual(lvl: LogrusLogLevel | undefined, ref: LogrusLogLevel | undefined): boolean {
219
if (lvl === undefined) {
220
return false;
221
}
222
if (ref === undefined) {
223
return true;
224
}
225
return getLevelArity(lvl) >= getLevelArity(ref);
226
}
227
function getLevelArity(lvl: LogrusLogLevel): number {
228
return Object.keys(LogrusLogLevels).findIndex((l) => l === lvl);
229
}
230
export function getFromEnv(): LogrusLogLevel | undefined {
231
const lvlStr = process.env.LOG_LEVEL;
232
if (!lvlStr) {
233
return undefined;
234
}
235
const lvl = lvlStr as LogrusLogLevel;
236
const exists = LogrusLogLevels[lvl];
237
if (!exists) {
238
return undefined;
239
}
240
return lvl;
241
}
242
}
243
244
// Source: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
245
type GoogleLogSeverity = "EMERGENCY" | "ALERT" | "CRITICAL" | "ERROR" | "WARNING" | "INFO" | "DEBUG";
246
namespace GoogleLogSeverity {
247
export const isGreaterOrEqualThanWarning = (severity: GoogleLogSeverity) => {
248
switch (severity) {
249
case "INFO":
250
case "DEBUG":
251
return false;
252
default:
253
return true;
254
}
255
};
256
}
257
258
function doLog(calledViaConsole: boolean, consoleLog: ConsoleLog, severity: GoogleLogSeverity, args: unknown[]): void {
259
if (!jsonLogging) {
260
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
261
consoleLog(...args);
262
return;
263
}
264
265
if (args.length == 0) {
266
// console.xyz()
267
return;
268
}
269
270
let context: LogContext | undefined;
271
let message: string | undefined;
272
let error: Error | undefined;
273
let payloadArgs: unknown[];
274
275
if (args[0] instanceof Error) {
276
// console.xyz(Error, ...any) / log.xyz(Error) / log.xyz(Error, LogPayload)
277
error = args[0];
278
payloadArgs = args.slice(1);
279
} else if (typeof args[0] === "string") {
280
message = args[0];
281
if (args.length < 2 || !(args[1] instanceof Error)) {
282
// console.xyz(string) / console.xyz(string, !Error, ...any) / log.xyz(string) / log.xyz(string, LogPayload)
283
payloadArgs = args.slice(1);
284
} else {
285
// console.xyz(string, Error, ...any) / log.xyz(string, Error) / log.xyz(string, Error, LogPayload)
286
error = args[1];
287
payloadArgs = args.slice(2);
288
}
289
} else if (calledViaConsole || args.length < 2) {
290
// console.xyz(!string & !Error, ...any) / wrong call of log.xyz (can happen when juggling with 'any'
291
// or when passing, by mistake, log.xyz instead of console.xyz to third-party code as a callback (*))
292
payloadArgs = args;
293
} else {
294
context = args[0] instanceof Object ? args[0] : undefined;
295
if (args[1] instanceof Error) {
296
// log.xyz(LogContext, Error) / log.xyz(LogContext, Error, LogPayload)
297
error = args[1];
298
payloadArgs = args.slice(2);
299
} else if (typeof args[1] === "string") {
300
message = args[1];
301
if (args.length < 3 || !(args[2] instanceof Error)) {
302
// log.xyz(LogContext, string) / log.xyz(LogContext, string, LogPayload)
303
payloadArgs = args.slice(2);
304
} else {
305
// log.xyz(LogContext, string, Error) / log.xyz(LogContext, string, Error, LogPayload)
306
error = args[2];
307
payloadArgs = args.slice(3);
308
}
309
} else {
310
// wrong call of log.xyz (see (*) above)
311
context = undefined;
312
payloadArgs = args;
313
}
314
}
315
316
const logItem: string | undefined = makeLogItem(severity, context, message, error, payloadArgs, calledViaConsole);
317
if (logItem !== undefined) {
318
consoleLog(logItem);
319
}
320
}
321
322
function makeLogItem(
323
severity: GoogleLogSeverity,
324
context: LogContext | undefined,
325
message: string | undefined,
326
error: Error | undefined,
327
payloadArgs: unknown[],
328
calledViaConsole: boolean,
329
): string | undefined {
330
if (context !== undefined && Object.keys(context).length == 0) {
331
context = undefined;
332
}
333
context = logContextAugmenter(context);
334
context = scrubPayload(context, plainLogging);
335
336
let reportedErrorEvent: {} = {};
337
if (GoogleLogSeverity.isGreaterOrEqualThanWarning(severity)) {
338
reportedErrorEvent = makeReportedErrorEvent(error);
339
}
340
341
payloadArgs = payloadArgs.map((arg) => scrubPayload(arg, plainLogging));
342
const payload: unknown =
343
payloadArgs.length == 0 ? undefined : payloadArgs.length == 1 ? payloadArgs[0] : payloadArgs;
344
const logItem: LogItem = {
345
// undefined fields get eliminated in JSON.stringify()
346
...reportedErrorEvent,
347
component,
348
severity,
349
time: new Date().toISOString(),
350
context,
351
message,
352
error,
353
payload,
354
loggedViaConsole: calledViaConsole ? true : undefined,
355
};
356
if (logItemHook) {
357
try {
358
logItemHook(logItem);
359
} catch (err) {}
360
}
361
if (plainLogging) {
362
return `[${logItem.severity}] [${logItem.component}] ${logItem.message}
363
${JSON.stringify(payload || "", undefined, " ")}
364
${error || ""}
365
`.trim();
366
}
367
let result: string = stringifyLogItem(logItem);
368
369
if (result.length > maxAllowedLogItemLength && payload !== undefined) {
370
delete logItem.payload;
371
(<any>(
372
logItem
373
)).payloadStub = `Payload stripped as log item was longer than ${maxAllowedLogItemLength} characters`;
374
375
result = stringifyLogItem(logItem);
376
377
if (result.length <= maxAllowedLogItemLength) {
378
log.warn("Log item too large, stripping payload", { logItemStub: makeLogItemStub(logItem) });
379
}
380
}
381
if (result.length > maxAllowedLogItemLength) {
382
log.error("Log item too large w/o payload, discarding", { logItemStub: makeLogItemStub(logItem) });
383
return undefined;
384
}
385
386
return result;
387
}
388
389
function scrubPayload<T>(payload: T, plainLogging: boolean): T {
390
if (plainLogging) {
391
return payload;
392
}
393
return scrubber.scrub(payload, false);
394
}
395
396
// See https://cloud.google.com/error-reporting/docs/formatting-error-messages
397
// and https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#ReportedErrorEvent
398
function makeReportedErrorEvent(error: Error | undefined): {} {
399
const result = {
400
// Serves as marker only
401
"@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent",
402
// This is useful for filtering in the UI
403
serviceContext: {
404
service: component || "<ts-not-set>",
405
version: version || "<ts-not-set>",
406
},
407
408
// According to: https://cloud.google.com/error-reporting/docs/formatting-error-messages#json_representation
409
stack_trace: error?.stack,
410
};
411
412
return result;
413
}
414
415
type LogItem = {
416
component?: string;
417
severity: string;
418
time?: string;
419
context?: LogContext;
420
environment?: string;
421
region?: string;
422
message?: string;
423
messageStub?: string;
424
errorStub?: string;
425
error?: unknown;
426
payload?: unknown;
427
loggedViaConsole?: boolean;
428
};
429
430
function makeLogItemStub(logItem: LogItem): LogItem {
431
const result = <LogItem>{
432
component: logItem.component,
433
severity: logItem.severity,
434
time: logItem.time,
435
environment: logItem.environment,
436
region: logItem.region,
437
};
438
if (typeof logItem.message === "string") {
439
if (logItem.message.length <= maxMessageStubLength) {
440
result.message = logItem.message;
441
} else {
442
result.messageStub = logItem.message.substring(0, maxMessageStubLength) + " ... (too long, truncated)";
443
}
444
}
445
if (logItem.error instanceof Error && logItem.error.stack) {
446
if (logItem.error.stack.length <= maxErrorStubLength) {
447
result.error = logItem.error.stack;
448
} else {
449
result.errorStub = logItem.error.stack.substring(0, maxErrorStubLength) + " ... (too long, truncated)";
450
}
451
}
452
return result;
453
}
454
455
function stringifyLogItem(logItem: LogItem): string {
456
try {
457
return jsonStringifyWithErrors(logItem);
458
} catch (err) {
459
if (err instanceof TypeError && logItem.payload !== undefined) {
460
// payload contains circular references: save it as a string in the form console.xyz() would print
461
logItem.payload = inspect(logItem.payload);
462
return jsonStringifyWithErrors(logItem);
463
}
464
throw err;
465
}
466
}
467
468
/**
469
* Jsonifies Errors properly, not as {} only.
470
*/
471
function jsonStringifyWithErrors(value: unknown): string {
472
return JSON.stringify(value, (_, value) => {
473
return value instanceof Error ? value.stack : value;
474
});
475
}
476
477
type ConsoleLog = (message?: unknown, ...optionalArgs: unknown[]) => void; // signature of console.xyz
478
const logConsoleLog: ConsoleLog = console.log;
479
const errorConsoleLog: ConsoleLog = console.error;
480
const warnConsoleLog: ConsoleLog = console.warn;
481
const infoConsoleLog: ConsoleLog = console.info;
482
const debugConsoleLog: ConsoleLog = console.debug;
483
484
// according to https://cloud.google.com/logging/quotas#logging_usage_limits, the log item must fit in 100 KB (internal data
485
// size; its relation to the stringified JSON's size is unknown), so let's have a sufficient safe margin
486
const maxAllowedLogItemLength: number = 32 * 1024;
487
const maxMessageStubLength: number = 1024;
488
const maxErrorStubLength: number = 4096;
489
490