@unito/integration-sdk
Version:
Integration SDK
237 lines (236 loc) • 9.04 kB
JavaScript
import { styleText } from 'util';
var LogLevel;
(function (LogLevel) {
LogLevel["ERROR"] = "error";
LogLevel["WARN"] = "warn";
LogLevel["INFO"] = "info";
LogLevel["LOG"] = "log";
LogLevel["DEBUG"] = "debug";
})(LogLevel || (LogLevel = {}));
/**
* See https://docs.datadoghq.com/logs/log_collection/?tab=host#custom-log-forwarding
* - Datadog Agent splits at 256kB (256000 bytes)...
* - ... but the same docs say that "for optimal performance, it is
* recommended that an individual log be no greater than 25kB"
* -> Truncating at 25kB - a bit of wiggle room for metadata = 20kB.
*/
const MAX_LOG_MESSAGE_SIZE = parseInt(process.env.MAX_LOG_MESSAGE_SIZE ?? '20000', 10);
const LOG_LINE_TRUNCATED_SUFFIX = ' - LOG LINE TRUNCATED';
/**
* For *LogMeta* sanitization, we let in anything that was passed, except for clearly-problematic keys
*/
const LOGMETA_BLACKLIST = [
// Security
'access_token',
'bot_auth_code',
'client_secret',
'jwt',
'oauth_token',
'password',
'refresh_token',
'shared_secret',
'token',
// Privacy
'billing_email',
'email',
'first_name',
'last_name',
];
/**
* Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
*/
export default class Logger {
isDisabled;
metadata;
constructor(metadata = {}, isDisabled = false) {
this.isDisabled = isDisabled;
this.metadata = structuredClone(metadata);
}
/**
* Logs a message with the 'log' log level.
* @param message The message to be logged.
* @param metadata Optional metadata to be associated with the log message.
*/
log(message, metadata) {
this.send(LogLevel.LOG, message, metadata);
}
/**
* Logs an error message with the 'error' log level.
* @param message The error message to be logged.
* @param metadata Optional metadata to be associated with the log message.
*/
error(message, metadata) {
this.send(LogLevel.ERROR, message, metadata);
}
/**
* Logs a warning message with the 'warn' log level.
* @param message The warning message to be logged.
* @param metadata Optional metadata to be associated with the log message.
*/
warn(message, metadata) {
this.send(LogLevel.WARN, message, metadata);
}
/**
* Logs an informational message with the 'info' log level.
* @param message The informational message to be logged.
* @param metadata Optional metadata to be associated with the log message.
*/
info(message, metadata) {
this.send(LogLevel.INFO, message, metadata);
}
/**
* Logs a debug message with the 'debug' log level.
* @param message The debug message to be logged.
* @param metadata Optional metadata to be associated with the log message.
*/
debug(message, metadata) {
this.send(LogLevel.DEBUG, message, metadata);
}
/**
* Decorates the logger with additional metadata.
* @param metadata Additional metadata to be added to the logger.
*/
decorate(metadata) {
this.metadata = { ...this.metadata, ...metadata };
}
/**
* Return a copy of the Logger's metadata.
* @returns The {@link Metadata} associated with the logger.
*/
getMetadata() {
return structuredClone(this.metadata);
}
/**
* Sets a key-value pair in the metadata. If the key already exists, it will be overwritten.
*
* @param key Key of the metadata to be set.
* May be any string other than 'message', which is reserved for the actual message logged.
* @param value Value of the metadata to be set.
*/
setMetadata(key, value) {
this.metadata[key] = value;
}
/**
* Clears the Logger's metadata.
*/
clearMetadata() {
this.metadata = {};
}
send(logLevel, message, metadata) {
if (this.isDisabled) {
return;
}
// We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
const date = Date.now();
if (message.length > MAX_LOG_MESSAGE_SIZE) {
message = `${message.substring(0, MAX_LOG_MESSAGE_SIZE)}${LOG_LINE_TRUNCATED_SUFFIX}`;
}
let processedMetadata = Logger.snakifyKeys({ ...this.metadata, ...metadata, logMessageSize: message.length });
processedMetadata = Logger.pruneSensitiveMetadata(processedMetadata);
const processedLogs = {
...processedMetadata,
message,
date,
status: logLevel,
};
if (process.env.NODE_ENV === 'development') {
const coloredMessage = Logger.colorize(message, processedLogs, logLevel);
const metadata = {
date: new Date(processedLogs.date).toISOString(),
...(processedMetadata.error && { error: processedMetadata.error }),
};
const metadataString = Object.keys(metadata).length > 1 ? ` ${JSON.stringify(metadata, null, 2)}` : ` ${JSON.stringify(metadata)}`;
console[logLevel](`${coloredMessage}${metadataString}`);
}
else {
console[logLevel](JSON.stringify(processedLogs));
}
}
static snakifyKeys(value) {
const result = {};
for (const key in value) {
let deepValue;
if (Array.isArray(value[key])) {
deepValue = value[key].map(item => this.snakifyKeys(item));
}
else if (typeof value[key] === 'object' && value[key] !== null) {
deepValue = this.snakifyKeys(value[key]);
}
else {
deepValue = value[key];
}
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
result[snakifiedKey] = deepValue;
}
return result;
}
static pruneSensitiveMetadata(metadata, topLevelMeta) {
const prunedMetadata = {};
for (const key in metadata) {
if (LOGMETA_BLACKLIST.includes(key)) {
prunedMetadata[key] = '[REDACTED]';
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
}
else if (Array.isArray(metadata[key])) {
prunedMetadata[key] = metadata[key].map(value => Logger.pruneSensitiveMetadata(value, topLevelMeta ?? prunedMetadata));
}
else if (typeof metadata[key] === 'object' && metadata[key] !== null) {
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
}
else {
prunedMetadata[key] = metadata[key];
}
}
return prunedMetadata;
}
/**
* Colorizes the log message based on the log level and status codes.
* @param message The message to colorize.
* @param metadata The metadata associated with the log.
* @param logLevel The log level of the message.
* @returns The colorized output string.
*/
static colorize(message, metadata, logLevel) {
if (!process.stdout.isTTY) {
return `${logLevel}: ${message}`;
}
// Extract status code from logs
let statusCode;
if (metadata.http && typeof metadata.http === 'object' && !Array.isArray(metadata.http)) {
const statusCodeValue = metadata.http.status_code;
if (typeof statusCodeValue === 'number') {
statusCode = statusCodeValue;
}
else if (typeof statusCodeValue === 'string') {
statusCode = parseInt(statusCodeValue, 10);
}
}
// Color based on status code first
if (statusCode) {
if (statusCode >= 400) {
return `${styleText('red', logLevel)}: ${styleText('red', message)}`;
}
else if (statusCode >= 300) {
return `${styleText('yellow', logLevel)}: ${styleText('yellow', message)}`;
}
else if (statusCode >= 200) {
return `${styleText('green', logLevel)}: ${styleText('green', message)}`;
}
}
// Fall back to log level if no status code found
switch (logLevel) {
case LogLevel.ERROR:
return `${styleText('red', logLevel)}: ${styleText('red', message)}`;
case LogLevel.WARN:
return `${styleText('yellow', logLevel)}: ${styleText('yellow', message)}`;
case LogLevel.INFO:
case LogLevel.LOG:
return `${styleText('green', logLevel)}: ${styleText('green', message)}`;
case LogLevel.DEBUG:
return `${styleText('cyan', logLevel)}: ${styleText('cyan', message)}`;
default:
return `${logLevel}: ${message}`;
}
}
}
export const NULL_LOGGER = new Logger({}, true);