@alexify/migronaut 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/README.md +8 -1
- package/bullmq.d.ts +35 -2
- package/index.d.ts +257 -3
- package/package.json +2 -1
- package/src/bullmq/background-processor.js +77 -5
- package/src/bullmq/processor.js +221 -8
- package/src/bullmq/service.js +4 -0
- package/src/core/background-dry-run.js +9 -0
- package/src/core/background-engine.js +47 -16
- package/src/core/background-kit.js +18 -11
- package/src/core/background-watch.js +5 -0
- package/src/core/background.js +6 -0
- package/src/core/migration-logger.js +279 -0
- package/src/core/migrator.js +129 -16
- package/src/core/options.js +20 -0
- package/src/core/run-recorder.js +6 -1
- package/src/core/runner.js +33 -7
- package/src/utils/job-ref.js +44 -0
- package/src/utils/redact.js +140 -3
- package/src/utils/telemetry.js +3 -0
package/src/utils/redact.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { isPlainObject } = require('./canonical.js');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Credential redaction for anything that leaves the process — error messages,
|
|
3
5
|
* stacks, `--json` payloads, log lines. The MongoDB driver echoes the raw
|
|
@@ -69,6 +71,24 @@ function redactOutbound(text) {
|
|
|
69
71
|
return redactUris(text).replace(DUPLICATE_KEY_VALUES, '$1{ <redacted> }');
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Set `key` on a copy as an own property. A plain assignment of `__proto__` —
|
|
76
|
+
* a key `JSON.parse` happily makes — would replace the copy's prototype and
|
|
77
|
+
* lose the key instead.
|
|
78
|
+
*/
|
|
79
|
+
function put(target, key, value) {
|
|
80
|
+
if (key === '__proto__') {
|
|
81
|
+
Object.defineProperty(target, key, {
|
|
82
|
+
value,
|
|
83
|
+
enumerable: true,
|
|
84
|
+
writable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
target[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
/**
|
|
73
93
|
* Redact every string reachable from `value` (plain objects and arrays only —
|
|
74
94
|
* class instances are left alone rather than cloned into broken shapes).
|
|
@@ -81,12 +101,129 @@ function redactDeep(value) {
|
|
|
81
101
|
for (let index = 0; index < value.length; index++) copy[index] = redactDeep(value[index]);
|
|
82
102
|
return copy;
|
|
83
103
|
}
|
|
84
|
-
if (value
|
|
104
|
+
if (isPlainObject(value)) {
|
|
85
105
|
const copy = {};
|
|
86
|
-
for (const key of Object.keys(value)) copy
|
|
106
|
+
for (const key of Object.keys(value)) put(copy, key, redactDeep(value[key]));
|
|
87
107
|
return copy;
|
|
88
108
|
}
|
|
89
109
|
return value;
|
|
90
110
|
}
|
|
91
111
|
|
|
92
|
-
|
|
112
|
+
/**
|
|
113
|
+
* How much of a value {@link redactBounded} copies: nesting, entries in all,
|
|
114
|
+
* the length of one string and of one piece of binary data. What a migration
|
|
115
|
+
* hands to `ctx.logger` is the application's own data, of any size and shape —
|
|
116
|
+
* a cycle included — and a subscriber stores it; these keep one call's copy
|
|
117
|
+
* small and finite.
|
|
118
|
+
*/
|
|
119
|
+
const BOUNDS = Object.freeze({ depth: 8, entries: 1000, string: 4096, bytes: 4096 });
|
|
120
|
+
|
|
121
|
+
/** Stands in for what {@link redactBounded} left out */
|
|
122
|
+
const TRUNCATED = '[truncated]';
|
|
123
|
+
|
|
124
|
+
/** The size of binary data — a Buffer or another Uint8Array, a BSON Binary — or undefined */
|
|
125
|
+
function byteLength(item) {
|
|
126
|
+
if (item instanceof Uint8Array) return item.byteLength;
|
|
127
|
+
if (item._bsontype === 'Binary') return item.position;
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* An Error as data: its name, its message — credentials and the values a
|
|
133
|
+
* server error quotes masked, as for anything that leaves the process — and
|
|
134
|
+
* its code. Not the stack, and not what else it carries: a driver error's raw
|
|
135
|
+
* server response repeats the offending document's values (`keyValue`,
|
|
136
|
+
* `errmsg`), and its `message` is not even enumerable, so the error itself
|
|
137
|
+
* would be stored as an empty document.
|
|
138
|
+
*/
|
|
139
|
+
function errorData(error) {
|
|
140
|
+
const data = {
|
|
141
|
+
name: String(error.name),
|
|
142
|
+
message: redactOutbound(typeof error.message === 'string' ? error.message : ''),
|
|
143
|
+
};
|
|
144
|
+
if (typeof error.code === 'number' || typeof error.code === 'string') data.code = error.code;
|
|
145
|
+
if (typeof error.codeName === 'string') data.codeName = error.codeName;
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* {@link redactDeep} within {@link BOUNDS}, for data that leaves the process
|
|
151
|
+
* as a document: every string reachable redacted ({@link redactOutbound}: the
|
|
152
|
+
* values a server error quotes too) and clipped, nesting past
|
|
153
|
+
* the depth (which is what ends a cycle) and entries past the budget replaced
|
|
154
|
+
* or dropped. What the driver stores as a value of its own is kept as is — a
|
|
155
|
+
* Date, a RegExp, an ObjectId or another BSON value, and binary data within
|
|
156
|
+
* its bound. An Error becomes `{ name, message, code?, codeName? }`; a Map is
|
|
157
|
+
* copied as an object and a Set as an array; any other instance goes by its
|
|
158
|
+
* `toJSON()`, or its own fields, the way `JSON.stringify` would see it.
|
|
159
|
+
* `omit` names a key left out of the top level only. Returns
|
|
160
|
+
* `{ value, truncated }`; never mutates the input.
|
|
161
|
+
*/
|
|
162
|
+
function redactBounded(value, { omit } = {}) {
|
|
163
|
+
let entries = 0;
|
|
164
|
+
let truncated = false;
|
|
165
|
+
/** One more entry, or false once the budget is spent */
|
|
166
|
+
const take = () => {
|
|
167
|
+
if (entries >= BOUNDS.entries) {
|
|
168
|
+
truncated = true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
entries += 1;
|
|
172
|
+
return true;
|
|
173
|
+
};
|
|
174
|
+
const copy = (item, depth) => {
|
|
175
|
+
if (typeof item === 'string') {
|
|
176
|
+
// Outbound, like the event's message: an error's text copied into a
|
|
177
|
+
// field (`{ error: err.message }`) loses the values it quotes too.
|
|
178
|
+
const text = redactOutbound(item);
|
|
179
|
+
if (text.length <= BOUNDS.string) return text;
|
|
180
|
+
truncated = true;
|
|
181
|
+
return `${text.slice(0, BOUNDS.string)}…`;
|
|
182
|
+
}
|
|
183
|
+
if (item === null || typeof item !== 'object') return item;
|
|
184
|
+
if (item instanceof Date || item instanceof RegExp) return item;
|
|
185
|
+
const bytes = byteLength(item);
|
|
186
|
+
if (bytes !== undefined) {
|
|
187
|
+
if (bytes <= BOUNDS.bytes) return item;
|
|
188
|
+
truncated = true;
|
|
189
|
+
return TRUNCATED;
|
|
190
|
+
}
|
|
191
|
+
if (typeof item._bsontype === 'string') return item;
|
|
192
|
+
if (item instanceof Error) return copy(errorData(item), depth);
|
|
193
|
+
if (depth >= BOUNDS.depth) {
|
|
194
|
+
truncated = true;
|
|
195
|
+
return TRUNCATED;
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(item) || item instanceof Set) {
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const element of item) {
|
|
200
|
+
if (!take()) break;
|
|
201
|
+
out.push(copy(element, depth + 1));
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
const out = {};
|
|
206
|
+
if (item instanceof Map) {
|
|
207
|
+
for (const [key, element] of item) {
|
|
208
|
+
if (!take()) break;
|
|
209
|
+
put(out, String(key), copy(element, depth + 1));
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
// What JSON.stringify would see: a toJSON() result one level down (so a
|
|
214
|
+
// chain of them ends at the depth bound), else the instance's own fields.
|
|
215
|
+
if (!isPlainObject(item) && typeof item.toJSON === 'function') {
|
|
216
|
+
return copy(item.toJSON(), depth + 1);
|
|
217
|
+
}
|
|
218
|
+
for (const key of Object.keys(item)) {
|
|
219
|
+
if (depth === 0 && key === omit) continue;
|
|
220
|
+
if (!take()) break;
|
|
221
|
+
put(out, key, copy(item[key], depth + 1));
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
};
|
|
225
|
+
const result = copy(value, 0);
|
|
226
|
+
return { value: result, truncated };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = { BOUNDS, redactBounded, redactDeep, redactOutbound, redactUris };
|
package/src/utils/telemetry.js
CHANGED
|
@@ -37,6 +37,9 @@ const ATTRIBUTES = {
|
|
|
37
37
|
MIGRATION_INDEX: 'migronaut.migration.index',
|
|
38
38
|
MIGRATION_TOTAL: 'migronaut.migration.total',
|
|
39
39
|
MIGRATION_TRANSACTION: 'migronaut.migration.transaction',
|
|
40
|
+
MIGRATION_ATTEMPTS: 'migronaut.migration.attempts',
|
|
41
|
+
JOB_ID: 'migronaut.job.id',
|
|
42
|
+
JOB_GROUP_ID: 'migronaut.job.group_id',
|
|
40
43
|
BACKGROUND_NAME: 'migronaut.background.name',
|
|
41
44
|
BACKGROUND_OUTCOME: 'migronaut.background.outcome',
|
|
42
45
|
BACKGROUND_RESULT: 'migronaut.background.result',
|