@elisra-devops/docgen-data-provider 1.141.0 → 1.143.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/bin/helpers/requestContext.d.ts +66 -0
- package/bin/helpers/requestContext.js +255 -0
- package/bin/helpers/requestContext.js.map +1 -0
- package/bin/helpers/tfs.d.ts +5 -0
- package/bin/helpers/tfs.js +31 -31
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.js +3 -2
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/JfrogDataProvider.js +2 -1
- package/bin/modules/JfrogDataProvider.js.map +1 -1
- package/bin/modules/PipelinesDataProvider.js +4 -3
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/ResultDataProvider.js +12 -11
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +4 -3
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.js +13 -12
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/requestContext.test.d.ts +1 -0
- package/bin/tests/helpers/requestContext.test.js +204 -0
- package/bin/tests/helpers/requestContext.test.js.map +1 -0
- package/bin/tests/helpers/tfs.test.js +73 -0
- package/bin/tests/helpers/tfs.test.js.map +1 -1
- package/bin/tests/modules/JfrogDataProvider.test.js +1 -1
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +5 -2
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/tests/modules/gitDataProvider.test.js +1 -1
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
- package/bin/tests/modules/testDataProvider.test.js +1 -1
- package/bin/tests/modules/testDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +2 -2
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/bin/tests/utils/logger.test.js +120 -0
- package/bin/tests/utils/logger.test.js.map +1 -1
- package/bin/utils/logSink.d.ts +14 -0
- package/bin/utils/logSink.js +12 -0
- package/bin/utils/logSink.js.map +1 -1
- package/bin/utils/logger.d.ts +3 -0
- package/bin/utils/logger.js +96 -10
- package/bin/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/requestContext.ts +258 -0
- package/src/helpers/tfs.ts +38 -28
- package/src/modules/GitDataProvider.ts +3 -2
- package/src/modules/JfrogDataProvider.ts +2 -1
- package/src/modules/PipelinesDataProvider.ts +4 -3
- package/src/modules/ResultDataProvider.ts +12 -11
- package/src/modules/TestDataProvider.ts +4 -3
- package/src/modules/TicketsDataProvider.ts +13 -12
- package/src/tests/helpers/requestContext.test.ts +252 -0
- package/src/tests/helpers/tfs.test.ts +107 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +2 -1
- package/src/tests/modules/ResultDataProvider.test.ts +6 -2
- package/src/tests/modules/gitDataProvider.test.ts +2 -1
- package/src/tests/modules/pipelineDataProvider.test.ts +2 -1
- package/src/tests/modules/testDataProvider.test.ts +4 -1
- package/src/tests/modules/ticketsDataProvider.test.ts +2 -2
- package/src/tests/utils/logger.test.ts +146 -1
- package/src/utils/logSink.ts +18 -3
- package/src/utils/logger.ts +85 -7
package/src/utils/logger.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as Transport from 'winston-transport';
|
|
6
6
|
import { runContextStore } from './runContext';
|
|
7
|
-
import { getLogSink, DiagnosticEvent } from './logSink';
|
|
7
|
+
import { getLogSink, DiagnosticEvent, CONTEXT_LIMITS } from './logSink';
|
|
8
8
|
let logger: winston.Logger;
|
|
9
9
|
|
|
10
10
|
// Merges the ambient runId (set by content-control's request middleware, once Phase 3 lands
|
|
@@ -29,7 +29,24 @@ export const withRunContext = winston.format((info) => {
|
|
|
29
29
|
// "accesskey" (not just "minioaccesskey") so this also catches an *AccessKeyId-style field —
|
|
30
30
|
// a real gap found in api-gate's copy during the Phase 5 manifest work: only the *SecretKey
|
|
31
31
|
// sibling was covered before, via "secret". Applied here too to keep the four copies in sync.
|
|
32
|
-
|
|
32
|
+
// Word-based, not substring: the key is split into lower-case words (camelCase, kebab-case and
|
|
33
|
+
// snake_case alike) and judged by how it *ends*. A substring test matched "pat" inside
|
|
34
|
+
// "areaPath" / "path" / "patch" and blanked ordinary ADO fields; this keeps `accessToken`,
|
|
35
|
+
// `x-docgen-ingest-token`, `minioSecretKey`, `PAT`, `password` redacted while `areaPath` stays.
|
|
36
|
+
const SENSITIVE_TAILS = ['token', 'password', 'secret', 'authorization', 'accesskey', 'secretkey', 'apikey'];
|
|
37
|
+
const SENSITIVE_LAST_WORDS = new Set(['pat', 'pwd', 'cookie', 'tokens', 'passwords', 'secrets']);
|
|
38
|
+
export function isSensitiveKey(key: string): boolean {
|
|
39
|
+
const words = key
|
|
40
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
41
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.split(/[^a-z0-9]+/)
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
if (words.length === 0) return false;
|
|
46
|
+
if (SENSITIVE_LAST_WORDS.has(words[words.length - 1])) return true;
|
|
47
|
+
const tail = words.slice(-2).join('');
|
|
48
|
+
return SENSITIVE_TAILS.some((t) => tail.endsWith(t));
|
|
49
|
+
}
|
|
33
50
|
|
|
34
51
|
// A per-key try/catch on the *read*, not just around the whole loop: a getter that throws
|
|
35
52
|
// (e.g. `{ get boom() { throw ... } }`) would otherwise abort redaction for every remaining
|
|
@@ -44,7 +61,7 @@ function safeRead(obj: Record<string, unknown>, key: string): { ok: true; value:
|
|
|
44
61
|
return { ok: false };
|
|
45
62
|
}
|
|
46
63
|
}
|
|
47
|
-
function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
64
|
+
export function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
48
65
|
if (depth > 6 || value === null || typeof value !== 'object') return value;
|
|
49
66
|
if (seen.has(value as object)) return '[Circular]';
|
|
50
67
|
seen.add(value as object);
|
|
@@ -56,7 +73,7 @@ function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): u
|
|
|
56
73
|
out[key] = '[Unreadable property]';
|
|
57
74
|
continue;
|
|
58
75
|
}
|
|
59
|
-
out[key] =
|
|
76
|
+
out[key] = isSensitiveKey(key) ? '[REDACTED]' : redactValue(read.value, depth + 1, seen);
|
|
60
77
|
}
|
|
61
78
|
return out;
|
|
62
79
|
}
|
|
@@ -75,7 +92,7 @@ export const redact = winston.format((info) => {
|
|
|
75
92
|
? '[Unreadable property]'
|
|
76
93
|
: // A top-level primitive (e.g. token: "abc") has no children for redactValue to walk into —
|
|
77
94
|
// the key itself has to be checked here too, not just inside the recursive object walk.
|
|
78
|
-
|
|
95
|
+
isSensitiveKey(key)
|
|
79
96
|
? '[REDACTED]'
|
|
80
97
|
: redactValue(read.value);
|
|
81
98
|
try {
|
|
@@ -116,7 +133,26 @@ function readOwnVersion(): string {
|
|
|
116
133
|
const safeMessageString = (value: unknown): string =>
|
|
117
134
|
typeof value === 'symbol' ? value.toString() : String(value);
|
|
118
135
|
|
|
119
|
-
|
|
136
|
+
// A failed ADO request's description (helpers/requestContext.ts), attached to the thrown error
|
|
137
|
+
// and merged onto the record by winston. In text mode — the default, which prints only the
|
|
138
|
+
// message — this is the only way the URL reaches stdout for a record whose own message doesn't
|
|
139
|
+
// carry it. Never throws: this runs inside the formatter, which has no try/catch around it.
|
|
140
|
+
const requestSuffix = (ctx: unknown): string => {
|
|
141
|
+
try {
|
|
142
|
+
if (!ctx || typeof ctx !== 'object') return '';
|
|
143
|
+
const c = ctx as Record<string, unknown>;
|
|
144
|
+
if (typeof c.url !== 'string') return '';
|
|
145
|
+
const method = typeof c.method === 'string' ? `${c.method} ` : '';
|
|
146
|
+
const status = typeof c.status === 'number' ? ` -> ${c.status}` : '';
|
|
147
|
+
return ` [${method}${c.url}${status}]`;
|
|
148
|
+
} catch {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const textFormat = winston.format.printf(
|
|
154
|
+
(info) => `${info.timestamp} - ${info.level}: ${safeMessageString(info.message)}${requestSuffix(info.adoRequest)}`
|
|
155
|
+
);
|
|
120
156
|
|
|
121
157
|
// Bounded so one oversized message/stack can't produce an unbounded LogEvent document.
|
|
122
158
|
const MAX_MESSAGE_LEN = 2000;
|
|
@@ -126,6 +162,26 @@ function clamp(value: unknown, max: number): string | undefined {
|
|
|
126
162
|
return value.length > max ? value.slice(0, max) : value;
|
|
127
163
|
}
|
|
128
164
|
|
|
165
|
+
// Re-validates the request description at the transport instead of trusting its shape — it
|
|
166
|
+
// arrives via an Error's own properties, which anything upstream could have set. Allowlisted
|
|
167
|
+
// keys only, each type-checked and bounded.
|
|
168
|
+
function pickContext(raw: unknown): DiagnosticEvent['context'] {
|
|
169
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
170
|
+
const c = raw as Record<string, unknown>;
|
|
171
|
+
const context: NonNullable<DiagnosticEvent['context']> = {};
|
|
172
|
+
const method = clamp(c.method, CONTEXT_LIMITS.method);
|
|
173
|
+
const url = clamp(c.url, CONTEXT_LIMITS.url);
|
|
174
|
+
const requestBody = clamp(c.requestBody, CONTEXT_LIMITS.requestBody);
|
|
175
|
+
const responseExcerpt = clamp(c.responseExcerpt, CONTEXT_LIMITS.responseExcerpt);
|
|
176
|
+
if (method) context.method = method;
|
|
177
|
+
if (url) context.url = url;
|
|
178
|
+
if (requestBody) context.requestBody = requestBody;
|
|
179
|
+
if (responseExcerpt) context.responseExcerpt = responseExcerpt;
|
|
180
|
+
if (typeof c.status === 'number' && Number.isFinite(c.status)) context.status = c.status;
|
|
181
|
+
if (typeof c.attempt === 'number' && Number.isFinite(c.attempt)) context.attempt = c.attempt;
|
|
182
|
+
return Object.keys(context).length ? context : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
129
185
|
// Ships warn/error records to whatever LogSink the host process (docgen-content-control's
|
|
130
186
|
// index.ts) has installed — this package never installs one itself, only reads it. This is
|
|
131
187
|
// the dominant source of real ADO error records (tfs.ts's executeWithRetry is the single
|
|
@@ -177,11 +233,14 @@ export class DiagnosticsTransport extends Transport {
|
|
|
177
233
|
message,
|
|
178
234
|
err: hasErr
|
|
179
235
|
? {
|
|
180
|
-
message
|
|
236
|
+
// The error's own message when the call site supplied it (describeError); else
|
|
237
|
+
// the combined winston message, which is all a bare logger.error(err) leaves.
|
|
238
|
+
message: clamp(info.errMessage, MAX_MESSAGE_LEN) ?? message,
|
|
181
239
|
code: typeof info.code === 'string' ? info.code : undefined,
|
|
182
240
|
stack: clamp(info.stack, MAX_STACK_LEN),
|
|
183
241
|
}
|
|
184
242
|
: undefined,
|
|
243
|
+
context: pickContext(info.adoRequest),
|
|
185
244
|
retainPending: !isWarnOrError && captureMode === 'retain-on-failure' ? true : undefined,
|
|
186
245
|
};
|
|
187
246
|
getLogSink()?.push(event);
|
|
@@ -208,11 +267,29 @@ const useJson = (process.env.LOG_FORMAT || 'text').toLowerCase() === 'json';
|
|
|
208
267
|
// for free; info/warn/error volume (Phase 4's focus) is unaffected either way.
|
|
209
268
|
const CONSOLE_LOG_LEVEL = process.env.LOG_LEVEL || 'info';
|
|
210
269
|
|
|
270
|
+
// Drops a record before the (comparatively expensive) redact/splat/format work when nothing
|
|
271
|
+
// would consume it: stdout wouldn't print it, and DiagnosticsTransport wouldn't persist it
|
|
272
|
+
// (only debug/info under a verbose/retain-on-failure run are captured below warn). The logger's
|
|
273
|
+
// own level gate must stay 'debug' for that capture, so this is the cheap early exit instead.
|
|
274
|
+
const LEVEL_RANK: Record<string, number> = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 };
|
|
275
|
+
export const skipUncaptured = winston.format((info) => {
|
|
276
|
+
const printRank = LEVEL_RANK[CONSOLE_LOG_LEVEL];
|
|
277
|
+
const rank = LEVEL_RANK[String(info.level)];
|
|
278
|
+
if (printRank === undefined || rank === undefined || rank <= printRank) return info;
|
|
279
|
+
const mode = runContextStore.getStore()?.captureMode;
|
|
280
|
+
const captured =
|
|
281
|
+
DIAGNOSTICS_CAPTURE_ENABLED &&
|
|
282
|
+
(info.level === 'debug' || info.level === 'info') &&
|
|
283
|
+
(mode === 'verbose' || mode === 'retain-on-failure');
|
|
284
|
+
return captured ? info : false;
|
|
285
|
+
});
|
|
286
|
+
|
|
211
287
|
logger = winston.createLogger({
|
|
212
288
|
level: 'debug',
|
|
213
289
|
defaultMeta: { service: '@elisra-devops/docgen-data-provider', version: readOwnVersion() },
|
|
214
290
|
format: useJson
|
|
215
291
|
? winston.format.combine(
|
|
292
|
+
skipUncaptured(),
|
|
216
293
|
winston.format.errors({ stack: true }),
|
|
217
294
|
winston.format.timestamp(),
|
|
218
295
|
withRunContext(),
|
|
@@ -221,6 +298,7 @@ logger = winston.createLogger({
|
|
|
221
298
|
winston.format.json()
|
|
222
299
|
)
|
|
223
300
|
: winston.format.combine(
|
|
301
|
+
skipUncaptured(),
|
|
224
302
|
winston.format.errors({ stack: true }),
|
|
225
303
|
winston.format.timestamp(),
|
|
226
304
|
withRunContext(),
|