@contrail/flexplm 1.8.1-alpha.85853aa → 2.0.0-alpha.9af4b96
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 +212 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/interfaces/interfaces.d.ts +1 -1
- 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 +243 -132
- package/lib/publish/base-process-publish-assortment.spec.js +188 -0
- package/lib/util/config-defaults.js +2 -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
|
@@ -6,6 +6,11 @@ const data_converter_1 = require("../util/data-converter");
|
|
|
6
6
|
const base_entity_processor_1 = require("./base-entity-processor");
|
|
7
7
|
const type_conversion_utils_1 = require("../util/type-conversion-utils");
|
|
8
8
|
const thumbnail_util_1 = require("../util/thumbnail-util");
|
|
9
|
+
const telemetry_test_utils_1 = require("../__tests__/telemetry-test-utils");
|
|
10
|
+
const telemetry_attributes_1 = require("../util/telemetry-attributes");
|
|
11
|
+
const event_short_message_status_1 = require("../util/event-short-message-status");
|
|
12
|
+
const telemetry_1 = require("@contrail/telemetry");
|
|
13
|
+
const api_1 = require("@opentelemetry/api");
|
|
9
14
|
const mockRootType = {
|
|
10
15
|
typeProperties: [
|
|
11
16
|
{ slug: 'rootText' }
|
|
@@ -663,4 +668,211 @@ describe('BaseEntityProcessor', () => {
|
|
|
663
668
|
expect(mockEntitiesGet).toHaveBeenCalledTimes(1);
|
|
664
669
|
});
|
|
665
670
|
});
|
|
671
|
+
describe('telemetry', () => {
|
|
672
|
+
const config = {
|
|
673
|
+
apiHost: 'https://flexplm.example.com',
|
|
674
|
+
urlContext: '/FlexPLM',
|
|
675
|
+
vibeEventEndpoint: '/api/events',
|
|
676
|
+
csrfEndpoint: '/csrf',
|
|
677
|
+
plmEnviornment: 'test',
|
|
678
|
+
userName: () => 'user',
|
|
679
|
+
password: () => 'pass',
|
|
680
|
+
payloadDefaultAsArray: true,
|
|
681
|
+
};
|
|
682
|
+
const csrfResponse = () => ({ status: 200, json: jest.fn().mockResolvedValue({ items: [{ attributes: { nonce_key: 'K', nonce: 'N' } }] }) });
|
|
683
|
+
const sendResponse = (status = 200) => ({ status, json: jest.fn().mockResolvedValue({}), text: jest.fn().mockResolvedValue('error text') });
|
|
684
|
+
const outboundEvent = { entityType: 'item', eventType: 'update', id: 'item-1', newData: { flexPLMObjectClass: 'LCSProduct' } };
|
|
685
|
+
const inboundEvent = { objectClass: 'LCSProduct', federatedId: 'fed-1', data: {}, eventType: 'PERSIST' };
|
|
686
|
+
const globalFetch = global.fetch;
|
|
687
|
+
let exporter;
|
|
688
|
+
let btep;
|
|
689
|
+
let infoLog;
|
|
690
|
+
let warnLog;
|
|
691
|
+
let errorLog;
|
|
692
|
+
beforeAll(() => {
|
|
693
|
+
exporter = (0, telemetry_test_utils_1.registerInMemoryTracing)();
|
|
694
|
+
});
|
|
695
|
+
beforeEach(() => {
|
|
696
|
+
exporter.reset();
|
|
697
|
+
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
698
|
+
btep = new TestBaseEntityProcessor(config, new data_converter_1.DataConverter(config, mapFileUtil), mapFileUtil, 'item');
|
|
699
|
+
jest.spyOn(btep, 'getOutgoingUpsertPayload').mockResolvedValue({ objectClass: 'LCSProduct', data: {} });
|
|
700
|
+
infoLog = jest.spyOn(telemetry_1.logger, 'info').mockImplementation(() => undefined);
|
|
701
|
+
warnLog = jest.spyOn(telemetry_1.logger, 'warn').mockImplementation(() => undefined);
|
|
702
|
+
errorLog = jest.spyOn(telemetry_1.logger, 'error').mockImplementation(() => undefined);
|
|
703
|
+
});
|
|
704
|
+
afterEach(() => {
|
|
705
|
+
jest.restoreAllMocks();
|
|
706
|
+
});
|
|
707
|
+
afterAll(() => {
|
|
708
|
+
global.fetch = globalFetch;
|
|
709
|
+
});
|
|
710
|
+
it('should record send_to_flexplm as a child of process_outbound_entity', async () => {
|
|
711
|
+
global.fetch = jest.fn().mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(sendResponse());
|
|
712
|
+
await btep.outbound(outboundEvent);
|
|
713
|
+
const outbound = (0, telemetry_test_utils_1.findSpan)(exporter, 'process_outbound_entity');
|
|
714
|
+
const upsert = (0, telemetry_test_utils_1.findSpan)(exporter, 'upsert_entity_in_flexplm');
|
|
715
|
+
const send = (0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm');
|
|
716
|
+
expect(outbound.attributes[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_TYPE]).toBe('item');
|
|
717
|
+
expect(outbound.attributes[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_ID]).toBe('item-1');
|
|
718
|
+
expect(upsert.attributes[telemetry_attributes_1.ATTR_FLEXPLM_EVENT_OBJECT_CLASS]).toBe('LCSProduct');
|
|
719
|
+
expect((0, telemetry_test_utils_1.isChildOf)(upsert, outbound)).toBe(true);
|
|
720
|
+
expect((0, telemetry_test_utils_1.isChildOf)(send, upsert)).toBe(true);
|
|
721
|
+
});
|
|
722
|
+
it('should record a successful outbound status on the span and in one info log', async () => {
|
|
723
|
+
global.fetch = jest.fn().mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(sendResponse());
|
|
724
|
+
await btep.outbound(outboundEvent);
|
|
725
|
+
const upsert = (0, telemetry_test_utils_1.findSpan)(exporter, 'upsert_entity_in_flexplm');
|
|
726
|
+
expect(upsert.attributes).toEqual(expect.objectContaining({
|
|
727
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]: event_short_message_status_1.EventShortMessageStatus.SUCCESS,
|
|
728
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_ID]: 'item-1',
|
|
729
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_UPDATED_FROM_RESPONSE]: false,
|
|
730
|
+
}));
|
|
731
|
+
expect(infoLog).toHaveBeenCalledTimes(1);
|
|
732
|
+
const flexplm = {
|
|
733
|
+
direction: 'outbound', status: event_short_message_status_1.EventShortMessageStatus.SUCCESS, entity_type: 'item', entity_id: 'item-1',
|
|
734
|
+
object_class: 'LCSProduct', http_status: 200, updated_from_response: false,
|
|
735
|
+
};
|
|
736
|
+
expect(infoLog).toHaveBeenCalledWith({ flexplm }, 'FlexPLM outbound entity sent: %j', flexplm);
|
|
737
|
+
});
|
|
738
|
+
it('should log an outbound failure with the error and rethrow it', async () => {
|
|
739
|
+
global.fetch = jest.fn().mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(sendResponse(500));
|
|
740
|
+
await expect(btep.outbound(outboundEvent)).rejects.toThrow('Error sending data to FlexPLM:status: 500');
|
|
741
|
+
const upsert = (0, telemetry_test_utils_1.findSpan)(exporter, 'upsert_entity_in_flexplm');
|
|
742
|
+
expect(upsert.status.code).toBe(api_1.SpanStatusCode.ERROR);
|
|
743
|
+
expect(upsert.attributes[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]).toBe(event_short_message_status_1.EventShortMessageStatus.FAILURE);
|
|
744
|
+
expect(errorLog).toHaveBeenCalledWith({
|
|
745
|
+
error: expect.any(Error),
|
|
746
|
+
flexplm: expect.objectContaining({ status: event_short_message_status_1.EventShortMessageStatus.FAILURE, entity_id: 'item-1', object_class: 'LCSProduct', http_status: 500 }),
|
|
747
|
+
}, 'FlexPLM outbound entity failed: %j', expect.any(Object));
|
|
748
|
+
expect(infoLog).not.toHaveBeenCalled();
|
|
749
|
+
});
|
|
750
|
+
it('should record an item FlexPLM rejected with HTTP 200 as a Failure warning and keep the span OK', async () => {
|
|
751
|
+
const rejectedResponse = { status: 200, json: jest.fn().mockResolvedValue({ payload: [{ status: 'FAIL' }] }) };
|
|
752
|
+
global.fetch = jest.fn().mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(rejectedResponse);
|
|
753
|
+
const result = await btep.outbound(outboundEvent);
|
|
754
|
+
expect(result.status).toBe(200);
|
|
755
|
+
const upsert = (0, telemetry_test_utils_1.findSpan)(exporter, 'upsert_entity_in_flexplm');
|
|
756
|
+
expect(upsert.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
|
|
757
|
+
expect(upsert.attributes).toEqual(expect.objectContaining({
|
|
758
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]: event_short_message_status_1.EventShortMessageStatus.FAILURE,
|
|
759
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_RESPONSE_ITEM_STATUS]: 'FAIL',
|
|
760
|
+
}));
|
|
761
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({
|
|
762
|
+
direction: 'outbound', status: event_short_message_status_1.EventShortMessageStatus.FAILURE, entity_id: 'item-1', http_status: 200, item_status: 'FAIL',
|
|
763
|
+
}) }, 'FlexPLM outbound entity rejected: %j', expect.any(Object));
|
|
764
|
+
expect(infoLog).not.toHaveBeenCalled();
|
|
765
|
+
expect(errorLog).not.toHaveBeenCalled();
|
|
766
|
+
});
|
|
767
|
+
it('should record an OK item status as a Success', async () => {
|
|
768
|
+
const okResponse = { status: 200, json: jest.fn().mockResolvedValue({ payload: [{ status: 'OK' }] }) };
|
|
769
|
+
global.fetch = jest.fn().mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(okResponse);
|
|
770
|
+
jest.spyOn(btep, 'getOutboundEntityUpdates').mockResolvedValue(undefined);
|
|
771
|
+
await btep.outbound(outboundEvent);
|
|
772
|
+
const upsert = (0, telemetry_test_utils_1.findSpan)(exporter, 'upsert_entity_in_flexplm');
|
|
773
|
+
expect(upsert.attributes[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]).toBe(event_short_message_status_1.EventShortMessageStatus.SUCCESS);
|
|
774
|
+
expect(upsert.attributes[telemetry_attributes_1.ATTR_FLEXPLM_RESPONSE_ITEM_STATUS]).toBe('OK');
|
|
775
|
+
expect(infoLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({ item_status: 'OK' }) }, 'FlexPLM outbound entity sent: %j', expect.any(Object));
|
|
776
|
+
});
|
|
777
|
+
it('should record an inbound No_Changes outcome on the span and in one info log', async () => {
|
|
778
|
+
jest.spyOn(btep, 'getTransformedData').mockResolvedValue({});
|
|
779
|
+
jest.spyOn(btep, 'getIncomingEntity').mockResolvedValue({ entity: { id: 'existing-1' } });
|
|
780
|
+
jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({});
|
|
781
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'syncInboundImages').mockResolvedValue(false);
|
|
782
|
+
await btep.inbound(inboundEvent);
|
|
783
|
+
const inbound = (0, telemetry_test_utils_1.findSpan)(exporter, 'process_inbound_entity');
|
|
784
|
+
expect(inbound.attributes).toEqual(expect.objectContaining({
|
|
785
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]: event_short_message_status_1.EventShortMessageStatus.SUCCESS,
|
|
786
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_OUTCOME]: event_short_message_status_1.EventShortMessageStatus.NO_CHANGES,
|
|
787
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_ID]: 'existing-1',
|
|
788
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_FEDERATED_ID]: 'fed-1',
|
|
789
|
+
}));
|
|
790
|
+
expect(infoLog).toHaveBeenCalledTimes(1);
|
|
791
|
+
const flexplm = {
|
|
792
|
+
direction: 'inbound', status: event_short_message_status_1.EventShortMessageStatus.SUCCESS, outcome: event_short_message_status_1.EventShortMessageStatus.NO_CHANGES,
|
|
793
|
+
entity_type: 'item', entity_id: 'existing-1', object_class: 'LCSProduct', federated_id: 'fed-1',
|
|
794
|
+
};
|
|
795
|
+
expect(infoLog).toHaveBeenCalledWith({ flexplm }, 'FlexPLM inbound entity processed: %j', flexplm);
|
|
796
|
+
});
|
|
797
|
+
it('should record the created entity id on an inbound create', async () => {
|
|
798
|
+
jest.spyOn(btep, 'getTransformedData').mockResolvedValue({});
|
|
799
|
+
jest.spyOn(btep, 'getIncomingEntity').mockResolvedValue({ entity: null });
|
|
800
|
+
jest.spyOn(btep, 'getCreateEntity').mockResolvedValue({ entity: { name: 'new' } });
|
|
801
|
+
jest.spyOn(btep, 'createEntity').mockResolvedValue({ id: 'created-1' });
|
|
802
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'syncInboundImages').mockResolvedValue(false);
|
|
803
|
+
await btep.inbound(inboundEvent);
|
|
804
|
+
const inbound = (0, telemetry_test_utils_1.findSpan)(exporter, 'process_inbound_entity');
|
|
805
|
+
expect(inbound.attributes[telemetry_attributes_1.ATTR_FLEXPLM_OUTCOME]).toBe(event_short_message_status_1.EventShortMessageStatus.CREATED);
|
|
806
|
+
expect(inbound.attributes[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_ID]).toBe('created-1');
|
|
807
|
+
});
|
|
808
|
+
it('should record the event type on the inbound and outbound spans', async () => {
|
|
809
|
+
jest.spyOn(btep, 'handleIncomingDelete').mockResolvedValue(undefined);
|
|
810
|
+
jest.spyOn(btep, 'handleOutgoingUpsert').mockResolvedValue(undefined);
|
|
811
|
+
await btep.inbound({ ...inboundEvent, eventType: 'DELETE' });
|
|
812
|
+
await btep.outbound(outboundEvent);
|
|
813
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'process_inbound_entity').attributes[telemetry_attributes_1.ATTR_FLEXPLM_EVENT_TYPE]).toBe('DELETE');
|
|
814
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'process_outbound_entity').attributes[telemetry_attributes_1.ATTR_FLEXPLM_EVENT_TYPE]).toBe('update');
|
|
815
|
+
});
|
|
816
|
+
it('should log unconfigured deletes as structured warnings without the event data', async () => {
|
|
817
|
+
await btep.inbound({ ...inboundEvent, eventType: 'DELETE', data: { secretField: 'inbound-value' } });
|
|
818
|
+
await btep.outbound({ ...outboundEvent, eventType: 'delete', oldData: { secretField: 'outbound-value' } });
|
|
819
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: {
|
|
820
|
+
direction: 'inbound', event_type: 'DELETE', entity_type: 'item', object_class: 'LCSProduct', federated_id: 'fed-1',
|
|
821
|
+
} }, 'FlexPLM delete is not configured');
|
|
822
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: {
|
|
823
|
+
direction: 'outbound', event_type: 'delete', entity_type: 'item', entity_id: 'item-1',
|
|
824
|
+
} }, 'FlexPLM delete is not configured');
|
|
825
|
+
expect(JSON.stringify(warnLog.mock.calls)).not.toMatch(/inbound-value|outbound-value/);
|
|
826
|
+
});
|
|
827
|
+
it('should record create_vibeiq_entity with the property count and created id', async () => {
|
|
828
|
+
jest.spyOn(sdk_1.Entities.prototype, 'create').mockResolvedValue({ id: 'created-1' });
|
|
829
|
+
await btep.createEntity('item', { name: 'n', color: 'red' });
|
|
830
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'create_vibeiq_entity');
|
|
831
|
+
expect(span.attributes).toEqual(expect.objectContaining({
|
|
832
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_TYPE]: 'item', [telemetry_attributes_1.ATTR_FLEXPLM_CHANGED_PROPERTY_COUNT]: 2, [telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_ID]: 'created-1',
|
|
833
|
+
}));
|
|
834
|
+
});
|
|
835
|
+
it('should record update_vibeiq_entity with the changed property count', async () => {
|
|
836
|
+
jest.spyOn(sdk_1.Entities.prototype, 'update').mockResolvedValue({ id: 'existing-1' });
|
|
837
|
+
await btep.updateEntity('item', { id: 'existing-1' }, { name: 'n' });
|
|
838
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'update_vibeiq_entity');
|
|
839
|
+
expect(span.attributes).toEqual(expect.objectContaining({
|
|
840
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_ID]: 'existing-1', [telemetry_attributes_1.ATTR_FLEXPLM_CHANGED_PROPERTY_COUNT]: 1,
|
|
841
|
+
}));
|
|
842
|
+
});
|
|
843
|
+
it('should record whether get_entity_by_identity found the entity', async () => {
|
|
844
|
+
const get = jest.spyOn(btep['entities'], 'get')
|
|
845
|
+
.mockResolvedValueOnce([{ entityReference: 'item:abc' }]).mockResolvedValueOnce({ id: 'abc' })
|
|
846
|
+
.mockResolvedValueOnce([]);
|
|
847
|
+
const params = { poolKey: 'item', propertyName: 'itemNumber', propertyValue: '1' };
|
|
848
|
+
await btep.getEntityUsingIdentityService(params);
|
|
849
|
+
await btep.getEntityUsingIdentityService(params);
|
|
850
|
+
expect(get).toHaveBeenCalledTimes(3);
|
|
851
|
+
const [found, missing] = exporter.getFinishedSpans().filter(span => span.name === 'get_entity_by_identity');
|
|
852
|
+
expect(found.attributes[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_FOUND]).toBe(true);
|
|
853
|
+
expect(missing.attributes[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_FOUND]).toBe(false);
|
|
854
|
+
});
|
|
855
|
+
it('should record the result count of query_entities_by_criteria', async () => {
|
|
856
|
+
jest.spyOn(btep, 'getCriteriaForEntity').mockResolvedValue({ rootTypeCriteria: {}, subTypeCriteria: {} });
|
|
857
|
+
jest.spyOn(btep['dc'], 'getAllObjectReferences').mockResolvedValue([{ id: '1' }, { id: '2' }]);
|
|
858
|
+
await btep.queryEntityWithSubTypeCriteria('item', 'item', { itemNumber: '1' });
|
|
859
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'query_entities_by_criteria');
|
|
860
|
+
expect(span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_ENTITY_RESULT_COUNT]).toBe(2);
|
|
861
|
+
});
|
|
862
|
+
it('should log an inbound early return as a warning and keep the span OK', async () => {
|
|
863
|
+
jest.spyOn(btep, 'getTransformedData').mockResolvedValue({});
|
|
864
|
+
jest.spyOn(btep, 'getIncomingEntity').mockResolvedValue({
|
|
865
|
+
earlyReturn: { status: 400, shortStatusMessage: event_short_message_status_1.EventShortMessageStatus.ITEM_FAMILY_NOT_FOUND },
|
|
866
|
+
});
|
|
867
|
+
await btep.inbound(inboundEvent);
|
|
868
|
+
const inbound = (0, telemetry_test_utils_1.findSpan)(exporter, 'process_inbound_entity');
|
|
869
|
+
expect(inbound.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
|
|
870
|
+
expect(inbound.attributes[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]).toBe(event_short_message_status_1.EventShortMessageStatus.FAILURE);
|
|
871
|
+
expect(inbound.attributes[telemetry_attributes_1.ATTR_FLEXPLM_OUTCOME]).toBe(event_short_message_status_1.EventShortMessageStatus.ITEM_FAMILY_NOT_FOUND);
|
|
872
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({
|
|
873
|
+
direction: 'inbound', status: event_short_message_status_1.EventShortMessageStatus.FAILURE, outcome: event_short_message_status_1.EventShortMessageStatus.ITEM_FAMILY_NOT_FOUND,
|
|
874
|
+
}) }, 'FlexPLM inbound entity not processed: %j', expect.any(Object));
|
|
875
|
+
expect(infoLog).not.toHaveBeenCalled();
|
|
876
|
+
});
|
|
877
|
+
});
|
|
666
878
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export * from './publish/base-process-publish-assortment';
|
|
|
21
21
|
export * from './publish/base-process-publish-assortment-callback';
|
|
22
22
|
export * from './entity-processor/base-entity-processor';
|
|
23
23
|
export * from './transform/identifier-conversion';
|
|
24
|
+
export * from './util/telemetry-attributes';
|
package/lib/index.js
CHANGED
|
@@ -37,3 +37,4 @@ __exportStar(require("./publish/base-process-publish-assortment"), exports);
|
|
|
37
37
|
__exportStar(require("./publish/base-process-publish-assortment-callback"), exports);
|
|
38
38
|
__exportStar(require("./entity-processor/base-entity-processor"), exports);
|
|
39
39
|
__exportStar(require("./transform/identifier-conversion"), exports);
|
|
40
|
+
__exportStar(require("./util/telemetry-attributes"), exports);
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
12
|
exports.BaseProcessPublishAssortmentCallback = void 0;
|
|
4
13
|
const sdk_1 = require("@contrail/sdk");
|
|
14
|
+
const telemetry_1 = require("@contrail/telemetry");
|
|
15
|
+
const telemetry_utils_1 = require("../util/telemetry-utils");
|
|
16
|
+
const telemetry_attributes_1 = require("../util/telemetry-attributes");
|
|
5
17
|
class BaseProcessPublishAssortmentCallback {
|
|
6
18
|
constructor(_config) {
|
|
7
19
|
this.config = _config;
|
|
@@ -21,7 +33,17 @@ class BaseProcessPublishAssortmentCallback {
|
|
|
21
33
|
}
|
|
22
34
|
async checkFailuresAndSendNotification(event) {
|
|
23
35
|
const statusCounts = JSON.parse(this.config["statusCounts"]);
|
|
36
|
+
const failureCount = Number(statusCounts["FAILURE"]) || 0;
|
|
37
|
+
const invalidCount = Number(statusCounts["INVALID"]) || 0;
|
|
38
|
+
(0, telemetry_utils_1.setActiveSpanAttributes)({
|
|
39
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_FAILURE_COUNT]: failureCount,
|
|
40
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_INVALID_COUNT]: invalidCount,
|
|
41
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_NOTIFICATION_SENT]: false,
|
|
42
|
+
});
|
|
43
|
+
// FlexPLM's per-item results for the publish; this is the only place they are reported.
|
|
44
|
+
const flexplm = { direction: 'publish', status_counts: statusCounts, failure_count: failureCount, invalid_count: invalidCount };
|
|
24
45
|
if (statusCounts["INVALID"] > 0 || statusCounts["FAILURE"] > 0) {
|
|
46
|
+
telemetry_1.logger.warn({ flexplm }, 'FlexPLM publish had failed items: %j', flexplm);
|
|
25
47
|
await new sdk_1.Notification().Dispatch({
|
|
26
48
|
message: `There were errors when publishing the season data. Please review the event and config information from context.`,
|
|
27
49
|
level: sdk_1.LogLevel.WARN,
|
|
@@ -30,9 +52,17 @@ class BaseProcessPublishAssortmentCallback {
|
|
|
30
52
|
config: this.config,
|
|
31
53
|
},
|
|
32
54
|
});
|
|
55
|
+
(0, telemetry_utils_1.setActiveSpanAttributes)({ [telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_NOTIFICATION_SENT]: true });
|
|
33
56
|
return 'sent notification';
|
|
34
57
|
}
|
|
58
|
+
telemetry_1.logger.info({ flexplm }, 'FlexPLM publish items processed: %j', flexplm);
|
|
35
59
|
return 'no notification needed';
|
|
36
60
|
}
|
|
37
61
|
}
|
|
38
62
|
exports.BaseProcessPublishAssortmentCallback = BaseProcessPublishAssortmentCallback;
|
|
63
|
+
__decorate([
|
|
64
|
+
(0, telemetry_1.Traced)("process_publish_callback"),
|
|
65
|
+
__metadata("design:type", Function),
|
|
66
|
+
__metadata("design:paramtypes", [Object]),
|
|
67
|
+
__metadata("design:returntype", Promise)
|
|
68
|
+
], BaseProcessPublishAssortmentCallback.prototype, "process", null);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const telemetry_1 = require("@contrail/telemetry");
|
|
4
|
+
const base_process_publish_assortment_callback_1 = require("./base-process-publish-assortment-callback");
|
|
5
|
+
const telemetry_test_utils_1 = require("../__tests__/telemetry-test-utils");
|
|
6
|
+
const telemetry_attributes_1 = require("../util/telemetry-attributes");
|
|
7
|
+
const mockDispatch = jest.fn();
|
|
8
|
+
jest.mock('@contrail/sdk', () => ({
|
|
9
|
+
...jest.requireActual('@contrail/sdk'),
|
|
10
|
+
Notification: jest.fn().mockImplementation(() => ({ Dispatch: mockDispatch })),
|
|
11
|
+
}));
|
|
12
|
+
describe('BaseProcessPublishAssortmentCallback telemetry', () => {
|
|
13
|
+
let exporter;
|
|
14
|
+
let infoLog;
|
|
15
|
+
let warnLog;
|
|
16
|
+
const process = (statusCounts) => new base_process_publish_assortment_callback_1.BaseProcessPublishAssortmentCallback({ statusCounts: JSON.stringify(statusCounts) }).process({});
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
exporter = (0, telemetry_test_utils_1.registerInMemoryTracing)();
|
|
19
|
+
});
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
exporter.reset();
|
|
22
|
+
mockDispatch.mockReset().mockResolvedValue(undefined);
|
|
23
|
+
infoLog = jest.spyOn(telemetry_1.logger, 'info').mockImplementation(() => undefined);
|
|
24
|
+
warnLog = jest.spyOn(telemetry_1.logger, 'warn').mockImplementation(() => undefined);
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
// Not restoreAllMocks: that would also clear the Notification mock implementation above.
|
|
28
|
+
infoLog.mockRestore();
|
|
29
|
+
warnLog.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
it('should record the failure counts, warn and mark the notification sent', async () => {
|
|
32
|
+
const result = await process({ SUCCESS: 5, FAILURE: 2, INVALID: 1 });
|
|
33
|
+
expect(result.message).toContain('sent notification');
|
|
34
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'process_publish_callback');
|
|
35
|
+
expect(span.attributes).toEqual(expect.objectContaining({
|
|
36
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_FAILURE_COUNT]: 2,
|
|
37
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_INVALID_COUNT]: 1,
|
|
38
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_NOTIFICATION_SENT]: true,
|
|
39
|
+
}));
|
|
40
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: {
|
|
41
|
+
direction: 'publish', status_counts: { SUCCESS: 5, FAILURE: 2, INVALID: 1 }, failure_count: 2, invalid_count: 1,
|
|
42
|
+
} }, 'FlexPLM publish had failed items: %j', expect.any(Object));
|
|
43
|
+
expect(infoLog).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
it('should record zero counts and an info log when every item succeeded', async () => {
|
|
46
|
+
await process({ SUCCESS: 5 });
|
|
47
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'process_publish_callback');
|
|
48
|
+
expect(span.attributes).toEqual(expect.objectContaining({
|
|
49
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_FAILURE_COUNT]: 0,
|
|
50
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_INVALID_COUNT]: 0,
|
|
51
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_NOTIFICATION_SENT]: false,
|
|
52
|
+
}));
|
|
53
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
54
|
+
expect(infoLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({ failure_count: 0 }) }, 'FlexPLM publish items processed: %j', expect.any(Object));
|
|
55
|
+
});
|
|
56
|
+
it('should not mark the notification sent when dispatching it fails', async () => {
|
|
57
|
+
mockDispatch.mockRejectedValue(new Error('notification service down'));
|
|
58
|
+
await expect(process({ FAILURE: 1 })).rejects.toThrow('notification service down');
|
|
59
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'process_publish_callback');
|
|
60
|
+
expect(span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_NOTIFICATION_SENT]).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -3,6 +3,21 @@ import { DataConverter } from '../util/data-converter';
|
|
|
3
3
|
import { ItemFamilyChanges } from '../interfaces/item-family-changes';
|
|
4
4
|
import { PublishChangeData } from '../interfaces/publish-change-data';
|
|
5
5
|
import { MapFileUtil } from '@contrail/transform-data';
|
|
6
|
+
import { EventShortMessageStatus } from '../util/event-short-message-status';
|
|
7
|
+
export interface PublishStatus {
|
|
8
|
+
status: EventShortMessageStatus;
|
|
9
|
+
assortmentId: string;
|
|
10
|
+
assortmentPublishChangeId: string;
|
|
11
|
+
resultsCount?: {
|
|
12
|
+
productSeasons: number;
|
|
13
|
+
colorwaySeasons: number;
|
|
14
|
+
};
|
|
15
|
+
/** Why the publish was skipped, for the expected skip statuses. */
|
|
16
|
+
message?: string;
|
|
17
|
+
/** FlexPLM accepted the publish, but the external-event that records it could not be created. */
|
|
18
|
+
sentUnrecorded?: boolean;
|
|
19
|
+
error?: unknown;
|
|
20
|
+
}
|
|
6
21
|
export declare class BaseProcessPublishAssortment {
|
|
7
22
|
private TTL;
|
|
8
23
|
static ASSORTMENT_NOT_PUBLISHABLE: string;
|
|
@@ -31,6 +46,10 @@ export declare class BaseProcessPublishAssortment {
|
|
|
31
46
|
resultsCount: any;
|
|
32
47
|
skip_await?: undefined;
|
|
33
48
|
}>;
|
|
49
|
+
/** Records the result of one assortment publish: attributes on the active span plus one structured log.
|
|
50
|
+
* Not_Publishable / No_Federation_Information are handled skips, so the span stays OK for them.
|
|
51
|
+
*/
|
|
52
|
+
protected recordPublishStatus(statusObject: PublishStatus): void;
|
|
34
53
|
getPublishInfo(assortmentId: string, assortmentPublishChangeId: string, apcHistory: any, publisher: any): Promise<any>;
|
|
35
54
|
/** Gets the version number of the snapshot that was created for the publish change.
|
|
36
55
|
* But if no snapshot was found for the publish change, and the last snapshot was
|
|
@@ -54,7 +73,12 @@ export declare class BaseProcessPublishAssortment {
|
|
|
54
73
|
downloadAssortmentPublishChange(assortmentId: string, assortmentPublishChangeId: string): Promise<any>;
|
|
55
74
|
downloadHydratedChangeDetail(assortmentPublishChange: any): Promise<any>;
|
|
56
75
|
downloadAssortmentBaseline(assortmentPublishChange: any): Promise<any>;
|
|
76
|
+
/** Downloads one publish file as JSON. Returns undefined when it can't be read, so the publish continues without it;
|
|
77
|
+
* the span is marked ERROR here because nothing is thrown. The link is never recorded (it is a signed URL).
|
|
78
|
+
*/
|
|
79
|
+
private downloadPublishFile;
|
|
57
80
|
getDeleteChanges(assortmentPublishChange: any, apcHistory: any[], assortmentBaseline: any, sinceDate: Date): Promise<[]>;
|
|
81
|
+
private collectDeleteChanges;
|
|
58
82
|
getBaselineItemIds(assortmentBaseline: any): string[];
|
|
59
83
|
downloadDeleteChanges(apc: any): Promise<any>;
|
|
60
84
|
buildDeleteChanges(apc: any, previousApc: any): Promise<any>;
|