diff --git a/packages/core/core/src/aggregate/get-aggregates-interop-builder.ts b/packages/core/core/src/aggregate/get-aggregates-interop-builder.ts index cad378436f..7050c92b52 100644 --- a/packages/core/core/src/aggregate/get-aggregates-interop-builder.ts +++ b/packages/core/core/src/aggregate/get-aggregates-interop-builder.ts @@ -17,8 +17,6 @@ import { CommandResult, } from '../types/core' -import { makeMonitoringSafe } from '../helpers' - type AggregateData = { aggregateVersion: number aggregateId: string @@ -372,13 +370,24 @@ const executeCommand = async ( runtime: AggregateRuntime, command: Command ): Promise => { - const monitoring = makeMonitoringSafe(runtime.monitoring) + const monitoringGroup = + runtime.monitoring != null + ? runtime.monitoring + .group({ Part: 'Command' }) + .group({ AggregateName: command.aggregateName }) + .group({ Type: command.type }) + : null + + if (monitoringGroup != null) { + monitoringGroup.time('Execution') + } + const { jwt: actualJwt, jwtToken: deprecatedJwt } = command const jwt = actualJwt || deprecatedJwt const subSegment = getPerformanceTracerSubsegment( - monitoring, + runtime.monitoring, 'executeCommand' ) @@ -419,7 +428,7 @@ const executeCommand = async ( context: CommandContext ): Promise => { const subSegment = getPerformanceTracerSubsegment( - monitoring, + runtime.monitoring, 'processCommand' ) try { @@ -483,7 +492,10 @@ const executeCommand = async ( } await (async (): Promise => { - const subSegment = getPerformanceTracerSubsegment(monitoring, 'saveEvent') + const subSegment = getPerformanceTracerSubsegment( + runtime.monitoring, + 'saveEvent' + ) try { return await saveEvent(runtime, aggregate, command, processedEvent) @@ -498,9 +510,16 @@ const executeCommand = async ( return processedEvent } catch (error) { subSegment.addError(error) - await monitoring?.error?.(error, 'command', { command }) + + if (monitoringGroup != null) { + monitoringGroup.group({ AggregateId: command.aggregateId }).error(error) + } throw error } finally { + if (monitoringGroup != null) { + monitoringGroup.timeEnd('Execution') + } + subSegment.close() } } diff --git a/packages/core/core/src/aggregate/types.ts b/packages/core/core/src/aggregate/types.ts index 2969b86e0a..5ac4f26b8b 100644 --- a/packages/core/core/src/aggregate/types.ts +++ b/packages/core/core/src/aggregate/types.ts @@ -44,7 +44,7 @@ export type AggregateRuntimeHooks = { } export type AggregateRuntime = { - monitoring: Monitoring + monitoring?: Monitoring secretsManager: SecretsManager eventstore: Eventstore hooks?: AggregateRuntimeHooks diff --git a/packages/core/core/src/helpers.ts b/packages/core/core/src/helpers.ts index 4c0e04cde1..40d79823a4 100644 --- a/packages/core/core/src/helpers.ts +++ b/packages/core/core/src/helpers.ts @@ -7,11 +7,11 @@ export function firstOfType( return vars.find((i) => selector(i)) as T } -const createSafeHandler = Promise>( - fn: (...args: Parameters) => Promise -) => async (...args: Parameters): Promise => { +const createSafeHandler = void>( + fn: (...args: Parameters) => void +) => (...args: Parameters): void => { try { - await fn(...args) + fn(...args) } catch (e) {} } diff --git a/packages/core/core/src/read-model/get-read-models-interop-builder.ts b/packages/core/core/src/read-model/get-read-models-interop-builder.ts index 956482f4ed..ee45ff41e9 100644 --- a/packages/core/core/src/read-model/get-read-models-interop-builder.ts +++ b/packages/core/core/src/read-model/get-read-models-interop-builder.ts @@ -11,12 +11,20 @@ import { getPerformanceTracerSubsegment } from '../utils' import { ReadModelMeta } from '../types/runtime' import getLog from '../get-log' -const monitoredError = async ( +const monitoredError = ( runtime: ReadModelRuntime, error: Error, - meta: any + readModelName: string, + resolverName: string ) => { - await runtime.monitoring?.error?.(error, 'readModelResolver', meta) + if (runtime.monitoring != null) { + const monitoringGroup = runtime.monitoring + .group({ Part: 'ReadModelResolver' }) + .group({ ReadModel: readModelName }) + .group({ Resolver: resolverName }) + + monitoringGroup.error(error) + } return error } @@ -43,16 +51,14 @@ const getReadModelInterop = ( const invoker = resolverInvokerMap[resolver] if (invoker == null) { log.error(`unable to find invoker for the resolver`) - throw await monitoredError( + throw monitoredError( runtime, createHttpError( HttpStatusCodes.UnprocessableEntity, `Resolver "${resolver}" does not exist` ), - { - readModelName: name, - resolverName: resolver, - } + name, + resolver ) } @@ -69,6 +75,18 @@ const getReadModelInterop = ( } ) + const monitoringGroup = + monitoring != null + ? monitoring + .group({ Part: 'ReadModelResolver' }) + .group({ ReadModel: name }) + .group({ Resolver: resolver }) + : null + + if (monitoringGroup != null) { + monitoringGroup.time('Execution') + } + try { log.debug(`invoking the resolver`) const data = await invoker(connection, args, { @@ -84,12 +102,16 @@ const getReadModelInterop = ( if (subSegment != null) { subSegment.addError(error) } - await monitoring?.error?.(error, 'readModelResolver', { - readModelName: name, - resolverName: resolver, - }) + + if (monitoringGroup != null) { + monitoringGroup.error(error) + } throw error } finally { + if (monitoringGroup != null) { + monitoringGroup.timeEnd('Execution') + } + if (subSegment != null) { subSegment.close() } @@ -113,10 +135,14 @@ const getReadModelInterop = ( try { return await handler() } catch (error) { - await monitoring?.error?.(error, 'readModelProjection', { - readModelName: readModel.name, - eventType, - }) + if (monitoring != null) { + const monitoringGroup = monitoring + .group({ Part: 'ReadModelProjection' }) + .group({ ReadModel: readModel.name }) + .group({ EventType: eventType }) + + monitoringGroup.error(error) + } throw error } } diff --git a/packages/core/core/src/read-model/types.ts b/packages/core/core/src/read-model/types.ts index 26ac2afdb6..f5acd0d541 100644 --- a/packages/core/core/src/read-model/types.ts +++ b/packages/core/core/src/read-model/types.ts @@ -10,7 +10,7 @@ export type ReadModelRuntimeEventHandler = () => Promise export type ReadModelRuntime = { secretsManager: SecretsManager - monitoring: Monitoring + monitoring?: Monitoring } export type ReadModelInterop = { diff --git a/packages/core/core/src/saga/get-sagas-interop-builder.ts b/packages/core/core/src/saga/get-sagas-interop-builder.ts index 6202156623..4d4e099b35 100644 --- a/packages/core/core/src/saga/get-sagas-interop-builder.ts +++ b/packages/core/core/src/saga/get-sagas-interop-builder.ts @@ -55,10 +55,16 @@ const getInterop = ( try { return await handler() } catch (error) { - await runtime.monitoring?.error?.(error, 'readModelProjection', { - readModelName: saga.name, - eventType, - }) + if (runtime.monitoring != null) { + const monitoringGroup = runtime.monitoring + .group({ + Part: 'SagaProjection', + }) + .group({ Saga: saga.name }) + .group({ EventType: eventType }) + + monitoringGroup.error(error) + } throw error } } diff --git a/packages/core/core/src/types/runtime.ts b/packages/core/core/src/types/runtime.ts index 5d7ca5da04..1f4c2add45 100644 --- a/packages/core/core/src/types/runtime.ts +++ b/packages/core/core/src/types/runtime.ts @@ -11,7 +11,6 @@ import { AggregateProjection, EventHandlerEncryptionFactory, ReadModelResolvers, - Command, } from './core' export type PerformanceSubsegment = { @@ -27,6 +26,15 @@ export type PerformanceTracer = { getSegment: () => PerformanceSegment } +export type Monitoring = { + group: (config: Record) => Monitoring + error: (error: Error) => void + time: (name: string, timestamp?: number) => void + timeEnd: (name: string, timestamp?: number) => void + publish: () => Promise + performance?: PerformanceTracer +} + export type Eventstore = { saveEvent: (event: any) => Promise saveSnapshot: Function @@ -82,37 +90,3 @@ export type ViewModelMeta = { encryption: EventHandlerEncryptionFactory invariantHash: string } - -export type MonitoringPartMap = { - command: { - command: Command - } - readModelProjection: { - readModelName: string - eventType: string - } - readModelResolver: { - readModelName: string - resolverName: string - } - viewModelProjection: { - name: string - eventType: string - } - viewModelResolver: { - name: string - } -} -export type MonitoringPart = keyof MonitoringPartMap -export type MonitoringMeta< - TPart extends MonitoringPart -> = MonitoringPartMap[TPart] - -export type Monitoring = { - error?: >( - error: Error, - part: K, - meta: U - ) => Promise - performance?: PerformanceTracer -} diff --git a/packages/core/core/src/view-model/get-view-models-interop-builder.ts b/packages/core/core/src/view-model/get-view-models-interop-builder.ts index ca0b485c5b..f07bc0d5c4 100644 --- a/packages/core/core/src/view-model/get-view-models-interop-builder.ts +++ b/packages/core/core/src/view-model/get-view-models-interop-builder.ts @@ -111,10 +111,15 @@ const buildViewModel = async ( } catch (error) { subSegment.addError(error) log.error(error.message) - await monitoring?.error?.(error, 'viewModelProjection', { - name, - eventType: event.type, - }) + + if (monitoring != null) { + const monitoringGroup = monitoring + .group({ Part: 'ViewModelProjection' }) + .group({ ViewModel: name }) + .group({ EventType: event.type }) + + monitoringGroup.error(error) + } throw error } finally { subSegment.close() @@ -220,9 +225,13 @@ const getViewModelInterop = ( } catch (error) { subSegment.addError(error) - await monitoring?.error?.(error, 'viewModelResolver', { - name, - }) + if (monitoring != null) { + const monitoringGroup = monitoring + .group({ Part: 'ViewModelResolver' }) + .group({ ViewModel: name }) + + monitoringGroup.error(error) + } throw error } finally { subSegment.close() diff --git a/packages/core/core/src/view-model/types.ts b/packages/core/core/src/view-model/types.ts index 1bca99a1ec..e3d4cfdf4e 100644 --- a/packages/core/core/src/view-model/types.ts +++ b/packages/core/core/src/view-model/types.ts @@ -17,7 +17,7 @@ export type ViewModelBuildResult = { } export type ViewModelRuntime = { - monitoring: Monitoring + monitoring?: Monitoring eventstore: Eventstore secretsManager: SecretsManager } diff --git a/packages/core/core/test/get-aggregates-interop-builder.unit.test.ts b/packages/core/core/test/get-aggregates-interop-builder.unit.test.ts index cb9a563d7d..b8019e5a6e 100644 --- a/packages/core/core/test/get-aggregates-interop-builder.unit.test.ts +++ b/packages/core/core/test/get-aggregates-interop-builder.unit.test.ts @@ -56,6 +56,10 @@ const makeTestRuntime = (storedEvents: Event[] = []): AggregateRuntime => { const monitoring: Monitoring = { error: jest.fn(), + group: jest.fn(() => monitoring), + time: jest.fn(), + timeEnd: jest.fn(), + publish: jest.fn(), performance: performanceTracer, } @@ -958,13 +962,27 @@ describe('Monitoring', () => { throw new Error('Test must be failed') } catch (e) { - expect(runtime.monitoring.error).toBeCalledWith(e, 'command', { - command: { - aggregateName: 'empty', - aggregateId: 'aggregateId', - type: 'emptyCommand', - }, - }) + if (runtime.monitoring != null) { + expect(runtime.monitoring.group).toBeCalledWith({ + Part: 'Command', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + AggregateName: 'empty', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + AggregateId: 'aggregateId', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + Type: 'emptyCommand', + }) + + expect(runtime.monitoring.error).toBeCalledWith(e) + } else { + throw new Error('Monitoring must exist') + } } }) @@ -991,13 +1009,27 @@ describe('Monitoring', () => { throw new Error('Test must be failed') } catch (e) { - expect(runtime.monitoring.error).toBeCalledWith(e, 'command', { - command: { - aggregateName: 'empty', - aggregateId: 'aggregateId', - type: 'unknownCommand', - }, - }) + if (runtime.monitoring != null) { + expect(runtime.monitoring.group).toBeCalledWith({ + Part: 'Command', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + AggregateName: 'empty', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + AggregateId: 'aggregateId', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + Type: 'unknownCommand', + }) + + expect(runtime.monitoring.error).toBeCalledWith(e) + } else { + throw new Error('Monitoring must exist') + } } }) @@ -1024,49 +1056,34 @@ describe('Monitoring', () => { throw new Error('Test must be failed') } catch (e) { - expect(runtime.monitoring.error).toBeCalledWith(e, 'command', { - command: { - aggregateName: 'unknown', - aggregateId: 'aggregateId', - type: 'unknownCommand', - }, - }) - } - }) - - test('does not affect command workflow if monitoring.error is failed', async () => { - const runtime = makeTestRuntime() - runtime.monitoring.error = () => { - throw new Error('onCommandFailed failed') - } - - const { executeCommand } = getAggregatesInteropBuilder([ - makeAggregateMeta({ - encryption: () => Promise.resolve({}), - name: 'empty', - commands: { - emptyCommand: () => { - throw new Error('Empty command failed') - }, - }, - }), - ])(runtime) - - try { - await executeCommand({ - aggregateName: 'empty', - aggregateId: 'aggregateId', - type: 'emptyCommand', - }) - - throw new Error('Test must be failed') - } catch (e) { - expect(e.message).toContain('Empty command failed') + if (runtime.monitoring != null) { + expect(runtime.monitoring.group).toBeCalledWith({ + Part: 'Command', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + AggregateName: 'unknown', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + AggregateId: 'aggregateId', + }) + + expect(runtime.monitoring.group).toBeCalledWith({ + Type: 'unknownCommand', + }) + + expect(runtime.monitoring.error).toBeCalledWith(e) + } else { + throw new Error('Monitoring must exist') + } } }) test('does not affect command workflow if monitoring is absent', async () => { const runtime = makeTestRuntime() + delete runtime.monitoring + const { executeCommand } = getAggregatesInteropBuilder([ makeAggregateMeta({ encryption: () => Promise.resolve({}), diff --git a/packages/core/core/test/get-read-models-interop-builder.unit.test.ts b/packages/core/core/test/get-read-models-interop-builder.unit.test.ts index 03cbe63ff0..dd094b7902 100644 --- a/packages/core/core/test/get-read-models-interop-builder.unit.test.ts +++ b/packages/core/core/test/get-read-models-interop-builder.unit.test.ts @@ -23,13 +23,23 @@ const secretsManager: SecretsManager = { let monitoring: { error: jest.MockedFunction> + group: jest.MockedFunction> + time: jest.MockedFunction> + timeEnd: jest.MockedFunction> + publish: jest.MockedFunction> } const makeTestRuntime = (): ReadModelRuntime => { monitoring = { + group: jest.fn(), error: jest.fn(), + time: jest.fn(), + timeEnd: jest.fn(), + publish: jest.fn(), } + monitoring.group.mockReturnValue(monitoring) + return { secretsManager, monitoring, @@ -163,15 +173,22 @@ describe('Read models', () => { } } catch {} + expect(monitoring.group.mock.calls[0][0]).toEqual({ + Part: 'ReadModelProjection', + }) + + expect(monitoring.group.mock.calls[1][0]).toEqual({ + ReadModel: 'TestReadModel', + }) + + expect(monitoring.group.mock.calls[2][0]).toEqual({ + EventType: 'Init', + }) + expect(monitoring.error.mock.calls[0][0]).toBeInstanceOf(Error) expect(monitoring.error.mock.calls[0][0].message).toEqual( 'Projection error' ) - expect(monitoring.error.mock.calls[0][1]).toEqual('readModelProjection') - expect(monitoring.error.mock.calls[0][2]).toEqual({ - readModelName: 'TestReadModel', - eventType: 'Init', - }) }) test('#1797: error meta within monitored error on event handler ', async () => { @@ -201,15 +218,20 @@ describe('Read models', () => { } } catch {} + expect(monitoring.group.mock.calls[0][0]).toEqual({ + Part: 'ReadModelProjection', + }) + expect(monitoring.group.mock.calls[1][0]).toEqual({ + ReadModel: 'TestReadModel', + }) + expect(monitoring.group.mock.calls[2][0]).toEqual({ + EventType: 'Failed', + }) + expect(monitoring.error.mock.calls[0][0]).toBeInstanceOf(Error) expect(monitoring.error.mock.calls[0][0].message).toEqual( 'Projection error' ) - expect(monitoring.error.mock.calls[0][1]).toEqual('readModelProjection') - expect(monitoring.error.mock.calls[0][2]).toEqual({ - readModelName: 'TestReadModel', - eventType: 'Failed', - }) }) test('should register error if resolver not found', async () => { @@ -228,15 +250,20 @@ describe('Read models', () => { readModelInterop.acquireResolver('not-existing-resolver', {}, {}) ).rejects.toBeInstanceOf(Error) + expect(monitoring.group.mock.calls[0][0]).toEqual({ + Part: 'ReadModelResolver', + }) + expect(monitoring.group.mock.calls[1][0]).toEqual({ + ReadModel: 'TestReadModel', + }) + expect(monitoring.group.mock.calls[2][0]).toEqual({ + Resolver: 'not-existing-resolver', + }) + expect(monitoring.error.mock.calls[0][0]).toBeInstanceOf(Error) expect(monitoring.error.mock.calls[0][0].message).toEqual( expect.stringContaining(`not-existing-resolver`) ) - expect(monitoring.error.mock.calls[0][1]).toEqual('readModelResolver') - expect(monitoring.error.mock.calls[0][2]).toEqual({ - readModelName: 'TestReadModel', - resolverName: 'not-existing-resolver', - }) }) test('should register error or resolver failure', async () => { @@ -257,12 +284,17 @@ describe('Read models', () => { await expect(resolver(null, null)).rejects.toBeInstanceOf(Error) + expect(monitoring.group.mock.calls[0][0]).toEqual({ + Part: 'ReadModelResolver', + }) + expect(monitoring.group.mock.calls[1][0]).toEqual({ + ReadModel: 'TestReadModel', + }) + expect(monitoring.group.mock.calls[2][0]).toEqual({ + Resolver: 'fail', + }) + expect(monitoring.error.mock.calls[0][0]).toBeInstanceOf(Error) expect(monitoring.error.mock.calls[0][0].message).toEqual('failed resolver') - expect(monitoring.error.mock.calls[0][1]).toEqual('readModelResolver') - expect(monitoring.error.mock.calls[0][2]).toEqual({ - readModelName: 'TestReadModel', - resolverName: 'fail', - }) }) }) diff --git a/packages/core/core/test/get-sagas-interop-builder.unit.test.ts b/packages/core/core/test/get-sagas-interop-builder.unit.test.ts index 470a1ca235..84d7c3b49f 100644 --- a/packages/core/core/test/get-sagas-interop-builder.unit.test.ts +++ b/packages/core/core/test/get-sagas-interop-builder.unit.test.ts @@ -25,12 +25,21 @@ const secretsManager: SecretsManager = { let monitoring: { error: jest.MockedFunction> + group: jest.MockedFunction> + time: jest.MockedFunction> + timeEnd: jest.MockedFunction> + publish: jest.MockedFunction> } const makeTestRuntime = (): SagaRuntime => { monitoring = { + group: jest.fn(), error: jest.fn(), + time: jest.fn(), + timeEnd: jest.fn(), + publish: jest.fn(), } + monitoring.group.mockReturnValue(monitoring) const scheduler = { addEntries: jest.fn(), clearEntries: jest.fn(), @@ -223,7 +232,7 @@ describe('Sagas', () => { taskId: 'validAggregateId', }) }) - test('#1797: error meta within monitored error on Init handler ', async () => { + test('#1797: error group on Init handler ', async () => { const sagaParams = { name: 'dummySaga', handlers: { @@ -248,17 +257,22 @@ describe('Sagas', () => { } } catch {} + expect(monitoring.group.mock.calls[0][0]).toEqual({ + Part: 'SagaProjection', + }) + expect(monitoring.group.mock.calls[1][0]).toEqual({ + Saga: 'dummySaga', + }) + expect(monitoring.group.mock.calls[2][0]).toEqual({ + EventType: 'Init', + }) + expect(monitoring.error.mock.calls[0][0]).toBeInstanceOf(Error) expect(monitoring.error.mock.calls[0][0].message).toEqual( 'Projection error' ) - expect(monitoring.error.mock.calls[0][1]).toEqual('readModelProjection') - expect(monitoring.error.mock.calls[0][2]).toEqual({ - readModelName: 'dummySaga', - eventType: 'Init', - }) }) - test('#1797: error meta within monitored error on event handler ', async () => { + test('#1797: error group on event handler ', async () => { const sagaParams = { name: 'dummySaga', handlers: { @@ -288,14 +302,19 @@ describe('Sagas', () => { } } catch {} + expect(monitoring.group.mock.calls[0][0]).toEqual({ + Part: 'SagaProjection', + }) + expect(monitoring.group.mock.calls[1][0]).toEqual({ + Saga: 'dummySaga', + }) + expect(monitoring.group.mock.calls[2][0]).toEqual({ + EventType: 'Failed', + }) + expect(monitoring.error.mock.calls[0][0]).toBeInstanceOf(Error) expect(monitoring.error.mock.calls[0][0].message).toEqual( 'Projection error' ) - expect(monitoring.error.mock.calls[0][1]).toEqual('readModelProjection') - expect(monitoring.error.mock.calls[0][2]).toEqual({ - readModelName: 'dummySaga', - eventType: 'Failed', - }) }) }) diff --git a/packages/core/core/test/get-view-models-interop-builder.unit.test.ts b/packages/core/core/test/get-view-models-interop-builder.unit.test.ts index 1e82a6152b..778386afba 100644 --- a/packages/core/core/test/get-view-models-interop-builder.unit.test.ts +++ b/packages/core/core/test/get-view-models-interop-builder.unit.test.ts @@ -1,3 +1,5 @@ +import { mocked } from 'ts-jest/utils' + import { SecretsManager, Event } from '../src/types/core' import { ViewModelMeta, Eventstore, Monitoring } from '../src/types/runtime' import { getViewModelsInteropBuilder } from '../src/view-model/get-view-models-interop-builder' @@ -8,6 +10,8 @@ import { ViewModelRuntime, } from '../src/view-model/types' +let monitoring: Monitoring + const dummyEncryption = () => Promise.resolve({}) const makeViewModelMeta = (params: any): ViewModelMeta[] => [ @@ -48,10 +52,16 @@ const makeTestRuntime = (storedEvents: Event[] = []): ViewModelRuntime => { getEventSubscribers: jest.fn().mockResolvedValue([]), } - const monitoring: Monitoring = { + monitoring = { + group: jest.fn(), error: jest.fn(), + time: jest.fn(), + timeEnd: jest.fn(), + publish: jest.fn(), } + mocked(monitoring.group).mockReturnValue(monitoring) + return { eventstore, secretsManager, @@ -123,4 +133,114 @@ describe('View models', () => { expect(eventCount).toEqual(2) expect(cursor).toEqual(2) }) + + test('collects error if event handler is failed', async () => { + const error = new Error('TestError') + + const resolver = setUpTestViewModelResolver( + { + name: 'TestViewModel', + projection: { + Init: () => [], + dummyEvent: () => { + throw error + }, + }, + resolver: (resolve: any, params: any) => + resolve.buildViewModel('TestViewModel', params), + }, + [ + { + type: 'dummyEvent', + payload: { text: 'first' }, + aggregateId: 'validAggregateId', + aggregateVersion: 1, + timestamp: 1, + }, + { + type: 'dummyEvent', + payload: { text: 'second' }, + aggregateId: 'invalidAggregateId', + aggregateVersion: 1, + timestamp: 2, + }, + { + type: 'dummyEvent', + payload: { text: 'third' }, + aggregateId: 'validAggregateId', + aggregateVersion: 2, + timestamp: 3, + }, + ] + ) + + try { + await resolver({ + aggregateIds: ['validAggregateId'], + aggregateArgs: null, + }) + + throw new Error('Building must be failed') + } catch (e) {} + + expect(monitoring.group).toBeCalledWith({ Part: 'ViewModelProjection' }) + expect(monitoring.group).toBeCalledWith({ ViewModel: 'TestViewModel' }) + expect(monitoring.group).toBeCalledWith({ EventType: 'dummyEvent' }) + expect(monitoring.error).toBeCalledWith(error) + }) + + test('collects error if resolver is failed', async () => { + const error = new Error('TestError') + + const resolver = setUpTestViewModelResolver( + { + name: 'TestViewModel', + projection: { + Init: () => [], + dummyEvent: (state: any, event: any) => { + return [...state, event.payload.text] + }, + }, + resolver: () => { + throw error + }, + }, + [ + { + type: 'dummyEvent', + payload: { text: 'first' }, + aggregateId: 'validAggregateId', + aggregateVersion: 1, + timestamp: 1, + }, + { + type: 'dummyEvent', + payload: { text: 'second' }, + aggregateId: 'invalidAggregateId', + aggregateVersion: 1, + timestamp: 2, + }, + { + type: 'dummyEvent', + payload: { text: 'third' }, + aggregateId: 'validAggregateId', + aggregateVersion: 2, + timestamp: 3, + }, + ] + ) + + try { + await resolver({ + aggregateIds: ['validAggregateId'], + aggregateArgs: null, + }) + + throw new Error('Building must be failed') + } catch (e) {} + + expect(monitoring.group).toBeCalledWith({ Part: 'ViewModelResolver' }) + expect(monitoring.group).toBeCalledWith({ ViewModel: 'TestViewModel' }) + expect(monitoring.error).toBeCalledWith(error) + }) }) diff --git a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/create-adapter.ts b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/create-adapter.ts index 5726e11036..1c7ff7e331 100644 --- a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/create-adapter.ts +++ b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/create-adapter.ts @@ -92,13 +92,14 @@ const createAdapter = < } } - const { performanceTracer, ...adapterOptions } = options + const { performanceTracer, monitoring, ...adapterOptions } = options const pool: BaseAdapterPool = { - commonAdapterPool: { performanceTracer }, + commonAdapterPool: { performanceTracer, monitoring }, adapterPoolMap: new Map(), withPerformanceTracer, performanceTracer, + monitoring, } const adapter: AdapterApi = { diff --git a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/types.ts b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/types.ts index 36436d3a3b..ff42baf53d 100644 --- a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/types.ts +++ b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/types.ts @@ -135,6 +135,14 @@ export type PerformanceTracerLike = { } | null } +export type MonitoringLike = { + time: (name: string, timestamp?: number) => void + timeEnd: (name: string, timestamp?: number) => void + group: (config: Record) => MonitoringLike + publish: () => Promise + performance?: PerformanceTracerLike +} + export type ReadModelCursor = Cursor // TODO brand type export type ReadModelEvent = SavedEvent @@ -146,10 +154,12 @@ export type EventstoreAdapterLike = { } export type CommonAdapterPool = { + monitoring?: MonitoringLike performanceTracer?: PerformanceTracerLike } export type CommonAdapterOptions = { + monitoring?: MonitoringLike performanceTracer?: PerformanceTracerLike } @@ -187,6 +197,7 @@ export type ReadModelStoreImpl< CurrentStoreApi[K] > } & { + monitoring?: MonitoringLike performanceTracer?: PerformanceTracerLike } @@ -325,6 +336,7 @@ export type BaseAdapterPool = { commonAdapterPool: CommonAdapterPool adapterPoolMap: Map>, AdapterPool> withPerformanceTracer: WithPerformanceTracerMethod + monitoring?: MonitoringLike performanceTracer?: PerformanceTracerLike } diff --git a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/wrap-connect.ts b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/wrap-connect.ts index 4579dc0fad..cf7343ece4 100644 --- a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/wrap-connect.ts +++ b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/wrap-connect.ts @@ -40,6 +40,7 @@ const connectImpl = async < update: update.bind(null, adapterPool, readModelName), delete: del.bind(null, adapterPool, readModelName), performanceTracer: pool.performanceTracer, + monitoring: pool.monitoring, } Object.freeze(store) diff --git a/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/src/build.ts b/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/src/build.ts index 74498a1687..cc3f2d34fe 100644 --- a/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/src/build.ts +++ b/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/src/build.ts @@ -166,6 +166,7 @@ export const buildEvents: ( inputCursor: ReadModelCursor readModelLedger: ReadModelLedger xaKey: string + metricData: any }, ...args: Parameters ) => ReturnType = async ( @@ -193,6 +194,7 @@ export const buildEvents: ( xaKey, eventTypes, inputCursor, + metricData, } = pool let lastSuccessEvent: ReadModelEvent | null = null @@ -213,6 +215,8 @@ export const buildEvents: ( : RDS_TRANSACTION_FAILED_KEY ) + const firstEventsLoadStartTimestamp = Date.now() + let eventsPromise: Promise> = eventstoreAdapter .loadEvents({ eventTypes, @@ -220,7 +224,11 @@ export const buildEvents: ( limit: 100, cursor, }) - .then((result) => (result != null ? result.events : [])) + .then((result) => { + metricData.eventBatchLoadTime += + Date.now() - firstEventsLoadStartTimestamp + return result != null ? result.events : [] + }) let transactionId: string = await transactionIdPromise let rootSavePointId: string = generateGuid(transactionId, 'ROOT') @@ -271,7 +279,7 @@ export const buildEvents: ( acquireTrxPromise, ]) - while (true) { + for (metricData.eventLoopCount = 0; true; metricData.eventLoopCount++) { if (events.length === 0) { throw new PassthroughError(transactionId) } @@ -292,7 +300,7 @@ export const buildEvents: ( cursor, events ) - + const eventsLoadStartTimestamp = Date.now() eventsPromise = eventstoreAdapter .loadEvents({ eventTypes, @@ -300,7 +308,10 @@ export const buildEvents: ( limit: 1000, cursor: nextCursor, }) - .then((result) => (result != null ? result.events : [])) + .then((result) => { + metricData.eventBatchLoadTime += Date.now() - eventsLoadStartTimestamp + return result != null ? result.events : [] + }) let appliedEventsCount = 0 try { @@ -314,7 +325,16 @@ export const buildEvents: ( `SAVEPOINT ${savePointId}`, transactionId ) - await handler() + const projectionApplyStartTimestamp = Date.now() + try { + metricData.insideProjection = true + await handler() + } finally { + metricData.insideProjection = false + } + metricData.pureProjectionApplyTime += + Date.now() - projectionApplyStartTimestamp + await inlineLedgerExecuteStatement( pool, `RELEASE SAVEPOINT ${savePointId}`, @@ -501,8 +521,16 @@ const build: ExternalMethods['build'] = async ( modelInterop, next, eventstoreAdapter, - getVacantTimeInMillis + getVacantTimeInMillis, + ...args ) => { + const metricData: any = { + ...(args as any)[0], + eventBatchLoadTime: 0, + pureProjectionApplyTime: 0, + pureLedgerTime: 0, + } + const { PassthroughError, dbClusterOrInstanceArn, @@ -511,10 +539,49 @@ const build: ExternalMethods['build'] = async ( escapeId, escapeStr, rdsDataService, - inlineLedgerExecuteStatement, + inlineLedgerExecuteStatement: ledgerStatement, generateGuid, + monitoring, } = basePool + const now = Date.now() + + const hasSendTime = typeof metricData.sendTime === 'number' + + const groupMonitoring = + monitoring != null + ? monitoring + .group({ Part: 'ReadModelProjection' }) + .group({ ReadModel: readModelName }) + : null + + if (hasSendTime) { + void [monitoring, groupMonitoring].forEach((innerMonitoring) => { + if (innerMonitoring == null) { + return + } + + innerMonitoring.time('EventDelivery', metricData.sendTime) + innerMonitoring.timeEnd('EventDelivery', now) + + innerMonitoring.time('EventApply', metricData.sendTime) + }) + } + + const inlineLedgerExecuteStatement: typeof ledgerStatement = Object.assign( + async (...args: any[]): Promise => { + const inlineLedgerStartTimestamp = Date.now() + try { + return await (ledgerStatement as any)(...args) + } finally { + if (!metricData.insideProjection) { + metricData.pureLedgerTime += Date.now() - inlineLedgerStartTimestamp + } + } + }, + ledgerStatement + ) + try { basePool.activePassthrough = true const databaseNameAsId = escapeId(schemaName) @@ -597,6 +664,7 @@ const build: ExternalMethods['build'] = async ( readModelLedger, eventTypes, xaKey, + metricData, } const buildMethod = cursor == null ? buildInit : buildEvents @@ -641,6 +709,28 @@ const build: ExternalMethods['build'] = async ( } } finally { basePool.activePassthrough = false + + void [monitoring, groupMonitoring].forEach((innerMonitoring) => { + if (innerMonitoring == null) { + return + } + + if (hasSendTime) { + innerMonitoring.timeEnd('EventApply') + } + + innerMonitoring.time('EventBatchLoad', 0) + innerMonitoring.timeEnd('EventBatchLoad', metricData.eventBatchLoadTime) + + innerMonitoring.time('EventProjectionApply', 0) + innerMonitoring.timeEnd( + 'EventProjectionApply', + metricData.pureProjectionApplyTime + ) + + innerMonitoring.time('Ledger', 0) + innerMonitoring.timeEnd('Ledger', metricData.pureLedgerTime) + }) } } diff --git a/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/test/build-events.test.ts b/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/test/build-events.test.ts index f41405bf21..790cc88cd7 100644 --- a/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/test/build-events.test.ts +++ b/packages/runtime/adapters/readmodel-adapters/readmodel-postgresql-serverless/test/build-events.test.ts @@ -111,6 +111,7 @@ describe('buildEvents', () => { //eslint-disable-next-line @typescript-eslint/no-non-null-assertion readModelLedger: null! as any, xaKey, + metricData: {}, }, { PassthroughError, @@ -227,6 +228,7 @@ describe('buildEvents', () => { //eslint-disable-next-line @typescript-eslint/no-non-null-assertion readModelLedger: null! as any, xaKey, + metricData: {}, }, { PassthroughError, diff --git a/packages/runtime/runtime/src/cloud/api-gateway-handler.js b/packages/runtime/runtime/src/cloud/api-gateway-handler.js index 3e95d1c3b5..91714f1c7f 100644 --- a/packages/runtime/runtime/src/cloud/api-gateway-handler.js +++ b/packages/runtime/runtime/src/cloud/api-gateway-handler.js @@ -4,18 +4,10 @@ import mainHandler from '../common/handlers/main-handler' const getCustomParameters = async (resolve) => ({ resolve }) const apiGatewayHandler = async (lambdaEvent, lambdaContext, resolve) => { - const onError = async (error, path) => { - try { - await resolve.monitoring.error(error, 'apiHandler', { - path, - }) - } catch (e) {} - } - const executor = wrapApiHandler( mainHandler, getCustomParameters.bind(null, resolve), - onError + resolve.monitoring.group({ Part: 'ApiHandler' }) ) const segment = resolve.performanceTracer.getSegment() diff --git a/packages/runtime/runtime/src/cloud/index.js b/packages/runtime/runtime/src/cloud/index.js index a0ca6c8af3..e015b66d72 100644 --- a/packages/runtime/runtime/src/cloud/index.js +++ b/packages/runtime/runtime/src/cloud/index.js @@ -17,11 +17,10 @@ import wrapTrie from '../common/wrap-trie' import initUploader from './init-uploader' import gatherEventListeners from '../common/gather-event-listeners' import getSubscribeAdapterOptions from './get-subscribe-adapter-options' -import { putInternalError } from './metrics' const log = debugLevels('resolve:runtime:cloud-entry') -const index = async ({ assemblies, constants, domain }) => { +const index = async ({ assemblies, constants, domain, resolveVersion }) => { let subSegment = null log.debug(`starting lambda 'cold start'`) @@ -40,6 +39,7 @@ const index = async ({ assemblies, constants, domain }) => { domainInterop, eventListeners: gatherEventListeners(domain, domainInterop), upstream: true, + resolveVersion, } log.debug('preparing performance tracer') @@ -121,7 +121,6 @@ const index = async ({ assemblies, constants, domain }) => { } catch (error) { log.error(`lambda 'cold start' failure`, error) subSegment.addError(error) - await putInternalError(error) } finally { if (subSegment != null) { subSegment.close() diff --git a/packages/runtime/runtime/src/cloud/init-monitoring.js b/packages/runtime/runtime/src/cloud/init-monitoring.js index 47081f1549..5ddd2728b7 100644 --- a/packages/runtime/runtime/src/cloud/init-monitoring.js +++ b/packages/runtime/runtime/src/cloud/init-monitoring.js @@ -1,81 +1,10 @@ -import debugLevels from '@resolve-js/debug-levels' - -import { - putCommandMetrics, - putReadModelProjectionMetrics, - putReadModelResolverMetrics, - putViewModelProjectionMetrics, - putViewModelResolverMetrics, - putApiHandlerMetrics, - putSagaMetrics, -} from './metrics' - -const getLog = (name) => debugLevels(`resolve:cloud:scheduler:${name}`) +import createMonitoring from './monitoring' const initMonitoring = (resolve) => { - const log = getLog('monitoring') - - resolve.monitoring = { - error: async (error, part, meta) => { - try { - log.verbose(`Collect error for '${part}' part`) - - switch (part) { - case 'command': { - await putCommandMetrics( - meta.command.aggregateName, - meta.command.type, - meta.command.aggregateId, - error - ) - break - } - case 'readModelProjection': { - await putReadModelProjectionMetrics( - meta.readModelName, - meta.eventType, - error - ) - break - } - case 'readModelResolver': { - await putReadModelResolverMetrics( - meta.readModelName, - meta.resolverName, - error - ) - break - } - case 'viewModelProjection': { - await putViewModelProjectionMetrics( - meta.viewModelName, - meta.eventType, - error - ) - break - } - case 'viewModelResolver': { - await putViewModelResolverMetrics(meta.readModelName, error) - break - } - case 'apiHandler': { - await putApiHandlerMetrics(meta.path, error) - break - } - case 'sagaProjection': { - await putSagaMetrics(meta.sagaName, meta.eventType, error) - break - } - default: { - log.verbose(`Unknown error part: ${part}`) - break - } - } - } catch (error) { - log.verbose(`Failed to collect error`, error) - } - }, - } + resolve.monitoring = createMonitoring({ + deploymentId: process.env.RESOLVE_DEPLOYMENT_ID, + resolveVersion: resolve.resolveVersion, + }) } export default initMonitoring diff --git a/packages/runtime/runtime/src/cloud/lambda-worker.js b/packages/runtime/runtime/src/cloud/lambda-worker.js index 0870aa9381..30c4422def 100644 --- a/packages/runtime/runtime/src/cloud/lambda-worker.js +++ b/packages/runtime/runtime/src/cloud/lambda-worker.js @@ -6,7 +6,7 @@ import handleDeployServiceEvent from './deploy-service-event-handler' import handleSchedulerEvent from './scheduler-event-handler' import initScheduler from './init-scheduler' import initMonitoring from './init-monitoring' -import { putDurationMetrics, putInternalError } from './metrics' +import { putDurationMetrics } from './metrics' import initResolve from '../common/init-resolve' import disposeResolve from '../common/dispose-resolve' import handleWebsocketEvent from './websocket-event-handler' @@ -92,7 +92,13 @@ const lambdaWorker = async (resolveBase, lambdaEvent, lambdaContext) => { log.debug('identified event source: event-subscriber-direct') const { method, payload } = lambdaEvent - const executorResult = await resolve.eventSubscriber[method](payload) + + const actualPayload = + method === 'build' ? { ...payload, coldStart } : payload + + const executorResult = await resolve.eventSubscriber[method]( + actualPayload + ) log.verbose(`executorResult: ${JSON.stringify(executorResult)}`) @@ -158,7 +164,7 @@ const lambdaWorker = async (resolveBase, lambdaEvent, lambdaContext) => { } catch (error) { log.error('top-level event handler execution error!') - putInternalError(error) + resolve.monitoring.error('internal', error) if (error instanceof Error) { log.error('error', error.message) @@ -170,6 +176,7 @@ const lambdaWorker = async (resolveBase, lambdaEvent, lambdaContext) => { throw error } finally { await disposeResolve(resolve) + if (process.env.RESOLVE_PERFORMANCE_MONITORING) { await putDurationMetrics( lambdaEvent, @@ -178,8 +185,13 @@ const lambdaWorker = async (resolveBase, lambdaEvent, lambdaContext) => { lambdaRemainingTimeStart ) } + coldStart = false - log.debug('reSolve framework was disposed') + log.debug('reSolve framework was disposed. publishing metrics') + + await resolve.monitoring.publish() + + log.debug(`metrics published`) } } diff --git a/packages/runtime/runtime/src/cloud/metrics.js b/packages/runtime/runtime/src/cloud/metrics.js index 548ce9b5be..ab61e3d7d0 100644 --- a/packages/runtime/runtime/src/cloud/metrics.js +++ b/packages/runtime/runtime/src/cloud/metrics.js @@ -1,7 +1,4 @@ import CloudWatch from 'aws-sdk/clients/cloudwatch' -import debugLevels from '@resolve-js/debug-levels' - -const MAX_METRICS_DIMENSION_VALUE_LENGTH = 256 const kindByEvent = (event) => { const { part, path = '' } = event @@ -83,369 +80,3 @@ export const putDurationMetrics = async ( await cloudWatch.putMetricData(params).promise() } } - -const getErrorMessage = (error) => { - let errorMessage = error.message - - if (errorMessage.length > MAX_METRICS_DIMENSION_VALUE_LENGTH) { - const messageEnd = '...' - - errorMessage = `${errorMessage.slice( - 0, - MAX_METRICS_DIMENSION_VALUE_LENGTH - messageEnd.length - )}${messageEnd}` - } - - return errorMessage -} - -const putDataMetrics = async (dataMap, commonMap, errorMap) => { - const log = debugLevels('resolve:runtime:cloud-entry:putDataMetrics') - const cw = new CloudWatch() - - if (dataMap.DeploymentId == null) { - log.warn('Deployment ID not found') - return - } - - const now = new Date() - const metricName = dataMap.ErrorMessage != null ? 'Errors' : 'Executions' - - const metricData = commonMap.map((dimensionNames) => ({ - MetricName: metricName, - Timestamp: now, - Unit: 'Count', - Value: 1, - Dimensions: dimensionNames.map((name) => ({ - Name: name, - Value: dataMap[name], - })), - })) - - if (dataMap.Error != null && errorMap != null) { - metricData.push( - ...errorMap.map((dimensionNames) => ({ - MetricName: 'Errors', - Timestamp: now, - Unit: 'Count', - Value: 1, - Dimensions: dimensionNames.map((name) => ({ - Name: name, - Value: dataMap[name], - })), - })) - ) - } - - try { - await cw - .putMetricData({ - Namespace: 'RESOLVE_METRICS', - MetricData: metricData, - }) - .promise() - - log.verbose('Put metrics succeeded') - } catch (e) { - log.verbose('Put metrics failed') - log.warn(e) - } -} - -export const putCommandMetrics = async ( - aggregateName, - commandType, - aggregateId, - error -) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'Command', - AggregateName: aggregateName, - CommandType: commandType, - AggregateId: aggregateId, - } - - if (error != null) { - metricDataMap.ErrorMessage = getErrorMessage(error) - metricDataMap.ErrorName = error.name - } - - return putDataMetrics( - metricDataMap, - [ - ['DeploymentId'], - ['DeploymentId', 'Part'], - ['DeploymentId', 'Part', 'AggregateName'], - ['DeploymentId', 'Part', 'AggregateName', 'CommandType'], - ['DeploymentId', 'Part', 'AggregateName', 'CommandType', 'AggregateId'], - ], - [ - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName'], - [ - 'DeploymentId', - 'Part', - 'AggregateName', - 'CommandType', - 'AggregateId', - 'ErrorName', - 'ErrorMessage', - ], - [ - 'DeploymentId', - 'Part', - 'AggregateName', - 'CommandType', - 'AggregateId', - 'ErrorName', - ], - ] - ) -} - -export const putReadModelProjectionMetrics = async ( - readModelName, - eventType, - error -) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'ReadModelProjection', - ReadModel: readModelName, - EventType: eventType, - } - - if (error != null) { - metricDataMap.ErrorMessage = getErrorMessage(error) - metricDataMap.ErrorName = error.name - } - - return putDataMetrics( - metricDataMap, - [ - ['DeploymentId'], - ['DeploymentId', 'Part'], - ['DeploymentId', 'Part', 'ReadModel'], - ['DeploymentId', 'Part', 'ReadModel', 'EventType'], - ], - [ - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - [ - 'DeploymentId', - 'Part', - 'ReadModel', - 'EventType', - 'ErrorName', - 'ErrorMessage', - ], - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName'], - ['DeploymentId', 'Part', 'ReadModel', 'EventType', 'ErrorName'], - ] - ) -} - -export const putReadModelResolverMetrics = async ( - readModelName, - resolverName, - error -) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'ReadModelResolver', - ReadModel: readModelName, - Resolver: resolverName, - } - - if (error != null) { - metricDataMap.ErrorMessage = getErrorMessage(error) - metricDataMap.ErrorName = error.name - } - - return putDataMetrics( - metricDataMap, - [ - ['DeploymentId'], - ['DeploymentId', 'Part'], - ['DeploymentId', 'Part', 'ReadModel'], - ['DeploymentId', 'Part', 'ReadModel', 'Resolver'], - ], - [ - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - [ - 'DeploymentId', - 'Part', - 'ReadModel', - 'Resolver', - 'ErrorName', - 'ErrorMessage', - ], - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName'], - ['DeploymentId', 'Part', 'ReadModel', 'Resolver', 'ErrorName'], - ] - ) -} - -export const putViewModelProjectionMetrics = async ( - viewModelName, - eventType, - error -) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'ViewModelProjection', - ViewModel: viewModelName, - EventType: eventType, - } - - if (error != null) { - metricDataMap.ErrorMessage = getErrorMessage(error) - metricDataMap.ErrorName = error.name - } - - return putDataMetrics( - metricDataMap, - [ - ['DeploymentId'], - ['DeploymentId', 'Part'], - ['DeploymentId', 'Part', 'ViewModel'], - ['DeploymentId', 'Part', 'ViewModel', 'EventType'], - ], - [ - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName'], - ['DeploymentId', 'Part', 'ViewModel', 'EventType', 'ErrorName'], - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - [ - 'DeploymentId', - 'Part', - 'ViewModel', - 'EventType', - 'ErrorName', - 'ErrorMessage', - ], - ] - ) -} - -export const putViewModelResolverMetrics = async (viewModelName, error) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'ViewModelResolver', - ViewModel: viewModelName, - } - - if (error != null) { - metricDataMap.ErrorMessage = getErrorMessage(error) - metricDataMap.ErrorName = error.name - } - - return putDataMetrics( - metricDataMap, - [ - ['DeploymentId'], - ['DeploymentId', 'Part'], - ['DeploymentId', 'Part', 'ViewModel'], - ], - [ - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName'], - ['DeploymentId', 'Part', 'ViewModel', 'ErrorName'], - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ViewModel', 'ErrorName', 'ErrorMessage'], - ] - ) -} - -export const putApiHandlerMetrics = async (apiHandlerPath, error) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'ApiHandler', - Path: apiHandlerPath, - } - - if (error != null) { - metricDataMap.ErrorMessage = getErrorMessage(error) - metricDataMap.ErrorName = error.name - } - - return putDataMetrics( - metricDataMap, - [ - ['DeploymentId'], - ['DeploymentId', 'Part'], - ['DeploymentId', 'Part', 'Path'], - ], - [ - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName'], - ['DeploymentId', 'Part', 'Path', 'ErrorName'], - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'Path', 'ErrorName', 'ErrorMessage'], - ] - ) -} - -export const putSagaMetrics = async (sagaName, eventType, error) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'SagaProjection', - Saga: sagaName, - EventType: eventType, - } - - if (error != null) { - metricDataMap.ErrorMessage = getErrorMessage(error) - metricDataMap.ErrorName = error.name - } - - return putDataMetrics( - metricDataMap, - [ - ['DeploymentId'], - ['DeploymentId', 'Part'], - ['DeploymentId', 'Part', 'Saga'], - ['DeploymentId', 'Part', 'Saga', 'EventType'], - ], - [ - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName'], - ['DeploymentId', 'Part', 'Saga', 'EventType', 'ErrorName'], - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - [ - 'DeploymentId', - 'Part', - 'Saga', - 'EventType', - 'ErrorName', - 'ErrorMessage', - ], - ] - ) -} - -export const putInternalError = async (error) => { - const metricDataMap = { - DeploymentId: process.env.RESOLVE_DEPLOYMENT_ID, - Part: 'Internal', - ErrorMessage: getErrorMessage(error), - ErrorName: error.name, - } - - return putDataMetrics(metricDataMap, [ - ['DeploymentId', 'ErrorName'], - ['DeploymentId', 'Part', 'ErrorName'], - ['DeploymentId', 'ErrorName', 'ErrorMessage'], - ['DeploymentId', 'Part', 'ErrorName', 'ErrorMessage'], - ]) -} diff --git a/packages/runtime/runtime/src/cloud/monitoring.js b/packages/runtime/runtime/src/cloud/monitoring.js new file mode 100644 index 0000000000..ee8dbea36f --- /dev/null +++ b/packages/runtime/runtime/src/cloud/monitoring.js @@ -0,0 +1,277 @@ +import CloudWatch from 'aws-sdk/clients/cloudwatch' +import debugLevels from '@resolve-js/debug-levels' +import { retry } from 'resolve-cloud-common/utils' + +const MAX_DIMENSION_VALUE_LENGTH = 256 +const MAX_METRIC_COUNT = 20 +const MAX_DIMENSION_COUNT = 10 + +const getLog = (name) => debugLevels(`resolve:cloud:${name}`) + +const getErrorMessage = (error) => { + let errorMessage = error.message.split(/\n|\r|\r\n/g)[0] + + if (errorMessage.length > MAX_DIMENSION_VALUE_LENGTH) { + const messageEnd = '...' + + errorMessage = `${errorMessage.slice( + 0, + MAX_DIMENSION_VALUE_LENGTH - messageEnd.length + )}${messageEnd}` + } + + return errorMessage +} + +const createErrorDimensionsList = (error) => [ + [ + { Name: 'ErrorName', Value: error.name }, + { Name: 'ErrorMessage', Value: getErrorMessage(error) }, + ], + [{ Name: 'ErrorName', Value: error.name }], + [], +] + +const monitoringErrorCallback = async ( + log, + monitoringData, + groupData, + error +) => { + try { + log.verbose(`Collect error`) + + const dimensionsList = createErrorDimensionsList(error).reduce( + (acc, errorDimensions) => + acc.concat( + groupData.errorMetricDimensionsList.map((groupDimensions) => [ + ...groupDimensions, + ...errorDimensions, + ]) + ), + [] + ) + + const now = new Date() + let isDimensionCountLimitReached = false + + monitoringData.metricData = monitoringData.metricData.concat( + dimensionsList.reduce((acc, dimensions) => { + if (dimensions.length <= MAX_DIMENSION_COUNT) { + acc.push({ + MetricName: 'Errors', + Timestamp: now, + Unit: 'Count', + Value: 1, + Dimensions: dimensions, + }) + } else { + isDimensionCountLimitReached = true + } + + return acc + }, []) + ) + + if (isDimensionCountLimitReached) { + log.warn( + `Error collecting missed some or all metric data because of dimension count limit` + ) + } + } catch (error) { + log.verbose(`Failed to collect error`, error) + } +} + +const monitoringTimeCallback = async ( + log, + monitoringData, + groupData, + label, + timestamp = Date.now() +) => { + if (!Number.isFinite(timestamp)) { + log.warn( + `Timer '${label}' is not started because timestamp must be a finite number` + ) + return + } + + if (typeof groupData.timerMap[label] !== 'number') { + groupData.timerMap[label] = timestamp + } else { + log.warn(`Timer '${label}' already exists`) + } +} + +const monitoringTimeEndCallback = async ( + log, + monitoringData, + groupData, + label, + timestamp = Date.now() +) => { + if (!Number.isFinite(timestamp)) { + log.warn( + `Timer '${label}' is not ended because timestamp must be a finite number` + ) + return + } + + if (typeof groupData.timerMap[label] === 'number') { + const duration = timestamp - groupData.timerMap[label] + + const durationDimensions = [{ Name: 'Label', Value: label }] + const now = new Date() + + let isDimensionCountLimitReached = false + + monitoringData.metricData = monitoringData.metricData.concat( + groupData.durationMetricDimensionsList.reduce((acc, groupDimensions) => { + const dimensions = [...groupDimensions, ...durationDimensions] + + if (dimensions.length <= MAX_DIMENSION_COUNT) { + acc.push({ + MetricName: 'Duration', + Timestamp: now, + Unit: 'Milliseconds', + Value: duration, + Dimensions: [...groupDimensions, ...durationDimensions], + }) + } else { + isDimensionCountLimitReached = true + } + + return acc + }, []) + ) + + delete groupData.timerMap[label] + + if (isDimensionCountLimitReached) { + log.warn( + `Timer '${label}' missed some or all metric data because of dimension count limit` + ) + } + } else { + log.warn(`Timer '${label}' does not exist`) + } +} + +const monitoringPublishCallback = async (log, monitoringData) => { + try { + log.verbose(`Sending ${monitoringData.metricData.length} metrics`) + log.verbose(JSON.stringify(monitoringData.metricData)) + + const promises = [] + + const cw = new CloudWatch() + const putMetricData = retry(cw, cw.putMetricData) + + for ( + let i = 0; + i < monitoringData.metricData.length; + i += MAX_METRIC_COUNT + ) { + promises.push( + putMetricData({ + Namespace: 'RESOLVE_METRICS', + MetricData: monitoringData.metricData.slice(i, i + MAX_METRIC_COUNT), + }) + ) + } + + monitoringData.metricData = [] + + await Promise.all(promises) + + log.verbose(`Metrics data sent`) + } catch (e) { + log.warn(`Metrics data sending failed: ${e}`) + } +} + +const createGroupDimensions = (config) => + Object.keys(config).reduce( + (acc, key) => + config[key] != null + ? acc.concat({ + Name: key, + Value: config[key], + }) + : acc, + [] + ) + +const createMonitoringImplementation = (log, monitoringData, groupData) => { + return { + group: (config) => { + const groupDimensions = createGroupDimensions(config) + + const nextGroupData = { + timerMap: {}, + metricDimensions: groupData.metricDimensions.concat(groupDimensions), + durationMetricDimensionsList: groupData.durationMetricDimensionsList.map( + (dimensions) => [...dimensions, ...groupDimensions] + ), + errorMetricDimensionsList: [ + ...groupData.errorMetricDimensionsList, + groupData.errorMetricDimensionsList[ + groupData.errorMetricDimensionsList.length - 1 + ].concat(groupDimensions), + ], + } + + return createMonitoringImplementation(log, monitoringData, nextGroupData) + }, + error: monitoringErrorCallback.bind(null, log, monitoringData, groupData), + time: monitoringTimeCallback.bind(null, log, monitoringData, groupData), + timeEnd: monitoringTimeEndCallback.bind( + null, + log, + monitoringData, + groupData + ), + publish: monitoringPublishCallback.bind(null, log, monitoringData), + } +} + +const createDeploymentDimensions = (deploymentId, resolveVersion) => [ + [ + { Name: 'DeploymentId', Value: deploymentId }, + { Name: 'ResolveVersion', Value: resolveVersion }, + ], + [{ Name: 'ResolveVersion', Value: resolveVersion }], + [{ Name: 'DeploymentId', Value: deploymentId }], +] + +const createMonitoring = ({ deploymentId, resolveVersion }) => { + const monitoringData = { + metricData: [], + metricDimensions: createDeploymentDimensions(deploymentId, resolveVersion), + } + + const monitoringGroupData = { + timerMap: {}, + metricDimensions: [], + durationMetricDimensionsList: [ + [ + { Name: 'DeploymentId', Value: deploymentId }, + { Name: 'ResolveVersion', Value: resolveVersion }, + ], + [{ Name: 'ResolveVersion', Value: resolveVersion }], + [{ Name: 'DeploymentId', Value: deploymentId }], + ], + errorMetricDimensionsList: [ + [{ Name: 'DeploymentId', Value: deploymentId }], + ], + } + + return createMonitoringImplementation( + getLog('monitoring'), + monitoringData, + monitoringGroupData + ) +} + +export default createMonitoring diff --git a/packages/runtime/runtime/src/cloud/wrap-api-handler.js b/packages/runtime/runtime/src/cloud/wrap-api-handler.js index 8e9883fb77..0143cfdb05 100644 --- a/packages/runtime/runtime/src/cloud/wrap-api-handler.js +++ b/packages/runtime/runtime/src/cloud/wrap-api-handler.js @@ -312,11 +312,18 @@ const createResponse = () => { return Object.freeze(res) } -const wrapApiHandler = ( - handler, - getCustomParameters, - onError = async () => void 0 -) => async (lambdaEvent, lambdaContext, lambdaCallback) => { +const wrapApiHandler = (handler, getCustomParameters, monitoring) => async ( + lambdaEvent, + lambdaContext, + lambdaCallback +) => { + const startTimestamp = Date.now() + + if (monitoring != null) { + monitoring.time('Execution', startTimestamp) + } + + let pathMonitoring let result let isLambdaEdgeRequest let req @@ -327,6 +334,12 @@ const wrapApiHandler = ( : {} req = await createRequest(lambdaEvent, customParameters) + + if (monitoring != null) { + pathMonitoring = monitoring.group({ Path: req.path }) + pathMonitoring.time('Execution', startTimestamp) + } + const res = createResponse() await handler(req, res) @@ -365,8 +378,8 @@ const wrapApiHandler = ( ? `${error.stack}` : `Unknown error ${error}` - if (req != null) { - await onError(error, req.path) + if (pathMonitoring != null) { + pathMonitoring.error(error) } // eslint-disable-next-line no-console @@ -387,6 +400,16 @@ const wrapApiHandler = ( } } + const endTimestamp = Date.now() + + if (pathMonitoring != null) { + pathMonitoring.timeEnd('Execution', endTimestamp) + } + + if (monitoring != null) { + monitoring.timeEnd('Execution', endTimestamp) + } + if (typeof lambdaCallback === 'function') { return lambdaCallback(null, result) } else { diff --git a/packages/runtime/runtime/src/common/init-resolve.js b/packages/runtime/runtime/src/common/init-resolve.js index 496194fd6b..e5c43f6149 100644 --- a/packages/runtime/runtime/src/common/init-resolve.js +++ b/packages/runtime/runtime/src/common/init-resolve.js @@ -34,6 +34,7 @@ const initResolve = async (resolve) => { for (const name of Object.keys(readModelConnectorsCreators)) { readModelConnectors[name] = readModelConnectorsCreators[name]({ performanceTracer, + monitoring, }) } @@ -50,15 +51,10 @@ const initResolve = async (resolve) => { const getVacantTimeInMillis = resolve.getVacantTimeInMillis const onCommandExecuted = createOnCommandExecuted(resolve) - const domainMonitoring = { - error: monitoring?.error, - performance: performanceTracer, - } - const secretsManager = await eventstoreAdapter.getSecretsManager() const aggregateRuntime = { - monitoring: domainMonitoring, + monitoring, secretsManager, eventstore: eventstoreAdapter, hooks: { @@ -94,11 +90,11 @@ const initResolve = async (resolve) => { getVacantTimeInMillis, monitoring, readModelsInterop: domainInterop.readModelDomain.acquireReadModelsInterop({ - monitoring: domainMonitoring, + monitoring, secretsManager, }), viewModelsInterop: domainInterop.viewModelDomain.acquireViewModelsInterop({ - monitoring: domainMonitoring, + monitoring, eventstore: eventstoreAdapter, secretsManager, }), diff --git a/packages/runtime/runtime/src/common/on-command-executed.js b/packages/runtime/runtime/src/common/on-command-executed.js index a3f24802c9..3c7a89eb92 100644 --- a/packages/runtime/runtime/src/common/on-command-executed.js +++ b/packages/runtime/runtime/src/common/on-command-executed.js @@ -47,7 +47,15 @@ const notifyEventSubscribers = async (resolve) => { const inlineLedgerPromise = (async () => { const promises = [] for (const { name: eventListener } of resolve.eventListeners.values()) { - promises.push(resolve.invokeEventSubscriberAsync(eventListener, 'build')) + promises.push( + resolve.invokeEventSubscriberAsync(eventListener, 'build', { + initiator: 'command', + notificationId: `NT-${Date.now()}${Math.floor( + Math.random() * 1000000 + )}`, + sendTime: Date.now(), + }) + ) } const eventSubscribers = await resolve.eventstoreAdapter.getEventSubscribers() diff --git a/packages/runtime/runtime/src/common/query/wrap-read-model.ts b/packages/runtime/runtime/src/common/query/wrap-read-model.ts index 7fef29e6f3..701aa765fb 100644 --- a/packages/runtime/runtime/src/common/query/wrap-read-model.ts +++ b/packages/runtime/runtime/src/common/query/wrap-read-model.ts @@ -57,10 +57,16 @@ const read = async ( try { if (isDisposed) { const error = new Error(`Read model "${readModelName}" is disposed`) - await monitoring?.error?.(error, 'readModelResolver', { - readModelName, - resolverName, - }) + + if (monitoring != null) { + const monitoringGroup = monitoring + .group({ Part: 'ReadModelResolver' }) + .group({ ReadModel: readModelName }) + .group({ Resolver: resolverName }) + + monitoringGroup.error(error) + } + if (subSegment != null) { subSegment.addError(error) } @@ -92,7 +98,11 @@ const next = async ( if (args.length > 0) { throw new TypeError('Next should be invoked with no arguments') } - await pool.invokeEventSubscriberAsync(eventListener, 'build') + await pool.invokeEventSubscriberAsync(eventListener, 'build', { + initiator: 'read-model-next', + notificationId: `NT-${Date.now()}${Math.floor(Math.random() * 1000000)}`, + sendTime: Date.now(), + }) } const updateCustomReadModel = async ( @@ -581,6 +591,7 @@ const operationMethods = { next.bind(null, pool, readModelName), pool.eventstoreAdapter, pool.getVacantTimeInMillis, + parameters, ] ), diff --git a/packages/runtime/runtime/src/common/saga/index.js b/packages/runtime/runtime/src/common/saga/index.js index 90d7196e1e..95468f6c75 100644 --- a/packages/runtime/runtime/src/common/saga/index.js +++ b/packages/runtime/runtime/src/common/saga/index.js @@ -21,9 +21,9 @@ const createSaga = ({ const sagaMonitoring = monitoring != null ? { - error: async (error, part, meta) => { + error: (error, part, meta) => { if (monitoring.error != null) { - await monitoring.error(error, 'sagaProjection', meta) + monitoring.error(error, 'sagaProjection', meta) } }, } diff --git a/packages/runtime/runtime/test/cloud-entry.test.js b/packages/runtime/runtime/test/cloud-entry.test.js index c8dc79b13d..6106a685d9 100644 --- a/packages/runtime/runtime/test/cloud-entry.test.js +++ b/packages/runtime/runtime/test/cloud-entry.test.js @@ -364,7 +364,7 @@ describe('Cloud entry', () => { expect(JSON.parse(result.body)).toEqual({ aggregateId: 'aggregateId', aggregateVersion: 1, - timestamp: 1, + timestamp: 2, type: 'SET', payload: { key: 'key1', diff --git a/packages/runtime/runtime/test/metrics.test.js b/packages/runtime/runtime/test/cloud/metrics.test.js similarity index 96% rename from packages/runtime/runtime/test/metrics.test.js rename to packages/runtime/runtime/test/cloud/metrics.test.js index 6e953c9587..9ac368ec68 100644 --- a/packages/runtime/runtime/test/metrics.test.js +++ b/packages/runtime/runtime/test/cloud/metrics.test.js @@ -1,5 +1,6 @@ /* eslint-disable no-console */ -import { putDurationMetrics } from '../src/cloud/metrics' +import { putDurationMetrics } from '../../src/cloud/metrics' + import CloudWatch from 'aws-sdk/clients/cloudwatch' const lambdaContext = { @@ -8,15 +9,27 @@ const lambdaContext = { const consoleInfoOldHandler = console.info +let originalEnv + +beforeAll(() => { + originalEnv = process.env + process.env = { + ...originalEnv, + RESOLVE_DEPLOYMENT_ID: 'deployment-id', + } +}) + +afterAll(() => { + process.env = originalEnv +}) + describe('put duration metrics', () => { beforeAll(async () => { console.info = jest.fn() - process.env.RESOLVE_DEPLOYMENT_ID = 'deployment-id' }) afterAll(async () => { console.info = consoleInfoOldHandler - delete process.env.RESOLVE_DEPLOYMENT_ID }) beforeEach(async () => { diff --git a/packages/runtime/runtime/test/cloud/monitoring.test.js b/packages/runtime/runtime/test/cloud/monitoring.test.js new file mode 100644 index 0000000000..ad41baaf43 --- /dev/null +++ b/packages/runtime/runtime/test/cloud/monitoring.test.js @@ -0,0 +1,804 @@ +import CloudWatch from 'aws-sdk/clients/cloudwatch' + +import createMonitoring from '../../src/cloud/monitoring' + +afterEach(() => { + CloudWatch.putMetricData.mockClear() +}) + +let originalNow + +beforeEach(() => { + originalNow = Date.now + Date.now = jest.fn().mockReturnValue(1234) +}) + +afterEach(() => { + Date.now = originalNow +}) + +describe('common', () => { + test('sends correct metric data on publish', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.error(new Error('test')) + + await monitoring.publish() + + expect(CloudWatch.putMetricData).toBeCalledTimes(1) + + expect(CloudWatch.putMetricData).toBeCalledWith({ + Namespace: 'RESOLVE_METRICS', + MetricData: expect.any(Array), + }) + }) + + test('does nothing with no metric data on publish', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + await monitoring.publish() + + expect(CloudWatch.putMetricData).toBeCalledTimes(0) + }) + + test('sends metric data multiple times on multiple publish call', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.error(new Error('test-1')) + await monitoring.publish() + + monitoring.error(new Error('test-2')) + await monitoring.publish() + + expect(CloudWatch.putMetricData).toBeCalledTimes(2) + + expect(CloudWatch.putMetricData).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'Error' }, + { Name: 'ErrorMessage', Value: 'test-1' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).not.toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'Error' }, + { Name: 'ErrorMessage', Value: 'test-2' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'Error' }, + { Name: 'ErrorMessage', Value: 'test-2' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).not.toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'Error' }, + { Name: 'ErrorMessage', Value: 'test-1' }, + ], + }), + ]), + }) + ) + }) + + test('combines multiple metric data', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.error(new Error('test-error')) + monitoring.time('test', 500) + monitoring.timeEnd('test', 800) + + await monitoring.publish() + + expect(CloudWatch.putMetricData).toBeCalledTimes(1) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + { + MetricName: 'Errors', + Unit: 'Count', + Value: 1, + Timestamp: expect.any(Date), + Dimensions: expect.any(Array), + }, + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + { + MetricName: 'Duration', + Unit: 'Milliseconds', + Value: 300, + Timestamp: expect.any(Date), + Dimensions: expect.any(Array), + }, + ]), + }) + ) + }) + + test('splits metrics sending if metric data array has length more than 20', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + for (let i = 0; i < 8; i++) { + monitoring.error(new Error(`test-${i + 1}`)) + } + + await monitoring.publish() + + expect(CloudWatch.putMetricData).toBeCalledTimes(2) + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength( + 20 + ) + + expect(CloudWatch.putMetricData.mock.calls[1][0].MetricData).toHaveLength(4) + }) + + test('does not reject on publish if putMetricData is failed', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.error(new Error('test')) + + CloudWatch.putMetricData.mockReturnValueOnce({ + promise: () => Promise.reject(new Error('Something went wrong')), + }) + + await monitoring.publish() + }) +}) + +describe('error', () => { + test('sends correct metrics base data', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }).group({ 'test-group-name': 'test-group' }) + + class TestError extends Error { + name = 'test-error' + } + + monitoring.error(new TestError('test-message')) + + await monitoring.publish() + + for (const metricData of CloudWatch.putMetricData.mock.calls[0][0] + .MetricData) { + expect(metricData).toEqual({ + MetricName: 'Errors', + Unit: 'Count', + Value: 1, + Timestamp: expect.any(Date), + Dimensions: expect.any(Array), + }) + } + }) + + test('contains default dimensions', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + class TestError extends Error { + name = 'test-error' + } + + monitoring.error(new TestError('test-message')) + + await monitoring.publish() + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength(3) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'test-error' }, + { Name: 'ErrorMessage', Value: 'test-message' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'test-error' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [{ Name: 'DeploymentId', Value: 'test-deployment' }], + }), + ]), + }) + ) + }) + + test('contains default and group dimensions', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + const groupMonitoring = monitoring.group({ + 'test-group-name': 'test-group', + }) + + class TestError extends Error { + name = 'test-error' + } + + groupMonitoring.error(new TestError('test-message')) + + await monitoring.publish() + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength(6) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'test-group-name', Value: 'test-group' }, + { Name: 'ErrorName', Value: 'test-error' }, + { Name: 'ErrorMessage', Value: 'test-message' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'test-group-name', Value: 'test-group' }, + { Name: 'ErrorName', Value: 'test-error' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'test-group-name', Value: 'test-group' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'test-error' }, + { Name: 'ErrorMessage', Value: 'test-message' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'test-error' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [{ Name: 'DeploymentId', Value: 'test-deployment' }], + }), + ]), + }) + ) + }) + + test('contains default and group dimensions for multiple group calls', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + const groupMonitoring = monitoring + .group({ + 'first-group-name': 'first-group', + }) + .group({ + 'second-group-name': 'second-group', + }) + + class TestError extends Error { + name = 'test-error' + } + + groupMonitoring.error(new TestError('test-message')) + + await monitoring.publish() + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength(9) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'first-group-name', Value: 'first-group' }, + { Name: 'second-group-name', Value: 'second-group' }, + { Name: 'ErrorName', Value: 'test-error' }, + { Name: 'ErrorMessage', Value: 'test-message' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'first-group-name', Value: 'first-group' }, + { Name: 'second-group-name', Value: 'second-group' }, + { Name: 'ErrorName', Value: 'test-error' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'first-group-name', Value: 'first-group' }, + { Name: 'second-group-name', Value: 'second-group' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'first-group-name', Value: 'first-group' }, + { Name: 'ErrorName', Value: 'test-error' }, + { Name: 'ErrorMessage', Value: 'test-message' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'first-group-name', Value: 'first-group' }, + { Name: 'ErrorName', Value: 'test-error' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'first-group-name', Value: 'first-group' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'test-error' }, + { Name: 'ErrorMessage', Value: 'test-message' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'test-error' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [{ Name: 'DeploymentId', Value: 'test-deployment' }], + }), + ]), + }) + ) + }) + + test('contains correct dimensions if multiple errors are passed', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + class TestError extends Error { + name = 'test-error' + } + + monitoring.error(new TestError('test-message-1')) + monitoring.error(new TestError('test-message-2')) + + await monitoring.publish() + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength(6) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ErrorName', Value: 'test-error' }, + { Name: 'ErrorMessage', Value: 'test-message-2' }, + ], + }), + ]), + }) + ) + }) +}) + +describe('duration', () => { + test('sends correct metrics base data', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.time('test-label', 1000) + monitoring.timeEnd('test-label', 2000) + + await monitoring.publish() + + expect(CloudWatch.putMetricData).toBeCalledTimes(1) + + expect(CloudWatch.putMetricData).toBeCalledWith({ + Namespace: 'RESOLVE_METRICS', + MetricData: expect.any(Array), + }) + + for (const metricData of CloudWatch.putMetricData.mock.calls[0][0] + .MetricData) { + expect(metricData).toEqual({ + MetricName: 'Duration', + Unit: 'Milliseconds', + Value: 1000, + Timestamp: expect.any(Date), + Dimensions: expect.any(Array), + }) + } + }) + + test('contains default dimensions', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.time('test-label', 1000) + monitoring.timeEnd('test-label', 2000) + + await monitoring.publish() + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength(3) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'Label', Value: 'test-label' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'ResolveVersion', Value: '1.0.0-test' }, + { Name: 'Label', Value: 'test-label' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ResolveVersion', Value: '1.0.0-test' }, + { Name: 'Label', Value: 'test-label' }, + ], + }), + ]), + }) + ) + }) + + test('contains default and group dimensions', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + const monitoringGroup = monitoring.group({ + 'test-group': 'test-group-name', + }) + + monitoringGroup.time('test-label', 1000) + monitoringGroup.timeEnd('test-label', 2000) + + await monitoring.publish() + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength(3) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'test-group', Value: 'test-group-name' }, + { Name: 'Label', Value: 'test-label' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'ResolveVersion', Value: '1.0.0-test' }, + { Name: 'test-group', Value: 'test-group-name' }, + { Name: 'Label', Value: 'test-label' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'ResolveVersion', Value: '1.0.0-test' }, + { Name: 'test-group', Value: 'test-group-name' }, + { Name: 'Label', Value: 'test-label' }, + ], + }), + ]), + }) + ) + }) + + test('sends correct duration metrics with specified timestamps', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.time('test-label', 3000) + monitoring.timeEnd('test-label', 5000) + + await monitoring.publish() + + for (const metricData of CloudWatch.putMetricData.mock.calls[0][0] + .MetricData) { + expect(metricData).toEqual({ + MetricName: 'Duration', + Unit: 'Milliseconds', + Value: 2000, + Timestamp: expect.any(Date), + Dimensions: expect.any(Array), + }) + } + }) + + test('sends correct duration metrics using Date.now', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + Date.now.mockReturnValueOnce(15000).mockReturnValueOnce(19500) + + monitoring.time('test-label') + monitoring.timeEnd('test-label') + + await monitoring.publish() + + for (const metricData of CloudWatch.putMetricData.mock.calls[0][0] + .MetricData) { + expect(metricData).toEqual({ + MetricName: 'Duration', + Unit: 'Milliseconds', + Value: 4500, + Timestamp: expect.any(Date), + Dimensions: expect.any(Array), + }) + } + }) + + test('sends correct metrics with multiple labels', async () => { + const monitoring = createMonitoring({ + deploymentId: 'test-deployment', + resolveVersion: '1.0.0-test', + }) + + monitoring.time('test-label-1', 1000) + monitoring.timeEnd('test-label-1', 2000) + + monitoring.time('test-label-2', 1000) + monitoring.timeEnd('test-label-2', 2000) + + await monitoring.publish() + + expect(CloudWatch.putMetricData.mock.calls[0][0].MetricData).toHaveLength(6) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'Label', Value: 'test-label-1' }, + ], + }), + ]), + }) + ) + + expect(CloudWatch.putMetricData).toBeCalledWith( + expect.objectContaining({ + MetricData: expect.arrayContaining([ + expect.objectContaining({ + Dimensions: [ + { Name: 'DeploymentId', Value: 'test-deployment' }, + { Name: 'Label', Value: 'test-label-2' }, + ], + }), + ]), + }) + ) + }) +}) diff --git a/packages/tools/scripts/src/alias/$resolve.resolveVersion.js b/packages/tools/scripts/src/alias/$resolve.resolveVersion.js new file mode 100644 index 0000000000..5212bd6f4e --- /dev/null +++ b/packages/tools/scripts/src/alias/$resolve.resolveVersion.js @@ -0,0 +1,17 @@ +import resolveFileOrModule from '../resolve_file_or_module' + +export default () => { + const exports = [] + + const runtimePackageJson = require(resolveFileOrModule( + '@resolve-js/runtime/package.json' + )) + + exports.push( + `const resolveVersion = ${JSON.stringify(runtimePackageJson.version)}`, + ``, + `export default resolveVersion` + ) + + return exports.join('\r\n') +} diff --git a/packages/tools/scripts/src/alias/$resolve.serverAssemblies.js b/packages/tools/scripts/src/alias/$resolve.serverAssemblies.js index e8a3fc8691..09a5b5acd0 100644 --- a/packages/tools/scripts/src/alias/$resolve.serverAssemblies.js +++ b/packages/tools/scripts/src/alias/$resolve.serverAssemblies.js @@ -2,6 +2,7 @@ export default () => ` import '$resolve.guardOnlyServer' import interopRequireDefault from "@babel/runtime/helpers/interopRequireDefault" import constants from '$resolve.constants' + import resolveVersion from '$resolve.resolveVersion' const assemblies = Object.create(Object.prototype, { seedClientEnvs: { @@ -72,6 +73,7 @@ export default () => ` export default { assemblies, constants, - domain + domain, + resolveVersion } ` diff --git a/packages/tools/scripts/test/__snapshots__/get-webpack-configs.test.js.snap b/packages/tools/scripts/test/__snapshots__/get-webpack-configs.test.js.snap index 4b40e3e6b2..863675f868 100644 --- a/packages/tools/scripts/test/__snapshots__/get-webpack-configs.test.js.snap +++ b/packages/tools/scripts/test/__snapshots__/get-webpack-configs.test.js.snap @@ -56,6 +56,7 @@ exports[`should make webpack configs for cloud mode 1`] = ` \\"$resolve.port\\": \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"$resolve.readModelConnectors\\": \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"$resolve.readModels\\": \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"$resolve.resolveVersion\\": \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"$resolve.rootPath\\": \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"$resolve.sagas\\": \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"$resolve.seedClientEnvs\\": \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -90,6 +91,7 @@ exports[`should make webpack configs for cloud mode 1`] = ` \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -260,6 +262,7 @@ exports[`should make webpack configs for cloud mode 1`] = ` \\"$resolve.port\\": \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"$resolve.readModelConnectors\\": \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"$resolve.readModels\\": \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"$resolve.resolveVersion\\": \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"$resolve.rootPath\\": \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"$resolve.sagas\\": \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"$resolve.seedClientEnvs\\": \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -301,6 +304,7 @@ exports[`should make webpack configs for cloud mode 1`] = ` \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -479,6 +483,7 @@ exports[`should make webpack configs for cloud mode 1`] = ` \\"$resolve.port\\": \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"$resolve.readModelConnectors\\": \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"$resolve.readModels\\": \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"$resolve.resolveVersion\\": \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"$resolve.rootPath\\": \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"$resolve.sagas\\": \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"$resolve.seedClientEnvs\\": \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -521,6 +526,7 @@ exports[`should make webpack configs for cloud mode 1`] = ` \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -727,6 +733,7 @@ exports[`should make webpack configs for local mode 1`] = ` \\"$resolve.port\\": \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"$resolve.readModelConnectors\\": \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"$resolve.readModels\\": \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"$resolve.resolveVersion\\": \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"$resolve.rootPath\\": \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"$resolve.sagas\\": \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"$resolve.seedClientEnvs\\": \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -761,6 +768,7 @@ exports[`should make webpack configs for local mode 1`] = ` \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -931,6 +939,7 @@ exports[`should make webpack configs for local mode 1`] = ` \\"$resolve.port\\": \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"$resolve.readModelConnectors\\": \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"$resolve.readModels\\": \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"$resolve.resolveVersion\\": \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"$resolve.rootPath\\": \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"$resolve.sagas\\": \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"$resolve.seedClientEnvs\\": \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -972,6 +981,7 @@ exports[`should make webpack configs for local mode 1`] = ` \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -1148,6 +1158,7 @@ exports[`should make webpack configs for local mode 1`] = ` \\"$resolve.port\\": \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"$resolve.readModelConnectors\\": \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"$resolve.readModels\\": \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"$resolve.resolveVersion\\": \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"$resolve.rootPath\\": \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"$resolve.sagas\\": \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"$resolve.seedClientEnvs\\": \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", @@ -1190,6 +1201,7 @@ exports[`should make webpack configs for local mode 1`] = ` \\"/packages/tools/scripts/src/alias/$resolve.port.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModelConnectors.js\\", \\"/packages/tools/scripts/src/alias/$resolve.readModels.js\\", + \\"/packages/tools/scripts/src/alias/$resolve.resolveVersion.js\\", \\"/packages/tools/scripts/src/alias/$resolve.rootPath.js\\", \\"/packages/tools/scripts/src/alias/$resolve.sagas.js\\", \\"/packages/tools/scripts/src/alias/$resolve.seedClientEnvs.js\\", diff --git a/packages/tools/scripts/test/alias/__snapshots__/$resolve.serverAssemblies.test.js.snap b/packages/tools/scripts/test/alias/__snapshots__/$resolve.serverAssemblies.test.js.snap index 88e326268f..559bab5c49 100644 --- a/packages/tools/scripts/test/alias/__snapshots__/$resolve.serverAssemblies.test.js.snap +++ b/packages/tools/scripts/test/alias/__snapshots__/$resolve.serverAssemblies.test.js.snap @@ -6,6 +6,7 @@ exports[`works correctly 1`] = ` import '$resolve.guardOnlyServer' import interopRequireDefault from \\"@babel/runtime/helpers/interopRequireDefault\\" import constants from '$resolve.constants' + import resolveVersion from '$resolve.resolveVersion' const assemblies = Object.create(Object.prototype, { seedClientEnvs: { @@ -76,7 +77,8 @@ exports[`works correctly 1`] = ` export default { assemblies, constants, - domain + domain, + resolveVersion } " diff --git a/packages/tools/testing-tools/src/flow/aggregate/make-test-environment.ts b/packages/tools/testing-tools/src/flow/aggregate/make-test-environment.ts index 8a444846e0..6266fb4a95 100644 --- a/packages/tools/testing-tools/src/flow/aggregate/make-test-environment.ts +++ b/packages/tools/testing-tools/src/flow/aggregate/make-test-environment.ts @@ -92,7 +92,6 @@ export const makeTestEnvironment = ( aggregatesInterop: domain.aggregateDomain.acquireAggregatesInterop({ eventstore: getEventStore(events, { aggregateId }), secretsManager, - monitoring: {}, hooks: {}, }), }) diff --git a/packages/tools/testing-tools/src/flow/read-model/make-test-environment.ts b/packages/tools/testing-tools/src/flow/read-model/make-test-environment.ts index f4fd774a3f..f13437d441 100644 --- a/packages/tools/testing-tools/src/flow/read-model/make-test-environment.ts +++ b/packages/tools/testing-tools/src/flow/read-model/make-test-environment.ts @@ -3,7 +3,6 @@ import { initDomain, Monitoring, SecretsManager, - MonitoringPart, } from '@resolve-js/core' import { createQuery } from '@resolve-js/runtime' import { @@ -96,13 +95,24 @@ export const makeTestEnvironment = ( }) const liveErrors: Array = [] - const monitoring: Monitoring = { - error: async (error: Error, part: MonitoringPart) => { - if (part === 'readModelProjection') { - liveErrors.push(error) - } - }, + + const makeMonitoring = ( + error: Monitoring['error'] = () => void 0 + ): Monitoring => { + return { + group: (config) => + config.Part === 'ReadModelProjection' + ? makeMonitoring((error: Error) => { + liveErrors.push(error) + }) + : makeMonitoring(error), + time: () => void 0, + timeEnd: () => void 0, + error, + publish: async () => void 0, + } } + const eventstoreAdapter = getEventStore(events) const errors = [] @@ -121,7 +131,7 @@ export const makeTestEnvironment = ( eventstoreAdapter, readModelsInterop: domain.readModelDomain.acquireReadModelsInterop({ secretsManager, - monitoring, + monitoring: makeMonitoring(), }), viewModelsInterop: {}, performanceTracer: null, diff --git a/packages/tools/testing-tools/src/flow/saga/make-test-environment.ts b/packages/tools/testing-tools/src/flow/saga/make-test-environment.ts index a1f44d59bd..2efbe5f75a 100644 --- a/packages/tools/testing-tools/src/flow/saga/make-test-environment.ts +++ b/packages/tools/testing-tools/src/flow/saga/make-test-environment.ts @@ -2,7 +2,6 @@ import { EventHandlerEncryptionFactory, initDomain, Monitoring, - MonitoringPart, SecretsManager, } from '@resolve-js/core' import { createQuery } from '@resolve-js/runtime' @@ -177,11 +176,22 @@ export const makeTestEnvironment = ( const liveErrors: Array = [] const monitoring: Monitoring = { - error: async (error: Error, part: MonitoringPart) => { - if (part === 'readModelProjection') { - liveErrors.push(error) + group: (config: Record) => { + if (config.Part !== 'ReadModelProjection') { + return monitoring + } + + return { + ...monitoring, + error: (error: Error) => { + liveErrors.push(error) + }, } }, + time: () => void 0, + timeEnd: () => void 0, + error: () => void 0, + publish: async () => void 0, } const eventstoreAdapter = getEventStore(events) diff --git a/tests/eventstore-init-drop/index.test.ts b/tests/eventstore-init-drop/index.test.ts index 4a29e7244c..c3f4f01917 100644 --- a/tests/eventstore-init-drop/index.test.ts +++ b/tests/eventstore-init-drop/index.test.ts @@ -3,7 +3,9 @@ import { EventstoreResourceNotExistError, } from '@resolve-js/eventstore-base' -import { adapterFactory, adapters } from '../eventstore-test-utils' +import { adapterFactory, adapters, jestTimeout } from '../eventstore-test-utils' + +jest.setTimeout(jestTimeout()) describe(`${adapterFactory.name}. Eventstore adapter init and drop`, () => { beforeAll(adapterFactory.create('init_and_drop_testing'))