UNPKG

@connectedcars/logutil

Version:
221 lines (212 loc) 7.88 kB
"use strict"; 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