1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | 2× 2× 2× 2× 2× 19× 342× 19× 19× 19× 19× 19× 2× 209× 209× 627× 627× 627× 209× 570× 570× 532× 209× 209× 19× 19× 38× 19× 190× 323× 19× 19× 19× 19× 19× 19× 19× 19× 19× | 'use strict'; /** * @function treeZip * @desc Zips two "trees" - objects with nesting - and applies the given * function to leaf nodes to produce a new tree. The trees are expected to * follow similar structure. * * This is a left-biased zip, in the sense that the function will be called for * leaves that appear in the left tree, but some leaves in the right tree will * remain untraversed. * * Example: * treeZip({x: {y: 1}, z: 2}, {x: 3}, f) * * produces * * {x: f({y:1}, 3), z: f(2, undefined)} * * @param {object} xs - the tree that's walked * @param {object} ys - the parallel tree that determines which of the nodes in * xs are expected to be leaves * @param {function} f - the function that is invoked to produce new subtrees * where ys contains a leaf, or where a subtree is present in xs, but not in ys */ const treeZip = (xs, ys, f) => { if (typeof xs !== 'object' || typeof ys !== 'object') { return f(xs, ys); } const zs = {}; for(let k in xs) { let z = treeZip(xs[k], ys[k], f); if (z !== undefined) { zs[k] = z; } } return zs; }; const treeWalk = (xs, f) => treeZip(xs, xs, f); const PNR_COMMON_WRAP = {}; const unwrap = x => x.prototype === PNR_COMMON_WRAP? x(): x; module.exports = exports = { /** * @function wrap * * @desc winston@2.2.0 deep-clones all meta. The manner in which it is done * destroys information about the original instance class, so formatters no * longer can treat Errors specially - for example, to hide stack traces from * customer-visible logs. * * Wrapping such values creates a function, so it is not cloneable by * winston. The formatters should expect wrapped values for errors, or in * other places, where cloning is not desirable, but that wrapping should be * used in a concerted fashion. * * @param {any} x - anything to wrap * @return {any} to use as the value in the structured log context */ wrap: x => { const f = () => x; f.prototype = PNR_COMMON_WRAP; return f; }, /** * @function kvpToStr * @desc Given an iterable of key-value pairs (each pair represented by a * list of two values - a key and a value), preprocesses the list to filter * out, censor, or add new key-value pairs, and then joins the pairs in a * generic fashion to produce a string. * * @param {iterable} kvps - the iterable of key-value pairs * @param {function} preProcess - the function to filter, censor, or add new * key-value pairs * @return {string} representation of they key-value pairs */ kvpToStr: (kvps, preProcess) => (preProcess || (xs => xs))(Array.from(kvps)). map(ks => ks[0] + '=' + JSON.stringify(ks[1].toString())). join(' '), /** * @function logMetaFormatter * * @desc Given opts from a logger.log, walks the meta to produce ordered key-value * pairs, which can be used to construct the opts.meta as a prefix of * opts.message, and returns the new opts to be formatted using winstonCommon; * Treats error key in opts.meta in a special way: if error_special, leaves * the error to winstonCommon to format (to include stack traces, etc). If * you don't want the stack traces, leave error_special undefined; the error * will be added as an object with the key starting with '_' - that way * only the error type and message will be added. * * Treats keys of opts.meta starting with '_' in a special way - they will * appear last. * * Expects opts.meta.error may be wrapped to work around winston@2.2.0 * deep-cloning. * * @param {object} opts - the opts as appears in logger.log * @param {boolean} error_special - the flag telling whether opts.meta.error * should be left alone for formatting by the caller * @return {object} opts - the opts as they should appear in logger.log, with * meta and message */ logMetaFormatter: (opts, error_special) => { // This formatter sorts the keys // so if you want the keys to appear in groups, // wrap the values into objects, and assign to // keys in alphabetical order. // If the value for a given key is falsy, and the type is undefined or // object, it is skipped const newopts = Object.assign({}, opts); const meta = Object.assign({}, opts.meta); const err = meta.error; delete newopts.meta; // treat errors specially: they are left to be formatted by winstonCommon, // to produce stack traces etc Iif (err) { delete meta.error; if (error_special) { newopts.meta = unwrap(err); } else { let o = meta; // inserting errors to appear last // assuming no reference loops while(o._) { o = o._ = Object.assign({}, o._); } o._ = unwrap(err); } } function* kvps(o) { const ks = o ? Object.keys(o): []; ks.sort((a,b) => { // treating keys starting with _ in a special way - they should // appear at the end of the list const ba = a.startsWith('_'); const bb = b.startsWith('_'); return ba - bb || -(a < b) || +(a > b); }); for(let i = 0; i < ks.length; i++) { let k = ks[i]; if (typeof o[k] === 'undefined' || typeof o[k] === 'function') { // no-op } else if (typeof o[k] === 'object') { Iif (o[k] instanceof Error) { // unified treatment of errors as objects with a name and // a message yield* kvps({error: o[k].name, errorMessage: o[k].message}); } else if (Array.isArray(o[k])) { let keys = Object.keys(o[k]); // let nested = o[k].every((v, i) => ... JS spec works // differently for every() and findIndex() let nested = o[k].findIndex( (v, i) => keys.indexOf(i.toString()) < 0 ) >= 0? // assume o[k] is a sparse Array - treat it more // object-like // // TODO: what's a unified way of treating Arrays? // currently will squeeze everything into a string built // from index-value pairs exports.kvpToStr(kvps(o[k])): // assume o[k] is a proper Array of primitive values o[k].map(JSON.stringify).join(', '); yield [k, nested]; } else { // o[k] may be null or {} yield* kvps(o[k]); } } else { yield [k, o[k]]; } } } return {kvps: kvps(meta), opts: newopts}; }, /** * @function logKVPFormatter * * @desc Flattens structured opts.meta, so only leaf elements are logged as * key-value pairs. The flattened list of key-value pairs can be * post-processed to add fixed fields, or modify the list in other ways, * before the list is stringified. * * @param {object} opts - opts as they appear in logger.log * @param {function} postProcess - optional post-processing of an iterable of * key-value pairs; should expect an iterable of lists with key and a * value, and return an iterable of lists with key and a value * @param {boolean} error_special - optional boolean flag with the same * meaning as in logMetaFormatter * @return {object} opts as they should appear in logger.log, with meta and * message */ logKVPFormatter: (opts, postProcess, error_special) => { Iif (typeof postProcess !== 'function') { error_special = postProcess; postProcess = undefined; } const r = exports.logMetaFormatter(opts, error_special); const newopts = r.opts; const kvps = exports.kvpToStr(r.kvps, postProcess); Eif (kvps) { newopts.message = (newopts.message?kvps + ' ' + newopts.message: kvps); } delete newopts.formatter; return newopts; }, /** * @function flattenFormatter * * @desc Given structured data, flattens to construct key-value pairs with * the keys being the "leaf" attributes in the structured opts.meta * * @param {object} opts - opts as they appear in logger.log * @param {function} postProcess - optional post-processing like in * logKVPFormatter * @param {boolean} error_special - optional boolean flag with the same * meaning as in logKVPFormatter * @return {object} opts as they should appear in logger.log, with meta and * message */ flattenFormatter: (opts, postProcess, error_special) => { if (typeof postProcess !== 'function') { error_special = postProcess; postProcess = undefined; } const r = exports.logMetaFormatter(opts, error_special); const newopts = r.opts; newopts.meta = {}; (postProcess || (ks => ks))(Array.from(r.kvps)). forEach( ks => newopts.meta[ks[0]] = ks[1] ); delete newopts.formatter; return newopts; }, /** * @function censor * @desc Removes attributes from the unformatted log message, based on the * clearance level of a given log transport. * * opts passed in should have the following fields: * { * clearance: {string}, // the string from clearance_levels * clearance_levels: {array}, // the array of levels in ascending order * clearance_labels: {object}, // the map from log message attribute to * // clearance level required to see it * } * * Given opts, the function produces a filter of opts.meta, which will * look up the clearance label in clearance_labels, and filter out those * fields that require * higher clearance level than specified in clearance. If a log message * attribute does not appear in clearance_labels, it is treated as * unclassified and is allowed to be seen in log transports of any clearance. * * If no clearance is specified in opts, log transport is allowed to see only * unclassified log message attributes. * * @param {object} opts - options for the censor * @param {object} msg - message as appears on input of winston log formatter * containing meta with structured message * @return {object} opts as they should appear in logger.log */ censor: opts => { const clearance_level = opts.clearance_levels.indexOf(opts.clearance); const censored_attributes = treeWalk( opts.clearance_labels, // deliberately returning true or undefined instead of false x => opts.clearance_levels.indexOf(x) > clearance_level || undefined ); return msg => Object.assign( {}, msg, {meta: treeZip( msg.meta, censored_attributes, // censor is either a bool true, or undefined, or an object // censor === undefined is the case when x is unclassified // censor === object is the case when only parts of x are // classified, but x has no parts (not an object) (x, censor) => censor === true? undefined: x ) } ); }, }; |