From 1e6e93215897f13367c99008b7932bf6f34dbd7c Mon Sep 17 00:00:00 2001 From: Timofey Traynenkov Date: Tue, 13 Jul 2021 14:32:17 +0300 Subject: [PATCH] Add more monitoring functional tests (#1941) * Update PR CI workflow * Rework monitoring tests to pass more info into metrics search * Improve tests readability. Add commands tests * Fix command tests * Improve read model resolver tests * Implement internal errors tests * Implement api handler tests * Delete redundant metrics * Add view model resolver tests * Add view model projection tests * Add yarn.lock change after install check * Delete one package from yarn.lock * Improve yarn.lock check * Revert "Delete one package from yarn.lock" This reverts commit b56f465b * Run prettier * Fix lint * Fix CI * Fix view models monitoring functional tests --- .eslintrc.js | 1 + .github/workflows/pr-dev.yml | 75 +- .prettierignore | 2 + .../hacker-news/client/components/Comment.tsx | 2 +- functional-tests/api/monitoring.test.ts | 1173 ++++++++++++++--- .../common/aggregates/monitoring.commands.js | 19 +- .../app/common/api-handlers/fail-api.js | 5 + functional-tests/app/common/event-types.js | 2 + .../read-models/monitoring.projection.js | 10 +- .../read-models/monitoring.resolvers.js | 7 +- .../view-models/init-failed.projection.js | 7 + .../view-models/monitoring.projection.js | 10 + .../view-models/resolver-failed.projection.js | 5 + .../view-models/resolver-failed.resolver.js | 5 + functional-tests/app/config.app.js | 19 + functional-tests/package.json | 1 + functional-tests/utils/utils.ts | 28 + .../get-view-models-interop-builder.ts | 25 +- .../readmodel-base/src/index.ts | 2 +- packages/runtime/runtime/src/cloud/metrics.js | 2 +- .../runtime/runtime/src/cloud/monitoring.js | 2 +- .../runtime/src/cloud/wrap-api-handler.js | 5 +- .../runtime/test/cloud/metrics.test.js | 12 +- .../runtime/test/cloud/monitoring.test.js | 12 +- yarn.lock | 38 + 25 files changed, 1224 insertions(+), 245 deletions(-) create mode 100644 functional-tests/app/common/api-handlers/fail-api.js create mode 100644 functional-tests/app/common/view-models/init-failed.projection.js create mode 100644 functional-tests/app/common/view-models/monitoring.projection.js create mode 100644 functional-tests/app/common/view-models/resolver-failed.projection.js create mode 100644 functional-tests/app/common/view-models/resolver-failed.resolver.js diff --git a/.eslintrc.js b/.eslintrc.js index 323425f1cb..5506a169fe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -468,6 +468,7 @@ module.exports = { 'undef', 'unfetch', 'unicode', + 'uint', 'unlink', 'unmarshall', 'unmocked', diff --git a/.github/workflows/pr-dev.yml b/.github/workflows/pr-dev.yml index b01dfeab56..2f00d72b1f 100644 --- a/.github/workflows/pr-dev.yml +++ b/.github/workflows/pr-dev.yml @@ -25,7 +25,8 @@ jobs: uses: actions/cache@v2 with: path: 'node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-root + key: ${{ runner.os }}-modules-root-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} + restore-keys: ${{ runner.os }}-modules-root- - name: Cache packages node_modules uses: actions/cache@v2 @@ -33,7 +34,8 @@ jobs: path: | packages/**/node_modules internal/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-packages + key: ${{ runner.os }}-modules-packages-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} + restore-keys: ${{ runner.os }}-modules-packages- - name: Cache examples node_modules uses: actions/cache@v2 @@ -41,13 +43,15 @@ jobs: path: | examples/**/node_modules templates/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-examples + key: ${{ runner.os }}-modules-examples-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} + restore-keys: ${{ runner.os }}-modules-examples- - name: Cache functional tests node_modules uses: actions/cache@v2 with: path: 'functional-tests/**/node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-functional-tests + key: ${{ runner.os }}-modules-functional-tests-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} + restore-keys: ${{ runner.os }}-modules-functional-tests- - name: Cache tests and website node_modules uses: actions/cache@v2 @@ -55,7 +59,8 @@ jobs: path: | tests/**/node_modules website/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-tests + key: ${{ runner.os }}-modules-tests-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} + restore-keys: ${{ runner.os }}-modules-tests- - name: Cache build uses: actions/cache@v2 @@ -72,12 +77,20 @@ jobs: internal/**/*.tsbuildinfo !internal/**/node_modules/** .packages/** - key: ${{ runner.os }}-${{ github.run_id }}-build + key: ${{ runner.os }}-build-${{ github.run_id }} - name: Install - run: | - yarn install --frozen-lockfile - yarn validate-lock-file + run: yarn install + + - name: Check if yarn.lock changed + run: echo "git_diff=$(git diff --name-only yarn.lock)" >> $GITHUB_ENV + + - name: Fail job if yarn.lock changed + if: env.git_diff + run: exit 1 + + - name: Validate yarn.lock + run: yarn validate-lock-file - name: Run Prettier run: yarn prettier:check @@ -117,7 +130,7 @@ jobs: uses: actions/cache@v2 with: path: 'node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-root + key: ${{ runner.os }}-modules-root-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache packages node_modules uses: actions/cache@v2 @@ -125,7 +138,7 @@ jobs: path: | packages/**/node_modules internal/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-packages + key: ${{ runner.os }}-modules-packages-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache tests and website node_modules uses: actions/cache@v2 @@ -133,7 +146,7 @@ jobs: path: | tests/**/node_modules website/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-tests + key: ${{ runner.os }}-modules-tests-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache build uses: actions/cache@v2 @@ -150,7 +163,7 @@ jobs: internal/**/*.tsbuildinfo !internal/**/node_modules/** .packages/** - key: ${{ runner.os }}-${{ github.run_id }}-build + key: ${{ runner.os }}-build-${{ github.run_id }} - name: Install run: | @@ -184,7 +197,7 @@ jobs: uses: actions/cache@v2 with: path: 'node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-root + key: ${{ runner.os }}-modules-root-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache packages node_modules uses: actions/cache@v2 @@ -192,7 +205,7 @@ jobs: path: | packages/**/node_modules internal/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-packages + key: ${{ runner.os }}-modules-packages-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache examples node_modules uses: actions/cache@v2 @@ -200,7 +213,7 @@ jobs: path: | examples/**/node_modules templates/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-examples + key: ${{ runner.os }}-modules-examples-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache build uses: actions/cache@v2 @@ -217,7 +230,7 @@ jobs: internal/**/*.tsbuildinfoF !internal/**/node_modules/** .packages/** - key: ${{ runner.os }}-${{ github.run_id }}-build + key: ${{ runner.os }}-build-${{ github.run_id }} - name: Install run: | @@ -249,7 +262,7 @@ jobs: uses: actions/cache@v2 with: path: 'node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-root + key: ${{ runner.os }}-modules-root-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache packages node_modules uses: actions/cache@v2 @@ -257,7 +270,7 @@ jobs: path: | packages/**/node_modules internal/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-packages + key: ${{ runner.os }}-modules-packages-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache examples node_modules uses: actions/cache@v2 @@ -265,13 +278,13 @@ jobs: path: | examples/**/node_modules templates/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-examples + key: ${{ runner.os }}-modules-examples-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache functional tests node_modules uses: actions/cache@v2 with: path: 'functional-tests/**/node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }}-modules-functional-tests + key: ${{ runner.os }}-modules-functional-tests-${{ hashFiles('**/yarn.lock', '!**/node_modules/**') }} - name: Cache build uses: actions/cache@v2 @@ -288,7 +301,7 @@ jobs: internal/**/*.tsbuildinfo !internal/**/node_modules/** .packages/** - key: ${{ runner.os }}-${{ github.run_id }}-build + key: ${{ runner.os }}-build-${{ github.run_id }} - name: Install run: | @@ -327,14 +340,6 @@ jobs: token: ${{ secrets.RESOLVE_BOT_PAT }} scopes: '@resolve-js' - - name: Integration Test PostgreSQL Serverless - env: - AWS_ACCESS_KEY_ID: ${{ secrets.TEST_CLOUD_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_CLOUD_AWS_SECRET_ACCESS_KEY }} - AWS_RDS_CLUSTER_ARN: ${{ steps.install_cloud.outputs.system_cluster_arn }} - AWS_RDS_ADMIN_SECRET_ARN: ${{ steps.install_cloud.outputs.postgres_admin_secret_arn }} - run: yarn test:integration-postgres-serverless - - name: Prepare test application run: | test_app_dir=$(mktemp -d -t test-app-XXXXXXXXX) @@ -374,6 +379,8 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_CLOUD_AWS_SECRET_ACCESS_KEY }} AWS_REGION: eu-central-1 RESOLVE_TESTS_TARGET_DEPLOYMENT_ID: ${{ steps.deploy.outputs.id }} + RESOLVE_TESTS_TARGET_VERSION: ${{ steps.publish.outputs.version }} + RESOLVE_TESTS_TARGET_STAGE: framework-test run: | cd functional-tests yarn run-test api --url=${{ steps.deploy.outputs.url }} @@ -382,3 +389,11 @@ jobs: run: | cd functional-tests yarn run-test testcafe --url=${{ steps.deploy.outputs.url }} --testcafe-browser=chrome --ci-mode --testcafe-timeout=10000 + + - name: Integration Test PostgreSQL Serverless + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TEST_CLOUD_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_CLOUD_AWS_SECRET_ACCESS_KEY }} + AWS_RDS_CLUSTER_ARN: ${{ steps.install_cloud.outputs.system_cluster_arn }} + AWS_RDS_ADMIN_SECRET_ARN: ${{ steps.install_cloud.outputs.postgres_admin_secret_arn }} + run: yarn test:integration-postgres-serverless diff --git a/.prettierignore b/.prettierignore index b07dd215ee..e5a6cb6306 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,8 +4,10 @@ packages/**/dist examples/**/dist examples/**/dist-replica examples/**/lib +templates/**/dist examples/shopping-list-advanced/native/resolve/ packages/core/zeromq/optional **/*.d.ts packages/core/react-hooks/src/index.ts website/** +functional-tests/app/**/dist diff --git a/examples/ts/hacker-news/client/components/Comment.tsx b/examples/ts/hacker-news/client/components/Comment.tsx index b7d8674616..ee07ff1fe9 100644 --- a/examples/ts/hacker-news/client/components/Comment.tsx +++ b/examples/ts/hacker-news/client/components/Comment.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react' +import React, { useState } from 'react' import sanitizer from 'sanitizer' import styled from 'styled-components' diff --git a/functional-tests/api/monitoring.test.ts b/functional-tests/api/monitoring.test.ts index 4b98a9bbc0..5fcce6e2ae 100644 --- a/functional-tests/api/monitoring.test.ts +++ b/functional-tests/api/monitoring.test.ts @@ -1,20 +1,60 @@ import { Client } from '@resolve-js/client' import { CloudWatch } from '@aws-sdk/client-cloudwatch' -import { getClient } from '../utils/utils' +import { Lambda } from '@aws-sdk/client-lambda' +import { getClient, getTargetURL } from '../utils/utils' import { isEqual } from 'lodash' import { customAlphabet } from 'nanoid' +import { parse as parseVersion } from 'semver' -type BaseMetrics = { - Errors: { - readModelProjection: { - Init: number - EventHandler: number - } - readModelResolver: { - resolverA: number - resolverB: number - } - } +interface CommandBaseMetrics { + partErrors: number + commandErrors: number + partExecutions: number + commandExecutions: number + executionDurationSamples: number +} + +type ResolverBaseMetrics = { + partErrors: number + resolverErrors: number + partExecutions: number + resolverExecutions: number + executionDurationSamples: number +} + +type SingleViewModelBaseMetrics = { + resolverErrors: number + resolverExecutions: number + resolverExecutionDurationSamples: number + projectionErrors: number +} + +type ViewModelBaseMetrics = { + resolverPartErrors: number + resolverPartExecutions: number + projectionPartErrors: number + + monitoring: SingleViewModelBaseMetrics + initFailed: SingleViewModelBaseMetrics + resolverFailed: SingleViewModelBaseMetrics +} + +type ApiHandlerBaseMetrics = { + partErrors: number + apiHandlerErrors: number + partExecutions: number + apiHandlerExecutions: number + executionDurationSamples: number +} + +interface InternalBaseMetrics { + partErrors: number + globalErrors: number +} + +interface Dimension { + Name: string + Value: string } const nanoid = customAlphabet('0123456789abcdef_', 16) @@ -24,38 +64,37 @@ const attemptPeriod = 2000 // eslint-disable-next-line spellcheck/spell-checker let deploymentId: string let cw: CloudWatch +let lambda: Lambda let client: Client let startTime: Date let endTime: Date -let baseMetrics: BaseMetrics +let commandBaseMetrics: CommandBaseMetrics +let readModelResolverBaseMetrics: ResolverBaseMetrics +let viewModelBaseMetrics: ViewModelBaseMetrics +let apiHandlerBaseMetrics: ApiHandlerBaseMetrics +let internalBaseMetrics: InternalBaseMetrics -const getMetricData = async ( - part: string, - ...dimensions: Array -): Promise => { +const getMetricData = async ({ + MetricName, + Stat, + Dimensions, +}: { + MetricName: string + Stat: string + Dimensions: Array +}): Promise => { const data = await cw.getMetricData({ MetricDataQueries: [ { Id: `q${nanoid()}`, MetricStat: { Metric: { - Namespace: 'RESOLVE_METRICS', - MetricName: 'Errors', - Dimensions: [ - { - Name: 'DeploymentId', - Value: deploymentId, - }, - { - Name: 'Part', - Value: part, - }, - ...dimensions, - ], + Namespace: 'ResolveJs', + MetricName, + Dimensions, }, - Stat: 'Sum', + Stat, Period: 31536000, // year - Unit: 'Count', }, }, ], @@ -70,214 +109,992 @@ const getMetricData = async ( if (valueCount === 1) { return data.MetricDataResults?.[0]?.Values?.[0] as number } - throw Error(`multiple metric ${part} values received`) + throw Error(`multiple metric ${MetricName} values received`) } -const collectBaseMetrics = async (): Promise => { - const metrics = await Promise.all([ - getMetricData( - 'ReadModelProjection', - { - Name: 'ReadModel', - Value: 'init-failed', - }, - { - Name: 'EventType', - Value: 'Init', - } - ), - getMetricData( - 'ReadModelProjection', - { - Name: 'ReadModel', - Value: 'monitoring', - }, - { - Name: 'EventType', - Value: 'MONITORING_FAILED_HANDLER', - } - ), - getMetricData( - 'ReadModelResolver', - { - Name: 'ReadModel', - Value: 'monitoring', - }, - { - Name: 'Resolver', - Value: 'resolverA', - } - ), - getMetricData( - 'ReadModelResolver', - { - Name: 'ReadModel', - Value: 'monitoring', - }, - { - Name: 'Resolver', - Value: 'resolverB', - } - ), +const createDimensions = (list: string[]): Dimension[] => + list.map((item) => { + const temp = item.split('=') + + return { + Name: temp[0], + Value: temp[1], + } + }) + +const collectReadModelResolverBaseMetrics = async (): Promise => { + const [ + partErrors, + resolverErrors, + partExecutions, + resolverExecutions, + executionDurationSamples, + ] = await Promise.all([ + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + ]), + }), + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + 'ReadModel=monitoring', + 'Resolver=failResolver', + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + 'ReadModel=monitoring', + 'Resolver=failResolver', + ]), + }), + getMetricData({ + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + 'ReadModel=monitoring', + 'Resolver=failResolver', + 'Label=Execution', + ]), + }), ]) return { - Errors: { - readModelProjection: { - Init: metrics[0], - EventHandler: metrics[1], - }, - readModelResolver: { - resolverA: metrics[2], - resolverB: metrics[3], - }, - }, + partErrors, + resolverErrors, + partExecutions, + resolverExecutions, + executionDurationSamples, + } +} + +const collectSingViewModelBaseMetrics = async ( + name: string, + skipProjectionErrors = false +): Promise => { + const [ + resolverErrors, + resolverExecutions, + resolverExecutionDurationSamples, + projectionErrors, + ] = await Promise.all([ + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + `ViewModel=${name}`, + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + `ViewModel=${name}`, + ]), + }), + getMetricData({ + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + `ViewModel=${name}`, + 'Label=Execution', + ]), + }), + !skipProjectionErrors + ? getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelProjection', + `ViewModel=${name}`, + ]), + }) + : 0, + ]) + + return { + resolverErrors, + resolverExecutions, + resolverExecutionDurationSamples, + projectionErrors, + } +} + +const collectViewModelBaseMetrics = async (): Promise => { + const [ + resolverPartErrors, + resolverPartExecutions, + projectionPartErrors, + initFailed, + resolverFailed, + monitoring, + ] = await Promise.all([ + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + ]), + }), + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelProjection', + ]), + }), + collectSingViewModelBaseMetrics('init-failed-view-model'), + collectSingViewModelBaseMetrics('resolver-failed-view-model', true), + collectSingViewModelBaseMetrics('monitoring-view-model'), + ]) + + return { + resolverPartErrors, + resolverPartExecutions, + projectionPartErrors, + initFailed, + monitoring, + resolverFailed, + } +} + +const collectCommandBaseMetrics = async (): Promise => { + const [ + partErrors, + commandErrors, + partExecutions, + commandExecutions, + executionDurationSamples, + ] = await Promise.all([ + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + ]), + }), + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + 'AggregateName=monitoring-aggregate', + 'Type=failCommand', + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + 'AggregateName=monitoring-aggregate', + 'Type=failCommand', + ]), + }), + getMetricData({ + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + 'AggregateName=monitoring-aggregate', + 'Type=failCommand', + 'Label=Execution', + ]), + }), + ]) + + return { + partErrors, + commandErrors, + partExecutions, + commandExecutions, + executionDurationSamples, + } +} + +const collectApiHandlerBaseMetrics = async (): Promise => { + const [ + partErrors, + apiHandlerErrors, + partExecutions, + apiHandlerExecutions, + executionDurationSamples, + ] = await Promise.all([ + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + ]), + }), + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + 'Path=/api/fail-api', + 'Method=GET', + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + ]), + }), + getMetricData({ + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + 'Path=/api/fail-api', + 'Method=GET', + ]), + }), + getMetricData({ + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + 'Path=/api/fail-api', + 'Method=GET', + 'Label=Execution', + ]), + }), + ]) + + return { + partErrors, + apiHandlerErrors, + partExecutions, + apiHandlerExecutions, + executionDurationSamples, + } +} + +const collectInternalBaseMetrics = async (): Promise => { + const [partErrors, globalErrors] = await Promise.all([ + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Internal', + ]), + }), + getMetricData({ + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions(['Part=Internal']), + }), + ]) + + return { + partErrors, + globalErrors, } } beforeAll(async () => { deploymentId = process.env.RESOLVE_TESTS_TARGET_DEPLOYMENT_ID || '' cw = new CloudWatch({}) + lambda = new Lambda({}) client = getClient() - endTime = new Date(Date.now() + 3600000) - startTime = new Date(Date.now() - 360000 * 24) - baseMetrics = await collectBaseMetrics() + endTime = new Date(Date.now() + 3600000) // next hour + startTime = new Date(Date.now() - 3600000 * 24 * 7) // previous day }) const awaitMetricValue = async ( - part: string, - dimensions: Array, + metricData: { + MetricName: string + Stat: string + Dimensions: Array + }, value: number, attempt = 0 ): Promise => { - const metric = await getMetricData(part, ...dimensions) + const metric = await getMetricData(metricData) if (!isEqual(metric, value)) { if (attempt >= maxAttempts) { + const lastDimension = + metricData.Dimensions[metricData.Dimensions.length - 1] + const dimensionString = `${lastDimension.Name}=${lastDimension.Value}` + throw Error( - `Metric data mismatch after ${attempt} attempts: expected ${value}, received last ${metric}` + [ + `Metric data mismatch after ${attempt} attempts: `, + `expected ${value}, received last ${metric} `, + `(last dimension: ${dimensionString})`, + ].join('') ) } await new Promise((resolve) => setTimeout(resolve, attemptPeriod)) - await awaitMetricValue(part, dimensions, value, attempt + 1) + + await awaitMetricValue(metricData, value, attempt + 1) } } -test('read model Init handler failed', async () => { - await awaitMetricValue( - 'ReadModelProjection', - [ +const getFunctionName = () => { + const version = process.env.RESOLVE_TESTS_TARGET_VERSION + const parsedVersion = parseVersion(version) + + if (parsedVersion == null) { + throw new Error(`Parse version "${version}" failed`) + } + + return [ + 'app', + deploymentId, + process.env.RESOLVE_TESTS_TARGET_STAGE, + parsedVersion.major, + parsedVersion.minor, + 'x', + ].join('-') +} + +describe('Commands metrics', () => { + beforeAll(async () => { + commandBaseMetrics = await collectCommandBaseMetrics() + }) + + test('aggregate command failed', async () => { + await expect( + client.command({ + aggregateId: 'any', + aggregateName: 'monitoring-aggregate', + type: 'failCommand', + payload: {}, + }) + ).rejects.toThrowError() + + commandBaseMetrics.commandErrors++ + commandBaseMetrics.partErrors++ + commandBaseMetrics.commandExecutions++ + commandBaseMetrics.partExecutions++ + commandBaseMetrics.executionDurationSamples++ + + await awaitMetricValue( { - Name: 'ReadModel', - Value: 'init-failed', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + 'AggregateName=monitoring-aggregate', + 'Type=failCommand', + ]), }, + commandBaseMetrics.commandErrors + ) + + await awaitMetricValue( { - Name: 'EventType', - Value: 'Init', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + ]), }, - ], - baseMetrics.Errors.readModelProjection.Init - ) + commandBaseMetrics.partErrors + ) + + await awaitMetricValue( + { + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + 'AggregateName=monitoring-aggregate', + 'Type=failCommand', + ]), + }, + commandBaseMetrics.commandExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + ]), + }, + commandBaseMetrics.partExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Command', + 'AggregateName=monitoring-aggregate', + 'Type=failCommand', + 'Label=Execution', + ]), + }, + commandBaseMetrics.executionDurationSamples + ) + }) }) -test('read model resolverA failed', async () => { - await expect( - client.query({ - name: 'monitoring', - resolver: 'resolverA', - args: {}, +describe('Read Model Projection metrics', () => { + test('read model Init handler failed', async () => { + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelProjection', + 'ReadModel=init-failed', + 'EventType=Init', + ]), + }, + 1 + ) + }) + + test('read model event handler', async () => { + await client.command({ + aggregateId: 'any', + aggregateName: 'monitoring-aggregate', + type: 'executeReadModelProjection', + payload: {}, + }) + + await client.command({ + aggregateId: 'any', + aggregateName: 'monitoring-aggregate', + type: 'failReadModelProjection', + payload: {}, }) - ).rejects.toBeInstanceOf(Error) - baseMetrics.Errors.readModelResolver.resolverA++ + await awaitMetricValue( + { + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelProjection', + 'ReadModel=monitoring', + 'Label=EventApply', + ]), + }, + 1 + ) - await awaitMetricValue( - 'ReadModelResolver', - [ + await awaitMetricValue( { - Name: 'ReadModel', - Value: 'monitoring', + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelProjection', + 'ReadModel=monitoring', + 'Label=EventProjectionApply', + ]), }, + 1 + ) + + await awaitMetricValue( { - Name: 'Resolver', - Value: 'resolverA', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelProjection', + 'ReadModel=monitoring', + 'EventType=MONITORING_FAILED_HANDLER', + ]), }, - ], - baseMetrics.Errors.readModelResolver.resolverA - ) + 1 + ) + }) +}) + +describe('Read Model Resolver metrics', () => { + beforeAll(async () => { + readModelResolverBaseMetrics = await collectReadModelResolverBaseMetrics() + }) - await awaitMetricValue( - 'ReadModelResolver', - [ + test('read model resolver failed', async () => { + await expect( + client.query({ + name: 'monitoring', + resolver: 'failResolver', + args: {}, + }) + ).rejects.toBeInstanceOf(Error) + + readModelResolverBaseMetrics.resolverErrors++ + readModelResolverBaseMetrics.partErrors++ + readModelResolverBaseMetrics.resolverExecutions++ + readModelResolverBaseMetrics.partExecutions++ + readModelResolverBaseMetrics.executionDurationSamples++ + + await awaitMetricValue( { - Name: 'ReadModel', - Value: 'monitoring', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + 'ReadModel=monitoring', + 'Resolver=failResolver', + ]), }, - ], - baseMetrics.Errors.readModelResolver.resolverB + - baseMetrics.Errors.readModelResolver.resolverA - ) + readModelResolverBaseMetrics.resolverErrors + ) + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + ]), + }, + readModelResolverBaseMetrics.partErrors + ) + + await awaitMetricValue( + { + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + 'ReadModel=monitoring', + 'Resolver=failResolver', + ]), + }, + readModelResolverBaseMetrics.resolverExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + ]), + }, + readModelResolverBaseMetrics.partExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ReadModelResolver', + 'ReadModel=monitoring', + 'Resolver=failResolver', + 'Label=Execution', + ]), + }, + readModelResolverBaseMetrics.executionDurationSamples + ) + }) }) -test('read model resolverB failed', async () => { - await expect( - client.query({ - name: 'monitoring', - resolver: 'resolverB', - args: {}, +describe('View Model metrics', () => { + beforeAll(async () => { + viewModelBaseMetrics = await collectViewModelBaseMetrics() + }) + + test('view model resolver failed', async () => { + await expect( + client.query({ + name: 'resolver-failed-view-model', + aggregateIds: ['test-aggregate'], + args: {}, + }) + ).rejects.toBeInstanceOf(Error) + + viewModelBaseMetrics.resolverPartErrors++ + viewModelBaseMetrics.resolverPartExecutions++ + viewModelBaseMetrics.resolverFailed.resolverErrors++ + viewModelBaseMetrics.resolverFailed.resolverExecutions++ + viewModelBaseMetrics.resolverFailed.resolverExecutionDurationSamples++ + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + 'ViewModel=resolver-failed-view-model', + ]), + }, + viewModelBaseMetrics.resolverFailed.resolverErrors + ) + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + ]), + }, + viewModelBaseMetrics.resolverPartErrors + ) + + await awaitMetricValue( + { + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + 'ViewModel=resolver-failed-view-model', + ]), + }, + viewModelBaseMetrics.resolverFailed.resolverExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + ]), + }, + viewModelBaseMetrics.resolverPartExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + 'ViewModel=resolver-failed-view-model', + 'Label=Execution', + ]), + }, + viewModelBaseMetrics.resolverFailed.resolverExecutionDurationSamples + ) + }) + + test('view model Init handler failed', async () => { + await expect( + client.query({ + name: 'init-failed-view-model', + aggregateIds: ['test-aggregate'], + args: {}, + }) + ).rejects.toThrowError() + + viewModelBaseMetrics.resolverPartErrors++ + viewModelBaseMetrics.resolverPartExecutions++ + viewModelBaseMetrics.projectionPartErrors++ + viewModelBaseMetrics.initFailed.resolverErrors++ + viewModelBaseMetrics.initFailed.resolverExecutions++ + viewModelBaseMetrics.initFailed.projectionErrors++ + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + 'ViewModel=init-failed-view-model', + ]), + }, + viewModelBaseMetrics.initFailed.resolverErrors + ) + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelProjection', + 'ViewModel=init-failed-view-model', + 'EventType=Init', + ]), + }, + viewModelBaseMetrics.initFailed.projectionErrors + ) + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelProjection', + ]), + }, + viewModelBaseMetrics.projectionPartErrors + ) + }) + + test('view model event handler failed', async () => { + await client.command({ + aggregateId: 'fail-aggregate', + aggregateName: 'monitoring-aggregate', + type: 'failViewModelProjection', + payload: {}, }) - ).rejects.toBeInstanceOf(Error) - baseMetrics.Errors.readModelResolver.resolverB++ + await expect( + client.query({ + name: 'monitoring-view-model', + aggregateIds: ['fail-aggregate'], + args: {}, + }) + ).rejects.toBeInstanceOf(Error) + + viewModelBaseMetrics.resolverPartErrors++ + viewModelBaseMetrics.resolverPartExecutions++ + viewModelBaseMetrics.projectionPartErrors++ + viewModelBaseMetrics.monitoring.resolverErrors++ + viewModelBaseMetrics.monitoring.resolverExecutions++ + viewModelBaseMetrics.monitoring.projectionErrors++ + viewModelBaseMetrics.monitoring.resolverExecutionDurationSamples++ - await awaitMetricValue( - 'ReadModelResolver', - [ + await awaitMetricValue( { - Name: 'ReadModel', - Value: 'monitoring', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelResolver', + 'ViewModel=monitoring-view-model', + ]), }, + viewModelBaseMetrics.monitoring.resolverErrors + ) + + await awaitMetricValue( { - Name: 'Resolver', - Value: 'resolverB', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelProjection', + 'ViewModel=monitoring-view-model', + 'EventType=MONITORING_VIEW_MODEL_FAILED', + ]), }, - ], - baseMetrics.Errors.readModelResolver.resolverB - ) + viewModelBaseMetrics.monitoring.projectionErrors + ) - await awaitMetricValue( - 'ReadModelResolver', - [ + await awaitMetricValue( { - Name: 'ReadModel', - Value: 'monitoring', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ViewModelProjection', + ]), }, - ], - baseMetrics.Errors.readModelResolver.resolverB + - baseMetrics.Errors.readModelResolver.resolverA - ) + viewModelBaseMetrics.projectionPartErrors + ) + }) }) -test('read model event handler failed', async () => { - await client.command({ - aggregateId: 'any', - aggregateName: 'monitoring-aggregate', - type: 'fail', - payload: {}, +describe('Api Handler metrics', () => { + beforeAll(async () => { + apiHandlerBaseMetrics = await collectApiHandlerBaseMetrics() }) - await awaitMetricValue( - 'ReadModelProjection', - [ + test('api handler failed', async () => { + await fetch(`${getTargetURL()}/api/fail-api`) + + apiHandlerBaseMetrics.apiHandlerErrors++ + apiHandlerBaseMetrics.partErrors++ + apiHandlerBaseMetrics.apiHandlerExecutions++ + apiHandlerBaseMetrics.partExecutions++ + apiHandlerBaseMetrics.executionDurationSamples++ + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + 'Path=/api/fail-api', + 'Method=GET', + ]), + }, + apiHandlerBaseMetrics.apiHandlerErrors + ) + + await awaitMetricValue( { - Name: 'ReadModel', - Value: 'monitoring', + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + ]), }, + apiHandlerBaseMetrics.partErrors + ) + + await awaitMetricValue( { - Name: 'EventType', - Value: 'MONITORING_FAILED_HANDLER', + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + 'Path=/api/fail-api', + 'Method=GET', + ]), }, - ], - baseMetrics.Errors.readModelProjection.EventHandler + 1 - ) + apiHandlerBaseMetrics.apiHandlerExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Executions', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + ]), + }, + apiHandlerBaseMetrics.partExecutions + ) + + await awaitMetricValue( + { + MetricName: 'Duration', + Stat: 'SampleCount', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=ApiHandler', + 'Path=/api/fail-api', + 'Method=GET', + 'Label=Execution', + ]), + }, + apiHandlerBaseMetrics.executionDurationSamples + ) + }) +}) + +describe('Internal metrics', () => { + beforeAll(async () => { + internalBaseMetrics = await collectInternalBaseMetrics() + }) + + test('collects errors thrown in lambda worker', async () => { + const json = JSON.stringify({ + key: 'monitoring-test', + }) + + const payload = new Uint8Array(json.length) + + for (let i = 0; i < json.length; i++) { + payload[i] = json.charCodeAt(i) + } + + await lambda.invoke({ + FunctionName: getFunctionName(), + Payload: payload, + }) + + internalBaseMetrics.partErrors++ + internalBaseMetrics.globalErrors++ + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions([ + `DeploymentId=${deploymentId}`, + 'Part=Internal', + ]), + }, + internalBaseMetrics.partErrors + ) + + await awaitMetricValue( + { + MetricName: 'Errors', + Stat: 'Sum', + Dimensions: createDimensions(['Part=Internal']), + }, + internalBaseMetrics.globalErrors + ) + }) }) diff --git a/functional-tests/app/common/aggregates/monitoring.commands.js b/functional-tests/app/common/aggregates/monitoring.commands.js index e38cb1de6f..74b2e60b57 100644 --- a/functional-tests/app/common/aggregates/monitoring.commands.js +++ b/functional-tests/app/common/aggregates/monitoring.commands.js @@ -1,10 +1,25 @@ -import { MONITORING_FAILED_HANDLER } from '../event-types' +import { + MONITORING_EXECUTED_HANDLER, + MONITORING_FAILED_HANDLER, + MONITORING_VIEW_MODEL_FAILED, +} from '../event-types' const aggregate = { - fail: () => ({ + executeReadModelProjection: () => ({ + type: MONITORING_EXECUTED_HANDLER, + payload: {}, + }), + failReadModelProjection: () => ({ type: MONITORING_FAILED_HANDLER, payload: {}, }), + failViewModelProjection: () => ({ + type: MONITORING_VIEW_MODEL_FAILED, + payload: {}, + }), + failCommand: () => { + throw new Error('Test aggregate: command failed') + }, } export default aggregate diff --git a/functional-tests/app/common/api-handlers/fail-api.js b/functional-tests/app/common/api-handlers/fail-api.js new file mode 100644 index 0000000000..923316b8cf --- /dev/null +++ b/functional-tests/app/common/api-handlers/fail-api.js @@ -0,0 +1,5 @@ +const failApi = () => { + throw new Error('Test API: handler failed') +} + +export default failApi diff --git a/functional-tests/app/common/event-types.js b/functional-tests/app/common/event-types.js index 9520c680ac..d1ae8b2d21 100644 --- a/functional-tests/app/common/event-types.js +++ b/functional-tests/app/common/event-types.js @@ -7,4 +7,6 @@ export const TEST_SCENARIO_RETRY_ON_ERROR_UNBLOCKED = 'TEST_SCENARIO_RETRY_ON_ERROR_UNBLOCKED' export const TEST_SCENARIO_RETRY_ON_ERROR_COMPLETED = 'TEST_SCENARIO_RETRY_ON_ERROR_COMPLETED' +export const MONITORING_EXECUTED_HANDLER = 'MONITORING_EXECUTED_HANDLER' export const MONITORING_FAILED_HANDLER = 'MONITORING_FAILED_HANDLER' +export const MONITORING_VIEW_MODEL_FAILED = 'MONITORING_VIEW_MODEL_FAILED' diff --git a/functional-tests/app/common/read-models/monitoring.projection.js b/functional-tests/app/common/read-models/monitoring.projection.js index 99cc6060f9..3d9e4eb826 100644 --- a/functional-tests/app/common/read-models/monitoring.projection.js +++ b/functional-tests/app/common/read-models/monitoring.projection.js @@ -1,9 +1,13 @@ -import { MONITORING_FAILED_HANDLER } from '../event-types' +import { + MONITORING_EXECUTED_HANDLER, + MONITORING_FAILED_HANDLER, +} from '../event-types' -const aggregate = { +const readModel = { + [MONITORING_EXECUTED_HANDLER]: async () => void 0, [MONITORING_FAILED_HANDLER]: async () => { throw Error('Test read model: event handler failed') }, } -export default aggregate +export default readModel diff --git a/functional-tests/app/common/read-models/monitoring.resolvers.js b/functional-tests/app/common/read-models/monitoring.resolvers.js index f9e367dffc..52ae81752e 100644 --- a/functional-tests/app/common/read-models/monitoring.resolvers.js +++ b/functional-tests/app/common/read-models/monitoring.resolvers.js @@ -1,9 +1,6 @@ const resolvers = { - resolverA: async () => { - throw Error('Test read model: resolverA failure') - }, - resolverB: async () => { - throw Error('Test read model: resolverB failure') + failResolver: async () => { + throw Error('Test read model: failResolver failure') }, } export default resolvers diff --git a/functional-tests/app/common/view-models/init-failed.projection.js b/functional-tests/app/common/view-models/init-failed.projection.js new file mode 100644 index 0000000000..7c905996dd --- /dev/null +++ b/functional-tests/app/common/view-models/init-failed.projection.js @@ -0,0 +1,7 @@ +const initFailed = { + Init: () => { + throw new Error('Test error: init failed') + }, +} + +export default initFailed diff --git a/functional-tests/app/common/view-models/monitoring.projection.js b/functional-tests/app/common/view-models/monitoring.projection.js new file mode 100644 index 0000000000..6bae652e3e --- /dev/null +++ b/functional-tests/app/common/view-models/monitoring.projection.js @@ -0,0 +1,10 @@ +import { MONITORING_VIEW_MODEL_FAILED } from '../event-types' + +const projection = { + Init: () => null, + [MONITORING_VIEW_MODEL_FAILED]: () => { + throw new Error('Test error: view model projection failed') + }, +} + +export default projection diff --git a/functional-tests/app/common/view-models/resolver-failed.projection.js b/functional-tests/app/common/view-models/resolver-failed.projection.js new file mode 100644 index 0000000000..3a4aae0e78 --- /dev/null +++ b/functional-tests/app/common/view-models/resolver-failed.projection.js @@ -0,0 +1,5 @@ +const projection = { + Init: () => null, +} + +export default projection diff --git a/functional-tests/app/common/view-models/resolver-failed.resolver.js b/functional-tests/app/common/view-models/resolver-failed.resolver.js new file mode 100644 index 0000000000..eaa3118f92 --- /dev/null +++ b/functional-tests/app/common/view-models/resolver-failed.resolver.js @@ -0,0 +1,5 @@ +const resolver = () => { + throw new Error('Test error: view model resolver failed') +} + +export default resolver diff --git a/functional-tests/app/config.app.js b/functional-tests/app/config.app.js index 92538065be..17e279101b 100644 --- a/functional-tests/app/config.app.js +++ b/functional-tests/app/config.app.js @@ -106,6 +106,20 @@ const appConfig = { projection: 'common/view-models/custom-aggregate-ids.projection.js', resolver: 'common/view-models/custom-aggregate-ids.resolver.js', }, + { + name: 'monitoring-view-model', + projection: 'common/view-models/monitoring.projection.js', + resolver: 'common/view-models/monitoring.resolver.js', + }, + { + name: 'init-failed-view-model', + projection: 'common/view-models/init-failed.projection.js', + }, + { + name: 'resolver-failed-view-model', + projection: 'common/view-models/resolver-failed.projection.js', + resolver: 'common/view-models/resolver-failed.resolver.js', + }, ], sagas: [ { @@ -123,6 +137,11 @@ const appConfig = { version: '@resolve-js/runtime/lib/common/utils/interop-options.js', }, apiHandlers: [ + { + handler: 'common/api-handlers/fail-api.js', + path: '/api/fail-api', + method: 'GET', + }, { handler: '@resolve-js/runtime/lib/local/query-is-ready-handler.js', path: '/api/query-is-ready', diff --git a/functional-tests/package.json b/functional-tests/package.json index 2d50420411..cb809becfa 100644 --- a/functional-tests/package.json +++ b/functional-tests/package.json @@ -29,6 +29,7 @@ }, "devDependencies": { "@aws-sdk/client-cloudwatch": "3.11.0", + "@aws-sdk/client-lambda": "3.11.0", "@types/fs-extra": "8.1.0", "@types/isomorphic-fetch": "0.0.35", "@types/nanoid": "2.1.0", diff --git a/functional-tests/utils/utils.ts b/functional-tests/utils/utils.ts index 3387a37f59..d154d8fd07 100644 --- a/functional-tests/utils/utils.ts +++ b/functional-tests/utils/utils.ts @@ -1,5 +1,13 @@ import { getClient as getClientInternal, Context } from '@resolve-js/client' +import monitoringResolver from '../app/common/view-models/resolver-failed.resolver' +import monitoringProjection from '../app/common/view-models/monitoring.projection' + +import initFailedProjection from '../app/common/view-models/init-failed.projection' + +import resolverFailedProjection from '../app/common/view-models/resolver-failed.projection' +import resolverFailedResolver from '../app/common/view-models/resolver-failed.resolver' + export const getTargetURL = () => process.env.RESOLVE_TESTS_TARGET_URL || 'http://0.0.0.0:3000' @@ -7,6 +15,26 @@ const buildContext = (contextOverrides: any): Context => ({ origin: getTargetURL(), rootPath: '', staticPath: 'static', + viewModels: [ + { + name: 'monitoring-view-model', + resolver: monitoringResolver, + projection: monitoringProjection, + deserializeState: (state) => JSON.parse(state), + }, + { + name: 'init-failed-view-model', + projection: initFailedProjection, + resolver: () => void 0, + deserializeState: (state) => JSON.parse(state), + }, + { + name: 'resolver-failed-view-model', + projection: resolverFailedProjection, + resolver: resolverFailedResolver, + deserializeState: (state) => JSON.parse(state), + }, + ], ...contextOverrides, }) 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 7776571963..c1fc25f988 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 @@ -35,6 +35,13 @@ const buildViewModel = async ( const { eventstore, secretsManager, monitoring } = runtime const { jwt } = context + const viewModelMonitoring = + monitoring != null + ? monitoring + .group({ Part: 'ViewModelProjection' }) + .group({ ViewModel: name }) + : null + const aggregateIds = Array().concat(rawIds) const log = getLog(`build-view-model:${name}`) @@ -68,7 +75,13 @@ const buildViewModel = async ( if (cursor == null && typeof projection.Init === 'function') { log.debug(`initializing view model from scratch`) - state = projection.Init() + + try { + state = projection.Init() + } catch (error) { + viewModelMonitoring?.group({ EventType: 'Init' }).error(error) + throw error + } } let eventCount = 0 @@ -111,15 +124,7 @@ const buildViewModel = async ( } catch (error) { subSegment.addError(error) log.error(error.message) - - if (monitoring != null) { - const monitoringGroup = monitoring - .group({ Part: 'ViewModelProjection' }) - .group({ ViewModel: name }) - .group({ EventType: event.type }) - - monitoringGroup.error(error) - } + viewModelMonitoring?.group({ EventType: event.type }).error(error) throw error } finally { subSegment.close() diff --git a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/index.ts b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/index.ts index c82796ad88..7d5558d73c 100644 --- a/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/index.ts +++ b/packages/runtime/adapters/readmodel-adapters/readmodel-base/src/index.ts @@ -1,3 +1,3 @@ -export { default as default } from './create-adapter' +export { default } from './create-adapter' export { default as splitNestedPath } from './split-nested-path' export * from './types' diff --git a/packages/runtime/runtime/src/cloud/metrics.js b/packages/runtime/runtime/src/cloud/metrics.js index ab61e3d7d0..96db6e03c4 100644 --- a/packages/runtime/runtime/src/cloud/metrics.js +++ b/packages/runtime/runtime/src/cloud/metrics.js @@ -52,7 +52,7 @@ export const putDurationMetrics = async ( Value: duration, }, ], - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', } if (coldStart) { diff --git a/packages/runtime/runtime/src/cloud/monitoring.js b/packages/runtime/runtime/src/cloud/monitoring.js index e7f3dcf23d..fa3e240f01 100644 --- a/packages/runtime/runtime/src/cloud/monitoring.js +++ b/packages/runtime/runtime/src/cloud/monitoring.js @@ -297,7 +297,7 @@ const monitoringPublish = async (log, monitoringData) => { ) { promises.push( putMetricData({ - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', MetricData: monitoringData.metricData.slice(i, i + MAX_METRIC_COUNT), }) ) diff --git a/packages/runtime/runtime/src/cloud/wrap-api-handler.js b/packages/runtime/runtime/src/cloud/wrap-api-handler.js index 5f606888e7..6973623e95 100644 --- a/packages/runtime/runtime/src/cloud/wrap-api-handler.js +++ b/packages/runtime/runtime/src/cloud/wrap-api-handler.js @@ -337,7 +337,10 @@ const wrapApiHandler = (handler, getCustomParameters, monitoring) => async ( req = await createRequest(lambdaEvent, customParameters) if (monitoring != null) { - pathMonitoring = monitoring.group({ Path: req.path }) + pathMonitoring = monitoring + .group({ Path: req.path }) + .group({ Method: req.method }) + pathMonitoring.time('Execution', startTimestamp) } diff --git a/packages/runtime/runtime/test/cloud/metrics.test.js b/packages/runtime/runtime/test/cloud/metrics.test.js index 9ac368ec68..a0d210157f 100644 --- a/packages/runtime/runtime/test/cloud/metrics.test.js +++ b/packages/runtime/runtime/test/cloud/metrics.test.js @@ -68,7 +68,7 @@ describe('put duration metrics', () => { Value: 2000, }, ], - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', }) expect(console.info).toBeCalledWith( ['[REQUEST INFO]', 'route', '', 2000].join('\n') @@ -103,7 +103,7 @@ describe('put duration metrics', () => { Value: 2000, }, ], - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', }) expect(console.info).toBeCalledWith( [ @@ -143,7 +143,7 @@ describe('put duration metrics', () => { Value: 2000, }, ], - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', }) expect(console.info).toBeCalledWith( [ @@ -183,7 +183,7 @@ describe('put duration metrics', () => { Value: 2000, }, ], - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', }) expect(console.info).toBeCalledWith( [ @@ -223,7 +223,7 @@ describe('put duration metrics', () => { Value: 2000, }, ], - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', }) expect(console.info).toBeCalledWith( [ @@ -279,7 +279,7 @@ describe('put duration metrics', () => { Value: 897000, }, ], - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', }) }) }) diff --git a/packages/runtime/runtime/test/cloud/monitoring.test.js b/packages/runtime/runtime/test/cloud/monitoring.test.js index 61f68b2760..769c01e406 100644 --- a/packages/runtime/runtime/test/cloud/monitoring.test.js +++ b/packages/runtime/runtime/test/cloud/monitoring.test.js @@ -37,7 +37,7 @@ describe('common', () => { expect(CloudWatch.putMetricData).toBeCalledTimes(1) expect(CloudWatch.putMetricData).toBeCalledWith({ - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', MetricData: expect.any(Array), }) }) @@ -1027,7 +1027,7 @@ describe('time and timeEnd', () => { expect(CloudWatch.putMetricData).toBeCalledTimes(1) expect(CloudWatch.putMetricData).toBeCalledWith({ - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', MetricData: expect.any(Array), }) @@ -1267,7 +1267,7 @@ describe('duration', () => { expect(CloudWatch.putMetricData).toBeCalledTimes(1) expect(CloudWatch.putMetricData).toBeCalledWith({ - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', MetricData: expect.any(Array), }) @@ -1297,7 +1297,7 @@ describe('duration', () => { expect(CloudWatch.putMetricData).toBeCalledTimes(1) expect(CloudWatch.putMetricData).toBeCalledWith({ - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', MetricData: expect.any(Array), }) @@ -1653,7 +1653,7 @@ describe('rate', () => { expect(CloudWatch.putMetricData).toBeCalledTimes(1) expect(CloudWatch.putMetricData).toBeCalledWith({ - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', MetricData: expect.any(Array), }) @@ -1682,7 +1682,7 @@ describe('rate', () => { expect(CloudWatch.putMetricData).toBeCalledTimes(1) expect(CloudWatch.putMetricData).toBeCalledWith({ - Namespace: 'RESOLVE_METRICS', + Namespace: 'ResolveJs', MetricData: expect.any(Array), }) diff --git a/yarn.lock b/yarn.lock index afdf85d3ed..b590afd6ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,6 +282,44 @@ fast-xml-parser "3.19.0" tslib "^2.0.0" +"@aws-sdk/client-lambda@3.11.0": + version "3.11.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-lambda/-/client-lambda-3.11.0.tgz#8121e631ee073f9cea60f80e0f6553c0afdf772e" + integrity sha512-uXYR7TpLfDGfwhsm5/cA91GeykxDlU3y9fDu9pKdaxl3+UnF5jqletSz/EItnpVHVStoaZik4ChCeS7WQBzX6A== + dependencies: + "@aws-crypto/sha256-browser" "^1.0.0" + "@aws-crypto/sha256-js" "^1.0.0" + "@aws-sdk/config-resolver" "3.10.0" + "@aws-sdk/credential-provider-node" "3.11.0" + "@aws-sdk/fetch-http-handler" "3.10.0" + "@aws-sdk/hash-node" "3.10.0" + "@aws-sdk/invalid-dependency" "3.10.0" + "@aws-sdk/middleware-content-length" "3.10.0" + "@aws-sdk/middleware-host-header" "3.10.0" + "@aws-sdk/middleware-logger" "3.10.0" + "@aws-sdk/middleware-retry" "3.10.0" + "@aws-sdk/middleware-serde" "3.10.0" + "@aws-sdk/middleware-signing" "3.10.0" + "@aws-sdk/middleware-stack" "3.10.0" + "@aws-sdk/middleware-user-agent" "3.10.0" + "@aws-sdk/node-config-provider" "3.10.0" + "@aws-sdk/node-http-handler" "3.10.0" + "@aws-sdk/protocol-http" "3.10.0" + "@aws-sdk/smithy-client" "3.10.0" + "@aws-sdk/types" "3.10.0" + "@aws-sdk/url-parser" "3.10.0" + "@aws-sdk/url-parser-native" "3.10.0" + "@aws-sdk/util-base64-browser" "3.10.0" + "@aws-sdk/util-base64-node" "3.10.0" + "@aws-sdk/util-body-length-browser" "3.10.0" + "@aws-sdk/util-body-length-node" "3.10.0" + "@aws-sdk/util-user-agent-browser" "3.10.0" + "@aws-sdk/util-user-agent-node" "3.10.0" + "@aws-sdk/util-utf8-browser" "3.10.0" + "@aws-sdk/util-utf8-node" "3.10.0" + "@aws-sdk/util-waiter" "3.10.0" + tslib "^2.0.0" + "@aws-sdk/client-sso@3.11.0": version "3.11.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.11.0.tgz#1586e72438c017a81a0ad2ed3facc027aa364cea"