| 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
)
}
);
},
};
|