mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-27 04:19:08 -05:00
116 lines
3.7 KiB
TypeScript
116 lines
3.7 KiB
TypeScript
import { format } from 'node:util'
|
|
import { isError, pick, omit } from 'lodash'
|
|
import { dim, red, yellow } from 'colors/safe'
|
|
import { getMonitoringContext } from './context'
|
|
|
|
// mapping JS log levels (e.g. functions on console object) to GCP log levels
|
|
const JS_TO_GCP_LEVELS = {
|
|
debug: 'DEBUG',
|
|
info: 'INFO',
|
|
warn: 'WARNING',
|
|
error: 'ERROR',
|
|
} as const
|
|
|
|
const JS_LEVELS = Object.keys(JS_TO_GCP_LEVELS) as LogLevel[]
|
|
const DEFAULT_LEVEL = 'info'
|
|
const IS_GCP = process.env.GOOGLE_CLOUD_PROJECT != null
|
|
|
|
// keys to put in front to categorize a log line in the console
|
|
const DISPLAY_CATEGORY_KEYS = ['endpoint', 'job'] as const
|
|
|
|
// keys to ignore when printing out log details in the console
|
|
const DISPLAY_EXCLUDED_KEYS = ['traceId'] as const
|
|
|
|
export type LogLevel = keyof typeof JS_TO_GCP_LEVELS
|
|
export type LogDetails = Record<string, unknown>
|
|
export type TextLogger = (msg: unknown, ...args: unknown[]) => void
|
|
export type StructuredLogger = (msg: unknown, props?: LogDetails) => void
|
|
export type Logger = TextLogger & {
|
|
[Property in LogLevel]: StructuredLogger
|
|
}
|
|
|
|
function toString(obj: unknown) {
|
|
if (isError(obj)) {
|
|
return obj.stack ?? obj.message // stack is formatted like "Error: message\n[stack]"
|
|
} else {
|
|
return String(obj)
|
|
}
|
|
}
|
|
|
|
function replacer(_key: string, value: unknown) {
|
|
if (typeof value === 'bigint') {
|
|
return value.toString()
|
|
} else if (isError(value)) {
|
|
return {
|
|
// custom enumerable properties on error, like e.g. status code on APIErrors
|
|
...value,
|
|
// these properties aren't enumerable so we need to include them explicitly
|
|
// see https://stackoverflow.com/questions/18391212/
|
|
name: value.name,
|
|
message: value.message,
|
|
stack: value.stack,
|
|
}
|
|
} else {
|
|
return value
|
|
}
|
|
}
|
|
|
|
function ts() {
|
|
return `[${new Date().toISOString()}]`
|
|
}
|
|
|
|
// handles both the cases where someone wants to write unstructured
|
|
// stream-of-consciousness console logging like log('count:', 1, 'user': u)
|
|
// and also structured key/value logging with severity
|
|
function writeLog(
|
|
level: LogLevel,
|
|
msg: unknown,
|
|
opts?: { props?: LogDetails; rest?: unknown[] }
|
|
) {
|
|
try {
|
|
const { props, rest } = opts ?? {}
|
|
const contextData = getMonitoringContext()
|
|
const message = format(toString(msg), ...(rest ?? []))
|
|
const data = { ...(contextData ?? {}), ...(props ?? {}) }
|
|
if (IS_GCP) {
|
|
const severity = JS_TO_GCP_LEVELS[level]
|
|
const output: LogDetails = { severity, message, ...data }
|
|
if (msg instanceof Error) {
|
|
// record error properties in GCP if you just do log(err)
|
|
output['error'] = msg
|
|
}
|
|
console.log(JSON.stringify(output, replacer))
|
|
} else {
|
|
const category = Object.values(pick(data, DISPLAY_CATEGORY_KEYS)).join()
|
|
const categoryLabel = category ? dim(category) + ' ' : ''
|
|
const details = Object.entries(
|
|
omit(data, [...DISPLAY_CATEGORY_KEYS, ...DISPLAY_EXCLUDED_KEYS])
|
|
).map(([key, value]) => `\n ${key}: ${JSON.stringify(value)}`)
|
|
const result = `${dim(ts())} ${categoryLabel}${message}${details}`
|
|
if (level === 'error') {
|
|
return console.error(red(result))
|
|
} else if (level === 'warn') {
|
|
return console.warn(yellow(result))
|
|
} else if (level === 'debug') {
|
|
return console.debug(dim(result))
|
|
} else {
|
|
return console[level](result)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Could not write log output.', e)
|
|
}
|
|
}
|
|
|
|
export function getLogger(): Logger {
|
|
const logger = ((msg: unknown, ...rest: unknown[]) =>
|
|
writeLog(DEFAULT_LEVEL, msg, { rest })) as Logger
|
|
for (const level of JS_LEVELS) {
|
|
logger[level] = (msg: unknown, props?: LogDetails) =>
|
|
writeLog(level, msg, { props })
|
|
}
|
|
return logger
|
|
}
|
|
|
|
export const log = getLogger()
|