@contrail/flexplm 1.8.1 → 2.0.0-alpha.df05bb5
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/lib/__tests__/telemetry-test-utils.d.ts +8 -0
- package/lib/__tests__/telemetry-test-utils.js +30 -0
- package/lib/cli/commands/compile.js +17 -7
- package/lib/cli/commands/compile.spec.js +17 -7
- package/lib/cli/commands/create.js +17 -7
- package/lib/cli/commands/create.spec.js +17 -7
- package/lib/cli/commands/upload.js +17 -7
- package/lib/cli/index.js +2 -2
- package/lib/entity-processor/base-entity-processor.d.ts +29 -0
- package/lib/entity-processor/base-entity-processor.js +238 -143
- package/lib/entity-processor/base-entity-processor.spec.js +210 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/publish/base-process-publish-assortment-callback.js +30 -0
- package/lib/publish/base-process-publish-assortment-callback.spec.d.ts +1 -0
- package/lib/publish/base-process-publish-assortment-callback.spec.js +62 -0
- package/lib/publish/base-process-publish-assortment.d.ts +24 -0
- package/lib/publish/base-process-publish-assortment.js +234 -132
- package/lib/publish/base-process-publish-assortment.spec.js +188 -0
- package/lib/util/error-response-object.js +2 -0
- package/lib/util/event-short-message-status.js +1 -1
- package/lib/util/flexplm-connect.d.ts +4 -0
- package/lib/util/flexplm-connect.js +210 -145
- package/lib/util/flexplm-connect.telemetry.spec.d.ts +1 -0
- package/lib/util/flexplm-connect.telemetry.spec.js +212 -0
- package/lib/util/logger-config.js +1 -2
- package/lib/util/telemetry-attributes.d.ts +68 -0
- package/lib/util/telemetry-attributes.js +73 -0
- package/lib/util/telemetry-utils.d.ts +3 -0
- package/lib/util/telemetry-utils.js +10 -0
- package/lib/util/thumbnail-util.d.ts +2 -1
- package/lib/util/thumbnail-util.js +35 -7
- package/lib/util/thumbnail-util.spec.js +42 -0
- package/package.json +10 -3
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FlexPLMConnect = void 0;
|
|
4
4
|
const app_framework_1 = require("@contrail/app-framework");
|
|
5
|
+
const telemetry_1 = require("@contrail/telemetry");
|
|
6
|
+
const telemetry_attributes_1 = require("./telemetry-attributes");
|
|
7
|
+
const telemetry_utils_1 = require("./telemetry-utils");
|
|
8
|
+
// Telemetry: only the path is recorded, never the host or the query string (it can carry file paths).
|
|
9
|
+
const toUrlPath = (path) => (path || '').split('?')[0];
|
|
10
|
+
const MAX_LOGGED_RESPONSE_BODY = 2048;
|
|
11
|
+
/** Reads the response body for an error log; never throws (the body may already be consumed). */
|
|
12
|
+
const readBodyForLog = async (response) => {
|
|
13
|
+
try {
|
|
14
|
+
const text = await response.text();
|
|
15
|
+
return text.length > MAX_LOGGED_RESPONSE_BODY ? text.substring(0, MAX_LOGGED_RESPONSE_BODY) + '...(truncated)' : text;
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
return '(response body unavailable)';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const logFlexPLMError = async (message, response, urlPath) => {
|
|
22
|
+
telemetry_1.logger.error({ flexplm: { status: response.status, url_path: urlPath, response_body: await readBodyForLog(response) } }, message);
|
|
23
|
+
};
|
|
5
24
|
// Module-level on purpose: a new FlexPLMConnect is constructed for every event. Declaring for Session reusing use case.
|
|
6
25
|
const sessionCache = new Map();
|
|
7
26
|
// Upper bound so users/option combinations that are never requested again cannot accumulate cookies forever.
|
|
@@ -98,6 +117,14 @@ class FlexPLMConnect {
|
|
|
98
117
|
}
|
|
99
118
|
})();
|
|
100
119
|
}
|
|
120
|
+
/** Marks the active span when this request is about to wait for another request's cold acquisition.
|
|
121
|
+
* Synchronous on purpose: it must not add an await to the wait loops (see waitForInFlightAcquisition).
|
|
122
|
+
*/
|
|
123
|
+
recordWaitForInFlightAcquisition() {
|
|
124
|
+
if (inFlightAcquisitions.has(this.getSessionKey())) {
|
|
125
|
+
(0, telemetry_utils_1.setActiveSpanAttributes)({ [telemetry_attributes_1.ATTR_FLEXPLM_SESSION_WAITED]: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
101
128
|
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
102
129
|
async runColdAcquisition(acquire) {
|
|
103
130
|
const key = this.getSessionKey();
|
|
@@ -223,8 +250,11 @@ class FlexPLMConnect {
|
|
|
223
250
|
// rejectedCookie: the cookie that was rejected by the caller, so a refresh already done by a concurrent event is not thrown away
|
|
224
251
|
async getCSRF(getNewSession = false, rejectedCookie) {
|
|
225
252
|
// Added for Session reuse - ET-390 - Starts
|
|
226
|
-
if (!this.sessionOptions.enabled)
|
|
253
|
+
if (!this.sessionOptions.enabled) {
|
|
254
|
+
(0, telemetry_utils_1.setActiveSpanAttributes)({ [telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]: 'disabled' });
|
|
227
255
|
return this.fetchCSRF(undefined);
|
|
256
|
+
}
|
|
257
|
+
this.recordWaitForInFlightAcquisition();
|
|
228
258
|
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
229
259
|
await wait;
|
|
230
260
|
if (getNewSession) {
|
|
@@ -233,6 +263,8 @@ class FlexPLMConnect {
|
|
|
233
263
|
this.clearSession();
|
|
234
264
|
}
|
|
235
265
|
const cached = this.getCachedSession();
|
|
266
|
+
// Set on the caller's span; a retry below overwrites it with the session that was finally used.
|
|
267
|
+
(0, telemetry_utils_1.setActiveSpanAttributes)({ [telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]: cached ? 'cached' : 'new' });
|
|
236
268
|
if (cached) {
|
|
237
269
|
try {
|
|
238
270
|
return await this.fetchCSRF(cached);
|
|
@@ -254,50 +286,62 @@ class FlexPLMConnect {
|
|
|
254
286
|
const csrfEndpoint = this.config.csrfEndpoint;
|
|
255
287
|
const csrfURL = this.config.apiHost + urlContext + csrfEndpoint;
|
|
256
288
|
console.info('csrfURL: ' + csrfURL);
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
289
|
+
const urlPath = toUrlPath(urlContext + csrfEndpoint);
|
|
290
|
+
const result = await (0, telemetry_1.traceSection)('fetch_flexplm_csrf', {
|
|
291
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'csrf',
|
|
292
|
+
[telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'GET',
|
|
293
|
+
[telemetry_attributes_1.ATTR_URL_PATH]: urlPath,
|
|
294
|
+
}, async (span) => {
|
|
295
|
+
// getRequestOptions will be called with additional sentCookie parameter
|
|
296
|
+
const csrfOptions = this.getRequestOptions('GET', sentCookie);
|
|
297
|
+
const response = await fetch(csrfURL, csrfOptions);
|
|
298
|
+
span.setAttribute(telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
299
|
+
// Added for Session reuse - ET-390 - Starts
|
|
300
|
+
if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
301
|
+
console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
|
|
302
|
+
// Expected and retried by getCSRF, so it is an event and the span stays OK.
|
|
303
|
+
span.addEvent('flexplm_session_rejected');
|
|
304
|
+
await this.discardBody(response);
|
|
305
|
+
return { sessionRejected: true };
|
|
306
|
+
}
|
|
307
|
+
// Added for Session reuse - ET-390 - Ends
|
|
308
|
+
if (response.status >= 300) {
|
|
309
|
+
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
310
|
+
await logFlexPLMError(message, response, urlPath);
|
|
311
|
+
const e = new Error(message);
|
|
312
|
+
e['httpResponseStatus'] = response.status;
|
|
313
|
+
throw e;
|
|
314
|
+
}
|
|
315
|
+
// Added for Session reuse - ET-390 - Starts
|
|
316
|
+
// If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
|
|
317
|
+
let sessionCookie = sentCookie;
|
|
318
|
+
if (this.sessionOptions.enabled) {
|
|
319
|
+
sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
|
|
320
|
+
if (sessionCookie)
|
|
321
|
+
this.storeSession(sessionCookie);
|
|
322
|
+
}
|
|
323
|
+
// Added for Session reuse - ET-390 - Ends
|
|
324
|
+
try {
|
|
325
|
+
const body = await response.json();
|
|
326
|
+
const nonce_key = body.items[0].attributes.nonce_key;
|
|
327
|
+
const nonce = body.items[0].attributes.nonce;
|
|
328
|
+
// ET-390 - adding sessionCookie to the return
|
|
329
|
+
return {
|
|
330
|
+
nonce_key,
|
|
331
|
+
nonce,
|
|
332
|
+
sessionCookie,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
const message = 'Error connecting to FlexPLM: ' + e.message;
|
|
337
|
+
await logFlexPLMError(message, response, urlPath);
|
|
338
|
+
throw new Error(message);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// Thrown outside the span so the expected rejection is not recorded as a span error.
|
|
342
|
+
if ('sessionRejected' in result)
|
|
264
343
|
throw new SessionRejectedError();
|
|
265
|
-
|
|
266
|
-
// Added for Session reuse - ET-390 - Ends
|
|
267
|
-
if (response.status >= 300) {
|
|
268
|
-
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
269
|
-
console.error(message);
|
|
270
|
-
console.error(await response.text());
|
|
271
|
-
const e = new Error(message);
|
|
272
|
-
e['httpResponseStatus'] = response.status;
|
|
273
|
-
throw e;
|
|
274
|
-
}
|
|
275
|
-
// Added for Session reuse - ET-390 - Starts
|
|
276
|
-
// If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
|
|
277
|
-
let sessionCookie = sentCookie;
|
|
278
|
-
if (this.sessionOptions.enabled) {
|
|
279
|
-
sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
|
|
280
|
-
if (sessionCookie)
|
|
281
|
-
this.storeSession(sessionCookie);
|
|
282
|
-
}
|
|
283
|
-
// Added for Session reuse - ET-390 - Ends
|
|
284
|
-
try {
|
|
285
|
-
const body = await response.json();
|
|
286
|
-
const nonce_key = body.items[0].attributes.nonce_key;
|
|
287
|
-
const nonce = body.items[0].attributes.nonce;
|
|
288
|
-
// ET-390 - adding sessionCookie to the return
|
|
289
|
-
return {
|
|
290
|
-
nonce_key,
|
|
291
|
-
nonce,
|
|
292
|
-
sessionCookie,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
catch (e) {
|
|
296
|
-
const message = 'Error connecting to FlexPLM: ' + e.message;
|
|
297
|
-
console.error(message);
|
|
298
|
-
console.error(await response.text());
|
|
299
|
-
throw new Error(message);
|
|
300
|
-
}
|
|
344
|
+
return result;
|
|
301
345
|
}
|
|
302
346
|
async sendRequest(csrf, payload) {
|
|
303
347
|
if (this.payloadSendAsArray && !Array.isArray(payload)) {
|
|
@@ -325,63 +369,73 @@ class FlexPLMConnect {
|
|
|
325
369
|
return eventResponse;
|
|
326
370
|
}
|
|
327
371
|
async processRequest(payload) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
372
|
+
const urlPath = toUrlPath(this.config.urlContext + '/servlet/rest' + this.vibeEventEndpoint);
|
|
373
|
+
return (0, telemetry_1.traceSection)('send_to_flexplm', {
|
|
374
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'send',
|
|
375
|
+
[telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'POST',
|
|
376
|
+
[telemetry_attributes_1.ATTR_URL_PATH]: urlPath,
|
|
377
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PAYLOAD_COUNT]: Array.isArray(payload) ? payload.length : (payload ? 1 : 0),
|
|
378
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
|
|
379
|
+
}, async (span) => {
|
|
380
|
+
if (!payload) {
|
|
381
|
+
const message = 'No payload to send to FlexPLM';
|
|
382
|
+
console.error(message);
|
|
383
|
+
throw new Error(message);
|
|
384
|
+
}
|
|
385
|
+
if (app_framework_1.Logger.isInfoOn()) {
|
|
386
|
+
console.info('payload: ' + JSON.stringify(payload));
|
|
387
|
+
}
|
|
388
|
+
let csrf = await this.getCSRF();
|
|
389
|
+
if (!csrf) {
|
|
390
|
+
const message = 'Failed to get CSRF nonce';
|
|
391
|
+
console.error(message);
|
|
392
|
+
throw new Error(message);
|
|
393
|
+
}
|
|
394
|
+
/// Added for Session reuse - ET-390 - Starts
|
|
395
|
+
// const response = await this.sendRequest(csrf, payload);
|
|
396
|
+
let response = await this.sendRequest(csrf, payload);
|
|
397
|
+
if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
398
|
+
console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
|
|
399
|
+
span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED, true);
|
|
400
|
+
await this.discardBody(response);
|
|
401
|
+
csrf = await this.getCSRF(true, csrf.sessionCookie);
|
|
402
|
+
response = await this.sendRequest(csrf, payload);
|
|
403
|
+
}
|
|
404
|
+
// Added for Session reuse - ET-390 - Ends
|
|
405
|
+
const status = response.status;
|
|
406
|
+
span.setAttribute(telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE, status);
|
|
407
|
+
if (status >= 300) {
|
|
408
|
+
// traceSection records the thrown error and sets the span status to ERROR.
|
|
409
|
+
const message = 'Error sending data to FlexPLM:status: ' + response.status;
|
|
410
|
+
await logFlexPLMError(message, response, urlPath);
|
|
411
|
+
const e = new Error(message);
|
|
412
|
+
e['httpResponseStatus'] = status;
|
|
413
|
+
throw e;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
const res = {
|
|
417
|
+
status,
|
|
418
|
+
};
|
|
419
|
+
if (![204, 205].includes(status)) {
|
|
420
|
+
try {
|
|
421
|
+
const data = await response.json();
|
|
422
|
+
res.data = data;
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
console.error('Error getting response body. Setting {} body: ' + e);
|
|
426
|
+
res.data = {};
|
|
427
|
+
}
|
|
373
428
|
}
|
|
429
|
+
console.log('eventResponse.status: ' + status);
|
|
430
|
+
// console.log('eventBody: ', JSON.stringify(data));
|
|
431
|
+
return res;
|
|
374
432
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
console.error(message);
|
|
382
|
-
console.error(await response.text());
|
|
383
|
-
throw new Error(message);
|
|
384
|
-
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
const message = 'Error getting json data from FlexPLM: ' + e.message;
|
|
435
|
+
await logFlexPLMError(message, response, urlPath);
|
|
436
|
+
throw new Error(message);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
385
439
|
}
|
|
386
440
|
async sendToFlexPLM(payload) {
|
|
387
441
|
return await this.processRequest(payload);
|
|
@@ -401,54 +455,65 @@ class FlexPLMConnect {
|
|
|
401
455
|
const urlContext = includeUrlContext ? this.config.urlContext : '';
|
|
402
456
|
const path = urlPath || ('/servlet/rest' + this.vibeEventEndpoint);
|
|
403
457
|
const vibeEventsURL = this.config.apiHost + urlContext + path;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const cookie = this.extractSessionCookie(res) ?? sent;
|
|
417
|
-
if (cookie)
|
|
418
|
-
this.storeSession(cookie);
|
|
458
|
+
const spanUrlPath = toUrlPath(urlContext + path);
|
|
459
|
+
return (0, telemetry_1.traceSection)('get_from_flexplm', {
|
|
460
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'get',
|
|
461
|
+
[telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'GET',
|
|
462
|
+
[telemetry_attributes_1.ATTR_URL_PATH]: spanUrlPath,
|
|
463
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
|
|
464
|
+
}, async (span) => {
|
|
465
|
+
// Added for Session reuse - ET-390 - Starts
|
|
466
|
+
const attempt = async () => {
|
|
467
|
+
if (!this.sessionOptions.enabled) {
|
|
468
|
+
span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE, 'disabled');
|
|
469
|
+
return { response: await fetch(vibeEventsURL, this.getRequestOptions('GET')), sent: undefined };
|
|
419
470
|
}
|
|
420
|
-
|
|
471
|
+
this.recordWaitForInFlightAcquisition();
|
|
472
|
+
for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
|
|
473
|
+
await wait;
|
|
474
|
+
const sent = this.getCachedSession();
|
|
475
|
+
span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE, sent ? 'cached' : 'new');
|
|
476
|
+
const call = async () => {
|
|
477
|
+
const res = await fetch(vibeEventsURL, this.getRequestOptions('GET', sent));
|
|
478
|
+
// A rejected cookie must not be stored again; only keep a cookie that was accepted or newly issued.
|
|
479
|
+
if (!(sent && this.sessionOptions.retryStatuses.includes(res.status))) {
|
|
480
|
+
const cookie = this.extractSessionCookie(res) ?? sent;
|
|
481
|
+
if (cookie)
|
|
482
|
+
this.storeSession(cookie);
|
|
483
|
+
}
|
|
484
|
+
return { response: res, sent };
|
|
485
|
+
};
|
|
486
|
+
return sent ? call() : this.runColdAcquisition(call);
|
|
421
487
|
};
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
488
|
+
const first = await attempt();
|
|
489
|
+
let response = first.response;
|
|
490
|
+
if (first.sent && this.sessionOptions.retryStatuses.includes(response.status)) {
|
|
491
|
+
span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED, true);
|
|
492
|
+
await this.discardBody(response);
|
|
493
|
+
if (this.getCachedSession() === first.sent)
|
|
494
|
+
this.clearSession();
|
|
495
|
+
response = (await attempt()).response;
|
|
496
|
+
}
|
|
497
|
+
// Added for Session reuse - ET-390 - Ends
|
|
498
|
+
span.setAttribute(telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
499
|
+
if (response.status >= 300) {
|
|
500
|
+
const message = 'Error connecting to FlexPLM:status: ' + response.status;
|
|
501
|
+
await logFlexPLMError(message, response, spanUrlPath);
|
|
502
|
+
throw new Error(message);
|
|
503
|
+
}
|
|
504
|
+
if (returnFullResponse) {
|
|
505
|
+
return response;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const data = await response.json();
|
|
509
|
+
return data;
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
const message = 'Error getting json data from FlexPLM: ' + e.message;
|
|
513
|
+
await logFlexPLMError(message, response, spanUrlPath);
|
|
514
|
+
throw new Error(message);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
452
517
|
}
|
|
453
518
|
}
|
|
454
519
|
exports.FlexPLMConnect = FlexPLMConnect;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_1 = require("@opentelemetry/api");
|
|
4
|
+
const telemetry_1 = require("@contrail/telemetry");
|
|
5
|
+
const flexplm_connect_1 = require("./flexplm-connect");
|
|
6
|
+
const telemetry_test_utils_1 = require("../__tests__/telemetry-test-utils");
|
|
7
|
+
const telemetry_attributes_1 = require("./telemetry-attributes");
|
|
8
|
+
// Same mocks as flexplm-connect.spec.ts
|
|
9
|
+
const csrfBody = { items: [{ attributes: { nonce_key: 'CSRF_NONCE', nonce: 'secret-nonce-value' } }] };
|
|
10
|
+
const resp = (status, setCookie = [], json = csrfBody) => ({
|
|
11
|
+
status,
|
|
12
|
+
json: jest.fn().mockResolvedValue(json),
|
|
13
|
+
text: jest.fn().mockResolvedValue('error text'),
|
|
14
|
+
headers: { getSetCookie: () => setCookie, get: () => null },
|
|
15
|
+
});
|
|
16
|
+
const createConfig = (overrides) => ({
|
|
17
|
+
apiHost: 'https://flexplm.example.com',
|
|
18
|
+
urlContext: '/FlexPLM',
|
|
19
|
+
vibeEventEndpoint: '/api/events',
|
|
20
|
+
csrfEndpoint: '/csrf',
|
|
21
|
+
plmEnviornment: 'test',
|
|
22
|
+
userName: () => 'user',
|
|
23
|
+
password: () => 'super-secret-password',
|
|
24
|
+
itemPreDevelopmentLifecycleStages: [],
|
|
25
|
+
payloadDefaultAsArray: true,
|
|
26
|
+
useDistinctRestEndPointForImages: false,
|
|
27
|
+
...overrides,
|
|
28
|
+
});
|
|
29
|
+
// The session cache is module level, so each test uses its own user to start cold.
|
|
30
|
+
let userCounter = 0;
|
|
31
|
+
const sessionConfig = () => {
|
|
32
|
+
const user = 'telemetry-user-' + (++userCounter);
|
|
33
|
+
return createConfig({ userName: () => user, flexplmConnect: { sessionReuse: { enabled: true } } });
|
|
34
|
+
};
|
|
35
|
+
const fetchMock = () => global.fetch;
|
|
36
|
+
const payload = { eventType: 'PERSIST', objectClass: 'LCSProduct', data: {} };
|
|
37
|
+
const allRecordedValues = (spans) => spans.flatMap(span => [
|
|
38
|
+
...Object.values(span.attributes).map(String),
|
|
39
|
+
...span.events.flatMap(event => [event.name, ...Object.values(event.attributes || {}).map(String)]),
|
|
40
|
+
]);
|
|
41
|
+
describe('FlexPLMConnect telemetry', () => {
|
|
42
|
+
let exporter;
|
|
43
|
+
const globalFetch = global.fetch;
|
|
44
|
+
beforeAll(() => {
|
|
45
|
+
exporter = (0, telemetry_test_utils_1.registerInMemoryTracing)();
|
|
46
|
+
});
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
exporter.reset();
|
|
49
|
+
global.fetch = jest.fn();
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
jest.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
afterAll(() => {
|
|
55
|
+
global.fetch = globalFetch;
|
|
56
|
+
});
|
|
57
|
+
it('should record a send_to_flexplm span with http.response.status_code 200', async () => {
|
|
58
|
+
fetchMock()
|
|
59
|
+
.mockResolvedValueOnce(resp(200))
|
|
60
|
+
.mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
61
|
+
await new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload);
|
|
62
|
+
const send = (0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm');
|
|
63
|
+
expect(send.attributes).toEqual(expect.objectContaining({
|
|
64
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'send',
|
|
65
|
+
[telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'POST',
|
|
66
|
+
[telemetry_attributes_1.ATTR_URL_PATH]: '/FlexPLM/servlet/rest/api/events',
|
|
67
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PAYLOAD_COUNT]: 1,
|
|
68
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
|
|
69
|
+
[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
|
|
70
|
+
}));
|
|
71
|
+
expect(send.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
|
|
72
|
+
const csrf = (0, telemetry_test_utils_1.findSpan)(exporter, 'fetch_flexplm_csrf');
|
|
73
|
+
expect(csrf.attributes).toEqual(expect.objectContaining({
|
|
74
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'csrf',
|
|
75
|
+
[telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'GET',
|
|
76
|
+
[telemetry_attributes_1.ATTR_URL_PATH]: '/FlexPLM/csrf',
|
|
77
|
+
[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
|
|
78
|
+
}));
|
|
79
|
+
expect((0, telemetry_test_utils_1.isChildOf)(csrf, send)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('should set retried true when the cached session is rejected', async () => {
|
|
82
|
+
fetchMock()
|
|
83
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=first; Path=/']))
|
|
84
|
+
.mockResolvedValueOnce(resp(403))
|
|
85
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=second; Path=/']))
|
|
86
|
+
.mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
87
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig()).sendToFlexPLM(payload);
|
|
88
|
+
const send = (0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm');
|
|
89
|
+
expect(send.attributes[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]).toBe(true);
|
|
90
|
+
expect(send.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(200);
|
|
91
|
+
expect(send.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
|
|
92
|
+
});
|
|
93
|
+
it('should record a rejected cached session on the csrf call as an event, not an error', async () => {
|
|
94
|
+
const config = sessionConfig();
|
|
95
|
+
// Warm the cache with a cookie.
|
|
96
|
+
fetchMock()
|
|
97
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=cached; Path=/']))
|
|
98
|
+
.mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
99
|
+
await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
|
|
100
|
+
exporter.reset();
|
|
101
|
+
fetchMock()
|
|
102
|
+
.mockResolvedValueOnce(resp(401))
|
|
103
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=fresh; Path=/']))
|
|
104
|
+
.mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
105
|
+
await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
|
|
106
|
+
const [rejected, fresh] = (0, telemetry_test_utils_1.findSpans)(exporter, 'fetch_flexplm_csrf');
|
|
107
|
+
expect(rejected.events.map(event => event.name)).toEqual(['flexplm_session_rejected']);
|
|
108
|
+
expect(rejected.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
|
|
109
|
+
expect(rejected.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(401);
|
|
110
|
+
expect(fresh.events).toEqual([]);
|
|
111
|
+
expect(fresh.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(200);
|
|
112
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm').status.code).not.toBe(api_1.SpanStatusCode.ERROR);
|
|
113
|
+
});
|
|
114
|
+
it('should set the span status to ERROR when FlexPLM returns 500', async () => {
|
|
115
|
+
const errorLog = jest.spyOn(telemetry_1.logger, 'error').mockImplementation(() => undefined);
|
|
116
|
+
fetchMock()
|
|
117
|
+
.mockResolvedValueOnce(resp(200))
|
|
118
|
+
.mockResolvedValueOnce(resp(500));
|
|
119
|
+
await expect(new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload))
|
|
120
|
+
.rejects.toThrow('Error sending data to FlexPLM:status: 500');
|
|
121
|
+
const send = (0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm');
|
|
122
|
+
expect(send.status.code).toBe(api_1.SpanStatusCode.ERROR);
|
|
123
|
+
expect(send.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(500);
|
|
124
|
+
expect(send.events.map(event => event.name)).toContain('exception');
|
|
125
|
+
expect(errorLog).toHaveBeenCalledWith({ flexplm: { status: 500, url_path: '/FlexPLM/servlet/rest/api/events', response_body: 'error text' } }, 'Error sending data to FlexPLM:status: 500');
|
|
126
|
+
});
|
|
127
|
+
it('should truncate the logged response body', async () => {
|
|
128
|
+
const errorLog = jest.spyOn(telemetry_1.logger, 'error').mockImplementation(() => undefined);
|
|
129
|
+
const failed = resp(500);
|
|
130
|
+
failed.text.mockResolvedValue('x'.repeat(5000));
|
|
131
|
+
fetchMock()
|
|
132
|
+
.mockResolvedValueOnce(resp(200))
|
|
133
|
+
.mockResolvedValueOnce(failed);
|
|
134
|
+
await expect(new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload)).rejects.toThrow();
|
|
135
|
+
const logged = errorLog.mock.calls[0][0].flexplm.response_body;
|
|
136
|
+
expect(logged.length).toBeLessThan(2100);
|
|
137
|
+
expect(logged.endsWith('...(truncated)')).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
it('should record only the path of a getRequest, without the host or query string', async () => {
|
|
140
|
+
fetchMock().mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
141
|
+
await new flexplm_connect_1.FlexPLMConnect(createConfig()).getRequest({ urlPath: '/servlet/rest/rfa/vibeiq/image?path=%2Fsecret%2Ffile.png' });
|
|
142
|
+
const get = (0, telemetry_test_utils_1.findSpan)(exporter, 'get_from_flexplm');
|
|
143
|
+
expect(get.attributes).toEqual(expect.objectContaining({
|
|
144
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'get',
|
|
145
|
+
[telemetry_attributes_1.ATTR_URL_PATH]: '/FlexPLM/servlet/rest/rfa/vibeiq/image',
|
|
146
|
+
[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
|
|
147
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
|
|
148
|
+
}));
|
|
149
|
+
});
|
|
150
|
+
it('should record the session source: disabled, then new, then cached', async () => {
|
|
151
|
+
fetchMock().mockResolvedValueOnce(resp(200)).mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
152
|
+
await new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload);
|
|
153
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm').attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('disabled');
|
|
154
|
+
const config = sessionConfig();
|
|
155
|
+
for (let i = 0; i < 2; i++) {
|
|
156
|
+
fetchMock().mockResolvedValueOnce(resp(200, ['JSESSIONID=reused; Path=/'])).mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
157
|
+
}
|
|
158
|
+
exporter.reset();
|
|
159
|
+
await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
|
|
160
|
+
await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
|
|
161
|
+
const [first, second] = (0, telemetry_test_utils_1.findSpans)(exporter, 'send_to_flexplm');
|
|
162
|
+
expect(first.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('new');
|
|
163
|
+
expect(second.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('cached');
|
|
164
|
+
expect(first.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_WAITED]).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
it('should record session.source new after a rejected cached session is replaced', async () => {
|
|
167
|
+
const config = sessionConfig();
|
|
168
|
+
fetchMock().mockResolvedValueOnce(resp(200, ['JSESSIONID=cached; Path=/'])).mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
169
|
+
await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
|
|
170
|
+
exporter.reset();
|
|
171
|
+
fetchMock()
|
|
172
|
+
.mockResolvedValueOnce(resp(401))
|
|
173
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=fresh; Path=/']))
|
|
174
|
+
.mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
175
|
+
await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
|
|
176
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm').attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('new');
|
|
177
|
+
});
|
|
178
|
+
it('should record session.waited when a request waits for another request\'s session acquisition', async () => {
|
|
179
|
+
const config = sessionConfig();
|
|
180
|
+
let release;
|
|
181
|
+
const gate = new Promise(r => { release = r; });
|
|
182
|
+
fetchMock().mockImplementation(async (_url, opts) => {
|
|
183
|
+
if (opts.method === 'GET' && !opts.headers.Cookie) {
|
|
184
|
+
await gate;
|
|
185
|
+
return resp(200, ['JSESSIONID=shared; Path=/']);
|
|
186
|
+
}
|
|
187
|
+
return opts.method === 'GET' ? resp(200) : resp(200, [], { ok: true });
|
|
188
|
+
});
|
|
189
|
+
const all = Promise.all([1, 2].map(() => new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload)));
|
|
190
|
+
await new Promise(r => setImmediate(r));
|
|
191
|
+
release();
|
|
192
|
+
await all;
|
|
193
|
+
const spans = (0, telemetry_test_utils_1.findSpans)(exporter, 'send_to_flexplm');
|
|
194
|
+
expect(spans.map(span => span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_WAITED]).sort()).toEqual([true, undefined]);
|
|
195
|
+
expect(spans.map(span => span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).sort()).toEqual(['cached', 'new']);
|
|
196
|
+
});
|
|
197
|
+
it('should not record Authorization, Cookie or nonce values on any span', async () => {
|
|
198
|
+
fetchMock()
|
|
199
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=cookie-value-1; Path=/']))
|
|
200
|
+
.mockResolvedValueOnce(resp(403))
|
|
201
|
+
.mockResolvedValueOnce(resp(200, ['JSESSIONID=cookie-value-2; Path=/']))
|
|
202
|
+
.mockResolvedValueOnce(resp(200, [], { ok: true }));
|
|
203
|
+
await new flexplm_connect_1.FlexPLMConnect(sessionConfig()).sendToFlexPLM(payload);
|
|
204
|
+
const spans = exporter.getFinishedSpans();
|
|
205
|
+
expect(spans.length).toBeGreaterThan(0);
|
|
206
|
+
const recorded = allRecordedValues(spans).join('\n');
|
|
207
|
+
const authorization = Buffer.from('telemetry-user-' + userCounter + ':super-secret-password').toString('base64');
|
|
208
|
+
for (const secret of ['super-secret-password', authorization, 'cookie-value-1', 'cookie-value-2', 'secret-nonce-value', 'CSRF_NONCE', 'flexplm.example.com']) {
|
|
209
|
+
expect(recorded).not.toContain(secret);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setLoggerConfig =
|
|
3
|
+
exports.setLoggerConfig = setLoggerConfig;
|
|
4
4
|
const app_framework_1 = require("@contrail/app-framework");
|
|
5
5
|
async function setLoggerConfig(appConfig) {
|
|
6
6
|
let logLevel = app_framework_1.LogLevel.INFO;
|
|
@@ -24,4 +24,3 @@ async function setLoggerConfig(appConfig) {
|
|
|
24
24
|
}
|
|
25
25
|
app_framework_1.Logger.setConfig({ logLevel: logLevel });
|
|
26
26
|
}
|
|
27
|
-
exports.setLoggerConfig = setLoggerConfig;
|