@connectedcars/logutil
Version:
Simple log formatting for Node
221 lines (212 loc) • 7.88 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TRACE_SAMPLED_KEY = exports.TRACE_KEY = exports.SPAN_ID_KEY = void 0;
exports.format = format;
exports.reachedMaxDepth = reachedMaxDepth;
var _levels = require("./levels");
const MAX_NESTED_DEPTH = 10;
const MAX_TEXT_LENGTH = 70 * 1024;
//https://github.com/googleapis/nodejs-logging/blob/9d1d480406c4d1526c8a7fafd9b18379c0c7fcea/src/entry.ts#L45-L47
const SPAN_ID_KEY = 'logging.googleapis.com/spanId';
exports.SPAN_ID_KEY = SPAN_ID_KEY;
const TRACE_KEY = 'logging.googleapis.com/trace';
exports.TRACE_KEY = TRACE_KEY;
const TRACE_SAMPLED_KEY = 'logging.googleapis.com/trace_sampled';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
exports.TRACE_SAMPLED_KEY = TRACE_SAMPLED_KEY;
function reachedMaxDepth(obj) {
let level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
for (const key in obj) {
if (typeof obj[key] == 'object') {
level++;
return level > MAX_NESTED_DEPTH || reachedMaxDepth(obj[key], level);
}
}
return false;
}
function depthLimited(contents) {
return stripStringify({
message: 'Depth limited ' + contents
});
}
function lengthLimited(contents) {
const truncated = contents.substring(0, MAX_TEXT_LENGTH);
return stripStringify({
message: 'Truncated ' + truncated
});
}
function stripStringify(mixedContent,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
walkerFunction) {
// this stops logs with multiline content being split into seperate entries on gcloud
return JSON.stringify(mixedContent, walkerFunction).replace(/\\n/g, '\\n');
}
function formatError(err) {
const errObj = {};
for (const errKey of Object.getOwnPropertyNames(err)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
errObj[errKey] = err[errKey];
}
errObj['__constructorName'] = err.constructor.name;
return errObj;
}
function formatContext(context) {
const newContext = {};
for (const key of Object.keys(context)) {
const val = context[key];
if (val instanceof Error) {
// Errors cannot be stringified.
newContext[key] = formatError(val);
} else {
newContext[key] = val;
}
}
return newContext;
}
function format(level) {
const output = {
message: '',
context: {},
data: []
};
/*
wrap it all in a try and catch, so if there was an
error we go through just the object that caused an
error, and not over zealously check every objects values
*/
try {
var _output$data3;
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
// Check for arguments
if (args.length > 0) {
if (args[0] instanceof Array) {
// Stringify lists as the message and add as data
output.message = args[0].join(', ');
output.data = args[0];
} else if (args[0] instanceof Object) {
// Override logging output for objects (and errors)
for (const key of Object.getOwnPropertyNames(args[0])) {
output[key] = args[0][key];
}
} else {
// Set primitive types as the message
output.message = `${args[0]}`;
}
if (args.length > 1) {
if (args[1] instanceof Object) {
// Set objects as secondary arguments as the context
output.context = formatContext(args[1]);
// Try to find trace info from context
// Based on field in LogEntry: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.trace_sampled
if (output.context['spanId'] && output.context['trace']) {
var _output$context$trace;
output[TRACE_KEY] = output.context['trace'];
output[SPAN_ID_KEY] = output.context['spanId'];
output[TRACE_SAMPLED_KEY] = (_output$context$trace = output.context['traceSampled']) !== null && _output$context$trace !== void 0 ? _output$context$trace : false;
// We don't need these fields in the context
delete output.context['spanId'];
delete output.context['trace'];
delete output.context['traceSampled'];
}
} else {
var _output$data;
// Set all other types as data
(_output$data = output.data) === null || _output$data === void 0 ? void 0 : _output$data.push(args[1]);
}
}
if (args.length > 2) {
for (let i = 2; i < args.length; i++) {
var _output$data2;
// Set additional arguments as data
(_output$data2 = output.data) === null || _output$data2 === void 0 ? void 0 : _output$data2.push(args[i]);
}
}
}
if (output.message === '') {
// Remove empty messages
delete output.message;
}
if (output.context && Object.keys(output.context).length === 0) {
// Remove empty contexts
delete output.context;
}
if (((_output$data3 = output.data) === null || _output$data3 === void 0 ? void 0 : _output$data3.length) === 0) {
// Remove empty data
delete output.data;
}
// Add level (Stackdriver support)
output.severity = (0, _levels.getLogLevelName)(level);
// Add timestamp
output.timestamp = new Date().toISOString();
// Ensure that message size is no more than half the total allowed size for the blob
// This is to truncate very large sql statements
if (output.message && output.message.length > MAX_TEXT_LENGTH / 2) {
output.message = `Truncated: ${output.message.substring(0, MAX_TEXT_LENGTH / 2)}...`;
}
// Stringify output
const blob = stripStringify(output);
if (blob.length <= MAX_TEXT_LENGTH) {
// Check for depth above 10
if (reachedMaxDepth(output)) {
// Wrap deep objects in a string
return depthLimited(blob);
}
// not over nesting limit or length limit
return blob;
}
// Truncate stringified output to 100 KB &
// Wrap truncated output in a string
return lengthLimited(blob);
} catch (err) {
if (err.message.indexOf('Converting circular structure to JSON') !== -1) {
/*
it is a circular reference and parse
through the object while keeping track
of elements occuring twice
*/
// keep track of object values, so we know if they occur twice (circular reference)
const seenValues = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const valueChecker = function (objKey, objValue) {
if (objValue !== null && typeof objValue === 'object') {
if (seenValues.indexOf(objValue) > -1) {
// let it be logged that there was something sanitized
return '[Circular:StrippedOut]';
} else {
// track that we have 'seen' it
seenValues.push(objValue);
}
}
// first time seeing it, return and proceed to next value
return objValue;
};
const returnBlob = stripStringify(output, valueChecker);
// still we want to curtail too long logs
if (returnBlob.length >= MAX_TEXT_LENGTH) {
// Wrap truncated output in a string
return lengthLimited(returnBlob);
}
// but what if it is still too long?
/*
don't check depth on the original object
which contains circular references. Parse
the santisized down object back to an
object to count object depth
*/
if (reachedMaxDepth(JSON.parse(returnBlob))) {
return depthLimited(returnBlob);
}
// the object wasn't too long or too nested, return the stringified object
return returnBlob;
} else {
// it isn't an error we know how to handle
throw err;
}
}
}
//# sourceMappingURL=format.js.map