├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── auto-merge.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ ├── upgrade-cdklabs-projen-project-types-main.yml │ ├── upgrade-dev-deps-main.yml │ └── upgrade-main.yml ├── .gitignore ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── API.md ├── LICENSE ├── README.md ├── example ├── cdk.json ├── index.ts ├── lambda │ └── index.ts └── sample.png ├── package.json ├── rosetta └── default.ts-fixture ├── src ├── api-gateway.ts ├── api.ts ├── aspect.ts ├── core │ └── monitoring.ts ├── dynamodb.ts ├── ecs.ts ├── index.ts ├── lambda.ts ├── monitoring │ └── aws │ │ ├── api-gateway │ │ └── metrics.ts │ │ ├── dynamodb │ │ └── metrics.ts │ │ ├── ecs │ │ └── metrics.ts │ │ ├── lambda │ │ └── metrics.ts │ │ ├── rds │ │ └── metrics.ts │ │ ├── redshift │ │ └── metrics.ts │ │ ├── sns │ │ └── metrics.ts │ │ ├── sqs │ │ ├── metrics.ts │ │ └── monitoring.ts │ │ └── state-machine │ │ └── metrics.ts ├── rds-aurora.ts ├── state-machine.ts ├── watchful.ts └── widget │ ├── axis.ts │ ├── constant.ts │ └── section.ts ├── test ├── monitoring │ └── aws │ │ ├── api-gateway │ │ ├── __snapshots__ │ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts │ │ ├── dynamodb │ │ ├── __snapshots__ │ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts │ │ ├── ecs │ │ ├── __snapshots__ │ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts │ │ ├── lambda │ │ ├── __snapshots__ │ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts │ │ ├── rds │ │ ├── __snapshots__ │ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts │ │ ├── redshift │ │ ├── __snapshots__ │ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts │ │ ├── sns │ │ ├── __snapshots__ │ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts │ │ ├── sqs │ │ ├── __snapshots__ │ │ │ ├── metrics.test.ts.snap │ │ │ └── monitoring.test.ts.snap │ │ ├── metrics.test.ts │ │ └── monitoring.test.ts │ │ └── state-machine │ │ ├── __snapshots__ │ │ └── metrics.test.ts.snap │ │ └── metrics.test.ts ├── watchful.test.ts └── widget │ └── section.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "*.d.ts", 39 | "node_modules/", 40 | "*.generated.ts", 41 | "coverage", 42 | "!.projenrc.ts", 43 | "!projenrc/**/*.ts" 44 | ], 45 | "rules": { 46 | "indent": [ 47 | "off" 48 | ], 49 | "@typescript-eslint/indent": [ 50 | "error", 51 | 2 52 | ], 53 | "quotes": [ 54 | "error", 55 | "single", 56 | { 57 | "avoidEscape": true 58 | } 59 | ], 60 | "comma-dangle": [ 61 | "error", 62 | "always-multiline" 63 | ], 64 | "comma-spacing": [ 65 | "error", 66 | { 67 | "before": false, 68 | "after": true 69 | } 70 | ], 71 | "no-multi-spaces": [ 72 | "error", 73 | { 74 | "ignoreEOLComments": false 75 | } 76 | ], 77 | "array-bracket-spacing": [ 78 | "error", 79 | "never" 80 | ], 81 | "array-bracket-newline": [ 82 | "error", 83 | "consistent" 84 | ], 85 | "object-curly-spacing": [ 86 | "error", 87 | "always" 88 | ], 89 | "object-curly-newline": [ 90 | "error", 91 | { 92 | "multiline": true, 93 | "consistent": true 94 | } 95 | ], 96 | "object-property-newline": [ 97 | "error", 98 | { 99 | "allowAllPropertiesOnSameLine": true 100 | } 101 | ], 102 | "keyword-spacing": [ 103 | "error" 104 | ], 105 | "brace-style": [ 106 | "error", 107 | "1tbs", 108 | { 109 | "allowSingleLine": true 110 | } 111 | ], 112 | "space-before-blocks": [ 113 | "error" 114 | ], 115 | "curly": [ 116 | "error", 117 | "multi-line", 118 | "consistent" 119 | ], 120 | "@typescript-eslint/member-delimiter-style": [ 121 | "error" 122 | ], 123 | "semi": [ 124 | "error", 125 | "always" 126 | ], 127 | "max-len": [ 128 | "error", 129 | { 130 | "code": 150, 131 | "ignoreUrls": true, 132 | "ignoreStrings": true, 133 | "ignoreTemplateLiterals": true, 134 | "ignoreComments": true, 135 | "ignoreRegExpLiterals": true 136 | } 137 | ], 138 | "quote-props": [ 139 | "error", 140 | "consistent-as-needed" 141 | ], 142 | "@typescript-eslint/no-require-imports": [ 143 | "error" 144 | ], 145 | "import/no-extraneous-dependencies": [ 146 | "error", 147 | { 148 | "devDependencies": [ 149 | "**/test/**", 150 | "**/build-tools/**", 151 | ".projenrc.ts", 152 | "projenrc/**/*.ts" 153 | ], 154 | "optionalDependencies": false, 155 | "peerDependencies": true 156 | } 157 | ], 158 | "import/no-unresolved": [ 159 | "error" 160 | ], 161 | "import/order": [ 162 | "warn", 163 | { 164 | "groups": [ 165 | "builtin", 166 | "external" 167 | ], 168 | "alphabetize": { 169 | "order": "asc", 170 | "caseInsensitive": true 171 | } 172 | } 173 | ], 174 | "import/no-duplicates": [ 175 | "error" 176 | ], 177 | "no-shadow": [ 178 | "off" 179 | ], 180 | "@typescript-eslint/no-shadow": [ 181 | "error" 182 | ], 183 | "key-spacing": [ 184 | "error" 185 | ], 186 | "no-multiple-empty-lines": [ 187 | "error" 188 | ], 189 | "@typescript-eslint/no-floating-promises": [ 190 | "error" 191 | ], 192 | "no-return-await": [ 193 | "off" 194 | ], 195 | "@typescript-eslint/return-await": [ 196 | "error" 197 | ], 198 | "no-trailing-spaces": [ 199 | "error" 200 | ], 201 | "dot-notation": [ 202 | "error" 203 | ], 204 | "no-bitwise": [ 205 | "error" 206 | ], 207 | "@typescript-eslint/member-ordering": [ 208 | "error", 209 | { 210 | "default": [ 211 | "public-static-field", 212 | "public-static-method", 213 | "protected-static-field", 214 | "protected-static-method", 215 | "private-static-field", 216 | "private-static-method", 217 | "field", 218 | "constructor", 219 | "method" 220 | ] 221 | } 222 | ] 223 | }, 224 | "overrides": [ 225 | { 226 | "files": [ 227 | ".projenrc.ts" 228 | ], 229 | "rules": { 230 | "@typescript-eslint/no-require-imports": "off", 231 | "import/no-extraneous-dependencies": "off" 232 | } 233 | } 234 | ] 235 | } 236 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/auto-approve.yml linguist-generated 9 | /.github/workflows/auto-merge.yml linguist-generated 10 | /.github/workflows/build.yml linguist-generated 11 | /.github/workflows/pull-request-lint.yml linguist-generated 12 | /.github/workflows/release.yml linguist-generated 13 | /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml linguist-generated 14 | /.github/workflows/upgrade-dev-deps-main.yml linguist-generated 15 | /.github/workflows/upgrade-main.yml linguist-generated 16 | /.gitignore linguist-generated 17 | /.npmignore linguist-generated 18 | /.projen/** linguist-generated 19 | /.projen/deps.json linguist-generated 20 | /.projen/files.json linguist-generated 21 | /.projen/tasks.json linguist-generated 22 | /API.md linguist-generated 23 | /LICENSE linguist-generated 24 | /package.json linguist-generated 25 | /tsconfig.dev.json linguist-generated 26 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'cdklabs-automation' || github.event.pull_request.user.login == 'dependabot[bot]') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-merge 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - reopened 9 | - ready_for_review 10 | jobs: 11 | enableAutoMerge: 12 | name: "Set AutoMerge on PR #${{ github.event.number }}" 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | steps: 18 | - uses: peter-evans/enable-pull-request-automerge@v2 19 | with: 20 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 21 | pull-request-number: ${{ github.event.number }} 22 | merge-method: squash 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | merge_group: 8 | branches: 9 | - main 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | outputs: 16 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 17 | env: 18 | CI: "true" 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | ref: ${{ github.event.pull_request.head.ref }} 24 | repository: ${{ github.event.pull_request.head.repo.full_name }} 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: lts/* 29 | - name: Install dependencies 30 | run: yarn install --check-files 31 | - name: build 32 | run: npx projen build 33 | - name: Find mutations 34 | id: self_mutation 35 | run: |- 36 | git add . 37 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 38 | working-directory: ./ 39 | - name: Upload patch 40 | if: steps.self_mutation.outputs.self_mutation_happened 41 | uses: actions/upload-artifact@v4.4.0 42 | with: 43 | name: repo.patch 44 | path: repo.patch 45 | overwrite: true 46 | - name: Fail build on mutation 47 | if: steps.self_mutation.outputs.self_mutation_happened 48 | run: |- 49 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 50 | cat repo.patch 51 | exit 1 52 | - name: Backup artifact permissions 53 | run: cd dist && getfacl -R . > permissions-backup.acl 54 | continue-on-error: true 55 | - name: Upload artifact 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | self-mutation: 62 | needs: build 63 | runs-on: ubuntu-latest 64 | permissions: 65 | contents: write 66 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v4 70 | with: 71 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 72 | ref: ${{ github.event.pull_request.head.ref }} 73 | repository: ${{ github.event.pull_request.head.repo.full_name }} 74 | - name: Download patch 75 | uses: actions/download-artifact@v4 76 | with: 77 | name: repo.patch 78 | path: ${{ runner.temp }} 79 | - name: Apply patch 80 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 81 | - name: Set git identity 82 | run: |- 83 | git config user.name "github-actions" 84 | git config user.email "github-actions@github.com" 85 | - name: Push changes 86 | env: 87 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 88 | run: |- 89 | git add . 90 | git commit -s -m "chore: self mutation" 91 | git push origin HEAD:$PULL_REQUEST_REF 92 | package-js: 93 | needs: build 94 | runs-on: ubuntu-latest 95 | permissions: 96 | contents: read 97 | if: ${{ !needs.build.outputs.self_mutation_happened }} 98 | steps: 99 | - uses: actions/setup-node@v4 100 | with: 101 | node-version: lts/* 102 | - name: Download build artifacts 103 | uses: actions/download-artifact@v4 104 | with: 105 | name: build-artifact 106 | path: dist 107 | - name: Restore build artifact permissions 108 | run: cd dist && setfacl --restore=permissions-backup.acl 109 | continue-on-error: true 110 | - name: Checkout 111 | uses: actions/checkout@v4 112 | with: 113 | ref: ${{ github.event.pull_request.head.ref }} 114 | repository: ${{ github.event.pull_request.head.repo.full_name }} 115 | path: .repo 116 | - name: Install Dependencies 117 | run: cd .repo && yarn install --check-files --frozen-lockfile 118 | - name: Extract build artifact 119 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 120 | - name: Move build artifact out of the way 121 | run: mv dist dist.old 122 | - name: Create js artifact 123 | run: cd .repo && npx projen package:js 124 | - name: Collect js artifact 125 | run: mv .repo/dist dist 126 | package-java: 127 | needs: build 128 | runs-on: ubuntu-latest 129 | permissions: 130 | contents: read 131 | if: ${{ !needs.build.outputs.self_mutation_happened }} 132 | steps: 133 | - uses: actions/setup-java@v4 134 | with: 135 | distribution: corretto 136 | java-version: "11" 137 | - uses: actions/setup-node@v4 138 | with: 139 | node-version: lts/* 140 | - name: Download build artifacts 141 | uses: actions/download-artifact@v4 142 | with: 143 | name: build-artifact 144 | path: dist 145 | - name: Restore build artifact permissions 146 | run: cd dist && setfacl --restore=permissions-backup.acl 147 | continue-on-error: true 148 | - name: Checkout 149 | uses: actions/checkout@v4 150 | with: 151 | ref: ${{ github.event.pull_request.head.ref }} 152 | repository: ${{ github.event.pull_request.head.repo.full_name }} 153 | path: .repo 154 | - name: Install Dependencies 155 | run: cd .repo && yarn install --check-files --frozen-lockfile 156 | - name: Extract build artifact 157 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 158 | - name: Move build artifact out of the way 159 | run: mv dist dist.old 160 | - name: Create java artifact 161 | run: cd .repo && npx projen package:java 162 | - name: Collect java artifact 163 | run: mv .repo/dist dist 164 | package-python: 165 | needs: build 166 | runs-on: ubuntu-latest 167 | permissions: 168 | contents: read 169 | if: ${{ !needs.build.outputs.self_mutation_happened }} 170 | steps: 171 | - uses: actions/setup-node@v4 172 | with: 173 | node-version: lts/* 174 | - uses: actions/setup-python@v5 175 | with: 176 | python-version: 3.x 177 | - name: Download build artifacts 178 | uses: actions/download-artifact@v4 179 | with: 180 | name: build-artifact 181 | path: dist 182 | - name: Restore build artifact permissions 183 | run: cd dist && setfacl --restore=permissions-backup.acl 184 | continue-on-error: true 185 | - name: Checkout 186 | uses: actions/checkout@v4 187 | with: 188 | ref: ${{ github.event.pull_request.head.ref }} 189 | repository: ${{ github.event.pull_request.head.repo.full_name }} 190 | path: .repo 191 | - name: Install Dependencies 192 | run: cd .repo && yarn install --check-files --frozen-lockfile 193 | - name: Extract build artifact 194 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 195 | - name: Move build artifact out of the way 196 | run: mv dist dist.old 197 | - name: Create python artifact 198 | run: cd .repo && npx projen package:python 199 | - name: Collect python artifact 200 | run: mv .repo/dist dist 201 | package-dotnet: 202 | needs: build 203 | runs-on: ubuntu-latest 204 | permissions: 205 | contents: read 206 | if: ${{ !needs.build.outputs.self_mutation_happened }} 207 | steps: 208 | - uses: actions/setup-node@v4 209 | with: 210 | node-version: lts/* 211 | - uses: actions/setup-dotnet@v4 212 | with: 213 | dotnet-version: 6.x 214 | - name: Download build artifacts 215 | uses: actions/download-artifact@v4 216 | with: 217 | name: build-artifact 218 | path: dist 219 | - name: Restore build artifact permissions 220 | run: cd dist && setfacl --restore=permissions-backup.acl 221 | continue-on-error: true 222 | - name: Checkout 223 | uses: actions/checkout@v4 224 | with: 225 | ref: ${{ github.event.pull_request.head.ref }} 226 | repository: ${{ github.event.pull_request.head.repo.full_name }} 227 | path: .repo 228 | - name: Install Dependencies 229 | run: cd .repo && yarn install --check-files --frozen-lockfile 230 | - name: Extract build artifact 231 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 232 | - name: Move build artifact out of the way 233 | run: mv dist dist.old 234 | - name: Create dotnet artifact 235 | run: cd .repo && npx projen package:dotnet 236 | - name: Collect dotnet artifact 237 | run: mv .repo/dist dist 238 | package-go: 239 | needs: build 240 | runs-on: ubuntu-latest 241 | permissions: 242 | contents: read 243 | if: ${{ !needs.build.outputs.self_mutation_happened }} 244 | steps: 245 | - uses: actions/setup-node@v4 246 | with: 247 | node-version: lts/* 248 | - uses: actions/setup-go@v5 249 | with: 250 | go-version: ^1.18.0 251 | - name: Download build artifacts 252 | uses: actions/download-artifact@v4 253 | with: 254 | name: build-artifact 255 | path: dist 256 | - name: Restore build artifact permissions 257 | run: cd dist && setfacl --restore=permissions-backup.acl 258 | continue-on-error: true 259 | - name: Checkout 260 | uses: actions/checkout@v4 261 | with: 262 | ref: ${{ github.event.pull_request.head.ref }} 263 | repository: ${{ github.event.pull_request.head.repo.full_name }} 264 | path: .repo 265 | - name: Install Dependencies 266 | run: cd .repo && yarn install --check-files --frozen-lockfile 267 | - name: Extract build artifact 268 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 269 | - name: Move build artifact out of the way 270 | run: mv dist dist.old 271 | - name: Create go artifact 272 | run: cd .repo && npx projen package:go 273 | - name: Collect go artifact 274 | run: mv .repo/dist dist 275 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: 14 | branches: 15 | - main 16 | jobs: 17 | validate: 18 | name: Validate PR title 19 | runs-on: ubuntu-latest 20 | permissions: 21 | pull-requests: write 22 | steps: 23 | - uses: amannn/action-semantic-pull-request@v5.4.0 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | with: 27 | types: |- 28 | feat 29 | fix 30 | chore 31 | requireScope: false 32 | if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: lts/* 35 | - name: Install dependencies 36 | run: yarn install --check-files --frozen-lockfile 37 | - name: release 38 | run: npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | - release_maven 67 | - release_pypi 68 | - release_nuget 69 | - release_golang 70 | runs-on: ubuntu-latest 71 | permissions: 72 | contents: write 73 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 74 | steps: 75 | - uses: actions/setup-node@v4 76 | with: 77 | node-version: lts/* 78 | - name: Download build artifacts 79 | uses: actions/download-artifact@v4 80 | with: 81 | name: build-artifact 82 | path: dist 83 | - name: Restore build artifact permissions 84 | run: cd dist && setfacl --restore=permissions-backup.acl 85 | continue-on-error: true 86 | - name: Release 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | GITHUB_REPOSITORY: ${{ github.repository }} 90 | GITHUB_REF: ${{ github.sha }} 91 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 92 | release_npm: 93 | name: Publish to npm 94 | needs: release 95 | runs-on: ubuntu-latest 96 | permissions: 97 | id-token: write 98 | contents: read 99 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 100 | steps: 101 | - uses: actions/setup-node@v4 102 | with: 103 | node-version: lts/* 104 | - name: Download build artifacts 105 | uses: actions/download-artifact@v4 106 | with: 107 | name: build-artifact 108 | path: dist 109 | - name: Restore build artifact permissions 110 | run: cd dist && setfacl --restore=permissions-backup.acl 111 | continue-on-error: true 112 | - name: Checkout 113 | uses: actions/checkout@v4 114 | with: 115 | path: .repo 116 | - name: Install Dependencies 117 | run: cd .repo && yarn install --check-files --frozen-lockfile 118 | - name: Extract build artifact 119 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 120 | - name: Move build artifact out of the way 121 | run: mv dist dist.old 122 | - name: Create js artifact 123 | run: cd .repo && npx projen package:js 124 | - name: Collect js artifact 125 | run: mv .repo/dist dist 126 | - name: Release 127 | env: 128 | NPM_DIST_TAG: latest 129 | NPM_REGISTRY: registry.npmjs.org 130 | NPM_CONFIG_PROVENANCE: "true" 131 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 132 | run: npx -p publib@latest publib-npm 133 | release_maven: 134 | name: Publish to Maven Central 135 | needs: release 136 | runs-on: ubuntu-latest 137 | permissions: 138 | contents: read 139 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 140 | steps: 141 | - uses: actions/setup-java@v4 142 | with: 143 | distribution: corretto 144 | java-version: "11" 145 | - uses: actions/setup-node@v4 146 | with: 147 | node-version: lts/* 148 | - name: Download build artifacts 149 | uses: actions/download-artifact@v4 150 | with: 151 | name: build-artifact 152 | path: dist 153 | - name: Restore build artifact permissions 154 | run: cd dist && setfacl --restore=permissions-backup.acl 155 | continue-on-error: true 156 | - name: Checkout 157 | uses: actions/checkout@v4 158 | with: 159 | path: .repo 160 | - name: Install Dependencies 161 | run: cd .repo && yarn install --check-files --frozen-lockfile 162 | - name: Extract build artifact 163 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 164 | - name: Move build artifact out of the way 165 | run: mv dist dist.old 166 | - name: Create java artifact 167 | run: cd .repo && npx projen package:java 168 | - name: Collect java artifact 169 | run: mv .repo/dist dist 170 | - name: Release 171 | env: 172 | MAVEN_ENDPOINT: https://s01.oss.sonatype.org 173 | MAVEN_SERVER_ID: central-ossrh 174 | MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 175 | MAVEN_GPG_PRIVATE_KEY_PASSPHRASE: ${{ secrets.MAVEN_GPG_PRIVATE_KEY_PASSPHRASE }} 176 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 177 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 178 | MAVEN_STAGING_PROFILE_ID: ${{ secrets.MAVEN_STAGING_PROFILE_ID }} 179 | run: npx -p publib@latest publib-maven 180 | release_pypi: 181 | name: Publish to PyPI 182 | needs: release 183 | runs-on: ubuntu-latest 184 | permissions: 185 | contents: read 186 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 187 | steps: 188 | - uses: actions/setup-node@v4 189 | with: 190 | node-version: lts/* 191 | - uses: actions/setup-python@v5 192 | with: 193 | python-version: 3.x 194 | - name: Download build artifacts 195 | uses: actions/download-artifact@v4 196 | with: 197 | name: build-artifact 198 | path: dist 199 | - name: Restore build artifact permissions 200 | run: cd dist && setfacl --restore=permissions-backup.acl 201 | continue-on-error: true 202 | - name: Checkout 203 | uses: actions/checkout@v4 204 | with: 205 | path: .repo 206 | - name: Install Dependencies 207 | run: cd .repo && yarn install --check-files --frozen-lockfile 208 | - name: Extract build artifact 209 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 210 | - name: Move build artifact out of the way 211 | run: mv dist dist.old 212 | - name: Create python artifact 213 | run: cd .repo && npx projen package:python 214 | - name: Collect python artifact 215 | run: mv .repo/dist dist 216 | - name: Release 217 | env: 218 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 219 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 220 | run: npx -p publib@latest publib-pypi 221 | release_nuget: 222 | name: Publish to NuGet Gallery 223 | needs: release 224 | runs-on: ubuntu-latest 225 | permissions: 226 | contents: read 227 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 228 | steps: 229 | - uses: actions/setup-node@v4 230 | with: 231 | node-version: lts/* 232 | - uses: actions/setup-dotnet@v4 233 | with: 234 | dotnet-version: 6.x 235 | - name: Download build artifacts 236 | uses: actions/download-artifact@v4 237 | with: 238 | name: build-artifact 239 | path: dist 240 | - name: Restore build artifact permissions 241 | run: cd dist && setfacl --restore=permissions-backup.acl 242 | continue-on-error: true 243 | - name: Checkout 244 | uses: actions/checkout@v4 245 | with: 246 | path: .repo 247 | - name: Install Dependencies 248 | run: cd .repo && yarn install --check-files --frozen-lockfile 249 | - name: Extract build artifact 250 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 251 | - name: Move build artifact out of the way 252 | run: mv dist dist.old 253 | - name: Create dotnet artifact 254 | run: cd .repo && npx projen package:dotnet 255 | - name: Collect dotnet artifact 256 | run: mv .repo/dist dist 257 | - name: Release 258 | env: 259 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 260 | run: npx -p publib@latest publib-nuget 261 | release_golang: 262 | name: Publish to GitHub Go Module Repository 263 | needs: release 264 | runs-on: ubuntu-latest 265 | permissions: 266 | contents: read 267 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 268 | steps: 269 | - uses: actions/setup-node@v4 270 | with: 271 | node-version: lts/* 272 | - uses: actions/setup-go@v5 273 | with: 274 | go-version: ^1.18.0 275 | - name: Download build artifacts 276 | uses: actions/download-artifact@v4 277 | with: 278 | name: build-artifact 279 | path: dist 280 | - name: Restore build artifact permissions 281 | run: cd dist && setfacl --restore=permissions-backup.acl 282 | continue-on-error: true 283 | - name: Checkout 284 | uses: actions/checkout@v4 285 | with: 286 | path: .repo 287 | - name: Install Dependencies 288 | run: cd .repo && yarn install --check-files --frozen-lockfile 289 | - name: Extract build artifact 290 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 291 | - name: Move build artifact out of the way 292 | run: mv dist dist.old 293 | - name: Create go artifact 294 | run: cd .repo && npx projen package:go 295 | - name: Collect go artifact 296 | run: mv .repo/dist dist 297 | - name: Release 298 | env: 299 | GIT_USER_NAME: github-actions 300 | GIT_USER_EMAIL: github-actions@github.com 301 | GITHUB_TOKEN: ${{ secrets.GO_GITHUB_TOKEN }} 302 | run: npx -p publib@latest publib-golang 303 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-cdklabs-projen-project-types-main 4 | on: 5 | workflow_dispatch: {} 6 | jobs: 7 | upgrade: 8 | name: Upgrade 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | outputs: 13 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | ref: main 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | - name: Install dependencies 22 | run: yarn install --check-files --frozen-lockfile 23 | - name: Upgrade dependencies 24 | run: npx projen upgrade-cdklabs-projen-project-types 25 | - name: Find mutations 26 | id: create_patch 27 | run: |- 28 | git add . 29 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 30 | working-directory: ./ 31 | - name: Upload patch 32 | if: steps.create_patch.outputs.patch_created 33 | uses: actions/upload-artifact@v4.4.0 34 | with: 35 | name: repo.patch 36 | path: repo.patch 37 | overwrite: true 38 | pr: 39 | name: Create Pull Request 40 | needs: upgrade 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | if: ${{ needs.upgrade.outputs.patch_created }} 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | ref: main 50 | - name: Download patch 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: repo.patch 54 | path: ${{ runner.temp }} 55 | - name: Apply patch 56 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 57 | - name: Set git identity 58 | run: |- 59 | git config user.name "github-actions" 60 | git config user.email "github-actions@github.com" 61 | - name: Create Pull Request 62 | id: create-pr 63 | uses: peter-evans/create-pull-request@v6 64 | with: 65 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 66 | commit-message: |- 67 | chore(deps): upgrade cdklabs-projen-project-types 68 | 69 | Upgrades project dependencies. See details in [workflow run]. 70 | 71 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 72 | 73 | ------ 74 | 75 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 76 | branch: github-actions/upgrade-cdklabs-projen-project-types-main 77 | title: "chore(deps): upgrade cdklabs-projen-project-types" 78 | labels: auto-approve 79 | body: |- 80 | Upgrades project dependencies. See details in [workflow run]. 81 | 82 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 83 | 84 | ------ 85 | 86 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 87 | author: github-actions 88 | committer: github-actions 89 | signoff: true 90 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-dev-deps-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-dev-deps-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade-dev-deps 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dev dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 80 | branch: github-actions/upgrade-dev-deps-main 81 | title: "chore(deps): upgrade dev dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 18 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | fix(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "fix(deps): upgrade dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | /test-reports/ 35 | junit.xml 36 | /coverage/ 37 | !/.github/workflows/build.yml 38 | /dist/changelog.md 39 | /dist/version.txt 40 | !/.github/workflows/release.yml 41 | !/.github/pull_request_template.md 42 | !/test/ 43 | !/tsconfig.dev.json 44 | !/src/ 45 | /lib 46 | /dist/ 47 | !/.eslintrc.json 48 | .jsii 49 | tsconfig.json 50 | !/API.md 51 | .jsii.tabl.json 52 | !/rosetta/default.ts-fixture 53 | !/.github/workflows/auto-merge.yml 54 | !/.github/workflows/upgrade-cdklabs-projen-project-types-main.yml 55 | !/.github/workflows/upgrade-main.yml 56 | !/.github/workflows/upgrade-dev-deps-main.yml 57 | .env 58 | .idea 59 | example/*.js 60 | example/*.d.ts 61 | !/.projenrc.ts 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /test/ 10 | /tsconfig.dev.json 11 | /src/ 12 | !/lib/ 13 | !/lib/**/*.js 14 | !/lib/**/*.d.ts 15 | dist 16 | /tsconfig.json 17 | /.github/ 18 | /.vscode/ 19 | /.idea/ 20 | /.projenrc.js 21 | tsconfig.tsbuildinfo 22 | /.eslintrc.json 23 | !.jsii 24 | /.gitattributes 25 | /.projenrc.ts 26 | /projenrc 27 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/jest", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/node", 9 | "version": "^18", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@typescript-eslint/eslint-plugin", 14 | "version": "^7", 15 | "type": "build" 16 | }, 17 | { 18 | "name": "@typescript-eslint/parser", 19 | "version": "^7", 20 | "type": "build" 21 | }, 22 | { 23 | "name": "aws-sdk", 24 | "type": "build" 25 | }, 26 | { 27 | "name": "cdklabs-projen-project-types", 28 | "type": "build" 29 | }, 30 | { 31 | "name": "commit-and-tag-version", 32 | "version": "^12", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "eslint-import-resolver-typescript", 37 | "type": "build" 38 | }, 39 | { 40 | "name": "eslint-plugin-import", 41 | "type": "build" 42 | }, 43 | { 44 | "name": "eslint", 45 | "version": "^8", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "jest", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "jest-junit", 54 | "version": "^15", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "jsii-diff", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "jsii-docgen", 63 | "version": "^1.8.110", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "jsii-pacmak", 68 | "type": "build" 69 | }, 70 | { 71 | "name": "jsii-rosetta", 72 | "type": "build" 73 | }, 74 | { 75 | "name": "jsii", 76 | "version": "~5.2", 77 | "type": "build" 78 | }, 79 | { 80 | "name": "projen", 81 | "type": "build" 82 | }, 83 | { 84 | "name": "ts-jest", 85 | "type": "build" 86 | }, 87 | { 88 | "name": "ts-node", 89 | "type": "build" 90 | }, 91 | { 92 | "name": "typescript", 93 | "type": "build" 94 | }, 95 | { 96 | "name": "@aws-cdk/integ-runner", 97 | "version": "latest", 98 | "type": "devenv" 99 | }, 100 | { 101 | "name": "@aws-cdk/integ-tests-alpha", 102 | "version": "latest", 103 | "type": "devenv" 104 | }, 105 | { 106 | "name": "aws-cdk-lib", 107 | "version": "^2.0.0", 108 | "type": "peer" 109 | }, 110 | { 111 | "name": "constructs", 112 | "version": "^10.0.5", 113 | "type": "peer" 114 | } 115 | ], 116 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 117 | } 118 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/auto-merge.yml", 8 | ".github/workflows/build.yml", 9 | ".github/workflows/pull-request-lint.yml", 10 | ".github/workflows/release.yml", 11 | ".github/workflows/upgrade-cdklabs-projen-project-types-main.yml", 12 | ".github/workflows/upgrade-dev-deps-main.yml", 13 | ".github/workflows/upgrade-main.yml", 14 | ".gitignore", 15 | ".projen/deps.json", 16 | ".projen/files.json", 17 | ".projen/tasks.json", 18 | "LICENSE", 19 | "tsconfig.dev.json" 20 | ], 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { CdklabsConstructLibrary } from 'cdklabs-projen-project-types'; 2 | import { DependencyType } from 'projen'; 3 | 4 | const project = new CdklabsConstructLibrary({ 5 | name: 'cdk-watchful', 6 | projenrcTs: true, 7 | private: false, 8 | enablePRAutoMerge: true, 9 | description: 'Watching your CDK apps since 2019', 10 | defaultReleaseBranch: 'main', 11 | 12 | author: 'Elad Ben-Israel', 13 | authorAddress: 'elad.benisrael@gmail.com', 14 | repositoryUrl: 'https://github.com/eladb/cdk-watchful.git', 15 | keywords: [ 16 | 'cloudwatch', 17 | 'monitoring', 18 | ], 19 | 20 | catalog: { 21 | twitter: 'emeshbi', 22 | }, 23 | 24 | cdkVersion: '2.0.0', 25 | peerDeps: [ 26 | 'aws-cdk-lib', 27 | ], 28 | 29 | devDeps: [ 30 | 'aws-sdk', 31 | 'cdklabs-projen-project-types', 32 | ], 33 | 34 | // jsii publishing 35 | 36 | publishToMaven: { 37 | javaPackage: 'io.github.cdklabs.watchful', 38 | mavenGroupId: 'io.github.cdklabs', 39 | mavenArtifactId: 'cdk-watchful', 40 | mavenServerId: 'central-ossrh', 41 | }, 42 | 43 | publishToPypi: { 44 | distName: 'cdk-watchful', 45 | module: 'cdk_watchful', 46 | }, 47 | 48 | publishToNuget: { 49 | dotNetNamespace: 'Cdklabs.CdkWatchful', 50 | packageId: 'Cdklabs.CdkWatchful', 51 | }, 52 | 53 | autoApproveOptions: { 54 | allowedUsernames: ['cdklabs-automation'], 55 | secret: 'GITHUB_TOKEN', 56 | }, 57 | 58 | autoApproveUpgrades: true, 59 | }); 60 | 61 | project.deps.addDependency('jsii-docgen@^1.8.110', DependencyType.BUILD); 62 | 63 | project.gitignore.exclude('.env', '.idea'); 64 | project.gitignore.exclude('example/*.js', 'example/*.d.ts'); 65 | 66 | project.synth(); 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ----- 2 | 3 | ### ✨ Have you heard of cdk-monitoring-constructs? ✨ 4 | 5 | Watchful on steroids. Check it out! 🔝 6 | 7 | ----- 8 | 9 | # cdk-watchful 10 | 11 | > Watching your CDK back since 2019 12 | 13 | Watchful is an [AWS CDK](https://github.com/awslabs/aws-cdk) construct library that makes it easy 14 | to monitor CDK apps. It automatically synthesizes alarms and dashboards for supported AWS resources. 15 | 16 | ```ts 17 | declare const myTable: dynamodb.Table; 18 | declare const myFunction: lambda.Function; 19 | declare const myRestApi: apigw.RestApi; 20 | 21 | const wf = new Watchful(this, 'watchful'); 22 | wf.watchDynamoTable('My Cute Little Table', myTable); 23 | wf.watchLambdaFunction('My Function', myFunction); 24 | wf.watchApiGateway('My REST API', myRestApi); 25 | ``` 26 | 27 | And... 28 | 29 | ![](https://raw.githubusercontent.com/eladb/cdk-watchful/master/example/sample.png) 30 | 31 | ## Initialize 32 | 33 | To get started, just define a `Watchful` construct in your CDK app. 34 | You can initialize using an email address, SQS ARN or both: 35 | 36 | ```ts 37 | import * as sns from 'aws-cdk-lib/aws-sns'; 38 | import * as sqs from 'aws-cdk-lib/aws-sqs'; 39 | 40 | const alarmSqs = sqs.Queue.fromQueueArn(this, 'AlarmQueue', 'arn:aws:sqs:us-east-1:444455556666:alarm-queue') 41 | const alarmSns = sns.Topic.fromTopicArn(this, 'AlarmTopic', 'arn:aws:sns:us-east-2:444455556666:MyTopic'); 42 | 43 | const wf = new Watchful(this, 'watchful', { 44 | alarmEmail: 'your@email.com', 45 | alarmSqs, 46 | alarmSns, 47 | alarmActionArns: [ 'arn:aws:sqs:us-east-1:444455556666:alarm-queue' ] 48 | }); 49 | ``` 50 | 51 | ## Add Resources 52 | 53 | Watchful manages a central dashboard and configures default alarming for: 54 | 55 | - Amazon DynamoDB: `watchful.watchDynamoTable` 56 | - AWS Lambda: `watchful.watchLambdaFunction` 57 | - Amazon API Gateway: `watchful.watchApiGateway` 58 | - [Request yours](https://github.com/eladb/cdk-watchful/issues/new) 59 | 60 | ## Watching Scopes 61 | 62 | Watchful can also watch complete CDK construct scopes. It will automatically 63 | discover all watchable resources within that scope (recursively), add them 64 | to your dashboard and configure alarms for them. 65 | 66 | ```ts 67 | declare const storageLayer: Stack; 68 | declare const wf: Watchful; 69 | 70 | wf.watchScope(storageLayer); 71 | ``` 72 | 73 | ## Example 74 | 75 | See a more complete [example](https://github.com/eladb/cdk-watchful/blob/master/example/index.ts). 76 | 77 | ## Contributing 78 | 79 | Contributions of all kinds are welcome and celebrated. Raise an issue, submit a PR, do the right thing. 80 | 81 | To set up a dev environment: 82 | 83 | 1. Clone this repo 84 | 2. `yarn` 85 | 86 | Development workflow (change code and run tests automatically): 87 | 88 | ```shell 89 | yarn test:watch 90 | ``` 91 | 92 | Build (like CI): 93 | 94 | ```shell 95 | yarn build 96 | ``` 97 | 98 | And then publish as a PR. 99 | 100 | ## License 101 | 102 | [Apache 2.0](https://github.com/eladb/cdk-watchful/blob/master/LICENSE) 103 | 104 | -------------------------------------------------------------------------------- /example/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "node index.js" 3 | } -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, App, Duration } from 'aws-cdk-lib'; 2 | import {Construct} from 'constructs' 3 | import { Watchful } from '../src'; 4 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 5 | import * as events from 'aws-cdk-lib/aws-events'; 6 | import * as sns from 'aws-cdk-lib/aws-sns'; 7 | import * as sqs from 'aws-cdk-lib/aws-sqs'; 8 | import * as events_targets from 'aws-cdk-lib/aws-events-targets'; 9 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 10 | import * as path from 'path'; 11 | 12 | class TestStack extends Stack { 13 | constructor(scope: Construct, id: string, props: StackProps = {}) { 14 | super(scope, id, props); 15 | 16 | const table1 = new dynamodb.Table(this, 'DynamoTable1', { 17 | writeCapacity: 10, 18 | partitionKey: { 19 | name: 'ID', 20 | type: dynamodb.AttributeType.STRING, 21 | }, 22 | }); 23 | 24 | const writeTraffic = new TrafficDriver(this, 'WriteTraffic', { 25 | table: table1, 26 | write: true, 27 | }); 28 | 29 | const readTraffic = new TrafficDriver(this, 'WriteReadTraffic', { 30 | table: table1, 31 | write: true, 32 | read: true, 33 | }); 34 | 35 | const alarmSqs = sqs.Queue.fromQueueArn(this, 'AlarmQueue', 'arn:aws:sqs:us-east-1:444455556666:alarm-queue') 36 | const alarmSns = sns.Topic.fromTopicArn(this, 'AlarmTopic', 'arn:aws:sns:us-east-2:444455556666:MyTopic'); 37 | 38 | const watchful = new Watchful(this, 'watchful', { 39 | alarmEmail: 'benisrae@amazon.com', 40 | alarmSqs, 41 | alarmSns, 42 | }); 43 | 44 | watchful.watchDynamoTable('My Cute Little Table', table1); 45 | 46 | watchful.watchScope(writeTraffic); 47 | watchful.watchScope(readTraffic); 48 | } 49 | } 50 | 51 | interface TrafficDriverProps { 52 | table: dynamodb.Table; 53 | read?: boolean; 54 | write?: boolean; 55 | } 56 | 57 | class TrafficDriver extends Construct { 58 | private readonly fn: lambda.Function; 59 | 60 | constructor(scope: Construct, id: string, props: TrafficDriverProps) { 61 | super(scope, id); 62 | 63 | if (!props.read && !props.write) { 64 | throw new Error('At least "read" or "write" must be set'); 65 | } 66 | 67 | this.fn = new lambda.Function(this, 'LambdaFunction', { 68 | code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')), 69 | runtime: lambda.Runtime.NODEJS_10_X, 70 | handler: 'index.handler', 71 | environment: { 72 | TABLE_NAME: props.table.tableName, 73 | READ: props.read ? 'TRUE' : '', 74 | WRITE: props.write ? 'TRUE': '', 75 | }, 76 | }); 77 | 78 | if (props.write) { 79 | props.table.grantWriteData(this.fn); 80 | } 81 | 82 | if (props.read) { 83 | props.table.grantReadData(this.fn); 84 | } 85 | 86 | new events.Rule(this, 'Tick', { 87 | schedule: events.Schedule.rate(Duration.minutes(1)), 88 | targets: [ new events_targets.LambdaFunction(this.fn) ], 89 | }); 90 | } 91 | } 92 | 93 | class TestApp extends App { 94 | constructor() { 95 | super(); 96 | 97 | new TestStack(this, 'watchful-example'); 98 | } 99 | } 100 | 101 | new TestApp().synth(); -------------------------------------------------------------------------------- /example/lambda/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import * as SDK from 'aws-sdk'; 3 | 4 | const TABLE_NAME = process.env.TABLE_NAME; 5 | if (!TABLE_NAME) { 6 | throw new Error('Missing TABLE_NAME environment variable'); 7 | } 8 | 9 | const READ = process.env.READ; 10 | const WRITE = process.env.WRITE; 11 | 12 | exports.handler = async (event: any) => { 13 | console.error(event); 14 | 15 | const dynamo = new SDK.DynamoDB(); 16 | 17 | if (WRITE) { 18 | const key = new Date().toISOString() + '.' + Math.floor(Math.random() * 99999); 19 | const req: SDK.DynamoDB.PutItemInput = { 20 | TableName: TABLE_NAME, 21 | Item: { ID: { S: key } }, 22 | }; 23 | 24 | await dynamo.putItem(req).promise(); 25 | } 26 | 27 | if (READ) { 28 | const req: SDK.DynamoDB.ScanInput = { 29 | TableName: TABLE_NAME, 30 | Limit: 1, 31 | }; 32 | await dynamo.scan(req).promise(); 33 | } 34 | }; -------------------------------------------------------------------------------- /example/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdklabs/cdk-watchful/606060017abbb972e3882ae63be19b9e10d2146e/example/sample.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-watchful", 3 | "description": "Watching your CDK apps since 2019", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/eladb/cdk-watchful.git" 7 | }, 8 | "scripts": { 9 | "build": "npx projen build", 10 | "bump": "npx projen bump", 11 | "clobber": "npx projen clobber", 12 | "compat": "npx projen compat", 13 | "compile": "npx projen compile", 14 | "default": "npx projen default", 15 | "docgen": "npx projen docgen", 16 | "eject": "npx projen eject", 17 | "eslint": "npx projen eslint", 18 | "integ": "npx projen integ", 19 | "integ:update": "npx projen integ:update", 20 | "package": "npx projen package", 21 | "package-all": "npx projen package-all", 22 | "package:dotnet": "npx projen package:dotnet", 23 | "package:go": "npx projen package:go", 24 | "package:java": "npx projen package:java", 25 | "package:js": "npx projen package:js", 26 | "package:python": "npx projen package:python", 27 | "post-compile": "npx projen post-compile", 28 | "post-upgrade": "npx projen post-upgrade", 29 | "pre-compile": "npx projen pre-compile", 30 | "release": "npx projen release", 31 | "rosetta:extract": "npx projen rosetta:extract", 32 | "test": "npx projen test", 33 | "test:watch": "npx projen test:watch", 34 | "unbump": "npx projen unbump", 35 | "upgrade": "npx projen upgrade", 36 | "upgrade-cdklabs-projen-project-types": "npx projen upgrade-cdklabs-projen-project-types", 37 | "upgrade-dev-deps": "npx projen upgrade-dev-deps", 38 | "watch": "npx projen watch", 39 | "projen": "npx projen" 40 | }, 41 | "author": { 42 | "name": "Amazon Web Services", 43 | "email": "aws-cdk-dev@amazon.com", 44 | "organization": true 45 | }, 46 | "devDependencies": { 47 | "@aws-cdk/integ-runner": "latest", 48 | "@aws-cdk/integ-tests-alpha": "latest", 49 | "@types/jest": "^27", 50 | "@types/node": "^18", 51 | "@typescript-eslint/eslint-plugin": "^7", 52 | "@typescript-eslint/parser": "^7", 53 | "aws-cdk-lib": "2.0.0", 54 | "aws-sdk": "^2.1692.0", 55 | "cdklabs-projen-project-types": "^0.1.204", 56 | "commit-and-tag-version": "^12", 57 | "constructs": "10.0.5", 58 | "eslint": "^8", 59 | "eslint-import-resolver-typescript": "^2.7.1", 60 | "eslint-plugin-import": "^2.31.0", 61 | "jest": "^27", 62 | "jest-junit": "^15", 63 | "jsii": "~5.2", 64 | "jsii-diff": "^1.112.0", 65 | "jsii-docgen": "^1.8.110", 66 | "jsii-pacmak": "^1.112.0", 67 | "jsii-rosetta": "^5.8.9", 68 | "projen": "^0.87.4", 69 | "ts-jest": "^27", 70 | "ts-node": "^10.9.2", 71 | "typescript": "^5.2.2" 72 | }, 73 | "peerDependencies": { 74 | "aws-cdk-lib": "^2.0.0", 75 | "constructs": "^10.0.5" 76 | }, 77 | "keywords": [ 78 | "cdk", 79 | "cloudwatch", 80 | "monitoring" 81 | ], 82 | "engines": { 83 | "node": ">= 18.12.0" 84 | }, 85 | "main": "lib/index.js", 86 | "license": "Apache-2.0", 87 | "publishConfig": { 88 | "access": "public" 89 | }, 90 | "version": "0.0.0", 91 | "jest": { 92 | "coverageProvider": "v8", 93 | "testMatch": [ 94 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 95 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 96 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 97 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 98 | ], 99 | "clearMocks": true, 100 | "collectCoverage": true, 101 | "coverageReporters": [ 102 | "json", 103 | "lcov", 104 | "clover", 105 | "cobertura", 106 | "text" 107 | ], 108 | "coverageDirectory": "coverage", 109 | "coveragePathIgnorePatterns": [ 110 | "/node_modules/" 111 | ], 112 | "testPathIgnorePatterns": [ 113 | "/node_modules/" 114 | ], 115 | "watchPathIgnorePatterns": [ 116 | "/node_modules/" 117 | ], 118 | "reporters": [ 119 | "default", 120 | [ 121 | "jest-junit", 122 | { 123 | "outputDirectory": "test-reports" 124 | } 125 | ] 126 | ], 127 | "preset": "ts-jest", 128 | "globals": { 129 | "ts-jest": { 130 | "tsconfig": "tsconfig.dev.json" 131 | } 132 | } 133 | }, 134 | "types": "lib/index.d.ts", 135 | "stability": "experimental", 136 | "jsii": { 137 | "outdir": "dist", 138 | "targets": { 139 | "java": { 140 | "package": "io.github.cdklabs.watchful", 141 | "maven": { 142 | "groupId": "io.github.cdklabs", 143 | "artifactId": "cdk-watchful" 144 | } 145 | }, 146 | "python": { 147 | "distName": "cdk-watchful", 148 | "module": "cdk_watchful" 149 | }, 150 | "dotnet": { 151 | "namespace": "Cdklabs.CdkWatchful", 152 | "packageId": "Cdklabs.CdkWatchful" 153 | }, 154 | "go": { 155 | "moduleName": "github.com/cdklabs/cdk-watchful-go" 156 | } 157 | }, 158 | "tsc": { 159 | "outDir": "lib", 160 | "rootDir": "src" 161 | } 162 | }, 163 | "awscdkio": { 164 | "twitter": "emeshbi" 165 | }, 166 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 167 | } 168 | -------------------------------------------------------------------------------- /rosetta/default.ts-fixture: -------------------------------------------------------------------------------- 1 | // Fixture with packages imported, but nothing else 2 | import { Construct } from 'constructs'; 3 | import { 4 | Stack, 5 | aws_dynamodb as dynamodb, 6 | aws_lambda as lambda, 7 | aws_apigateway as apigw, 8 | } from 'aws-cdk-lib'; 9 | import { Watchful } from 'cdk-watchful'; 10 | 11 | class Fixture extends Stack { 12 | constructor(scope: Construct, id: string) { 13 | super(scope, id); 14 | 15 | /// here 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/api-gateway.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import * as apigw from 'aws-cdk-lib/aws-apigateway'; 3 | import { ComparisonOperator, GraphWidget, HorizontalAnnotation } from 'aws-cdk-lib/aws-cloudwatch'; 4 | import { Construct } from 'constructs'; 5 | import { IWatchful } from './api'; 6 | import { ApiGatewayMetricFactory } from './monitoring/aws/api-gateway/metrics'; 7 | 8 | export interface WatchApiGatewayOptions { 9 | /** 10 | * Alarm when 5XX errors reach this threshold over 5 minutes. 11 | * 12 | * @default 1 any 5xx HTTP response will trigger the alarm 13 | */ 14 | readonly serverErrorThreshold?: number; 15 | 16 | /** 17 | * A list of operations to monitor separately. 18 | * 19 | * @default - only API-level monitoring is added. 20 | */ 21 | readonly watchedOperations?: WatchedOperation[]; 22 | 23 | /** 24 | * Include a dashboard graph for caching metrics 25 | * 26 | * @default false 27 | */ 28 | readonly cacheGraph?: boolean; 29 | } 30 | 31 | export interface WatchApiGatewayProps extends WatchApiGatewayOptions { 32 | /** 33 | * The title of this section. 34 | */ 35 | readonly title: string; 36 | 37 | /** 38 | * The Watchful instance to add widgets into. 39 | */ 40 | readonly watchful: IWatchful; 41 | 42 | /** 43 | * The API Gateway REST API that is being watched. 44 | */ 45 | readonly restApi: apigw.RestApi; 46 | } 47 | 48 | export class WatchApiGateway extends Construct { 49 | private readonly api: apigw.CfnRestApi; 50 | private readonly apiName: string; 51 | private readonly stage: string; 52 | private readonly watchful: IWatchful; 53 | private readonly metrics: ApiGatewayMetricFactory; 54 | 55 | constructor(scope: Construct, id: string, props: WatchApiGatewayProps) { 56 | super(scope, id); 57 | 58 | this.api = props.restApi.node.findChild('Resource') as apigw.CfnRestApi; 59 | this.apiName = this.api.name!; 60 | this.stage = props.restApi.deploymentStage.stageName; 61 | this.watchful = props.watchful; 62 | this.metrics = new ApiGatewayMetricFactory(); 63 | 64 | const alarmThreshold = props.serverErrorThreshold == null ? 1 : props.serverErrorThreshold; 65 | if (alarmThreshold) { 66 | 67 | const count5xxMetric = this.metrics.metricErrors(this.apiName, this.stage).count5XX.with({ 68 | statistic: 'sum', 69 | period: Duration.minutes(5), 70 | }); 71 | this.watchful.addAlarm( 72 | count5xxMetric.createAlarm(this, '5XXErrorAlarm', { 73 | alarmDescription: `at ${alarmThreshold}`, 74 | threshold: alarmThreshold, 75 | comparisonOperator: 76 | ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, 77 | evaluationPeriods: 1, 78 | }), 79 | ); 80 | } 81 | 82 | this.watchful.addSection(props.title, { 83 | links: [{ title: 'Amazon API Gateway Console', url: linkForApiGateway(props.restApi) }], 84 | }); 85 | [undefined, ...props.watchedOperations || []].forEach(operation => 86 | this.watchful.addWidgets( 87 | this.createCallGraphWidget(operation, alarmThreshold), 88 | ...props.cacheGraph ? [this.createCacheGraphWidget(operation)] : [], 89 | this.createLatencyGraphWidget(operation), 90 | this.createIntegrationLatencyGraphWidget(operation), 91 | ), 92 | ); 93 | } 94 | 95 | private createCallGraphWidget(opts?: WatchedOperation, alarmThreshold?: number) { 96 | const leftAnnotations: HorizontalAnnotation[] = alarmThreshold 97 | ? [{ value: alarmThreshold, color: '#ff0000', label: '5XX Errors Alarm' }] 98 | : []; 99 | 100 | return new GraphWidget({ 101 | title: `${opts ? `${opts.httpMethod} ${opts.resourcePath}` : 'Overall'} Calls/min`, 102 | width: 12, 103 | stacked: false, 104 | left: [ 105 | this.metrics.metricCalls(this.apiName, this.stage, opts), 106 | this.metrics.metricErrors(this.apiName, this.stage, opts).count4XX, 107 | this.metrics.metricErrors(this.apiName, this.stage, opts).count5XX, 108 | ], 109 | leftAnnotations, 110 | }); 111 | } 112 | 113 | private createCacheGraphWidget(opts?: WatchedOperation) { 114 | return new GraphWidget({ 115 | title: `${opts ? `${opts.httpMethod} ${opts.resourcePath}` : 'Overall'} Cache/min`, 116 | width: 12, 117 | stacked: false, 118 | left: [ 119 | this.metrics.metricCalls(this.apiName, this.stage, opts), 120 | this.metrics.metricCache(this.apiName, this.stage, opts).hits, 121 | this.metrics.metricCache(this.apiName, this.stage, opts).misses, 122 | ], 123 | }); 124 | } 125 | 126 | private createLatencyGraphWidget(opts?: WatchedOperation) { 127 | return new GraphWidget({ 128 | title: `${opts ? `${opts.httpMethod} ${opts.resourcePath}` : 'Overall'} (1-minute periods)`, 129 | width: 12, 130 | stacked: false, 131 | left: Object.values(this.metrics.metricLatency(this.apiName, this.stage, opts)), 132 | }); 133 | } 134 | 135 | private createIntegrationLatencyGraphWidget(opts?: WatchedOperation) { 136 | return new GraphWidget({ 137 | title: `${opts ? `${opts.httpMethod} ${opts.resourcePath}` : 'Overall'} Integration (1-minute periods)`, 138 | width: 12, 139 | stacked: false, 140 | left: Object.values(this.metrics.metricIntegrationLatency(this.apiName, this.stage, opts)), 141 | }); 142 | } 143 | } 144 | 145 | /** 146 | * An operation (path and method) worth monitoring. 147 | */ 148 | export interface WatchedOperation { 149 | /** 150 | * The HTTP method for the operation (GET, POST, ...) 151 | */ 152 | readonly httpMethod: string; 153 | 154 | /** 155 | * The REST API path for this operation (/, /resource/{id}, ...) 156 | */ 157 | readonly resourcePath: string; 158 | } 159 | 160 | function linkForApiGateway(api: apigw.IRestApi) { 161 | return `https://console.aws.amazon.com/apigateway/home?region=${api.stack.region}#/apis/${api.restApiId}`; 162 | } 163 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; 2 | 3 | export interface IWatchful { 4 | addSection(title: string, options?: SectionOptions): void; 5 | addAlarm(alarm: cloudwatch.IAlarm): void; 6 | addWidgets(...widgets: cloudwatch.IWidget[]): void; 7 | } 8 | 9 | export interface SectionOptions { 10 | readonly links?: QuickLink[]; 11 | } 12 | 13 | export interface QuickLink { 14 | readonly title: string; 15 | readonly url: string; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/aspect.ts: -------------------------------------------------------------------------------- 1 | import { IAspect } from 'aws-cdk-lib'; 2 | import * as apigw from 'aws-cdk-lib/aws-apigateway'; 3 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 4 | import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns'; 5 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 6 | import * as rds from 'aws-cdk-lib/aws-rds'; 7 | import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; 8 | import { IConstruct } from 'constructs'; 9 | 10 | 11 | export interface WatchfulAspectProps { 12 | /** 13 | * Automatically watch API Gateway APIs in the scope. 14 | * @default true 15 | */ 16 | readonly apiGateway?: boolean; 17 | 18 | /** 19 | * Automatically watch all Amazon DynamoDB tables in the scope. 20 | * @default true 21 | */ 22 | readonly dynamodb?: boolean; 23 | 24 | /** 25 | * Automatically watch AWS Lambda functions in the scope. 26 | * @default true 27 | */ 28 | readonly lambda?: boolean; 29 | 30 | /** 31 | * Automatically watch AWS state machines in the scope. 32 | * @default true 33 | */ 34 | readonly stateMachine?: boolean; 35 | 36 | /** 37 | * Automatically watch RDS Aurora clusters in the scope. 38 | * @default true 39 | */ 40 | readonly rdsaurora?: boolean; 41 | 42 | /** 43 | * Automatically watch ApplicationLoadBalanced Fargate Ecs Services in the scope (using ECS Pattern). 44 | * @default true 45 | */ 46 | readonly fargateecs?: boolean; 47 | 48 | /** 49 | * Automatically watch ApplicationLoadBalanced EC2 Ecs Services in the scope (using ECS Pattern). 50 | * @default true 51 | */ 52 | readonly ec2ecs?: boolean; 53 | 54 | } 55 | 56 | /** 57 | * A CDK aspect that can automatically watch all resources within a scope. 58 | */ 59 | export class WatchfulAspect implements IAspect { 60 | /** 61 | * Defines a watchful aspect 62 | * @param watchful The watchful to add those resources to 63 | * @param props Options 64 | */ 65 | constructor(private readonly watchful: Watchful, private readonly props: WatchfulAspectProps = { }) { 66 | 67 | } 68 | 69 | public visit(node: IConstruct): void { 70 | const watchApiGateway = this.props.apiGateway === undefined ? true : this.props.apiGateway; 71 | const watchDynamo = this.props.dynamodb === undefined ? true : this.props.dynamodb; 72 | const watchLambda = this.props.lambda === undefined ? true : this.props.lambda; 73 | const watchStateMachine = this.props.stateMachine === undefined ? true : this.props.stateMachine; 74 | const watchRdsAuroraCluster = this.props.rdsaurora === undefined ? true : this.props.rdsaurora; 75 | const watchFargateEcs = this.props.fargateecs === undefined ? true : this.props.fargateecs; 76 | const watchEc2Ecs = this.props.ec2ecs === undefined ? true : this.props.ec2ecs; 77 | 78 | if (watchApiGateway && node instanceof apigw.RestApi) { 79 | this.watchful.watchApiGateway(node.node.path, node); 80 | } 81 | 82 | if (watchDynamo && node instanceof dynamodb.Table) { 83 | this.watchful.watchDynamoTable(node.node.path, node); 84 | } 85 | 86 | if (watchLambda && node instanceof lambda.Function) { 87 | this.watchful.watchLambdaFunction(node.node.path, node); 88 | } 89 | 90 | if (watchStateMachine && node instanceof stepfunctions.StateMachine) { 91 | this.watchful.watchStateMachine(node.node.path, node); 92 | } 93 | 94 | if (watchRdsAuroraCluster && node instanceof rds.DatabaseCluster) { 95 | this.watchful.watchRdsAuroraCluster(node.node.path, node); 96 | } 97 | 98 | if (watchFargateEcs && node instanceof ecs_patterns.ApplicationLoadBalancedFargateService) { 99 | this.watchful.watchFargateEcs(node.node.path, node.service, node.targetGroup); 100 | } 101 | 102 | if (watchEc2Ecs && node instanceof ecs_patterns.ApplicationLoadBalancedEc2Service) { 103 | this.watchful.watchEc2Ecs(node.node.path, node.service, node.targetGroup); 104 | } 105 | } 106 | } 107 | 108 | import { Watchful } from './watchful'; 109 | -------------------------------------------------------------------------------- /src/core/monitoring.ts: -------------------------------------------------------------------------------- 1 | import { IWidget } from 'aws-cdk-lib/aws-cloudwatch'; 2 | 3 | /** 4 | * Base class for monitoring props. 5 | */ 6 | export interface MonitoringProps { 7 | /** 8 | * monitoring section title (might be markdown) 9 | * @default auto-generated title 10 | */ 11 | readonly titleMarkdown?: string; 12 | /** 13 | * monitoring section description (might be markdown) 14 | * @default empty 15 | */ 16 | readonly descriptionMarkdown?: string; 17 | } 18 | 19 | /** 20 | * Collection of metrics and alarms, represented by widgets. 21 | */ 22 | export abstract class Monitoring { 23 | /** 24 | * Returns the widgets representing this monitoring object. 25 | */ 26 | abstract getWidgets(): IWidget[]; 27 | } 28 | -------------------------------------------------------------------------------- /src/dynamodb.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; 3 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 4 | import { Construct } from 'constructs'; 5 | import { IWatchful } from './api'; 6 | import { DynamoDbMetricFactory } from './monitoring/aws/dynamodb/metrics'; 7 | 8 | const DEFAULT_PERCENT = 80; 9 | 10 | export interface WatchDynamoTableOptions { 11 | /** 12 | * Threshold for read capacity alarm (percentage) 13 | * @default 80 14 | */ 15 | readonly readCapacityThresholdPercent?: number; 16 | 17 | /** 18 | * Threshold for read capacity alarm (percentage) 19 | * @default 80 20 | */ 21 | readonly writeCapacityThresholdPercent?: number; 22 | } 23 | 24 | export interface WatchDynamoTableProps extends WatchDynamoTableOptions{ 25 | readonly title: string; 26 | readonly watchful: IWatchful; 27 | readonly table: dynamodb.Table; 28 | } 29 | 30 | export class WatchDynamoTable extends Construct { 31 | private readonly watchful: IWatchful; 32 | private readonly metrics: DynamoDbMetricFactory; 33 | 34 | constructor(scope: Construct, id: string, props: WatchDynamoTableProps) { 35 | super(scope, id); 36 | 37 | const table = props.table; 38 | this.watchful = props.watchful; 39 | this.metrics = new DynamoDbMetricFactory(); 40 | 41 | const cfnTable = table.node.defaultChild as dynamodb.CfnTable; 42 | const billingMode = cfnTable.billingMode as dynamodb.BillingMode ?? dynamodb.BillingMode.PROVISIONED; 43 | 44 | switch (billingMode) { 45 | case dynamodb.BillingMode.PAY_PER_REQUEST: 46 | this.createWidgetsForPayPerRequestTable(props.title, table); 47 | break; 48 | 49 | case dynamodb.BillingMode.PROVISIONED: 50 | this.createWidgetsForProvisionedTable(props.title, 51 | table, 52 | props.readCapacityThresholdPercent, 53 | props.writeCapacityThresholdPercent, 54 | ); 55 | break; 56 | } 57 | } 58 | 59 | /** 60 | * Create widgets for tables with billingMode=PROVISIONED 61 | * Include alarms when capacity is over 80% of the provisioned value 62 | */ 63 | private createWidgetsForProvisionedTable(title: string, 64 | table: dynamodb.Table, 65 | readCapacityThresholdPercent?: number, 66 | writeCapacityThresholdPercent?: number) { 67 | const cfnTable = table.node.defaultChild as dynamodb.CfnTable; 68 | 69 | const metrics = this.metrics.metricConsumedCapacityUnits(table.tableName); 70 | const readCapacityMetric = metrics.read; 71 | const writeCapacityMetric = metrics.write; 72 | const throughput = cfnTable.provisionedThroughput as dynamodb.CfnTable.ProvisionedThroughputProperty; 73 | 74 | this.watchful.addAlarm(this.createDynamoCapacityAlarm('read', readCapacityMetric, throughput.readCapacityUnits, readCapacityThresholdPercent)); 75 | this.watchful.addAlarm(this.createDynamoCapacityAlarm('write', writeCapacityMetric, throughput.writeCapacityUnits, writeCapacityThresholdPercent)); 76 | 77 | this.watchful.addSection(title, { 78 | links: [{ title: 'Amazon DynamoDB Console', url: linkForDynamoTable(table) }], 79 | }); 80 | 81 | this.watchful.addWidgets( 82 | this.createDynamoCapacityGraph('Read', readCapacityMetric, throughput.readCapacityUnits, readCapacityThresholdPercent), 83 | this.createDynamoCapacityGraph('Write', writeCapacityMetric, throughput.writeCapacityUnits, writeCapacityThresholdPercent), 84 | ); 85 | } 86 | 87 | /** 88 | * Create widgets for tables with billingMode=PAY_PER_REQUEST 89 | * Include consumed capacity metrics 90 | */ 91 | private createWidgetsForPayPerRequestTable(title: string, table: dynamodb.Table) { 92 | const metrics = this.metrics.metricConsumedCapacityUnits(table.tableName); 93 | const readCapacityMetric = metrics.read; 94 | const writeCapacityMetric = metrics.write; 95 | 96 | this.watchful.addSection(title, { 97 | links: [{ title: 'Amazon DynamoDB Console', url: linkForDynamoTable(table) }], 98 | }); 99 | 100 | this.watchful.addWidgets( 101 | this.createDynamoPPRGraph('Read', readCapacityMetric), 102 | this.createDynamoPPRGraph('Write', writeCapacityMetric), 103 | ); 104 | } 105 | 106 | private createDynamoCapacityGraph(type: string, metric: cloudwatch.Metric, provisioned: number, percent: number = DEFAULT_PERCENT) { 107 | return new cloudwatch.GraphWidget({ 108 | title: `${type} Capacity Units/${metric.period.toMinutes()}min`, 109 | width: 12, 110 | stacked: true, 111 | left: [metric], 112 | leftAnnotations: [ 113 | { 114 | label: 'Provisioned', 115 | value: provisioned * metric.period.toSeconds(), 116 | color: '#58D68D', 117 | }, 118 | { 119 | color: '#FF3333', 120 | label: `Alarm on ${percent}%`, 121 | value: calculateUnits(provisioned, percent, metric.period), 122 | }, 123 | ], 124 | }); 125 | } 126 | 127 | private createDynamoPPRGraph(type: string, metric: cloudwatch.Metric) { 128 | return new cloudwatch.GraphWidget({ 129 | title: `${type} Capacity Units/${metric.period.toMinutes()}min`, 130 | width: 12, 131 | stacked: true, 132 | left: [metric], 133 | }); 134 | } 135 | 136 | private createDynamoCapacityAlarm(type: string, metric: cloudwatch.Metric, provisioned: number, percent: number = DEFAULT_PERCENT) { 137 | const periodMinutes = 5; 138 | const threshold = calculateUnits(provisioned, percent, Duration.minutes(periodMinutes)); 139 | const metricWithPeriod = metric.with({ 140 | statistic: 'sum', 141 | period: Duration.minutes(periodMinutes), 142 | }); 143 | const alarm = metricWithPeriod.createAlarm(this, `CapacityAlarm:${type}`, { 144 | alarmDescription: `at ${threshold}% of ${type} capacity`, 145 | threshold, 146 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, 147 | evaluationPeriods: 1, 148 | }); 149 | return alarm; 150 | } 151 | } 152 | 153 | 154 | function linkForDynamoTable(table: dynamodb.Table, tab = 'overview') { 155 | return `https://console.aws.amazon.com/dynamodb/home?region=${table.stack.region}#tables:selected=${table.tableName};tab=${tab}`; 156 | } 157 | 158 | function calculateUnits(provisioned: number, percent: number | undefined, period: Duration) { 159 | return provisioned * ((percent === undefined ? DEFAULT_PERCENT : percent) / 100) * period.toSeconds(); 160 | } 161 | 162 | -------------------------------------------------------------------------------- /src/ecs.ts: -------------------------------------------------------------------------------- 1 | import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; 2 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 3 | import { ApplicationTargetGroup } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 4 | import { Construct } from 'constructs'; 5 | import { IWatchful } from './api'; 6 | import { EcsMetricFactory } from './monitoring/aws/ecs/metrics'; 7 | 8 | 9 | export interface WatchEcsServiceOptions { 10 | /** 11 | * Threshold for the Cpu Maximum utilization 12 | * 13 | * @default 80 14 | */ 15 | readonly cpuMaximumThresholdPercent?: number; 16 | 17 | /** 18 | * Threshold for the Memory Maximum utilization. 19 | * 20 | * @default - 0. 21 | */ 22 | readonly memoryMaximumThresholdPercent?: number; 23 | 24 | /** 25 | * Threshold for the Target Response Time. 26 | * 27 | * @default - 0. 28 | */ 29 | readonly targetResponseTimeThreshold?: number; 30 | 31 | /** 32 | * Threshold for the Number of Requests. 33 | * 34 | * @default - 0. 35 | */ 36 | readonly requestsThreshold?: number; 37 | 38 | /** 39 | * Threshold for the Number of Request Errors. 40 | * 41 | * @default - 0. 42 | */ 43 | readonly requestsErrorRateThreshold?: number; 44 | } 45 | 46 | export interface WatchEcsServiceProps extends WatchEcsServiceOptions { 47 | readonly title: string; 48 | readonly watchful: IWatchful; 49 | readonly fargateService?: ecs.FargateService; 50 | readonly ec2Service?: ecs.Ec2Service; 51 | readonly targetGroup: ApplicationTargetGroup; 52 | } 53 | 54 | export class WatchEcsService extends Construct { 55 | 56 | private readonly watchful: IWatchful; 57 | private readonly ecsService: any; 58 | private readonly targetGroup: ApplicationTargetGroup; 59 | private readonly serviceName: string; 60 | private readonly clusterName: string; 61 | private readonly targetGroupName: string; 62 | private readonly loadBalancerName: string; 63 | private readonly metrics: EcsMetricFactory; 64 | 65 | constructor(scope: Construct, id: string, props: WatchEcsServiceProps) { 66 | super(scope, id); 67 | 68 | this.watchful = props.watchful; 69 | if (props.ec2Service) { 70 | this.ecsService = props.ec2Service; 71 | this.serviceName = props.ec2Service.serviceName; 72 | this.clusterName = props.ec2Service.cluster.clusterName; 73 | } else if (props.fargateService) { 74 | this.ecsService = props.fargateService; 75 | this.serviceName = props.fargateService.serviceName; 76 | this.clusterName = props.fargateService.cluster.clusterName; 77 | } else { 78 | throw new Error('No service provided to monitor.'); 79 | } 80 | 81 | this.targetGroup = props.targetGroup; 82 | this.targetGroupName = this.targetGroup.targetGroupFullName; 83 | this.loadBalancerName = this.targetGroup.firstLoadBalancerFullName; 84 | this.metrics = new EcsMetricFactory(); 85 | 86 | this.watchful.addSection(props.title, { 87 | links: [ 88 | { title: 'ECS Service', url: linkForEcsService(this.ecsService) }, 89 | ], 90 | }); 91 | 92 | const { cpuUtilizationMetric, cpuUtilizationAlarm } = this.createCpuUtilizationMonitor(props.cpuMaximumThresholdPercent); 93 | const { memoryUtilizationMetric, memoryUtilizationAlarm } = this.createMemoryUtilizationMonitor(props.memoryMaximumThresholdPercent); 94 | 95 | const { targetResponseTimeMetric, targetResponseTimeAlarm } = this.createTargetResponseTimeMonitor(props.targetResponseTimeThreshold); 96 | const { healthyHostsMetric, unhealthyHostsMetric } = this.createHostCountMetrics(); 97 | 98 | const { requestsMetric, requestsAlarm } = this.createRequestsMonitor(props.requestsThreshold); 99 | const { http2xxMetric, http3xxMetric, http4xxMetric, http5xxMetric } = this.createHttpRequestsMetrics(); 100 | const { requestsErrorRateMetric, requestsErrorRateAlarm } = this.requestsErrorRate(props.requestsErrorRateThreshold); 101 | 102 | 103 | this.watchful.addWidgets( 104 | new cloudwatch.GraphWidget({ 105 | title: `CPUUtilization/${cpuUtilizationMetric.period.toMinutes()}min`, 106 | width: 12, 107 | left: [cpuUtilizationMetric], 108 | leftAnnotations: [cpuUtilizationAlarm.toAnnotation()], 109 | }), 110 | new cloudwatch.GraphWidget({ 111 | title: `MemoryUtilization/${memoryUtilizationMetric.period.toMinutes()}min`, 112 | width: 12, 113 | left: [memoryUtilizationMetric], 114 | leftAnnotations: [memoryUtilizationAlarm.toAnnotation()], 115 | }), 116 | ); 117 | this.watchful.addWidgets( 118 | new cloudwatch.SingleValueWidget({ 119 | title: 'Healthy Hosts', 120 | height: 6, 121 | width: 6, 122 | metrics: [healthyHostsMetric], 123 | }), 124 | new cloudwatch.SingleValueWidget({ 125 | title: 'UnHealthy Hosts', 126 | height: 6, 127 | width: 6, 128 | metrics: [unhealthyHostsMetric], 129 | }), 130 | new cloudwatch.GraphWidget({ 131 | title: `TargetResponseTime/${targetResponseTimeMetric.period.toMinutes()}min`, 132 | width: 6, 133 | left: [targetResponseTimeMetric], 134 | leftAnnotations: [targetResponseTimeAlarm.toAnnotation()], 135 | }), 136 | new cloudwatch.GraphWidget({ 137 | title: `Requests/${requestsMetric.period.toMinutes()}min`, 138 | width: 6, 139 | left: [requestsMetric], 140 | leftAnnotations: [requestsAlarm.toAnnotation()], 141 | }), 142 | ); 143 | this.watchful.addWidgets( 144 | new cloudwatch.GraphWidget({ 145 | title: 'HTTP Requests Overview', 146 | width: 12, 147 | left: [http2xxMetric, http3xxMetric, http4xxMetric, http5xxMetric], 148 | }), 149 | new cloudwatch.GraphWidget({ 150 | title: `HTTP Requests Error rate/${requestsErrorRateMetric.period.toMinutes()}min`, 151 | width: 12, 152 | left: [requestsErrorRateMetric], 153 | leftAnnotations: [requestsErrorRateAlarm.toAnnotation()], 154 | }), 155 | ); 156 | } 157 | 158 | private createCpuUtilizationMonitor(cpuMaximumThresholdPercent = 0) { 159 | const cpuUtilizationMetric = this.metrics.metricCpuUtilizationAverage(this.clusterName, this.serviceName); 160 | const cpuUtilizationAlarm = cpuUtilizationMetric.createAlarm(this, 'cpuUtilizationAlarm', { 161 | alarmDescription: 'cpuUtilizationAlarm', 162 | threshold: cpuMaximumThresholdPercent, 163 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 164 | evaluationPeriods: 3, 165 | }); 166 | this.watchful.addAlarm(cpuUtilizationAlarm); 167 | return { cpuUtilizationMetric, cpuUtilizationAlarm }; 168 | } 169 | 170 | private createMemoryUtilizationMonitor(memoryMaximumThresholdPercent = 0) { 171 | const memoryUtilizationMetric = this.metrics.metricMemoryUtilizationAverage(this.clusterName, this.serviceName); 172 | const memoryUtilizationAlarm = memoryUtilizationMetric.createAlarm(this, 'memoryUtilizationAlarm', { 173 | alarmDescription: 'memoryUtilizationAlarm', 174 | threshold: memoryMaximumThresholdPercent, 175 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 176 | evaluationPeriods: 3, 177 | }); 178 | this.watchful.addAlarm(memoryUtilizationAlarm); 179 | return { memoryUtilizationMetric, memoryUtilizationAlarm }; 180 | } 181 | 182 | private createTargetResponseTimeMonitor(targetResponseTimeThreshold = 0) { 183 | const targetResponseTimeMetric = this.metrics.metricTargetResponseTime(this.targetGroupName, this.loadBalancerName).avg; 184 | const targetResponseTimeAlarm = targetResponseTimeMetric.createAlarm(this, 'targetResponseTimeAlarm', { 185 | alarmDescription: 'targetResponseTimeAlarm', 186 | threshold: targetResponseTimeThreshold, 187 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 188 | evaluationPeriods: 3, 189 | }); 190 | this.watchful.addAlarm(targetResponseTimeAlarm); 191 | return { targetResponseTimeMetric, targetResponseTimeAlarm }; 192 | } 193 | 194 | private createRequestsMonitor(requestsThreshold = 0) { 195 | const requestsMetric = this.metrics.metricRequestCount(this.targetGroupName, this.loadBalancerName); 196 | const requestsAlarm = requestsMetric.createAlarm(this, 'requestsAlarm', { 197 | alarmDescription: 'requestsAlarm', 198 | threshold: requestsThreshold, 199 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 200 | evaluationPeriods: 3, 201 | }); 202 | this.watchful.addAlarm(requestsAlarm); 203 | return { requestsMetric, requestsAlarm }; 204 | } 205 | 206 | 207 | private createHttpRequestsMetrics() { 208 | const metrics = this.metrics.metricHttpStatusCodeCount(this.targetGroupName, this.loadBalancerName); 209 | const http2xxMetric = metrics.count2XX; 210 | const http3xxMetric = metrics.count3XX; 211 | const http4xxMetric = metrics.count4XX; 212 | const http5xxMetric = metrics.count5XX; 213 | return { http2xxMetric, http3xxMetric, http4xxMetric, http5xxMetric }; 214 | } 215 | 216 | private createHostCountMetrics() { 217 | const healthyHostsMetric = this.metrics.metricMinHealthyHostCount(this.targetGroupName, this.loadBalancerName); 218 | const unhealthyHostsMetric = this.metrics.metricMaxUnhealthyHostCount(this.targetGroupName, this.loadBalancerName); 219 | return { healthyHostsMetric, unhealthyHostsMetric }; 220 | } 221 | 222 | private requestsErrorRate(requestsErrorRateThreshold = 0) { 223 | const requestsErrorRateMetric = this.metrics.metricHttpErrorStatusCodeRate(this.targetGroupName, this.loadBalancerName); 224 | const requestsErrorRateAlarm = requestsErrorRateMetric.createAlarm(this, 'requestsErrorRateAlarm', { 225 | alarmDescription: 'requestsErrorRateAlarm', 226 | threshold: requestsErrorRateThreshold, 227 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 228 | evaluationPeriods: 3, 229 | }); 230 | this.watchful.addAlarm(requestsErrorRateAlarm); 231 | return { requestsErrorRateMetric, requestsErrorRateAlarm }; 232 | } 233 | 234 | } 235 | 236 | 237 | function linkForEcsService(ecsService: any) { 238 | return `https://console.aws.amazon.com/ecs/home?region=${ecsService.stack.region}#/clusters/${ecsService.cluster.clusterName}/services/${ecsService.serviceName}/details`; 239 | } 240 | 241 | 242 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './watchful'; 3 | export * from './aspect'; 4 | 5 | export * from './api-gateway'; 6 | export * from './dynamodb'; 7 | export * from './lambda'; 8 | export * from './state-machine'; 9 | export * from './rds-aurora'; 10 | export * from './ecs'; -------------------------------------------------------------------------------- /src/lambda.ts: -------------------------------------------------------------------------------- 1 | import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; 2 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 3 | import { Construct } from 'constructs'; 4 | import { IWatchful } from './api'; 5 | import { LambdaMetricFactory } from './monitoring/aws/lambda/metrics'; 6 | 7 | const DEFAULT_DURATION_THRESHOLD_PERCENT = 80; 8 | 9 | export interface WatchLambdaFunctionOptions { 10 | /** 11 | * Number of allowed errors per minute. If there are more errors than that, an alarm will trigger. 12 | * 13 | * @default 0 14 | */ 15 | readonly errorsPerMinuteThreshold?: number; 16 | 17 | /** 18 | * Number of allowed throttles per minute. 19 | * 20 | * @default 0 21 | */ 22 | readonly throttlesPerMinuteThreshold?: number; 23 | 24 | /** 25 | * Threshold for the duration alarm as percentage of the function's timeout 26 | * value. 27 | * 28 | * If this is set to 50%, the alarm will be set when p99 latency of the 29 | * function exceeds 50% of the function's timeout setting. 30 | * 31 | * @default 80 32 | */ 33 | readonly durationThresholdPercent?: number; 34 | } 35 | 36 | export interface WatchLambdaFunctionProps extends WatchLambdaFunctionOptions { 37 | readonly title: string; 38 | readonly watchful: IWatchful; 39 | readonly fn: lambda.Function; 40 | } 41 | 42 | export class WatchLambdaFunction extends Construct { 43 | 44 | private readonly watchful: IWatchful; 45 | private readonly fn: lambda.Function; 46 | private readonly metrics: LambdaMetricFactory; 47 | 48 | constructor(scope: Construct, id: string, props: WatchLambdaFunctionProps) { 49 | super(scope, id); 50 | 51 | const cfnFunction = props.fn.node.defaultChild as lambda.CfnFunction; 52 | const timeoutSec = cfnFunction.timeout || 3; 53 | 54 | this.watchful = props.watchful; 55 | this.fn = props.fn; 56 | this.metrics = new LambdaMetricFactory(); 57 | 58 | this.watchful.addSection(props.title, { 59 | links: [ 60 | { title: 'AWS Lambda Console', url: linkForLambdaFunction(this.fn) }, 61 | { title: 'CloudWatch Logs', url: linkForLambdaLogs(this.fn) }, 62 | ], 63 | }); 64 | 65 | const { errorsMetric, errorsAlarm } = this.createErrorsMonitor(props.errorsPerMinuteThreshold); 66 | const { throttlesMetric, throttlesAlarm } = this.createThrottlesMonitor(props.throttlesPerMinuteThreshold); 67 | const { durationMetric, durationAlarm } = this.createDurationMonitor(timeoutSec, props.durationThresholdPercent); 68 | const invocationsMetric = this.metrics.metricInvocations(this.fn.functionName); 69 | 70 | this.watchful.addWidgets( 71 | new cloudwatch.GraphWidget({ 72 | title: `Invocations/${invocationsMetric.period.toMinutes()}min`, 73 | width: 6, 74 | left: [invocationsMetric], 75 | }), 76 | new cloudwatch.GraphWidget({ 77 | title: `Errors/${errorsMetric.period.toMinutes()}min`, 78 | width: 6, 79 | left: [errorsMetric], 80 | leftAnnotations: [errorsAlarm.toAnnotation()], 81 | }), 82 | new cloudwatch.GraphWidget({ 83 | title: `Throttles/${throttlesMetric.period.toMinutes()}min`, 84 | width: 6, 85 | left: [throttlesMetric], 86 | leftAnnotations: [throttlesAlarm.toAnnotation()], 87 | }), 88 | new cloudwatch.GraphWidget({ 89 | title: `Duration/${durationMetric.period.toMinutes()}min`, 90 | width: 6, 91 | left: [durationMetric], 92 | leftAnnotations: [durationAlarm.toAnnotation()], 93 | }), 94 | ); 95 | } 96 | 97 | private createErrorsMonitor(errorsPerMinuteThreshold = 0) { 98 | const fn = this.fn; 99 | const errorsMetric = this.metrics.metricErrors(fn.functionName); 100 | const errorsAlarm = errorsMetric.createAlarm(this, 'ErrorsAlarm', { 101 | alarmDescription: `Over ${errorsPerMinuteThreshold} errors per minute`, 102 | threshold: errorsPerMinuteThreshold, 103 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 104 | evaluationPeriods: 3, 105 | }); 106 | this.watchful.addAlarm(errorsAlarm); 107 | return { errorsMetric, errorsAlarm }; 108 | } 109 | 110 | private createThrottlesMonitor(throttlesPerMinuteThreshold = 0) { 111 | const fn = this.fn; 112 | const throttlesMetric = this.metrics.metricThrottles(fn.functionName); 113 | const throttlesAlarm = throttlesMetric.createAlarm(this, 'ThrottlesAlarm', { 114 | alarmDescription: `Over ${throttlesPerMinuteThreshold} throttles per minute`, 115 | threshold: throttlesPerMinuteThreshold, 116 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 117 | evaluationPeriods: 3, 118 | }); 119 | this.watchful.addAlarm(throttlesAlarm); 120 | return { throttlesMetric, throttlesAlarm }; 121 | } 122 | 123 | private createDurationMonitor(timeoutSec: number, durationPercentThreshold: number = DEFAULT_DURATION_THRESHOLD_PERCENT) { 124 | const fn = this.fn; 125 | const durationMetric = this.metrics.metricDuration(fn.functionName).p99; 126 | const durationThresholdSec = Math.floor(durationPercentThreshold / 100 * timeoutSec); 127 | const durationAlarm = durationMetric.createAlarm(this, 'DurationAlarm', { 128 | alarmDescription: `p99 latency >= ${durationThresholdSec}s (${durationPercentThreshold}%)`, 129 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 130 | threshold: durationThresholdSec * 1000, // milliseconds 131 | evaluationPeriods: 3, 132 | }); 133 | this.watchful.addAlarm(durationAlarm); 134 | return { durationMetric, durationAlarm }; 135 | } 136 | } 137 | 138 | function linkForLambdaFunction(fn: lambda.Function, tab = 'graph') { 139 | return `https://console.aws.amazon.com/lambda/home?region=${fn.stack.region}#/functions/${fn.functionName}?tab=${tab}`; 140 | } 141 | 142 | function linkForLambdaLogs(fn: lambda.Function) { 143 | return `https://console.aws.amazon.com/cloudwatch/home?region=${fn.stack.region}#logEventViewer:group=/aws/lambda/${fn.functionName}`; 144 | } 145 | -------------------------------------------------------------------------------- /src/monitoring/aws/api-gateway/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | import { WatchedOperation } from '../../../api-gateway'; 4 | 5 | const enum Metrics { 6 | FourHundredError = '4XXError', 7 | FiveHundredError = '5XXError', 8 | CacheHitCount = 'CacheHitCount', 9 | CacheMissCount = 'CacheMissCount', 10 | Count = 'Count', 11 | IntegrationLatency = 'IntegrationLatency', 12 | Latency = 'Latency', 13 | } 14 | 15 | const Namespace = 'AWS/ApiGateway'; 16 | const StatisticP90 = 'p90'; 17 | const StatisticP95 = 'p95'; 18 | const StatisticP99 = 'p99'; 19 | 20 | export class ApiGatewayMetricFactory { 21 | metricErrors(apiName: string, stage: string, op?: WatchedOperation) { 22 | return { 23 | count4XX: this.metric(Metrics.FourHundredError, apiName, stage, op).with({ label: '4XX Errors', statistic: Statistic.SUM, color: '#ff7f0e' }), 24 | count5XX: this.metric(Metrics.FiveHundredError, apiName, stage, op).with({ label: '5XX Errors', statistic: Statistic.SUM, color: '#d62728' }), 25 | }; 26 | } 27 | 28 | metricCache(apiName: string, stage: string, op?: WatchedOperation) { 29 | return { 30 | hits: this.metric(Metrics.CacheHitCount, apiName, stage, op).with({ label: 'Cache Hit', statistic: Statistic.SUM, color: '#2ca02c' }), 31 | misses: this.metric(Metrics.CacheMissCount, apiName, stage, op).with({ label: 'Cache Miss', statistic: Statistic.SUM, color: '#d62728' }), 32 | }; 33 | } 34 | 35 | metricCalls(apiName: string, stage: string, op?: WatchedOperation) { 36 | return this.metric(Metrics.Count, apiName, stage, op).with({ label: 'Calls', color: '#1f77b4', statistic: Statistic.SUM }); 37 | } 38 | 39 | metricIntegrationLatency(apiName: string, stage: string, op?: WatchedOperation) { 40 | const baseMetric = this.metric(Metrics.IntegrationLatency, apiName, stage, op); 41 | 42 | return { 43 | min: baseMetric.with({ label: 'min', statistic: Statistic.MINIMUM }), 44 | avg: baseMetric.with({ label: 'avg', statistic: Statistic.AVERAGE }), 45 | p90: baseMetric.with({ label: 'p90', statistic: StatisticP90 }), 46 | p95: baseMetric.with({ label: 'p95', statistic: StatisticP95 }), 47 | p99: baseMetric.with({ label: 'p99', statistic: StatisticP99 }), 48 | max: baseMetric.with({ label: 'max', statistic: Statistic.MAXIMUM }), 49 | }; 50 | } 51 | 52 | metricLatency(apiName: string, stage: string, op?: WatchedOperation) { 53 | const baseMetric = this.metric(Metrics.Latency, apiName, stage, op); 54 | 55 | return { 56 | min: baseMetric.with({ label: 'min', statistic: Statistic.MINIMUM }), 57 | avg: baseMetric.with({ label: 'avg', statistic: Statistic.AVERAGE }), 58 | p90: baseMetric.with({ label: 'p90', statistic: StatisticP90 }), 59 | p95: baseMetric.with({ label: 'p95', statistic: StatisticP95 }), 60 | p99: baseMetric.with({ label: 'p99', statistic: StatisticP99 }), 61 | max: baseMetric.with({ label: 'max', statistic: Statistic.MAXIMUM }), 62 | }; 63 | } 64 | 65 | protected metric(metricName: Metrics, apiName: string, stage: string, op?: WatchedOperation) { 66 | return new Metric({ 67 | metricName, 68 | namespace: Namespace, 69 | period: Duration.minutes(1), 70 | dimensionsMap: { 71 | ApiName: apiName, 72 | Stage: stage, 73 | ...op && { 74 | Method: op.httpMethod, 75 | Resource: op.resourcePath, 76 | }, 77 | }, 78 | }); 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/monitoring/aws/dynamodb/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | 4 | const enum Metrics { 5 | ConsumedReadCapacityUnits = 'ConsumedReadCapacityUnits', 6 | ConsumedWriteCapacityUnits = 'ConsumedWriteCapacityUnits', 7 | } 8 | 9 | const Namespace = 'AWS/DynamoDB'; 10 | 11 | export class DynamoDbMetricFactory { 12 | metricConsumedCapacityUnits(tableName: string) { 13 | return { 14 | read: this.metric(Metrics.ConsumedReadCapacityUnits, tableName).with({ label: 'Consumed (Read)' }), 15 | write: this.metric(Metrics.ConsumedWriteCapacityUnits, tableName).with({ label: 'Consumed (Write)' }), 16 | }; 17 | } 18 | 19 | protected metric(metric: Metrics, tableName: string) { 20 | return new Metric({ 21 | metricName: metric, 22 | namespace: Namespace, 23 | period: Duration.minutes(1), 24 | statistic: Statistic.SUM, 25 | dimensionsMap: { 26 | TableName: tableName, 27 | }, 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/monitoring/aws/ecs/metrics.ts: -------------------------------------------------------------------------------- 1 | import { MathExpression, Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 2 | 3 | const enum ApplicationELBMetrics { 4 | HealthyHostCount = 'HealthyHostCount', 5 | UnHealthyHostCount = 'UnHealthyHostCount', 6 | TARGET_2XX_COUNT = 'HTTPCode_Target_2XX_Count', 7 | TARGET_3XX_COUNT = 'HTTPCode_Target_3XX_Count', 8 | TARGET_4XX_COUNT = 'HTTPCode_Target_4XX_Count', 9 | TARGET_5XX_COUNT = 'HTTPCode_Target_5XX_Count', 10 | TargetResponseTime = 'TargetResponseTime', 11 | RequestCount = 'RequestCount' 12 | } 13 | 14 | const enum EcsMetrics { 15 | MemoryUtilization = 'MemoryUtilization', 16 | CPUUtilization = 'CPUUtilization', 17 | } 18 | 19 | const EcsNamespace = 'AWS/ECS'; 20 | const ApplicationELBNamespace = 'AWS/ApplicationELB'; 21 | 22 | export class EcsMetricFactory { 23 | metricCpuUtilizationAverage(clusterName: string, serviceName: string) { 24 | return this 25 | .ecsMetric(EcsMetrics.CPUUtilization, clusterName, serviceName) 26 | .with({ statistic: Statistic.AVERAGE }); 27 | } 28 | 29 | metricMemoryUtilizationAverage(clusterName: string, serviceName: string) { 30 | return this 31 | .ecsMetric(EcsMetrics.MemoryUtilization, clusterName, serviceName) 32 | .with({ statistic: Statistic.AVERAGE }); 33 | } 34 | 35 | protected ecsMetric(metric: EcsMetrics, clusterName: string, serviceName: string) { 36 | return new Metric({ 37 | namespace: EcsNamespace, 38 | metricName: metric, 39 | dimensionsMap: { 40 | ClusterName: clusterName, 41 | ServiceName: serviceName, 42 | }, 43 | }); 44 | } 45 | 46 | metricMinHealthyHostCount(targetGroup: string, loadBalancer: string) { 47 | return this 48 | .albMetric(ApplicationELBMetrics.HealthyHostCount, targetGroup, loadBalancer) 49 | .with({ statistic: Statistic.MINIMUM }); 50 | } 51 | 52 | metricMaxUnhealthyHostCount(targetGroup: string, loadBalancer: string) { 53 | return this 54 | .albMetric(ApplicationELBMetrics.UnHealthyHostCount, targetGroup, loadBalancer) 55 | .with({ statistic: Statistic.MAXIMUM }); 56 | } 57 | 58 | metricTargetResponseTime(targetGroup: string, loadBalancer: string) { 59 | const baseMetric = this.albMetric(ApplicationELBMetrics.TargetResponseTime, targetGroup, loadBalancer); 60 | 61 | return { 62 | min: baseMetric.with({ statistic: Statistic.MINIMUM }), 63 | max: baseMetric.with({ statistic: Statistic.MAXIMUM }), 64 | avg: baseMetric.with({ statistic: Statistic.AVERAGE }), 65 | }; 66 | } 67 | 68 | metricRequestCount(targetGroup: string, loadBalancer: string) { 69 | return this 70 | .albMetric(ApplicationELBMetrics.RequestCount, targetGroup, loadBalancer) 71 | .with({ statistic: Statistic.SUM }); 72 | } 73 | 74 | metricHttpErrorStatusCodeRate(targetGroup: string, loadBalancer: string) { 75 | const requests = this.metricRequestCount(targetGroup, loadBalancer); 76 | const errors = this.metricHttpStatusCodeCount(targetGroup, loadBalancer); 77 | return new MathExpression({ 78 | expression: 'http4xx + http5xx / requests', 79 | usingMetrics: { 80 | http4xx: errors.count4XX, 81 | http5xx: errors.count5XX, 82 | requests, 83 | }, 84 | }); 85 | } 86 | 87 | metricHttpStatusCodeCount(targetGroup: string, loadBalancer: string) { 88 | return { 89 | count2XX: this.albMetric(ApplicationELBMetrics.TARGET_2XX_COUNT, targetGroup, loadBalancer).with({ statistic: Statistic.SUM }), 90 | count3XX: this.albMetric(ApplicationELBMetrics.TARGET_3XX_COUNT, targetGroup, loadBalancer).with({ statistic: Statistic.SUM }), 91 | count4XX: this.albMetric(ApplicationELBMetrics.TARGET_4XX_COUNT, targetGroup, loadBalancer).with({ statistic: Statistic.SUM }), 92 | count5XX: this.albMetric(ApplicationELBMetrics.TARGET_5XX_COUNT, targetGroup, loadBalancer).with({ statistic: Statistic.SUM }), 93 | }; 94 | } 95 | 96 | protected albMetric(metric: ApplicationELBMetrics, targetGroup: string, loadBalancer: string) { 97 | return new Metric({ 98 | namespace: ApplicationELBNamespace, 99 | metricName: metric, 100 | dimensionsMap: { 101 | TargetGroup: targetGroup, 102 | LoadBalancer: loadBalancer, 103 | }, 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/monitoring/aws/lambda/metrics.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | 4 | const enum Metrics { 5 | Invocations = 'Invocations', 6 | Duration = 'Duration', 7 | Errors = 'Errors', 8 | Throttles = 'Throttles' 9 | } 10 | 11 | const Namespace = 'AWS/Lambda'; 12 | 13 | export class LambdaMetricFactory { 14 | metricInvocations(functionName: string) { 15 | return this.metric(Metrics.Invocations, functionName).with({ statistic: Statistic.SUM }); 16 | } 17 | 18 | metricDuration(functionName: string) { 19 | const baseMetric = this.metric(Metrics.Duration, functionName); 20 | 21 | return { 22 | min: baseMetric.with({ statistic: Statistic.MINIMUM, label: Statistic.MINIMUM }), 23 | avg: baseMetric.with({ statistic: Statistic.AVERAGE, label: Statistic.AVERAGE }), 24 | p50: baseMetric.with({ statistic: 'p50', label: 'p50' }), 25 | p90: baseMetric.with({ statistic: 'p90', label: 'p90' }), 26 | p99: baseMetric.with({ statistic: 'p99', label: 'p99' }), 27 | max: baseMetric.with({ statistic: Statistic.MAXIMUM, label: Statistic.MAXIMUM }), 28 | }; 29 | } 30 | 31 | metricErrors(functionName: string) { 32 | return this.metric(Metrics.Errors, functionName).with({ statistic: Statistic.SUM }); 33 | } 34 | 35 | metricThrottles(functionName: string) { 36 | return this.metric(Metrics.Throttles, functionName).with({ statistic: Statistic.SUM }); 37 | } 38 | 39 | protected metric(metric: Metrics, functionName: string) { 40 | return new Metric({ 41 | metricName: metric, 42 | namespace: Namespace, 43 | period: cdk.Duration.minutes(5), 44 | dimensionsMap: { 45 | FunctionName: functionName, 46 | }, 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/monitoring/aws/rds/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | 4 | const enum Metrics { 5 | SelectThroughput = 'SelectThroughput', 6 | InsertThroughput = 'InsertThroughput', 7 | UpdateThroughput = 'UpdateThroughput', 8 | DeleteThroughput = 'DeleteThroughput', 9 | BufferCacheHitRatio = 'BufferCacheHitRatio', 10 | DatabaseConnections = 'DatabaseConnections', 11 | AuroraReplicaLag = 'AuroraReplicaLag', 12 | CPUUtilization = 'CPUUtilization' 13 | } 14 | 15 | const Namespace = 'AWS/RDS'; 16 | 17 | /** 18 | * Metrics for RDS Aurora. 19 | */ 20 | export class RdsAuroraMetricFactory { 21 | metricDmlThroughput(clusterIdentifier: string) { 22 | return { 23 | dbInsertThroughputMetric: this.metric(Metrics.InsertThroughput, clusterIdentifier).with({ statistic: Statistic.SUM }), 24 | dbUpdateThroughputMetric: this.metric(Metrics.UpdateThroughput, clusterIdentifier).with({ statistic: Statistic.SUM }), 25 | dbSelectThroughputMetric: this.metric(Metrics.SelectThroughput, clusterIdentifier).with({ statistic: Statistic.SUM }), 26 | dbDeleteThroughputMetric: this.metric(Metrics.DeleteThroughput, clusterIdentifier).with({ statistic: Statistic.SUM }), 27 | }; 28 | } 29 | 30 | metricBufferCacheHitRatio(clusterIdentifier: string) { 31 | return this.metric(Metrics.BufferCacheHitRatio, clusterIdentifier).with({ statistic: Statistic.AVERAGE }); 32 | } 33 | 34 | metricDbConnections(clusterIdentifier: string) { 35 | return this.metric(Metrics.DatabaseConnections, clusterIdentifier).with({ statistic: Statistic.AVERAGE }); 36 | } 37 | 38 | metricReplicaLag(clusterIdentifier: string) { 39 | return this.metric(Metrics.AuroraReplicaLag, clusterIdentifier).with({ statistic: Statistic.AVERAGE }); 40 | } 41 | 42 | metricCpuUtilization(clusterIdentifier: string) { 43 | return this.metric(Metrics.CPUUtilization, clusterIdentifier).with({ statistic: Statistic.AVERAGE }); 44 | } 45 | 46 | protected metric(metric: Metrics, clusterIdentifier: string) { 47 | return new Metric({ 48 | metricName: metric, 49 | namespace: Namespace, 50 | period: Duration.minutes(5), 51 | statistic: Statistic.SUM, 52 | dimensionsMap: { 53 | DBClusterIdentifier: clusterIdentifier, 54 | }, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/monitoring/aws/redshift/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | 4 | const enum Metrics { 5 | DatabaseConnections = 'DatabaseConnections', 6 | PercentageDiskSpaceUsed = 'PercentageDiskSpaceUsed', 7 | CPUUtilization = 'CPUUtilization', 8 | MaintenanceMode = 'MaintenanceMode', 9 | ReadLatency = 'ReadLatency', 10 | WriteLatency = 'WriteLatency', 11 | QueryDuration = 'QueryDuration' 12 | } 13 | 14 | const Namespace = 'AWS/Redshift'; 15 | 16 | export class RedshiftMetricFactory { 17 | metricAverageConnectionCount(clusterIdentifier: string) { 18 | return this.metric(Metrics.DatabaseConnections, clusterIdentifier).with({ statistic: Statistic.AVERAGE }); 19 | } 20 | 21 | metricAverageDiskSpaceUsageInPercent(clusterIdentifier: string) { 22 | return this.metric(Metrics.PercentageDiskSpaceUsed, clusterIdentifier).with({ statistic: Statistic.AVERAGE }); 23 | } 24 | 25 | metricAverageCpuUsageInPercent(clusterIdentifier: string) { 26 | return this.metric(Metrics.CPUUtilization, clusterIdentifier).with({ statistic: Statistic.AVERAGE }); 27 | } 28 | 29 | metricAverageQueryDurationInMicros(clusterIdentifier: string) { 30 | return { 31 | shortQueries: this.metricQueryDuration('short', clusterIdentifier).with({ statistic: Statistic.AVERAGE }), 32 | mediumQueries: this.metricQueryDuration('medium', clusterIdentifier).with({ statistic: Statistic.AVERAGE }), 33 | longQueries: this.metricQueryDuration('long', clusterIdentifier).with({ statistic: Statistic.AVERAGE }), 34 | }; 35 | } 36 | 37 | metricAverageLatencyInSeconds(clusterIdentifier: string) { 38 | return { 39 | read: this.metric(Metrics.ReadLatency, clusterIdentifier).with({ statistic: Statistic.AVERAGE }), 40 | write: this.metric(Metrics.WriteLatency, clusterIdentifier).with({ statistic: Statistic.AVERAGE }), 41 | }; 42 | } 43 | 44 | metricMaintenanceModeEnabled(clusterIdentifier: string) { 45 | return this.metric(Metrics.MaintenanceMode, clusterIdentifier).with({ statistic: Statistic.MAXIMUM }); 46 | } 47 | 48 | private metricQueryDuration(latency: string, clusterIdentifier: string) { 49 | return new Metric({ 50 | metricName: Metrics.QueryDuration, 51 | namespace: Namespace, 52 | period: Duration.minutes(5), 53 | dimensionsMap: { 54 | DBClusterIdentifier: clusterIdentifier, 55 | latency, 56 | }, 57 | }); 58 | } 59 | 60 | private metric(metric: Metrics, clusterIdentifier: string) { 61 | return new Metric({ 62 | metricName: metric, 63 | namespace: Namespace, 64 | period: Duration.minutes(5), 65 | dimensionsMap: { 66 | DBClusterIdentifier: clusterIdentifier, 67 | }, 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/monitoring/aws/sns/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | 4 | const enum Metrics { 5 | NumberOfMessagesPublished = 'NumberOfMessagesPublished', 6 | NumberOfNotificationsDelivered = 'NumberOfNotificationsDelivered', 7 | NumberOfNotificationsFailed = 'NumberOfNotificationsFailed', 8 | PublishSize = 'PublishSize' 9 | } 10 | 11 | const Namespace = 'AWS/SNS'; 12 | 13 | export class SnsMetricFactory { 14 | metricNumberOfMessagesPublished(topicName: string) { 15 | return this.metric(Metrics.NumberOfMessagesPublished, topicName).with({ statistic: Statistic.SUM }); 16 | } 17 | 18 | metricNumberOfMessagesDelivered(topicName: string) { 19 | return this.metric(Metrics.NumberOfNotificationsDelivered, topicName).with({ statistic: Statistic.SUM }); 20 | } 21 | 22 | metricNumberOfNotificationsFailed(topicName: string) { 23 | return this.metric(Metrics.NumberOfNotificationsFailed, topicName).with({ statistic: Statistic.SUM }); 24 | } 25 | 26 | metricAverageMessageSizeInBytes(topicName: string) { 27 | return this.metric(Metrics.PublishSize, topicName).with({ statistic: Statistic.AVERAGE }); 28 | } 29 | 30 | protected metric(metric: Metrics, topicName: string) { 31 | return new Metric({ 32 | metricName: metric, 33 | namespace: Namespace, 34 | period: Duration.minutes(5), 35 | dimensionsMap: { 36 | TopicName: topicName, 37 | }, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/monitoring/aws/sqs/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | 4 | const enum Metrics { 5 | ApproximateNumberOfMessagesVisible = 'ApproximateNumberOfMessagesVisible', 6 | NumberOfMessagesSent = 'NumberOfMessagesSent', 7 | ApproximateAgeOfOldestMessage = 'ApproximateAgeOfOldestMessage', 8 | SentMessageSize = 'SentMessageSize' 9 | } 10 | 11 | const Namespace = 'AWS/SQS'; 12 | 13 | export class SqsMetricFactory { 14 | metricApproximateVisibleMessages(queueName: string) { 15 | return this.metric(Metrics.ApproximateNumberOfMessagesVisible, queueName).with({ statistic: Statistic.MAXIMUM }); 16 | } 17 | 18 | metricIncomingMessages(queueName: string) { 19 | return this.metric(Metrics.NumberOfMessagesSent, queueName).with({ statistic: Statistic.SUM }); 20 | } 21 | 22 | metricAgeOfOldestMessageInSeconds(queueName: string) { 23 | return this.metric(Metrics.ApproximateAgeOfOldestMessage, queueName).with({ statistic: Statistic.MAXIMUM }); 24 | } 25 | 26 | metricAverageMessageSizeInBytes(queueName: string) { 27 | return this.metric(Metrics.SentMessageSize, queueName).with({ statistic: Statistic.AVERAGE }); 28 | } 29 | 30 | protected metric(metric: Metrics, queueName: string) { 31 | return new Metric({ 32 | metricName: metric, 33 | namespace: Namespace, 34 | period: Duration.minutes(5), 35 | dimensionsMap: { 36 | QueueName: queueName, 37 | }, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/monitoring/aws/sqs/monitoring.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from 'aws-cdk-lib'; 2 | import { GraphWidget, HorizontalAnnotation, IWidget, Metric } from 'aws-cdk-lib/aws-cloudwatch'; 3 | import { IQueue } from 'aws-cdk-lib/aws-sqs'; 4 | import { SqsMetricFactory } from './metrics'; 5 | import { Monitoring, MonitoringProps } from '../../../core/monitoring'; 6 | import { CountAxis, SizeBytesAxis, TimeSecondsAxis } from '../../../widget/axis'; 7 | import { CommonWidgetDimensions } from '../../../widget/constant'; 8 | import { SectionWidget, SectionWidgetProps } from '../../../widget/section'; 9 | 10 | /** 11 | * Properties to create SqsMonitoring. 12 | */ 13 | export interface SqsMonitoringProps extends MonitoringProps { 14 | queue: IQueue; 15 | } 16 | 17 | /** 18 | * Monitoring of SQS Queue. 19 | */ 20 | export class SqsMonitoring extends Monitoring { 21 | protected readonly section: SectionWidgetProps; 22 | protected readonly metrics: SqsMetricFactory; 23 | 24 | protected readonly visibleMessagesMetric: Metric; 25 | protected readonly incomingMessagesMetric: Metric; 26 | protected readonly oldestMessageAgeMetric: Metric; 27 | protected readonly messageSizeMetric: Metric; 28 | 29 | protected readonly countAnnotations: HorizontalAnnotation[]; 30 | protected readonly ageAnnotations: HorizontalAnnotation[]; 31 | 32 | constructor(props: SqsMonitoringProps) { 33 | super(); 34 | 35 | this.section = this.headerProps(props); 36 | this.metrics = new SqsMetricFactory(); 37 | 38 | this.visibleMessagesMetric = this.metrics.metricApproximateVisibleMessages(props.queue.queueName); 39 | this.incomingMessagesMetric = this.metrics.metricIncomingMessages(props.queue.queueName); 40 | this.oldestMessageAgeMetric = this.metrics.metricAgeOfOldestMessageInSeconds(props.queue.queueName); 41 | this.messageSizeMetric = this.metrics.metricAverageMessageSizeInBytes(props.queue.queueName); 42 | 43 | this.countAnnotations = []; 44 | this.ageAnnotations = []; 45 | } 46 | 47 | getWidgets(): IWidget[] { 48 | return [ 49 | this.headerWidget(), 50 | this.messageCountWidget(CommonWidgetDimensions.ThirdWidth, CommonWidgetDimensions.DefaultHeight), 51 | this.messageAgeWidget(CommonWidgetDimensions.ThirdWidth, CommonWidgetDimensions.DefaultHeight), 52 | this.messageSizeWidget(CommonWidgetDimensions.ThirdWidth, CommonWidgetDimensions.DefaultHeight), 53 | ]; 54 | } 55 | 56 | protected headerWidget() { 57 | return new SectionWidget(this.section); 58 | } 59 | 60 | protected messageCountWidget(width: number, height: number) { 61 | return new GraphWidget({ 62 | width, 63 | height, 64 | title: 'Message Count', 65 | left: [this.visibleMessagesMetric, this.incomingMessagesMetric], 66 | leftYAxis: CountAxis, 67 | leftAnnotations: this.countAnnotations, 68 | }); 69 | } 70 | 71 | protected messageAgeWidget(width: number, height: number) { 72 | return new GraphWidget({ 73 | width, 74 | height, 75 | title: 'Oldest Message Age', 76 | left: [this.oldestMessageAgeMetric], 77 | leftYAxis: TimeSecondsAxis, 78 | leftAnnotations: this.ageAnnotations, 79 | }); 80 | } 81 | 82 | protected messageSizeWidget(width: number, height: number) { 83 | return new GraphWidget({ 84 | width, 85 | height, 86 | title: 'Message Size', 87 | left: [this.messageSizeMetric], 88 | leftYAxis: SizeBytesAxis, 89 | }); 90 | } 91 | 92 | protected headerProps(props: SqsMonitoringProps) { 93 | return { 94 | titleMarkdown: `SQS Queue **${props.queue.queueName}**`, 95 | quicklinks: this.headerQuickLinks(props), 96 | ...props, 97 | }; 98 | } 99 | 100 | protected headerQuickLinks(props: SqsMonitoringProps) { 101 | return [ 102 | { title: 'Overview', url: this.urlForQueueOverview(props) }, 103 | ]; 104 | } 105 | 106 | protected urlForQueueOverview(props: SqsMonitoringProps) { 107 | const region = Stack.of(props.queue).region; 108 | const queueUrl = props.queue.queueUrl; 109 | return `https://${region}.console.aws.amazon.com/sqs/v2/home?region=${region}#/queues/${queueUrl}`; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/monitoring/aws/state-machine/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Metric, Statistic } from 'aws-cdk-lib/aws-cloudwatch'; 3 | 4 | const enum Metrics { 5 | ExecutionsStarted = 'ExecutionsStarted', 6 | ExecutionsSucceeded = 'ExecutionsSucceeded', 7 | ExecutionsFailed = 'ExecutionsFailed', 8 | ExecutionsAborted = 'ExecutionsAborted', 9 | ExecutionThrottled ='ExecutionThrottled', 10 | ExecutionsTimedOut = 'ExecutionsTimedOut' 11 | } 12 | 13 | const Namespace = 'AWS/States'; 14 | 15 | export class StateMachineMetricFactory { 16 | metricExecutions(stateMachineArn: string) { 17 | return { 18 | total: this.metric(Metrics.ExecutionsStarted, stateMachineArn).with({ label: 'Total', statistic: Statistic.SUM }), 19 | succeeded: this.metric(Metrics.ExecutionsSucceeded, stateMachineArn).with({ label: 'Executions Succeeded', statistic: Statistic.SUM }), 20 | failed: this.metric(Metrics.ExecutionsFailed, stateMachineArn).with({ label: 'Failed Executions', statistic: Statistic.SUM }), 21 | aborted: this.metric(Metrics.ExecutionsAborted, stateMachineArn).with({ label: 'Aborted Executions', statistic: Statistic.SUM }), 22 | throttled: this.metric(Metrics.ExecutionThrottled, stateMachineArn).with({ label: 'Executions Throttled', statistic: Statistic.SUM }), 23 | timedOut: this.metric(Metrics.ExecutionsTimedOut, stateMachineArn).with({ label: 'Executions TimedOut', statistic: Statistic.SUM }), 24 | }; 25 | } 26 | 27 | protected metric(metric: Metrics, stateMachineArn: string) { 28 | return new Metric({ 29 | metricName: metric, 30 | namespace: Namespace, 31 | period: Duration.minutes(1), 32 | dimensionsMap: { 33 | StateMachineArn: stateMachineArn, 34 | }, 35 | }); 36 | } 37 | } -------------------------------------------------------------------------------- /src/rds-aurora.ts: -------------------------------------------------------------------------------- 1 | import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; 2 | import * as rds from 'aws-cdk-lib/aws-rds'; 3 | import { Construct } from 'constructs'; 4 | import { IWatchful } from './api'; 5 | import { RdsAuroraMetricFactory } from './monitoring/aws/rds/metrics'; 6 | 7 | export interface WatchRdsAuroraOptions { 8 | /** 9 | * Threshold for the Cpu Maximum utilization 10 | * 11 | * @default 80 12 | */ 13 | readonly cpuMaximumThresholdPercent?: number; 14 | 15 | /** 16 | * Threshold for the Maximum Db Connections. 17 | * 18 | * @default - 0. 19 | */ 20 | readonly dbConnectionsMaximumThreshold?: number; 21 | 22 | /** 23 | * Threshold for the Maximum Db ReplicaLag. 24 | * 25 | * @default - 0. 26 | */ 27 | readonly dbReplicaLagMaximumThreshold?: number; 28 | 29 | /** 30 | * Threshold for the Maximum Db Throughput. 31 | * 32 | * @default - 0. 33 | */ 34 | readonly dbThroughputMaximumThreshold?: number; 35 | 36 | /** 37 | * Threshold for the Minimum Db Buffer Cache. 38 | * 39 | * @default - 0. 40 | */ 41 | readonly dbBufferCacheMinimumThreshold?: number; 42 | 43 | } 44 | 45 | export interface WatchRdsAuroraProps extends WatchRdsAuroraOptions { 46 | readonly title: string; 47 | readonly watchful: IWatchful; 48 | readonly cluster: rds.DatabaseCluster; 49 | } 50 | 51 | export class WatchRdsAurora extends Construct { 52 | 53 | private readonly watchful: IWatchful; 54 | private readonly cluster: rds.DatabaseCluster; 55 | private readonly metrics: RdsAuroraMetricFactory; 56 | 57 | constructor(scope: Construct, id: string, props: WatchRdsAuroraProps) { 58 | super(scope, id); 59 | 60 | this.watchful = props.watchful; 61 | this.cluster = props.cluster; 62 | this.metrics = new RdsAuroraMetricFactory(); 63 | 64 | this.watchful.addSection(props.title, { 65 | links: [ 66 | { title: 'AWS RDS Cluster', url: linkForRDS(this.cluster) }, 67 | ], 68 | }); 69 | 70 | const { cpuUtilizationMetric, cpuUtilizationAlarm } = this.createCpuUtilizationMonitor(props.cpuMaximumThresholdPercent); 71 | const { dbConnectionsMetric, dbConnectionsAlarm } = this.createDbConnectionsMonitor(props.dbConnectionsMaximumThreshold); 72 | const { dbReplicaLagMetric, dbReplicaLagAlarm } = this.createDbReplicaLagMonitor(props.dbReplicaLagMaximumThreshold); 73 | const { dbBufferCacheHitRatioMetric, dbBufferCacheHitRatioAlarm } = this.createDbBufferCacheMonitor(props.dbBufferCacheMinimumThreshold); 74 | 75 | const { dbInsertThroughputMetric, dbUpdateThroughputMetric, dbSelectThroughputMetric, dbDeleteThroughputMetric } = 76 | this.createDbDmlThroughputMonitor(props.dbThroughputMaximumThreshold); 77 | 78 | this.watchful.addWidgets( 79 | new cloudwatch.GraphWidget({ 80 | title: `CPUUtilization/${cpuUtilizationMetric.period.toMinutes()}min`, 81 | width: 6, 82 | left: [cpuUtilizationMetric], 83 | leftAnnotations: [cpuUtilizationAlarm.toAnnotation()], 84 | }), 85 | new cloudwatch.GraphWidget({ 86 | title: `DB Connections/${dbConnectionsMetric.period.toMinutes()}min`, 87 | width: 6, 88 | left: [dbConnectionsMetric], 89 | leftAnnotations: [dbConnectionsAlarm.toAnnotation()], 90 | }), 91 | new cloudwatch.GraphWidget({ 92 | title: `DB Replica Lag/${dbReplicaLagMetric.period.toMinutes()}min`, 93 | width: 6, 94 | left: [dbReplicaLagMetric], 95 | leftAnnotations: [dbReplicaLagAlarm.toAnnotation()], 96 | }), 97 | new cloudwatch.GraphWidget({ 98 | title: `DB BufferCache Hit Ratio/${dbBufferCacheHitRatioMetric.period.toMinutes()}min`, 99 | width: 6, 100 | left: [dbBufferCacheHitRatioMetric], 101 | leftAnnotations: [dbBufferCacheHitRatioAlarm.toAnnotation()], 102 | }), 103 | ); 104 | this.watchful.addWidgets( 105 | new cloudwatch.GraphWidget({ 106 | title: 'RDS DML Overview', 107 | width: 24, 108 | left: [dbInsertThroughputMetric, dbUpdateThroughputMetric, dbSelectThroughputMetric, dbDeleteThroughputMetric], 109 | }), 110 | ); 111 | } 112 | 113 | private createCpuUtilizationMonitor(cpuMaximumThresholdPercent = 80) { 114 | const cpuUtilizationMetric = this.metrics.metricCpuUtilization(this.cluster.clusterIdentifier); 115 | const cpuUtilizationAlarm = cpuUtilizationMetric.createAlarm(this, 'CpuUtilizationAlarm', { 116 | alarmDescription: 'cpuUtilizationAlarm', 117 | threshold: cpuMaximumThresholdPercent, 118 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 119 | evaluationPeriods: 3, 120 | }); 121 | return { cpuUtilizationMetric, cpuUtilizationAlarm }; 122 | } 123 | 124 | private createDbConnectionsMonitor(dbConnectionsMaximumThreshold = 0) { 125 | const dbConnectionsMetric = this.metrics.metricDbConnections(this.cluster.clusterIdentifier); 126 | const dbConnectionsAlarm = dbConnectionsMetric.createAlarm(this, 'DbConnectionsAlarm', { 127 | alarmDescription: 'dbConnectionsAlarm', 128 | threshold: dbConnectionsMaximumThreshold, 129 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 130 | evaluationPeriods: 3, 131 | }); 132 | return { dbConnectionsMetric, dbConnectionsAlarm }; 133 | } 134 | 135 | private createDbReplicaLagMonitor(dbReplicaLagMaximumThreshold = 0) { 136 | const dbReplicaLagMetric = this.metrics.metricReplicaLag(this.cluster.clusterIdentifier); 137 | const dbReplicaLagAlarm = dbReplicaLagMetric.createAlarm(this, 'DbReplicaLagAlarm', { 138 | alarmDescription: 'dbConnectionsAlarm', 139 | threshold: dbReplicaLagMaximumThreshold, 140 | comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, 141 | evaluationPeriods: 3, 142 | }); 143 | return { dbReplicaLagMetric, dbReplicaLagAlarm }; 144 | } 145 | 146 | private createDbBufferCacheMonitor(dbBufferCacheMinimumThreshold = 0) { 147 | const dbBufferCacheHitRatioMetric = this.metrics.metricBufferCacheHitRatio(this.cluster.clusterIdentifier); 148 | const dbBufferCacheHitRatioAlarm = dbBufferCacheHitRatioMetric.createAlarm(this, 'DbBufferCacheHitRatioAlarm', { 149 | alarmDescription: 'dbConnectionsAlarm', 150 | threshold: dbBufferCacheMinimumThreshold, 151 | comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, 152 | evaluationPeriods: 3, 153 | }); 154 | return { dbBufferCacheHitRatioMetric, dbBufferCacheHitRatioAlarm }; 155 | } 156 | private createDbDmlThroughputMonitor(dbThroughputMaximumThreshold = 0) { 157 | // @ts-ignore 158 | const AlarmThreshold = dbThroughputMaximumThreshold; 159 | return this.metrics.metricDmlThroughput(this.cluster.clusterIdentifier); 160 | } 161 | } 162 | 163 | function linkForRDS(cluster: rds.DatabaseCluster) { 164 | return `https://console.aws.amazon.com/rds/home?region=${cluster.stack.region}#database:id=${cluster.clusterIdentifier};is-cluster=true`; 165 | } 166 | 167 | 168 | -------------------------------------------------------------------------------- /src/state-machine.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { ComparisonOperator, GraphWidget } from 'aws-cdk-lib/aws-cloudwatch'; 3 | import { StateMachine } from 'aws-cdk-lib/aws-stepfunctions'; 4 | import { Construct } from 'constructs'; 5 | import { IWatchful } from './api'; 6 | import { StateMachineMetricFactory } from './monitoring/aws/state-machine/metrics'; 7 | 8 | export interface WatchStateMachineOptions { 9 | /** 10 | * Alarm when execution failures reach this threshold over 1 minute. 11 | * 12 | * @default 1 any execution failure will trigger the alarm 13 | */ 14 | readonly metricFailedThreshold?: number; 15 | } 16 | 17 | export interface WatchStateMachineProps extends WatchStateMachineOptions { 18 | readonly title: string; 19 | readonly watchful: IWatchful; 20 | readonly stateMachine: StateMachine; 21 | } 22 | 23 | export class WatchStateMachine extends Construct { 24 | private readonly title: string; 25 | private readonly watchful: IWatchful; 26 | private readonly stateMachine: StateMachine; 27 | private readonly metricFailedThreshold: number; 28 | 29 | private readonly metrics: StateMachineMetricFactory; 30 | 31 | constructor(scope: Construct, id: string, props: WatchStateMachineProps) { 32 | super(scope, id); 33 | 34 | this.title = props.title; 35 | this.watchful = props.watchful; 36 | this.stateMachine = props.stateMachine; 37 | 38 | this.metricFailedThreshold = props.metricFailedThreshold ?? 1; 39 | 40 | this.metrics = new StateMachineMetricFactory(); 41 | 42 | this.createLinks(); 43 | this.createExecutionMetrics(); 44 | } 45 | 46 | private createExecutionMetrics() { 47 | const execMetrics = this.metrics.metricExecutions(this.stateMachine.stateMachineArn); 48 | const { failed } = execMetrics; 49 | const failedWithPeriod = failed.with({ 50 | statistic: 'sum', 51 | period: Duration.minutes(5), 52 | }); 53 | const failureAlarm = failedWithPeriod.createAlarm(this, 'ExecutionFailures', { 54 | alarmDescription: `at ${this.metricFailedThreshold}`, 55 | threshold: this.metricFailedThreshold, 56 | comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, 57 | evaluationPeriods: 1, 58 | }); 59 | 60 | this.watchful.addAlarm(failureAlarm); 61 | 62 | this.watchful.addWidgets(new GraphWidget({ 63 | title: 'Overall Execution/min', 64 | width: 12, 65 | stacked: false, 66 | left: Object.values(execMetrics), 67 | leftAnnotations: [{ value: this.metricFailedThreshold, color: '#ff0000', label: 'Execution Failure Alarm' }], 68 | })); 69 | } 70 | 71 | private createLinks() { 72 | this.watchful.addSection(this.title, { 73 | links: [{ 74 | title: 'Amazon State Machine', 75 | url: `https://console.aws.amazon.com/states/home?region=${this.stateMachine.stack.region}#/statemachines/view/${this.stateMachine.stateMachineArn}`, 76 | }], 77 | }); 78 | } 79 | } -------------------------------------------------------------------------------- /src/watchful.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Aspects, Names } from 'aws-cdk-lib'; 2 | import * as apigw from 'aws-cdk-lib/aws-apigateway'; 3 | import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; 4 | import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions'; 5 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 6 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 7 | import { ApplicationTargetGroup } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 8 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 9 | import * as rds from 'aws-cdk-lib/aws-rds'; 10 | import * as sns from 'aws-cdk-lib/aws-sns'; 11 | import * as sns_subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; 12 | import * as sqs from 'aws-cdk-lib/aws-sqs'; 13 | import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; 14 | import { Construct } from 'constructs'; 15 | import { IWatchful, SectionOptions } from './api'; 16 | import { WatchApiGatewayOptions, WatchApiGateway } from './api-gateway'; 17 | import { WatchfulAspect, WatchfulAspectProps } from './aspect'; 18 | import { WatchDynamoTableOptions, WatchDynamoTable } from './dynamodb'; 19 | import { WatchEcsServiceOptions, WatchEcsService } from './ecs'; 20 | import { WatchLambdaFunctionOptions, WatchLambdaFunction } from './lambda'; 21 | import { WatchRdsAuroraOptions, WatchRdsAurora } from './rds-aurora'; 22 | import { WatchStateMachineOptions, WatchStateMachine } from './state-machine'; 23 | import { SectionWidget } from './widget/section'; 24 | 25 | export interface WatchfulProps { 26 | /** 27 | * Email address to send alarms to. 28 | * @default - alarms are not sent to an email recipient. 29 | */ 30 | readonly alarmEmail?: string; 31 | 32 | /** 33 | * SQS queue to send alarms to. 34 | * @default - alarms are not sent to an SQS queue. 35 | */ 36 | readonly alarmSqs?: sqs.IQueue; 37 | 38 | /** 39 | * SNS topic to send alarms to. 40 | * @default - alarms are not sent to an SNS Topic. 41 | */ 42 | readonly alarmSns?: sns.ITopic; 43 | 44 | /** 45 | * The name of the CloudWatch dashboard generated by Watchful. 46 | * @default - auto-generated 47 | */ 48 | readonly dashboardName?: string; 49 | 50 | /** 51 | * ARNs of actions to perform when alarms go off. These actions are in 52 | * addition to email/sqs/sns. 53 | * 54 | * @default [] 55 | * 56 | * You can use `alarmActions` instead as a strongly-typed alternative. 57 | */ 58 | readonly alarmActionArns?: string[]; 59 | 60 | /** 61 | * CloudWatch alarm actions to perform when alarms go off. These actions are 62 | * in addition to email/sqs/sns. 63 | */ 64 | readonly alarmActions?: cloudwatch.IAlarmAction[]; 65 | 66 | /** 67 | * Whether to generate CloudWatch dashboards 68 | * @default true 69 | */ 70 | readonly dashboard?: boolean; 71 | } 72 | 73 | export class Watchful extends Construct implements IWatchful { 74 | private readonly dash?: cloudwatch.Dashboard; 75 | private readonly alarmTopic?: sns.ITopic; 76 | private readonly alarmActions: cloudwatch.IAlarmAction[]; 77 | private createdAlarmCount = 0; 78 | 79 | constructor(scope: Construct, id: string, props: WatchfulProps = { }) { 80 | super(scope, id); 81 | 82 | this.alarmActions = [ 83 | ...(props.alarmActionArns ?? []).map((alarmActionArn) => ({ bind: () => ({ alarmActionArn }) })), 84 | ...(props.alarmActions ?? []), 85 | ]; 86 | 87 | if ((props.alarmEmail || props.alarmSqs) && !props.alarmSns) { 88 | this.alarmTopic = new sns.Topic(this, 'AlarmTopic', { displayName: 'Watchful Alarms' }); 89 | } 90 | 91 | if (props.alarmSns) { 92 | this.alarmTopic = props.alarmSns; 93 | } 94 | 95 | if (props.alarmEmail && this.alarmTopic) { 96 | this.alarmTopic.addSubscription( 97 | new sns_subscriptions.EmailSubscription(props.alarmEmail), 98 | ); 99 | } 100 | 101 | if (props.alarmSqs && this.alarmTopic) { 102 | this.alarmTopic.addSubscription( 103 | new sns_subscriptions.SqsSubscription( 104 | // sqs.Queue.fromQueueArn(this, 'AlarmQueue', props.alarmSqs) 105 | props.alarmSqs, 106 | ), 107 | ); 108 | } 109 | 110 | if (props.dashboard === false && props.dashboardName) { 111 | throw new Error('Dashboard name is provided but dashboard creation is disabled'); 112 | } 113 | if (props.dashboard !== false) { 114 | this.dash = new cloudwatch.Dashboard(this, 'Dashboard', { dashboardName: props.dashboardName }); 115 | 116 | new CfnOutput(this, 'WatchfulDashboard', { 117 | value: linkForDashboard(this.dash), 118 | }); 119 | } 120 | 121 | } 122 | 123 | public addWidgets(...widgets: cloudwatch.IWidget[]) { 124 | this.dash?.addWidgets(...widgets); 125 | } 126 | 127 | public addAlarm(alarm: cloudwatch.IAlarm) { 128 | const alarmWithAction = hasAlarmAction(alarm) ? alarm : new cloudwatch.CompositeAlarm(this, `Created Alarm ${this.createdAlarmCount++}`, { 129 | alarmRule: cloudwatch.AlarmRule.fromAlarm(alarm, cloudwatch.AlarmState.ALARM), 130 | }); 131 | if (this.alarmTopic) { 132 | alarmWithAction.addAlarmAction(new cloudwatch_actions.SnsAction(this.alarmTopic)); 133 | } 134 | 135 | alarmWithAction.addAlarmAction(...this.alarmActions); 136 | } 137 | 138 | public addSection(title: string, options: SectionOptions = {}) { 139 | this.addWidgets(new SectionWidget({ 140 | titleLevel: 1, 141 | titleMarkdown: title, 142 | quicklinks: options.links, 143 | })); 144 | } 145 | 146 | public watchScope(scope: Construct, options?: WatchfulAspectProps) { 147 | const aspect = new WatchfulAspect(this, options); 148 | Aspects.of(scope).add(aspect); 149 | } 150 | 151 | public watchDynamoTable(title: string, table: dynamodb.Table, options: WatchDynamoTableOptions = {}) { 152 | return new WatchDynamoTable(this, Names.uniqueId(table), { 153 | title, 154 | watchful: this, 155 | table, 156 | ...options, 157 | }); 158 | } 159 | 160 | public watchApiGateway(title: string, restApi: apigw.RestApi, options: WatchApiGatewayOptions = {}) { 161 | return new WatchApiGateway(this, Names.uniqueId(restApi), { 162 | title, watchful: this, restApi, ...options, 163 | }); 164 | } 165 | 166 | public watchLambdaFunction(title: string, fn: lambda.Function, options: WatchLambdaFunctionOptions = {}) { 167 | return new WatchLambdaFunction(this, Names.uniqueId(fn), { 168 | title, watchful: this, fn, ...options, 169 | }); 170 | } 171 | 172 | public watchStateMachine(title: string, stateMachine: stepfunctions.StateMachine, options: WatchStateMachineOptions = {}) { 173 | return new WatchStateMachine(this, Names.uniqueId(stateMachine), { 174 | title, watchful: this, stateMachine, ...options, 175 | }); 176 | } 177 | 178 | public watchRdsAuroraCluster(title: string, cluster: rds.DatabaseCluster, options: WatchRdsAuroraOptions = {}) { 179 | return new WatchRdsAurora(this, Names.uniqueId(cluster), { 180 | title, watchful: this, cluster, ...options, 181 | }); 182 | } 183 | public watchFargateEcs(title: string, fargateService: ecs.FargateService, targetGroup: ApplicationTargetGroup, 184 | options: WatchEcsServiceOptions = {}) { 185 | 186 | return new WatchEcsService(this, Names.uniqueId(fargateService), { 187 | title, watchful: this, fargateService, targetGroup, ...options, 188 | }); 189 | } 190 | public watchEc2Ecs(title: string, ec2Service: ecs.Ec2Service, targetGroup: ApplicationTargetGroup, options: WatchEcsServiceOptions = {}) { 191 | return new WatchEcsService(this, Names.uniqueId(ec2Service), { 192 | title, watchful: this, ec2Service, targetGroup, ...options, 193 | }); 194 | } 195 | } 196 | 197 | function linkForDashboard(dashboard: cloudwatch.Dashboard) { 198 | const cfnDashboard = dashboard.node.defaultChild as cloudwatch.CfnDashboard; 199 | return `https://console.aws.amazon.com/cloudwatch/home?region=${dashboard.stack.region}#dashboards:name=${cfnDashboard.ref}`; 200 | } 201 | 202 | function hasAlarmAction(alarm: cloudwatch.IAlarm): alarm is cloudwatch.IAlarm & { addAlarmAction: (...actions: cloudwatch.IAlarmAction[]) => void } { 203 | return 'addAlarmAction' in alarm; 204 | } 205 | 206 | -------------------------------------------------------------------------------- /src/widget/axis.ts: -------------------------------------------------------------------------------- 1 | import { YAxisProps } from 'aws-cdk-lib/aws-cloudwatch'; 2 | 3 | /** 4 | * Y-Axis showing percentage (0-100%, inclusive). 5 | */ 6 | export const PercentageAxis: YAxisProps = { 7 | min: 0, 8 | max: 100, 9 | label: '%', 10 | showUnits: false, 11 | }; 12 | 13 | /** 14 | * Y-Axis showing time in milliseconds. 15 | */ 16 | export const TimeMillisAxis: YAxisProps = { 17 | min: 0, 18 | label: 'ms', 19 | showUnits: false, 20 | }; 21 | 22 | /** 23 | * Y-Axis showing time in seconds. 24 | */ 25 | export const TimeSecondsAxis: YAxisProps = { 26 | min: 0, 27 | label: 'sec', 28 | showUnits: false, 29 | }; 30 | 31 | /** 32 | * Y-Axis showing count (no units). 33 | */ 34 | export const CountAxis: YAxisProps = { 35 | min: 0, 36 | showUnits: false, 37 | }; 38 | 39 | /** 40 | * Y-Axis showing rate (0.5 = 50%, 1 = 100%, etc). 41 | */ 42 | export const RateAxis: YAxisProps = { 43 | min: 0, 44 | label: 'rate', 45 | showUnits: false, 46 | }; 47 | 48 | /** 49 | * Y-Axis showing size in bytes. 50 | */ 51 | export const SizeBytesAxis: YAxisProps = { 52 | min: 0, 53 | label: 'bytes', 54 | showUnits: false, 55 | }; 56 | -------------------------------------------------------------------------------- /src/widget/constant.ts: -------------------------------------------------------------------------------- 1 | export class CommonWidgetDimensions { 2 | static readonly FullWidth = 24; 3 | static readonly HalfWidth = 12; 4 | static readonly ThirdWidth = 8; 5 | static readonly FourthWidth = 6; 6 | static readonly TwoThirdsWidth = 16; 7 | static readonly DefaultHeight = 5; 8 | } 9 | -------------------------------------------------------------------------------- /src/widget/section.ts: -------------------------------------------------------------------------------- 1 | import { TextWidget } from 'aws-cdk-lib/aws-cloudwatch'; 2 | import { QuickLink } from '../api'; 3 | 4 | /** 5 | * Props to create SectionWidget. 6 | */ 7 | export interface SectionWidgetProps { 8 | /** 9 | * widget width 10 | * @default full width 11 | */ 12 | readonly width?: number; 13 | /** 14 | * widget height 15 | * @default 2 16 | */ 17 | readonly height?: number; 18 | /** 19 | * title level (1 = H1, 2 = H2, etc) 20 | * @default 1 21 | */ 22 | readonly titleLevel?: number; 23 | /** 24 | * section title (might be markdown) 25 | */ 26 | readonly titleMarkdown: string; 27 | /** 28 | * section description (might be markdown) 29 | * @default empty 30 | */ 31 | readonly descriptionMarkdown?: string; 32 | /** 33 | * quick links, that will be rendered as buttons 34 | * @default none 35 | */ 36 | readonly quicklinks?: QuickLink[]; 37 | } 38 | 39 | /** 40 | * Renders a section header in the following format: 41 | * 42 | * # Header 43 | * Description 44 | * [button:label1](url1) [button:label2](url2) 45 | */ 46 | export class SectionWidget extends TextWidget { 47 | private static toMarkdown(props: SectionWidgetProps) { 48 | const lines: string[] = []; 49 | 50 | // title 51 | 52 | const titlePrefix = '#'.repeat(props.titleLevel ?? 1); 53 | lines.push(`${titlePrefix} ${props.titleMarkdown}`); 54 | 55 | // description 56 | 57 | if (props.descriptionMarkdown) { 58 | lines.push(props.descriptionMarkdown); 59 | } 60 | 61 | // quick links 62 | 63 | if (props.quicklinks && props.quicklinks.length > 0) { 64 | lines.push(props.quicklinks.map(link => `[button:${link.title}](${link.url})`).join(' ')); 65 | } 66 | 67 | return lines.join('\n\n'); 68 | } 69 | 70 | constructor(props: SectionWidgetProps) { 71 | super({ width: props.width ?? 24, height: props.height ?? 2, markdown: SectionWidget.toMarkdown(props) }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/monitoring/aws/api-gateway/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricCache 1`] = ` 4 | Object { 5 | "hits": Metric { 6 | "account": undefined, 7 | "color": "#2ca02c", 8 | "dimensions": Object { 9 | "ApiName": "DummyApiName", 10 | "Stage": "DummyStage", 11 | }, 12 | "label": "Cache Hit", 13 | "metricName": "CacheHitCount", 14 | "namespace": "AWS/ApiGateway", 15 | "period": Duration { 16 | "amount": 1, 17 | "unit": TimeUnit { 18 | "inMillis": 60000, 19 | "isoLabel": "M", 20 | "label": "minutes", 21 | }, 22 | }, 23 | "region": undefined, 24 | "statistic": "Sum", 25 | "unit": undefined, 26 | }, 27 | "misses": Metric { 28 | "account": undefined, 29 | "color": "#d62728", 30 | "dimensions": Object { 31 | "ApiName": "DummyApiName", 32 | "Stage": "DummyStage", 33 | }, 34 | "label": "Cache Miss", 35 | "metricName": "CacheMissCount", 36 | "namespace": "AWS/ApiGateway", 37 | "period": Duration { 38 | "amount": 1, 39 | "unit": TimeUnit { 40 | "inMillis": 60000, 41 | "isoLabel": "M", 42 | "label": "minutes", 43 | }, 44 | }, 45 | "region": undefined, 46 | "statistic": "Sum", 47 | "unit": undefined, 48 | }, 49 | } 50 | `; 51 | 52 | exports[`snapshot test: metricCalls 1`] = ` 53 | Metric { 54 | "account": undefined, 55 | "color": "#1f77b4", 56 | "dimensions": Object { 57 | "ApiName": "DummyApiName", 58 | "Stage": "DummyStage", 59 | }, 60 | "label": "Calls", 61 | "metricName": "Count", 62 | "namespace": "AWS/ApiGateway", 63 | "period": Duration { 64 | "amount": 1, 65 | "unit": TimeUnit { 66 | "inMillis": 60000, 67 | "isoLabel": "M", 68 | "label": "minutes", 69 | }, 70 | }, 71 | "region": undefined, 72 | "statistic": "Sum", 73 | "unit": undefined, 74 | } 75 | `; 76 | 77 | exports[`snapshot test: metricErrors 1`] = ` 78 | Object { 79 | "count4XX": Metric { 80 | "account": undefined, 81 | "color": "#ff7f0e", 82 | "dimensions": Object { 83 | "ApiName": "DummyApiName", 84 | "Stage": "DummyStage", 85 | }, 86 | "label": "4XX Errors", 87 | "metricName": "4XXError", 88 | "namespace": "AWS/ApiGateway", 89 | "period": Duration { 90 | "amount": 1, 91 | "unit": TimeUnit { 92 | "inMillis": 60000, 93 | "isoLabel": "M", 94 | "label": "minutes", 95 | }, 96 | }, 97 | "region": undefined, 98 | "statistic": "Sum", 99 | "unit": undefined, 100 | }, 101 | "count5XX": Metric { 102 | "account": undefined, 103 | "color": "#d62728", 104 | "dimensions": Object { 105 | "ApiName": "DummyApiName", 106 | "Stage": "DummyStage", 107 | }, 108 | "label": "5XX Errors", 109 | "metricName": "5XXError", 110 | "namespace": "AWS/ApiGateway", 111 | "period": Duration { 112 | "amount": 1, 113 | "unit": TimeUnit { 114 | "inMillis": 60000, 115 | "isoLabel": "M", 116 | "label": "minutes", 117 | }, 118 | }, 119 | "region": undefined, 120 | "statistic": "Sum", 121 | "unit": undefined, 122 | }, 123 | } 124 | `; 125 | 126 | exports[`snapshot test: metricIntegrationLatency 1`] = ` 127 | Object { 128 | "avg": Metric { 129 | "account": undefined, 130 | "color": undefined, 131 | "dimensions": Object { 132 | "ApiName": "DummyApiName", 133 | "Stage": "DummyStage", 134 | }, 135 | "label": "avg", 136 | "metricName": "IntegrationLatency", 137 | "namespace": "AWS/ApiGateway", 138 | "period": Duration { 139 | "amount": 1, 140 | "unit": TimeUnit { 141 | "inMillis": 60000, 142 | "isoLabel": "M", 143 | "label": "minutes", 144 | }, 145 | }, 146 | "region": undefined, 147 | "statistic": "Average", 148 | "unit": undefined, 149 | }, 150 | "max": Metric { 151 | "account": undefined, 152 | "color": undefined, 153 | "dimensions": Object { 154 | "ApiName": "DummyApiName", 155 | "Stage": "DummyStage", 156 | }, 157 | "label": "max", 158 | "metricName": "IntegrationLatency", 159 | "namespace": "AWS/ApiGateway", 160 | "period": Duration { 161 | "amount": 1, 162 | "unit": TimeUnit { 163 | "inMillis": 60000, 164 | "isoLabel": "M", 165 | "label": "minutes", 166 | }, 167 | }, 168 | "region": undefined, 169 | "statistic": "Maximum", 170 | "unit": undefined, 171 | }, 172 | "min": Metric { 173 | "account": undefined, 174 | "color": undefined, 175 | "dimensions": Object { 176 | "ApiName": "DummyApiName", 177 | "Stage": "DummyStage", 178 | }, 179 | "label": "min", 180 | "metricName": "IntegrationLatency", 181 | "namespace": "AWS/ApiGateway", 182 | "period": Duration { 183 | "amount": 1, 184 | "unit": TimeUnit { 185 | "inMillis": 60000, 186 | "isoLabel": "M", 187 | "label": "minutes", 188 | }, 189 | }, 190 | "region": undefined, 191 | "statistic": "Minimum", 192 | "unit": undefined, 193 | }, 194 | "p90": Metric { 195 | "account": undefined, 196 | "color": undefined, 197 | "dimensions": Object { 198 | "ApiName": "DummyApiName", 199 | "Stage": "DummyStage", 200 | }, 201 | "label": "p90", 202 | "metricName": "IntegrationLatency", 203 | "namespace": "AWS/ApiGateway", 204 | "period": Duration { 205 | "amount": 1, 206 | "unit": TimeUnit { 207 | "inMillis": 60000, 208 | "isoLabel": "M", 209 | "label": "minutes", 210 | }, 211 | }, 212 | "region": undefined, 213 | "statistic": "p90", 214 | "unit": undefined, 215 | }, 216 | "p95": Metric { 217 | "account": undefined, 218 | "color": undefined, 219 | "dimensions": Object { 220 | "ApiName": "DummyApiName", 221 | "Stage": "DummyStage", 222 | }, 223 | "label": "p95", 224 | "metricName": "IntegrationLatency", 225 | "namespace": "AWS/ApiGateway", 226 | "period": Duration { 227 | "amount": 1, 228 | "unit": TimeUnit { 229 | "inMillis": 60000, 230 | "isoLabel": "M", 231 | "label": "minutes", 232 | }, 233 | }, 234 | "region": undefined, 235 | "statistic": "p95", 236 | "unit": undefined, 237 | }, 238 | "p99": Metric { 239 | "account": undefined, 240 | "color": undefined, 241 | "dimensions": Object { 242 | "ApiName": "DummyApiName", 243 | "Stage": "DummyStage", 244 | }, 245 | "label": "p99", 246 | "metricName": "IntegrationLatency", 247 | "namespace": "AWS/ApiGateway", 248 | "period": Duration { 249 | "amount": 1, 250 | "unit": TimeUnit { 251 | "inMillis": 60000, 252 | "isoLabel": "M", 253 | "label": "minutes", 254 | }, 255 | }, 256 | "region": undefined, 257 | "statistic": "p99", 258 | "unit": undefined, 259 | }, 260 | } 261 | `; 262 | 263 | exports[`snapshot test: metricLatency 1`] = ` 264 | Object { 265 | "avg": Metric { 266 | "account": undefined, 267 | "color": undefined, 268 | "dimensions": Object { 269 | "ApiName": "DummyApiName", 270 | "Stage": "DummyStage", 271 | }, 272 | "label": "avg", 273 | "metricName": "Latency", 274 | "namespace": "AWS/ApiGateway", 275 | "period": Duration { 276 | "amount": 1, 277 | "unit": TimeUnit { 278 | "inMillis": 60000, 279 | "isoLabel": "M", 280 | "label": "minutes", 281 | }, 282 | }, 283 | "region": undefined, 284 | "statistic": "Average", 285 | "unit": undefined, 286 | }, 287 | "max": Metric { 288 | "account": undefined, 289 | "color": undefined, 290 | "dimensions": Object { 291 | "ApiName": "DummyApiName", 292 | "Stage": "DummyStage", 293 | }, 294 | "label": "max", 295 | "metricName": "Latency", 296 | "namespace": "AWS/ApiGateway", 297 | "period": Duration { 298 | "amount": 1, 299 | "unit": TimeUnit { 300 | "inMillis": 60000, 301 | "isoLabel": "M", 302 | "label": "minutes", 303 | }, 304 | }, 305 | "region": undefined, 306 | "statistic": "Maximum", 307 | "unit": undefined, 308 | }, 309 | "min": Metric { 310 | "account": undefined, 311 | "color": undefined, 312 | "dimensions": Object { 313 | "ApiName": "DummyApiName", 314 | "Stage": "DummyStage", 315 | }, 316 | "label": "min", 317 | "metricName": "Latency", 318 | "namespace": "AWS/ApiGateway", 319 | "period": Duration { 320 | "amount": 1, 321 | "unit": TimeUnit { 322 | "inMillis": 60000, 323 | "isoLabel": "M", 324 | "label": "minutes", 325 | }, 326 | }, 327 | "region": undefined, 328 | "statistic": "Minimum", 329 | "unit": undefined, 330 | }, 331 | "p90": Metric { 332 | "account": undefined, 333 | "color": undefined, 334 | "dimensions": Object { 335 | "ApiName": "DummyApiName", 336 | "Stage": "DummyStage", 337 | }, 338 | "label": "p90", 339 | "metricName": "Latency", 340 | "namespace": "AWS/ApiGateway", 341 | "period": Duration { 342 | "amount": 1, 343 | "unit": TimeUnit { 344 | "inMillis": 60000, 345 | "isoLabel": "M", 346 | "label": "minutes", 347 | }, 348 | }, 349 | "region": undefined, 350 | "statistic": "p90", 351 | "unit": undefined, 352 | }, 353 | "p95": Metric { 354 | "account": undefined, 355 | "color": undefined, 356 | "dimensions": Object { 357 | "ApiName": "DummyApiName", 358 | "Stage": "DummyStage", 359 | }, 360 | "label": "p95", 361 | "metricName": "Latency", 362 | "namespace": "AWS/ApiGateway", 363 | "period": Duration { 364 | "amount": 1, 365 | "unit": TimeUnit { 366 | "inMillis": 60000, 367 | "isoLabel": "M", 368 | "label": "minutes", 369 | }, 370 | }, 371 | "region": undefined, 372 | "statistic": "p95", 373 | "unit": undefined, 374 | }, 375 | "p99": Metric { 376 | "account": undefined, 377 | "color": undefined, 378 | "dimensions": Object { 379 | "ApiName": "DummyApiName", 380 | "Stage": "DummyStage", 381 | }, 382 | "label": "p99", 383 | "metricName": "Latency", 384 | "namespace": "AWS/ApiGateway", 385 | "period": Duration { 386 | "amount": 1, 387 | "unit": TimeUnit { 388 | "inMillis": 60000, 389 | "isoLabel": "M", 390 | "label": "minutes", 391 | }, 392 | }, 393 | "region": undefined, 394 | "statistic": "p99", 395 | "unit": undefined, 396 | }, 397 | } 398 | `; 399 | -------------------------------------------------------------------------------- /test/monitoring/aws/api-gateway/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { ApiGatewayMetricFactory } from '../../../../src/monitoring/aws/api-gateway/metrics'; 2 | 3 | const DummyApiName = 'DummyApiName'; 4 | const DummyStage = 'DummyStage'; 5 | 6 | test('snapshot test: metricErrors', () => { 7 | // GIVEN 8 | const unitToTest = new ApiGatewayMetricFactory(); 9 | 10 | // WHEN 11 | const metric = unitToTest.metricErrors(DummyApiName, DummyStage); 12 | 13 | // THEN 14 | expect(metric.count4XX.metricName).toStrictEqual('4XXError'); 15 | expect(metric.count4XX.dimensions).toStrictEqual({ ApiName: DummyApiName, Stage: DummyStage }); 16 | expect(metric.count5XX.metricName).toStrictEqual('5XXError'); 17 | expect(metric.count5XX.dimensions).toStrictEqual({ ApiName: DummyApiName, Stage: DummyStage }); 18 | expect(metric).toMatchSnapshot(); 19 | }); 20 | 21 | test('snapshot test: metricIntegrationLatency', () => { 22 | // GIVEN 23 | const unitToTest = new ApiGatewayMetricFactory(); 24 | 25 | // WHEN 26 | const metric = unitToTest.metricIntegrationLatency(DummyApiName, DummyStage); 27 | 28 | // THEN 29 | Object.values(metric).forEach(eachMetric => { 30 | expect(eachMetric.metricName).toStrictEqual('IntegrationLatency'); 31 | expect(eachMetric.dimensions).toStrictEqual({ ApiName: DummyApiName, Stage: DummyStage }); 32 | }); 33 | expect(metric).toMatchSnapshot(); 34 | }); 35 | 36 | test('snapshot test: metricLatency', () => { 37 | // GIVEN 38 | const unitToTest = new ApiGatewayMetricFactory(); 39 | 40 | // WHEN 41 | const metric = unitToTest.metricLatency(DummyApiName, DummyStage); 42 | 43 | // THEN 44 | Object.values(metric).forEach(eachMetric => { 45 | expect(eachMetric.metricName).toStrictEqual('Latency'); 46 | expect(eachMetric.dimensions).toStrictEqual({ ApiName: DummyApiName, Stage: DummyStage }); 47 | }); 48 | expect(metric).toMatchSnapshot(); 49 | }); 50 | 51 | test('snapshot test: metricCalls', () => { 52 | // GIVEN 53 | const unitToTest = new ApiGatewayMetricFactory(); 54 | 55 | // WHEN 56 | const metric = unitToTest.metricCalls(DummyApiName, DummyStage); 57 | 58 | // THEN 59 | expect(metric.metricName).toStrictEqual('Count'); 60 | expect(metric.dimensions).toStrictEqual({ ApiName: DummyApiName, Stage: DummyStage }); 61 | expect(metric).toMatchSnapshot(); 62 | }); 63 | 64 | test('snapshot test: metricCache', () => { 65 | // GIVEN 66 | const unitToTest = new ApiGatewayMetricFactory(); 67 | 68 | // WHEN 69 | const metric = unitToTest.metricCache(DummyApiName, DummyStage); 70 | 71 | // THEN 72 | expect(metric.hits.metricName).toStrictEqual('CacheHitCount'); 73 | expect(metric.hits.dimensions).toStrictEqual({ ApiName: DummyApiName, Stage: DummyStage }); 74 | expect(metric.misses.metricName).toStrictEqual('CacheMissCount'); 75 | expect(metric.misses.dimensions).toStrictEqual({ ApiName: DummyApiName, Stage: DummyStage }); 76 | expect(metric).toMatchSnapshot(); 77 | }); 78 | -------------------------------------------------------------------------------- /test/monitoring/aws/dynamodb/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricConsumedCapacityUnits 1`] = ` 4 | Object { 5 | "read": Metric { 6 | "account": undefined, 7 | "color": undefined, 8 | "dimensions": Object { 9 | "TableName": "DummyTableName", 10 | }, 11 | "label": "Consumed (Read)", 12 | "metricName": "ConsumedReadCapacityUnits", 13 | "namespace": "AWS/DynamoDB", 14 | "period": Duration { 15 | "amount": 1, 16 | "unit": TimeUnit { 17 | "inMillis": 60000, 18 | "isoLabel": "M", 19 | "label": "minutes", 20 | }, 21 | }, 22 | "region": undefined, 23 | "statistic": "Sum", 24 | "unit": undefined, 25 | }, 26 | "write": Metric { 27 | "account": undefined, 28 | "color": undefined, 29 | "dimensions": Object { 30 | "TableName": "DummyTableName", 31 | }, 32 | "label": "Consumed (Write)", 33 | "metricName": "ConsumedWriteCapacityUnits", 34 | "namespace": "AWS/DynamoDB", 35 | "period": Duration { 36 | "amount": 1, 37 | "unit": TimeUnit { 38 | "inMillis": 60000, 39 | "isoLabel": "M", 40 | "label": "minutes", 41 | }, 42 | }, 43 | "region": undefined, 44 | "statistic": "Sum", 45 | "unit": undefined, 46 | }, 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /test/monitoring/aws/dynamodb/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDbMetricFactory } from '../../../../src/monitoring/aws/dynamodb/metrics'; 2 | 3 | const DummyTableName = 'DummyTableName'; 4 | 5 | test('snapshot test: metricConsumedCapacityUnits', () => { 6 | // GIVEN 7 | const unitToTest = new DynamoDbMetricFactory(); 8 | 9 | // WHEN 10 | const metric = unitToTest.metricConsumedCapacityUnits(DummyTableName); 11 | 12 | // THEN 13 | expect(metric.read.metricName).toStrictEqual('ConsumedReadCapacityUnits'); 14 | expect(metric.read.dimensions).toStrictEqual({ TableName: DummyTableName }); 15 | expect(metric.write.metricName).toStrictEqual('ConsumedWriteCapacityUnits'); 16 | expect(metric.write.dimensions).toStrictEqual({ TableName: DummyTableName }); 17 | expect(metric).toMatchSnapshot(); 18 | }); 19 | -------------------------------------------------------------------------------- /test/monitoring/aws/ecs/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { EcsMetricFactory } from '../../../../src/monitoring/aws/ecs/metrics'; 2 | 3 | const DummyTargetGroup = 'DummyTargetGroup'; 4 | const DummyLoadBalancer = 'DummyLoadBalancer'; 5 | const DummyClusterName = 'DummyClusterName'; 6 | const DummyServiceName = 'DummyServiceName'; 7 | 8 | test('snapshot test: metricRequestCount', () => { 9 | // GIVEN 10 | const unitToTest = new EcsMetricFactory(); 11 | 12 | // WHEN 13 | const metric = unitToTest.metricRequestCount(DummyTargetGroup, DummyLoadBalancer); 14 | 15 | // THEN 16 | expect(metric.metricName).toStrictEqual('RequestCount'); 17 | expect(metric.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 18 | expect(metric).toMatchSnapshot(); 19 | }); 20 | 21 | test('snapshot test: metricTargetResponseTime', () => { 22 | // GIVEN 23 | const unitToTest = new EcsMetricFactory(); 24 | 25 | // WHEN 26 | const metric = unitToTest.metricTargetResponseTime(DummyTargetGroup, DummyLoadBalancer); 27 | 28 | // THEN 29 | Object.values(metric).forEach(eachMetric => { 30 | expect(eachMetric.metricName).toStrictEqual('TargetResponseTime'); 31 | expect(eachMetric.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 32 | }); 33 | expect(metric).toMatchSnapshot(); 34 | expect(metric).toMatchSnapshot(); 35 | }); 36 | 37 | test('snapshot test: metricCpuUtilizationAverage', () => { 38 | // GIVEN 39 | const unitToTest = new EcsMetricFactory(); 40 | 41 | // WHEN 42 | const metric = unitToTest.metricCpuUtilizationAverage(DummyClusterName, DummyServiceName); 43 | 44 | // THEN 45 | expect(metric.metricName).toStrictEqual('CPUUtilization'); 46 | expect(metric.dimensions).toStrictEqual({ ClusterName: DummyClusterName, ServiceName: DummyServiceName }); 47 | expect(metric).toMatchSnapshot(); 48 | }); 49 | 50 | test('snapshot test: metricMemoryUtilizationAverage', () => { 51 | // GIVEN 52 | const unitToTest = new EcsMetricFactory(); 53 | 54 | // WHEN 55 | const metric = unitToTest.metricMemoryUtilizationAverage(DummyClusterName, DummyServiceName); 56 | 57 | // THEN 58 | expect(metric.metricName).toStrictEqual('MemoryUtilization'); 59 | expect(metric.dimensions).toStrictEqual({ ClusterName: DummyClusterName, ServiceName: DummyServiceName }); 60 | expect(metric).toMatchSnapshot(); 61 | }); 62 | 63 | test('snapshot test: metricMinHealthyHostCount', () => { 64 | // GIVEN 65 | const unitToTest = new EcsMetricFactory(); 66 | 67 | // WHEN 68 | const metric = unitToTest.metricMinHealthyHostCount(DummyTargetGroup, DummyLoadBalancer); 69 | 70 | // THEN 71 | expect(metric.metricName).toStrictEqual('HealthyHostCount'); 72 | expect(metric.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 73 | expect(metric).toMatchSnapshot(); 74 | }); 75 | 76 | test('snapshot test: metricMaxUnhealthyHostCount', () => { 77 | // GIVEN 78 | const unitToTest = new EcsMetricFactory(); 79 | 80 | // WHEN 81 | const metric = unitToTest.metricMaxUnhealthyHostCount(DummyTargetGroup, DummyLoadBalancer); 82 | 83 | // THEN 84 | expect(metric.metricName).toStrictEqual('UnHealthyHostCount'); 85 | expect(metric.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 86 | expect(metric).toMatchSnapshot(); 87 | }); 88 | 89 | test('snapshot test: metricHttpErrorStatusCodeRate', () => { 90 | // GIVEN 91 | const unitToTest = new EcsMetricFactory(); 92 | 93 | // WHEN 94 | const metric = unitToTest.metricHttpErrorStatusCodeRate(DummyTargetGroup, DummyLoadBalancer); 95 | 96 | // THEN 97 | expect(metric).toMatchSnapshot(); 98 | }); 99 | 100 | test('snapshot test: metricHttpStatusCodeCount', () => { 101 | // GIVEN 102 | const unitToTest = new EcsMetricFactory(); 103 | 104 | // WHEN 105 | const metric = unitToTest.metricHttpStatusCodeCount(DummyTargetGroup, DummyLoadBalancer); 106 | 107 | // THEN 108 | expect(metric.count2XX.metricName).toStrictEqual('HTTPCode_Target_2XX_Count'); 109 | expect(metric.count2XX.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 110 | expect(metric.count3XX.metricName).toStrictEqual('HTTPCode_Target_3XX_Count'); 111 | expect(metric.count3XX.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 112 | expect(metric.count4XX.metricName).toStrictEqual('HTTPCode_Target_4XX_Count'); 113 | expect(metric.count4XX.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 114 | expect(metric.count5XX.metricName).toStrictEqual('HTTPCode_Target_5XX_Count'); 115 | expect(metric.count5XX.dimensions).toStrictEqual({ TargetGroup: DummyTargetGroup, LoadBalancer: DummyLoadBalancer }); 116 | expect(metric).toMatchSnapshot(); 117 | }); 118 | -------------------------------------------------------------------------------- /test/monitoring/aws/lambda/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricDuration 1`] = ` 4 | Object { 5 | "avg": Metric { 6 | "account": undefined, 7 | "color": undefined, 8 | "dimensions": Object { 9 | "FunctionName": "DummyFunctionName", 10 | }, 11 | "label": "Average", 12 | "metricName": "Duration", 13 | "namespace": "AWS/Lambda", 14 | "period": Duration { 15 | "amount": 5, 16 | "unit": TimeUnit { 17 | "inMillis": 60000, 18 | "isoLabel": "M", 19 | "label": "minutes", 20 | }, 21 | }, 22 | "region": undefined, 23 | "statistic": "Average", 24 | "unit": undefined, 25 | }, 26 | "max": Metric { 27 | "account": undefined, 28 | "color": undefined, 29 | "dimensions": Object { 30 | "FunctionName": "DummyFunctionName", 31 | }, 32 | "label": "Maximum", 33 | "metricName": "Duration", 34 | "namespace": "AWS/Lambda", 35 | "period": Duration { 36 | "amount": 5, 37 | "unit": TimeUnit { 38 | "inMillis": 60000, 39 | "isoLabel": "M", 40 | "label": "minutes", 41 | }, 42 | }, 43 | "region": undefined, 44 | "statistic": "Maximum", 45 | "unit": undefined, 46 | }, 47 | "min": Metric { 48 | "account": undefined, 49 | "color": undefined, 50 | "dimensions": Object { 51 | "FunctionName": "DummyFunctionName", 52 | }, 53 | "label": "Minimum", 54 | "metricName": "Duration", 55 | "namespace": "AWS/Lambda", 56 | "period": Duration { 57 | "amount": 5, 58 | "unit": TimeUnit { 59 | "inMillis": 60000, 60 | "isoLabel": "M", 61 | "label": "minutes", 62 | }, 63 | }, 64 | "region": undefined, 65 | "statistic": "Minimum", 66 | "unit": undefined, 67 | }, 68 | "p50": Metric { 69 | "account": undefined, 70 | "color": undefined, 71 | "dimensions": Object { 72 | "FunctionName": "DummyFunctionName", 73 | }, 74 | "label": "p50", 75 | "metricName": "Duration", 76 | "namespace": "AWS/Lambda", 77 | "period": Duration { 78 | "amount": 5, 79 | "unit": TimeUnit { 80 | "inMillis": 60000, 81 | "isoLabel": "M", 82 | "label": "minutes", 83 | }, 84 | }, 85 | "region": undefined, 86 | "statistic": "p50", 87 | "unit": undefined, 88 | }, 89 | "p90": Metric { 90 | "account": undefined, 91 | "color": undefined, 92 | "dimensions": Object { 93 | "FunctionName": "DummyFunctionName", 94 | }, 95 | "label": "p90", 96 | "metricName": "Duration", 97 | "namespace": "AWS/Lambda", 98 | "period": Duration { 99 | "amount": 5, 100 | "unit": TimeUnit { 101 | "inMillis": 60000, 102 | "isoLabel": "M", 103 | "label": "minutes", 104 | }, 105 | }, 106 | "region": undefined, 107 | "statistic": "p90", 108 | "unit": undefined, 109 | }, 110 | "p99": Metric { 111 | "account": undefined, 112 | "color": undefined, 113 | "dimensions": Object { 114 | "FunctionName": "DummyFunctionName", 115 | }, 116 | "label": "p99", 117 | "metricName": "Duration", 118 | "namespace": "AWS/Lambda", 119 | "period": Duration { 120 | "amount": 5, 121 | "unit": TimeUnit { 122 | "inMillis": 60000, 123 | "isoLabel": "M", 124 | "label": "minutes", 125 | }, 126 | }, 127 | "region": undefined, 128 | "statistic": "p99", 129 | "unit": undefined, 130 | }, 131 | } 132 | `; 133 | 134 | exports[`snapshot test: metricErrors 1`] = ` 135 | Metric { 136 | "account": undefined, 137 | "color": undefined, 138 | "dimensions": Object { 139 | "FunctionName": "DummyFunctionName", 140 | }, 141 | "label": undefined, 142 | "metricName": "Errors", 143 | "namespace": "AWS/Lambda", 144 | "period": Duration { 145 | "amount": 5, 146 | "unit": TimeUnit { 147 | "inMillis": 60000, 148 | "isoLabel": "M", 149 | "label": "minutes", 150 | }, 151 | }, 152 | "region": undefined, 153 | "statistic": "Sum", 154 | "unit": undefined, 155 | } 156 | `; 157 | 158 | exports[`snapshot test: metricInvocations 1`] = ` 159 | Metric { 160 | "account": undefined, 161 | "color": undefined, 162 | "dimensions": Object { 163 | "FunctionName": "DummyFunctionName", 164 | }, 165 | "label": undefined, 166 | "metricName": "Invocations", 167 | "namespace": "AWS/Lambda", 168 | "period": Duration { 169 | "amount": 5, 170 | "unit": TimeUnit { 171 | "inMillis": 60000, 172 | "isoLabel": "M", 173 | "label": "minutes", 174 | }, 175 | }, 176 | "region": undefined, 177 | "statistic": "Sum", 178 | "unit": undefined, 179 | } 180 | `; 181 | 182 | exports[`snapshot test: metricThrottles 1`] = ` 183 | Metric { 184 | "account": undefined, 185 | "color": undefined, 186 | "dimensions": Object { 187 | "FunctionName": "DummyFunctionName", 188 | }, 189 | "label": undefined, 190 | "metricName": "Throttles", 191 | "namespace": "AWS/Lambda", 192 | "period": Duration { 193 | "amount": 5, 194 | "unit": TimeUnit { 195 | "inMillis": 60000, 196 | "isoLabel": "M", 197 | "label": "minutes", 198 | }, 199 | }, 200 | "region": undefined, 201 | "statistic": "Sum", 202 | "unit": undefined, 203 | } 204 | `; 205 | -------------------------------------------------------------------------------- /test/monitoring/aws/lambda/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { LambdaMetricFactory } from '../../../../src/monitoring/aws/lambda/metrics'; 2 | 3 | const DummyFunctionName = 'DummyFunctionName'; 4 | 5 | test('snapshot test: metricInvocations', () => { 6 | // GIVEN 7 | const unitToTest = new LambdaMetricFactory(); 8 | 9 | // WHEN 10 | const metric = unitToTest.metricInvocations(DummyFunctionName); 11 | 12 | // THEN 13 | expect(metric.metricName).toStrictEqual('Invocations'); 14 | expect(metric.dimensions).toStrictEqual({ FunctionName: DummyFunctionName }); 15 | expect(metric).toMatchSnapshot(); 16 | }); 17 | 18 | test('snapshot test: metricDuration', () => { 19 | // GIVEN 20 | const unitToTest = new LambdaMetricFactory(); 21 | 22 | // WHEN 23 | const metric = unitToTest.metricDuration(DummyFunctionName); 24 | 25 | // THEN 26 | Object.values(metric).forEach(eachMetric => { 27 | expect(eachMetric.metricName).toStrictEqual('Duration'); 28 | expect(eachMetric.dimensions).toStrictEqual({ FunctionName: DummyFunctionName }); 29 | }); 30 | expect(metric).toMatchSnapshot(); 31 | }); 32 | 33 | test('snapshot test: metricErrors', () => { 34 | // GIVEN 35 | const unitToTest = new LambdaMetricFactory(); 36 | 37 | // WHEN 38 | const metric = unitToTest.metricErrors(DummyFunctionName); 39 | 40 | // THEN 41 | expect(metric.metricName).toStrictEqual('Errors'); 42 | expect(metric.dimensions).toStrictEqual({ FunctionName: DummyFunctionName }); 43 | expect(metric).toMatchSnapshot(); 44 | }); 45 | 46 | test('snapshot test: metricThrottles', () => { 47 | // GIVEN 48 | const unitToTest = new LambdaMetricFactory(); 49 | 50 | // WHEN 51 | const metric = unitToTest.metricThrottles(DummyFunctionName); 52 | 53 | // THEN 54 | expect(metric.metricName).toStrictEqual('Throttles'); 55 | expect(metric.dimensions).toStrictEqual({ FunctionName: DummyFunctionName }); 56 | expect(metric).toMatchSnapshot(); 57 | }); 58 | -------------------------------------------------------------------------------- /test/monitoring/aws/rds/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricBufferCacheHitRatio 1`] = ` 4 | Metric { 5 | "account": undefined, 6 | "color": undefined, 7 | "dimensions": Object { 8 | "DBClusterIdentifier": "DummyClusterId", 9 | }, 10 | "label": undefined, 11 | "metricName": "BufferCacheHitRatio", 12 | "namespace": "AWS/RDS", 13 | "period": Duration { 14 | "amount": 5, 15 | "unit": TimeUnit { 16 | "inMillis": 60000, 17 | "isoLabel": "M", 18 | "label": "minutes", 19 | }, 20 | }, 21 | "region": undefined, 22 | "statistic": "Average", 23 | "unit": undefined, 24 | } 25 | `; 26 | 27 | exports[`snapshot test: metricCpuUtilization 1`] = ` 28 | Metric { 29 | "account": undefined, 30 | "color": undefined, 31 | "dimensions": Object { 32 | "DBClusterIdentifier": "DummyClusterId", 33 | }, 34 | "label": undefined, 35 | "metricName": "CPUUtilization", 36 | "namespace": "AWS/RDS", 37 | "period": Duration { 38 | "amount": 5, 39 | "unit": TimeUnit { 40 | "inMillis": 60000, 41 | "isoLabel": "M", 42 | "label": "minutes", 43 | }, 44 | }, 45 | "region": undefined, 46 | "statistic": "Average", 47 | "unit": undefined, 48 | } 49 | `; 50 | 51 | exports[`snapshot test: metricDbConnections 1`] = ` 52 | Metric { 53 | "account": undefined, 54 | "color": undefined, 55 | "dimensions": Object { 56 | "DBClusterIdentifier": "DummyClusterId", 57 | }, 58 | "label": undefined, 59 | "metricName": "DatabaseConnections", 60 | "namespace": "AWS/RDS", 61 | "period": Duration { 62 | "amount": 5, 63 | "unit": TimeUnit { 64 | "inMillis": 60000, 65 | "isoLabel": "M", 66 | "label": "minutes", 67 | }, 68 | }, 69 | "region": undefined, 70 | "statistic": "Average", 71 | "unit": undefined, 72 | } 73 | `; 74 | 75 | exports[`snapshot test: metricDmlThroughput 1`] = ` 76 | Object { 77 | "dbDeleteThroughputMetric": Metric { 78 | "account": undefined, 79 | "color": undefined, 80 | "dimensions": Object { 81 | "DBClusterIdentifier": "DummyClusterId", 82 | }, 83 | "label": undefined, 84 | "metricName": "DeleteThroughput", 85 | "namespace": "AWS/RDS", 86 | "period": Duration { 87 | "amount": 5, 88 | "unit": TimeUnit { 89 | "inMillis": 60000, 90 | "isoLabel": "M", 91 | "label": "minutes", 92 | }, 93 | }, 94 | "region": undefined, 95 | "statistic": "Sum", 96 | "unit": undefined, 97 | }, 98 | "dbInsertThroughputMetric": Metric { 99 | "account": undefined, 100 | "color": undefined, 101 | "dimensions": Object { 102 | "DBClusterIdentifier": "DummyClusterId", 103 | }, 104 | "label": undefined, 105 | "metricName": "InsertThroughput", 106 | "namespace": "AWS/RDS", 107 | "period": Duration { 108 | "amount": 5, 109 | "unit": TimeUnit { 110 | "inMillis": 60000, 111 | "isoLabel": "M", 112 | "label": "minutes", 113 | }, 114 | }, 115 | "region": undefined, 116 | "statistic": "Sum", 117 | "unit": undefined, 118 | }, 119 | "dbSelectThroughputMetric": Metric { 120 | "account": undefined, 121 | "color": undefined, 122 | "dimensions": Object { 123 | "DBClusterIdentifier": "DummyClusterId", 124 | }, 125 | "label": undefined, 126 | "metricName": "SelectThroughput", 127 | "namespace": "AWS/RDS", 128 | "period": Duration { 129 | "amount": 5, 130 | "unit": TimeUnit { 131 | "inMillis": 60000, 132 | "isoLabel": "M", 133 | "label": "minutes", 134 | }, 135 | }, 136 | "region": undefined, 137 | "statistic": "Sum", 138 | "unit": undefined, 139 | }, 140 | "dbUpdateThroughputMetric": Metric { 141 | "account": undefined, 142 | "color": undefined, 143 | "dimensions": Object { 144 | "DBClusterIdentifier": "DummyClusterId", 145 | }, 146 | "label": undefined, 147 | "metricName": "UpdateThroughput", 148 | "namespace": "AWS/RDS", 149 | "period": Duration { 150 | "amount": 5, 151 | "unit": TimeUnit { 152 | "inMillis": 60000, 153 | "isoLabel": "M", 154 | "label": "minutes", 155 | }, 156 | }, 157 | "region": undefined, 158 | "statistic": "Sum", 159 | "unit": undefined, 160 | }, 161 | } 162 | `; 163 | 164 | exports[`snapshot test: metricReplicaLag 1`] = ` 165 | Metric { 166 | "account": undefined, 167 | "color": undefined, 168 | "dimensions": Object { 169 | "DBClusterIdentifier": "DummyClusterId", 170 | }, 171 | "label": undefined, 172 | "metricName": "AuroraReplicaLag", 173 | "namespace": "AWS/RDS", 174 | "period": Duration { 175 | "amount": 5, 176 | "unit": TimeUnit { 177 | "inMillis": 60000, 178 | "isoLabel": "M", 179 | "label": "minutes", 180 | }, 181 | }, 182 | "region": undefined, 183 | "statistic": "Average", 184 | "unit": undefined, 185 | } 186 | `; 187 | -------------------------------------------------------------------------------- /test/monitoring/aws/rds/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { RdsAuroraMetricFactory } from '../../../../src/monitoring/aws/rds/metrics'; 2 | 3 | const DummyClusterId = 'DummyClusterId'; 4 | 5 | test('snapshot test: metricDbConnections', () => { 6 | // GIVEN 7 | const unitToTest = new RdsAuroraMetricFactory(); 8 | 9 | // WHEN 10 | const metric = unitToTest.metricDbConnections(DummyClusterId); 11 | 12 | // THEN 13 | expect(metric.metricName).toStrictEqual('DatabaseConnections'); 14 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 15 | expect(metric).toMatchSnapshot(); 16 | }); 17 | 18 | test('snapshot test: metricReplicaLag', () => { 19 | // GIVEN 20 | const unitToTest = new RdsAuroraMetricFactory(); 21 | 22 | // WHEN 23 | const metric = unitToTest.metricReplicaLag(DummyClusterId); 24 | 25 | // THEN 26 | expect(metric.metricName).toStrictEqual('AuroraReplicaLag'); 27 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 28 | expect(metric).toMatchSnapshot(); 29 | }); 30 | 31 | test('snapshot test: metricBufferCacheHitRatio', () => { 32 | // GIVEN 33 | const unitToTest = new RdsAuroraMetricFactory(); 34 | 35 | // WHEN 36 | const metric = unitToTest.metricBufferCacheHitRatio(DummyClusterId); 37 | 38 | // THEN 39 | expect(metric.metricName).toStrictEqual('BufferCacheHitRatio'); 40 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 41 | expect(metric).toMatchSnapshot(); 42 | }); 43 | 44 | test('snapshot test: metricCpuUtilization', () => { 45 | // GIVEN 46 | const unitToTest = new RdsAuroraMetricFactory(); 47 | 48 | // WHEN 49 | const metric = unitToTest.metricCpuUtilization(DummyClusterId); 50 | 51 | // THEN 52 | expect(metric.metricName).toStrictEqual('CPUUtilization'); 53 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 54 | expect(metric).toMatchSnapshot(); 55 | }); 56 | 57 | test('snapshot test: metricDmlThroughput', () => { 58 | // GIVEN 59 | const unitToTest = new RdsAuroraMetricFactory(); 60 | 61 | // WHEN 62 | const metric = unitToTest.metricDmlThroughput(DummyClusterId); 63 | 64 | // THEN 65 | expect(metric.dbSelectThroughputMetric.metricName).toStrictEqual('SelectThroughput'); 66 | expect(metric.dbSelectThroughputMetric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 67 | expect(metric.dbInsertThroughputMetric.metricName).toStrictEqual('InsertThroughput'); 68 | expect(metric.dbInsertThroughputMetric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 69 | expect(metric.dbUpdateThroughputMetric.metricName).toStrictEqual('UpdateThroughput'); 70 | expect(metric.dbUpdateThroughputMetric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 71 | expect(metric.dbDeleteThroughputMetric.metricName).toStrictEqual('DeleteThroughput'); 72 | expect(metric.dbDeleteThroughputMetric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 73 | expect(metric).toMatchSnapshot(); 74 | }); 75 | -------------------------------------------------------------------------------- /test/monitoring/aws/redshift/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricAverageConnectionCount 1`] = ` 4 | Metric { 5 | "account": undefined, 6 | "color": undefined, 7 | "dimensions": Object { 8 | "DBClusterIdentifier": "DummyClusterId", 9 | }, 10 | "label": undefined, 11 | "metricName": "DatabaseConnections", 12 | "namespace": "AWS/Redshift", 13 | "period": Duration { 14 | "amount": 5, 15 | "unit": TimeUnit { 16 | "inMillis": 60000, 17 | "isoLabel": "M", 18 | "label": "minutes", 19 | }, 20 | }, 21 | "region": undefined, 22 | "statistic": "Average", 23 | "unit": undefined, 24 | } 25 | `; 26 | 27 | exports[`snapshot test: metricAverageCpuUsageInPercent 1`] = ` 28 | Metric { 29 | "account": undefined, 30 | "color": undefined, 31 | "dimensions": Object { 32 | "DBClusterIdentifier": "DummyClusterId", 33 | }, 34 | "label": undefined, 35 | "metricName": "CPUUtilization", 36 | "namespace": "AWS/Redshift", 37 | "period": Duration { 38 | "amount": 5, 39 | "unit": TimeUnit { 40 | "inMillis": 60000, 41 | "isoLabel": "M", 42 | "label": "minutes", 43 | }, 44 | }, 45 | "region": undefined, 46 | "statistic": "Average", 47 | "unit": undefined, 48 | } 49 | `; 50 | 51 | exports[`snapshot test: metricAverageDiskSpaceUsageInPercent 1`] = ` 52 | Metric { 53 | "account": undefined, 54 | "color": undefined, 55 | "dimensions": Object { 56 | "DBClusterIdentifier": "DummyClusterId", 57 | }, 58 | "label": undefined, 59 | "metricName": "PercentageDiskSpaceUsed", 60 | "namespace": "AWS/Redshift", 61 | "period": Duration { 62 | "amount": 5, 63 | "unit": TimeUnit { 64 | "inMillis": 60000, 65 | "isoLabel": "M", 66 | "label": "minutes", 67 | }, 68 | }, 69 | "region": undefined, 70 | "statistic": "Average", 71 | "unit": undefined, 72 | } 73 | `; 74 | 75 | exports[`snapshot test: metricAverageLatencyInSeconds 1`] = ` 76 | Object { 77 | "read": Metric { 78 | "account": undefined, 79 | "color": undefined, 80 | "dimensions": Object { 81 | "DBClusterIdentifier": "DummyClusterId", 82 | }, 83 | "label": undefined, 84 | "metricName": "ReadLatency", 85 | "namespace": "AWS/Redshift", 86 | "period": Duration { 87 | "amount": 5, 88 | "unit": TimeUnit { 89 | "inMillis": 60000, 90 | "isoLabel": "M", 91 | "label": "minutes", 92 | }, 93 | }, 94 | "region": undefined, 95 | "statistic": "Average", 96 | "unit": undefined, 97 | }, 98 | "write": Metric { 99 | "account": undefined, 100 | "color": undefined, 101 | "dimensions": Object { 102 | "DBClusterIdentifier": "DummyClusterId", 103 | }, 104 | "label": undefined, 105 | "metricName": "WriteLatency", 106 | "namespace": "AWS/Redshift", 107 | "period": Duration { 108 | "amount": 5, 109 | "unit": TimeUnit { 110 | "inMillis": 60000, 111 | "isoLabel": "M", 112 | "label": "minutes", 113 | }, 114 | }, 115 | "region": undefined, 116 | "statistic": "Average", 117 | "unit": undefined, 118 | }, 119 | } 120 | `; 121 | 122 | exports[`snapshot test: metricAverageQueryDurationInMicros 1`] = ` 123 | Object { 124 | "longQueries": Metric { 125 | "account": undefined, 126 | "color": undefined, 127 | "dimensions": Object { 128 | "DBClusterIdentifier": "DummyClusterId", 129 | "latency": "long", 130 | }, 131 | "label": undefined, 132 | "metricName": "QueryDuration", 133 | "namespace": "AWS/Redshift", 134 | "period": Duration { 135 | "amount": 5, 136 | "unit": TimeUnit { 137 | "inMillis": 60000, 138 | "isoLabel": "M", 139 | "label": "minutes", 140 | }, 141 | }, 142 | "region": undefined, 143 | "statistic": "Average", 144 | "unit": undefined, 145 | }, 146 | "mediumQueries": Metric { 147 | "account": undefined, 148 | "color": undefined, 149 | "dimensions": Object { 150 | "DBClusterIdentifier": "DummyClusterId", 151 | "latency": "medium", 152 | }, 153 | "label": undefined, 154 | "metricName": "QueryDuration", 155 | "namespace": "AWS/Redshift", 156 | "period": Duration { 157 | "amount": 5, 158 | "unit": TimeUnit { 159 | "inMillis": 60000, 160 | "isoLabel": "M", 161 | "label": "minutes", 162 | }, 163 | }, 164 | "region": undefined, 165 | "statistic": "Average", 166 | "unit": undefined, 167 | }, 168 | "shortQueries": Metric { 169 | "account": undefined, 170 | "color": undefined, 171 | "dimensions": Object { 172 | "DBClusterIdentifier": "DummyClusterId", 173 | "latency": "short", 174 | }, 175 | "label": undefined, 176 | "metricName": "QueryDuration", 177 | "namespace": "AWS/Redshift", 178 | "period": Duration { 179 | "amount": 5, 180 | "unit": TimeUnit { 181 | "inMillis": 60000, 182 | "isoLabel": "M", 183 | "label": "minutes", 184 | }, 185 | }, 186 | "region": undefined, 187 | "statistic": "Average", 188 | "unit": undefined, 189 | }, 190 | } 191 | `; 192 | 193 | exports[`snapshot test: metricMaintenanceModeEnabled 1`] = ` 194 | Metric { 195 | "account": undefined, 196 | "color": undefined, 197 | "dimensions": Object { 198 | "DBClusterIdentifier": "DummyClusterId", 199 | }, 200 | "label": undefined, 201 | "metricName": "MaintenanceMode", 202 | "namespace": "AWS/Redshift", 203 | "period": Duration { 204 | "amount": 5, 205 | "unit": TimeUnit { 206 | "inMillis": 60000, 207 | "isoLabel": "M", 208 | "label": "minutes", 209 | }, 210 | }, 211 | "region": undefined, 212 | "statistic": "Maximum", 213 | "unit": undefined, 214 | } 215 | `; 216 | -------------------------------------------------------------------------------- /test/monitoring/aws/redshift/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { RedshiftMetricFactory } from '../../../../src/monitoring/aws/redshift/metrics'; 2 | 3 | const DummyClusterId = 'DummyClusterId'; 4 | 5 | test('snapshot test: metricAverageConnectionCount', () => { 6 | // GIVEN 7 | const unitToTest = new RedshiftMetricFactory(); 8 | 9 | // WHEN 10 | const metric = unitToTest.metricAverageConnectionCount(DummyClusterId); 11 | 12 | // THEN 13 | expect(metric.metricName).toStrictEqual('DatabaseConnections'); 14 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 15 | expect(metric).toMatchSnapshot(); 16 | }); 17 | 18 | test('snapshot test: metricAverageCpuUsageInPercent', () => { 19 | // GIVEN 20 | const unitToTest = new RedshiftMetricFactory(); 21 | 22 | // WHEN 23 | const metric = unitToTest.metricAverageCpuUsageInPercent(DummyClusterId); 24 | 25 | // THEN 26 | expect(metric.metricName).toStrictEqual('CPUUtilization'); 27 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 28 | expect(metric).toMatchSnapshot(); 29 | }); 30 | 31 | test('snapshot test: metricAverageDiskSpaceUsageInPercent', () => { 32 | // GIVEN 33 | const unitToTest = new RedshiftMetricFactory(); 34 | 35 | // WHEN 36 | const metric = unitToTest.metricAverageDiskSpaceUsageInPercent(DummyClusterId); 37 | 38 | // THEN 39 | expect(metric.metricName).toStrictEqual('PercentageDiskSpaceUsed'); 40 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 41 | expect(metric).toMatchSnapshot(); 42 | }); 43 | 44 | test('snapshot test: metricAverageLatencyInSeconds', () => { 45 | // GIVEN 46 | const unitToTest = new RedshiftMetricFactory(); 47 | 48 | // WHEN 49 | const metric = unitToTest.metricAverageLatencyInSeconds(DummyClusterId); 50 | 51 | // THEN 52 | expect(metric.read.metricName).toStrictEqual('ReadLatency'); 53 | expect(metric.read.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 54 | expect(metric.write.metricName).toStrictEqual('WriteLatency'); 55 | expect(metric.write.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 56 | expect(metric).toMatchSnapshot(); 57 | }); 58 | 59 | test('snapshot test: metricAverageQueryDurationInMicros', () => { 60 | // GIVEN 61 | const unitToTest = new RedshiftMetricFactory(); 62 | 63 | // WHEN 64 | const metric = unitToTest.metricAverageQueryDurationInMicros(DummyClusterId); 65 | 66 | // THEN 67 | expect(metric.shortQueries.metricName).toStrictEqual('QueryDuration'); 68 | expect(metric.shortQueries.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId, latency: 'short' }); 69 | expect(metric.mediumQueries.metricName).toStrictEqual('QueryDuration'); 70 | expect(metric.mediumQueries.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId, latency: 'medium' }); 71 | expect(metric.longQueries.metricName).toStrictEqual('QueryDuration'); 72 | expect(metric.longQueries.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId, latency: 'long' }); 73 | expect(metric).toMatchSnapshot(); 74 | }); 75 | 76 | test('snapshot test: metricMaintenanceModeEnabled', () => { 77 | // GIVEN 78 | const unitToTest = new RedshiftMetricFactory(); 79 | 80 | // WHEN 81 | const metric = unitToTest.metricMaintenanceModeEnabled(DummyClusterId); 82 | 83 | // THEN 84 | expect(metric.metricName).toStrictEqual('MaintenanceMode'); 85 | expect(metric.dimensions).toStrictEqual({ DBClusterIdentifier: DummyClusterId }); 86 | expect(metric).toMatchSnapshot(); 87 | }); 88 | -------------------------------------------------------------------------------- /test/monitoring/aws/sns/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricAverageMessageSizeInBytes 1`] = ` 4 | Metric { 5 | "account": undefined, 6 | "color": undefined, 7 | "dimensions": Object { 8 | "TopicName": "DummyTopicName", 9 | }, 10 | "label": undefined, 11 | "metricName": "PublishSize", 12 | "namespace": "AWS/SNS", 13 | "period": Duration { 14 | "amount": 5, 15 | "unit": TimeUnit { 16 | "inMillis": 60000, 17 | "isoLabel": "M", 18 | "label": "minutes", 19 | }, 20 | }, 21 | "region": undefined, 22 | "statistic": "Average", 23 | "unit": undefined, 24 | } 25 | `; 26 | 27 | exports[`snapshot test: metricAverageMessageSizeInBytes 2`] = ` 28 | Metric { 29 | "account": undefined, 30 | "color": undefined, 31 | "dimensions": Object { 32 | "TopicName": "DummyTopicName", 33 | }, 34 | "label": undefined, 35 | "metricName": "PublishSize", 36 | "namespace": "AWS/SNS", 37 | "period": Duration { 38 | "amount": 5, 39 | "unit": TimeUnit { 40 | "inMillis": 60000, 41 | "isoLabel": "M", 42 | "label": "minutes", 43 | }, 44 | }, 45 | "region": undefined, 46 | "statistic": "Average", 47 | "unit": undefined, 48 | } 49 | `; 50 | 51 | exports[`snapshot test: metricNumberOfMessagesDelivered 1`] = ` 52 | Metric { 53 | "account": undefined, 54 | "color": undefined, 55 | "dimensions": Object { 56 | "TopicName": "DummyTopicName", 57 | }, 58 | "label": undefined, 59 | "metricName": "NumberOfNotificationsDelivered", 60 | "namespace": "AWS/SNS", 61 | "period": Duration { 62 | "amount": 5, 63 | "unit": TimeUnit { 64 | "inMillis": 60000, 65 | "isoLabel": "M", 66 | "label": "minutes", 67 | }, 68 | }, 69 | "region": undefined, 70 | "statistic": "Sum", 71 | "unit": undefined, 72 | } 73 | `; 74 | 75 | exports[`snapshot test: metricNumberOfMessagesPublished 1`] = ` 76 | Metric { 77 | "account": undefined, 78 | "color": undefined, 79 | "dimensions": Object { 80 | "TopicName": "DummyTopicName", 81 | }, 82 | "label": undefined, 83 | "metricName": "NumberOfMessagesPublished", 84 | "namespace": "AWS/SNS", 85 | "period": Duration { 86 | "amount": 5, 87 | "unit": TimeUnit { 88 | "inMillis": 60000, 89 | "isoLabel": "M", 90 | "label": "minutes", 91 | }, 92 | }, 93 | "region": undefined, 94 | "statistic": "Sum", 95 | "unit": undefined, 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /test/monitoring/aws/sns/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { SnsMetricFactory } from '../../../../src/monitoring/aws/sns/metrics'; 2 | 3 | const DummyTopicName = 'DummyTopicName'; 4 | 5 | test('snapshot test: metricAverageMessageSizeInBytes', () => { 6 | // GIVEN 7 | const unitToTest = new SnsMetricFactory(); 8 | 9 | // WHEN 10 | const metric = unitToTest.metricAverageMessageSizeInBytes(DummyTopicName); 11 | 12 | // THEN 13 | expect(metric.metricName).toStrictEqual('PublishSize'); 14 | expect(metric.dimensions).toStrictEqual({ TopicName: DummyTopicName }); 15 | expect(metric).toMatchSnapshot(); 16 | }); 17 | 18 | test('snapshot test: metricNumberOfMessagesDelivered', () => { 19 | // GIVEN 20 | const unitToTest = new SnsMetricFactory(); 21 | 22 | // WHEN 23 | const metric = unitToTest.metricNumberOfMessagesDelivered(DummyTopicName); 24 | 25 | // THEN 26 | expect(metric.metricName).toStrictEqual('NumberOfNotificationsDelivered'); 27 | expect(metric.dimensions).toStrictEqual({ TopicName: DummyTopicName }); 28 | expect(metric).toMatchSnapshot(); 29 | }); 30 | 31 | test('snapshot test: metricNumberOfMessagesPublished', () => { 32 | // GIVEN 33 | const unitToTest = new SnsMetricFactory(); 34 | 35 | // WHEN 36 | const metric = unitToTest.metricNumberOfMessagesPublished(DummyTopicName); 37 | 38 | // THEN 39 | expect(metric.metricName).toStrictEqual('NumberOfMessagesPublished'); 40 | expect(metric.dimensions).toStrictEqual({ TopicName: DummyTopicName }); 41 | expect(metric).toMatchSnapshot(); 42 | }); 43 | 44 | test('snapshot test: metricAverageMessageSizeInBytes', () => { 45 | // GIVEN 46 | const unitToTest = new SnsMetricFactory(); 47 | 48 | // WHEN 49 | const metric = unitToTest.metricAverageMessageSizeInBytes(DummyTopicName); 50 | 51 | // THEN 52 | expect(metric.metricName).toStrictEqual('PublishSize'); 53 | expect(metric.dimensions).toStrictEqual({ TopicName: DummyTopicName }); 54 | expect(metric).toMatchSnapshot(); 55 | }); 56 | -------------------------------------------------------------------------------- /test/monitoring/aws/sqs/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricAgeOfOldestMessageInSeconds 1`] = ` 4 | Metric { 5 | "account": undefined, 6 | "color": undefined, 7 | "dimensions": Object { 8 | "QueueName": "DummyQueueName", 9 | }, 10 | "label": undefined, 11 | "metricName": "ApproximateAgeOfOldestMessage", 12 | "namespace": "AWS/SQS", 13 | "period": Duration { 14 | "amount": 5, 15 | "unit": TimeUnit { 16 | "inMillis": 60000, 17 | "isoLabel": "M", 18 | "label": "minutes", 19 | }, 20 | }, 21 | "region": undefined, 22 | "statistic": "Maximum", 23 | "unit": undefined, 24 | } 25 | `; 26 | 27 | exports[`snapshot test: metricApproximateVisibleMessages 1`] = ` 28 | Metric { 29 | "account": undefined, 30 | "color": undefined, 31 | "dimensions": Object { 32 | "QueueName": "DummyQueueName", 33 | }, 34 | "label": undefined, 35 | "metricName": "ApproximateNumberOfMessagesVisible", 36 | "namespace": "AWS/SQS", 37 | "period": Duration { 38 | "amount": 5, 39 | "unit": TimeUnit { 40 | "inMillis": 60000, 41 | "isoLabel": "M", 42 | "label": "minutes", 43 | }, 44 | }, 45 | "region": undefined, 46 | "statistic": "Maximum", 47 | "unit": undefined, 48 | } 49 | `; 50 | 51 | exports[`snapshot test: metricAverageMessageSizeInBytes 1`] = ` 52 | Metric { 53 | "account": undefined, 54 | "color": undefined, 55 | "dimensions": Object { 56 | "QueueName": "DummyQueueName", 57 | }, 58 | "label": undefined, 59 | "metricName": "SentMessageSize", 60 | "namespace": "AWS/SQS", 61 | "period": Duration { 62 | "amount": 5, 63 | "unit": TimeUnit { 64 | "inMillis": 60000, 65 | "isoLabel": "M", 66 | "label": "minutes", 67 | }, 68 | }, 69 | "region": undefined, 70 | "statistic": "Average", 71 | "unit": undefined, 72 | } 73 | `; 74 | 75 | exports[`snapshot test: metricIncomingMessages 1`] = ` 76 | Metric { 77 | "account": undefined, 78 | "color": undefined, 79 | "dimensions": Object { 80 | "QueueName": "DummyQueueName", 81 | }, 82 | "label": undefined, 83 | "metricName": "NumberOfMessagesSent", 84 | "namespace": "AWS/SQS", 85 | "period": Duration { 86 | "amount": 5, 87 | "unit": TimeUnit { 88 | "inMillis": 60000, 89 | "isoLabel": "M", 90 | "label": "minutes", 91 | }, 92 | }, 93 | "region": undefined, 94 | "statistic": "Sum", 95 | "unit": undefined, 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /test/monitoring/aws/sqs/__snapshots__/monitoring.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test 1`] = ` 4 | Array [ 5 | Object { 6 | "height": 2, 7 | "markdown": Object { 8 | "Fn::Join": Array [ 9 | "", 10 | Array [ 11 | "# SQS Queue **", 12 | Object { 13 | "Fn::GetAtt": Array [ 14 | "Queue4A7E3555", 15 | "QueueName", 16 | ], 17 | }, 18 | "** 19 | 20 | [button:Overview](https://", 21 | Object { 22 | "Ref": "AWS::Region", 23 | }, 24 | ".console.aws.amazon.com/sqs/v2/home?region=", 25 | Object { 26 | "Ref": "AWS::Region", 27 | }, 28 | "#/queues/", 29 | Object { 30 | "Ref": "Queue4A7E3555", 31 | }, 32 | ")", 33 | ], 34 | ], 35 | }, 36 | "width": 24, 37 | }, 38 | Object { 39 | "height": 5, 40 | "leftMetrics": Array [ 41 | Object { 42 | "dimensions": Object { 43 | "QueueName": Object { 44 | "Fn::GetAtt": Array [ 45 | "Queue4A7E3555", 46 | "QueueName", 47 | ], 48 | }, 49 | }, 50 | "metricName": "ApproximateNumberOfMessagesVisible", 51 | "namespace": "AWS/SQS", 52 | "period": Object { 53 | "amount": 5, 54 | "unit": Object { 55 | "inMillis": 60000, 56 | "isoLabel": "M", 57 | "label": "minutes", 58 | }, 59 | }, 60 | "statistic": "Maximum", 61 | }, 62 | Object { 63 | "dimensions": Object { 64 | "QueueName": Object { 65 | "Fn::GetAtt": Array [ 66 | "Queue4A7E3555", 67 | "QueueName", 68 | ], 69 | }, 70 | }, 71 | "metricName": "NumberOfMessagesSent", 72 | "namespace": "AWS/SQS", 73 | "period": Object { 74 | "amount": 5, 75 | "unit": Object { 76 | "inMillis": 60000, 77 | "isoLabel": "M", 78 | "label": "minutes", 79 | }, 80 | }, 81 | "statistic": "Sum", 82 | }, 83 | ], 84 | "props": Object { 85 | "height": 5, 86 | "left": Array [ 87 | Object { 88 | "dimensions": Object { 89 | "QueueName": Object { 90 | "Fn::GetAtt": Array [ 91 | "Queue4A7E3555", 92 | "QueueName", 93 | ], 94 | }, 95 | }, 96 | "metricName": "ApproximateNumberOfMessagesVisible", 97 | "namespace": "AWS/SQS", 98 | "period": Object { 99 | "amount": 5, 100 | "unit": Object { 101 | "inMillis": 60000, 102 | "isoLabel": "M", 103 | "label": "minutes", 104 | }, 105 | }, 106 | "statistic": "Maximum", 107 | }, 108 | Object { 109 | "dimensions": Object { 110 | "QueueName": Object { 111 | "Fn::GetAtt": Array [ 112 | "Queue4A7E3555", 113 | "QueueName", 114 | ], 115 | }, 116 | }, 117 | "metricName": "NumberOfMessagesSent", 118 | "namespace": "AWS/SQS", 119 | "period": Object { 120 | "amount": 5, 121 | "unit": Object { 122 | "inMillis": 60000, 123 | "isoLabel": "M", 124 | "label": "minutes", 125 | }, 126 | }, 127 | "statistic": "Sum", 128 | }, 129 | ], 130 | "leftAnnotations": Array [], 131 | "leftYAxis": Object { 132 | "min": 0, 133 | "showUnits": false, 134 | }, 135 | "title": "Message Count", 136 | "width": 8, 137 | }, 138 | "rightMetrics": Array [], 139 | "width": 8, 140 | }, 141 | Object { 142 | "height": 5, 143 | "leftMetrics": Array [ 144 | Object { 145 | "dimensions": Object { 146 | "QueueName": Object { 147 | "Fn::GetAtt": Array [ 148 | "Queue4A7E3555", 149 | "QueueName", 150 | ], 151 | }, 152 | }, 153 | "metricName": "ApproximateAgeOfOldestMessage", 154 | "namespace": "AWS/SQS", 155 | "period": Object { 156 | "amount": 5, 157 | "unit": Object { 158 | "inMillis": 60000, 159 | "isoLabel": "M", 160 | "label": "minutes", 161 | }, 162 | }, 163 | "statistic": "Maximum", 164 | }, 165 | ], 166 | "props": Object { 167 | "height": 5, 168 | "left": Array [ 169 | Object { 170 | "dimensions": Object { 171 | "QueueName": Object { 172 | "Fn::GetAtt": Array [ 173 | "Queue4A7E3555", 174 | "QueueName", 175 | ], 176 | }, 177 | }, 178 | "metricName": "ApproximateAgeOfOldestMessage", 179 | "namespace": "AWS/SQS", 180 | "period": Object { 181 | "amount": 5, 182 | "unit": Object { 183 | "inMillis": 60000, 184 | "isoLabel": "M", 185 | "label": "minutes", 186 | }, 187 | }, 188 | "statistic": "Maximum", 189 | }, 190 | ], 191 | "leftAnnotations": Array [], 192 | "leftYAxis": Object { 193 | "label": "sec", 194 | "min": 0, 195 | "showUnits": false, 196 | }, 197 | "title": "Oldest Message Age", 198 | "width": 8, 199 | }, 200 | "rightMetrics": Array [], 201 | "width": 8, 202 | }, 203 | Object { 204 | "height": 5, 205 | "leftMetrics": Array [ 206 | Object { 207 | "dimensions": Object { 208 | "QueueName": Object { 209 | "Fn::GetAtt": Array [ 210 | "Queue4A7E3555", 211 | "QueueName", 212 | ], 213 | }, 214 | }, 215 | "metricName": "SentMessageSize", 216 | "namespace": "AWS/SQS", 217 | "period": Object { 218 | "amount": 5, 219 | "unit": Object { 220 | "inMillis": 60000, 221 | "isoLabel": "M", 222 | "label": "minutes", 223 | }, 224 | }, 225 | "statistic": "Average", 226 | }, 227 | ], 228 | "props": Object { 229 | "height": 5, 230 | "left": Array [ 231 | Object { 232 | "dimensions": Object { 233 | "QueueName": Object { 234 | "Fn::GetAtt": Array [ 235 | "Queue4A7E3555", 236 | "QueueName", 237 | ], 238 | }, 239 | }, 240 | "metricName": "SentMessageSize", 241 | "namespace": "AWS/SQS", 242 | "period": Object { 243 | "amount": 5, 244 | "unit": Object { 245 | "inMillis": 60000, 246 | "isoLabel": "M", 247 | "label": "minutes", 248 | }, 249 | }, 250 | "statistic": "Average", 251 | }, 252 | ], 253 | "leftYAxis": Object { 254 | "label": "bytes", 255 | "min": 0, 256 | "showUnits": false, 257 | }, 258 | "title": "Message Size", 259 | "width": 8, 260 | }, 261 | "rightMetrics": Array [], 262 | "width": 8, 263 | }, 264 | ] 265 | `; 266 | -------------------------------------------------------------------------------- /test/monitoring/aws/sqs/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { SqsMetricFactory } from '../../../../src/monitoring/aws/sqs/metrics'; 2 | 3 | const DummyQueueName = 'DummyQueueName'; 4 | 5 | test('snapshot test: metricAverageMessageSizeInBytes', () => { 6 | // GIVEN 7 | const unitToTest = new SqsMetricFactory(); 8 | 9 | // WHEN 10 | const metric = unitToTest.metricAverageMessageSizeInBytes(DummyQueueName); 11 | 12 | // THEN 13 | expect(metric.metricName).toStrictEqual('SentMessageSize'); 14 | expect(metric.dimensions).toStrictEqual({ QueueName: DummyQueueName }); 15 | expect(metric).toMatchSnapshot(); 16 | }); 17 | 18 | test('snapshot test: metricAgeOfOldestMessageInSeconds', () => { 19 | // GIVEN 20 | const unitToTest = new SqsMetricFactory(); 21 | 22 | // WHEN 23 | const metric = unitToTest.metricAgeOfOldestMessageInSeconds(DummyQueueName); 24 | 25 | // THEN 26 | expect(metric.metricName).toStrictEqual('ApproximateAgeOfOldestMessage'); 27 | expect(metric.dimensions).toStrictEqual({ QueueName: DummyQueueName }); 28 | expect(metric).toMatchSnapshot(); 29 | }); 30 | 31 | test('snapshot test: metricIncomingMessages', () => { 32 | // GIVEN 33 | const unitToTest = new SqsMetricFactory(); 34 | 35 | // WHEN 36 | const metric = unitToTest.metricIncomingMessages(DummyQueueName); 37 | 38 | // THEN 39 | expect(metric.metricName).toStrictEqual('NumberOfMessagesSent'); 40 | expect(metric.dimensions).toStrictEqual({ QueueName: DummyQueueName }); 41 | expect(metric).toMatchSnapshot(); 42 | }); 43 | 44 | test('snapshot test: metricApproximateVisibleMessages', () => { 45 | // GIVEN 46 | const unitToTest = new SqsMetricFactory(); 47 | 48 | // WHEN 49 | const metric = unitToTest.metricApproximateVisibleMessages(DummyQueueName); 50 | 51 | // THEN 52 | expect(metric.metricName).toStrictEqual('ApproximateNumberOfMessagesVisible'); 53 | expect(metric.dimensions).toStrictEqual({ QueueName: DummyQueueName }); 54 | expect(metric).toMatchSnapshot(); 55 | }); 56 | -------------------------------------------------------------------------------- /test/monitoring/aws/sqs/monitoring.test.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from 'aws-cdk-lib'; 2 | import { Queue } from 'aws-cdk-lib/aws-sqs'; 3 | import { SqsMonitoring } from '../../../../src/monitoring/aws/sqs/monitoring'; 4 | 5 | test('snapshot test', () => { 6 | // GIVEN 7 | const resources = createTestResources(); 8 | const unitToTest = new SqsMonitoring({ queue: resources.queue }); 9 | 10 | // WHEN 11 | const widgets = unitToTest.getWidgets(); 12 | const resolvedWidgets = resources.stack.resolve(widgets); 13 | 14 | // THEN 15 | expect(resolvedWidgets).toMatchSnapshot(); 16 | }); 17 | 18 | function createTestResources() { 19 | const stack = new Stack(); 20 | const queue = new Queue(stack, 'Queue', { queueName: 'DummyQueueName' }); 21 | return { stack, queue }; 22 | } 23 | -------------------------------------------------------------------------------- /test/monitoring/aws/state-machine/__snapshots__/metrics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot test: metricExecutions 1`] = ` 4 | Object { 5 | "aborted": Metric { 6 | "account": undefined, 7 | "color": undefined, 8 | "dimensions": Object { 9 | "StateMachineArn": "dummy-state-machine-arn", 10 | }, 11 | "label": "Aborted Executions", 12 | "metricName": "ExecutionsAborted", 13 | "namespace": "AWS/States", 14 | "period": Duration { 15 | "amount": 1, 16 | "unit": TimeUnit { 17 | "inMillis": 60000, 18 | "isoLabel": "M", 19 | "label": "minutes", 20 | }, 21 | }, 22 | "region": undefined, 23 | "statistic": "Sum", 24 | "unit": undefined, 25 | }, 26 | "failed": Metric { 27 | "account": undefined, 28 | "color": undefined, 29 | "dimensions": Object { 30 | "StateMachineArn": "dummy-state-machine-arn", 31 | }, 32 | "label": "Failed Executions", 33 | "metricName": "ExecutionsFailed", 34 | "namespace": "AWS/States", 35 | "period": Duration { 36 | "amount": 1, 37 | "unit": TimeUnit { 38 | "inMillis": 60000, 39 | "isoLabel": "M", 40 | "label": "minutes", 41 | }, 42 | }, 43 | "region": undefined, 44 | "statistic": "Sum", 45 | "unit": undefined, 46 | }, 47 | "succeeded": Metric { 48 | "account": undefined, 49 | "color": undefined, 50 | "dimensions": Object { 51 | "StateMachineArn": "dummy-state-machine-arn", 52 | }, 53 | "label": "Executions Succeeded", 54 | "metricName": "ExecutionsSucceeded", 55 | "namespace": "AWS/States", 56 | "period": Duration { 57 | "amount": 1, 58 | "unit": TimeUnit { 59 | "inMillis": 60000, 60 | "isoLabel": "M", 61 | "label": "minutes", 62 | }, 63 | }, 64 | "region": undefined, 65 | "statistic": "Sum", 66 | "unit": undefined, 67 | }, 68 | "throttled": Metric { 69 | "account": undefined, 70 | "color": undefined, 71 | "dimensions": Object { 72 | "StateMachineArn": "dummy-state-machine-arn", 73 | }, 74 | "label": "Executions Throttled", 75 | "metricName": "ExecutionThrottled", 76 | "namespace": "AWS/States", 77 | "period": Duration { 78 | "amount": 1, 79 | "unit": TimeUnit { 80 | "inMillis": 60000, 81 | "isoLabel": "M", 82 | "label": "minutes", 83 | }, 84 | }, 85 | "region": undefined, 86 | "statistic": "Sum", 87 | "unit": undefined, 88 | }, 89 | "timedOut": Metric { 90 | "account": undefined, 91 | "color": undefined, 92 | "dimensions": Object { 93 | "StateMachineArn": "dummy-state-machine-arn", 94 | }, 95 | "label": "Executions TimedOut", 96 | "metricName": "ExecutionsTimedOut", 97 | "namespace": "AWS/States", 98 | "period": Duration { 99 | "amount": 1, 100 | "unit": TimeUnit { 101 | "inMillis": 60000, 102 | "isoLabel": "M", 103 | "label": "minutes", 104 | }, 105 | }, 106 | "region": undefined, 107 | "statistic": "Sum", 108 | "unit": undefined, 109 | }, 110 | "total": Metric { 111 | "account": undefined, 112 | "color": undefined, 113 | "dimensions": Object { 114 | "StateMachineArn": "dummy-state-machine-arn", 115 | }, 116 | "label": "Total", 117 | "metricName": "ExecutionsStarted", 118 | "namespace": "AWS/States", 119 | "period": Duration { 120 | "amount": 1, 121 | "unit": TimeUnit { 122 | "inMillis": 60000, 123 | "isoLabel": "M", 124 | "label": "minutes", 125 | }, 126 | }, 127 | "region": undefined, 128 | "statistic": "Sum", 129 | "unit": undefined, 130 | }, 131 | } 132 | `; 133 | -------------------------------------------------------------------------------- /test/monitoring/aws/state-machine/metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { StateMachineMetricFactory } from '../../../../src/monitoring/aws/state-machine/metrics'; 2 | 3 | const dummyStateMachineArn = 'dummy-state-machine-arn'; 4 | 5 | test('snapshot test: metricExecutions', () => { 6 | // GIVEN 7 | const unitToTest = new StateMachineMetricFactory(); 8 | 9 | // WHEN 10 | const metric = unitToTest.metricExecutions(dummyStateMachineArn); 11 | 12 | // THEN 13 | expect(metric.failed.metricName).toStrictEqual('ExecutionsFailed'); 14 | Object.values(metric).forEach(eachMetric => { 15 | expect(eachMetric.dimensions).toStrictEqual({ StateMachineArn: dummyStateMachineArn }); 16 | }); 17 | expect(metric).toMatchSnapshot(); 18 | }); -------------------------------------------------------------------------------- /test/watchful.test.ts: -------------------------------------------------------------------------------- 1 | import { Resource, ResourceProps, Stack } from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; 4 | import * as ddb from 'aws-cdk-lib/aws-dynamodb'; 5 | import { Construct } from 'constructs'; 6 | import { Watchful } from '../src'; 7 | 8 | test('creates an empty dashboard', () => { 9 | // GIVEN 10 | const stack = new Stack(); 11 | 12 | // WHEN 13 | new Watchful(stack, 'watchful'); 14 | 15 | // THEN 16 | const template = Template.fromStack(stack); 17 | expect(template.findResources('AWS::CloudWatch::Dashboard')); 18 | }); 19 | 20 | test('alarmActionArns can be used to specify a list of custom alarm actions', () => { 21 | // GIVEN 22 | const stack = new Stack(); 23 | const table = new ddb.Table(stack, 'Table', { 24 | partitionKey: { name: 'ID', type: ddb.AttributeType.STRING }, 25 | }); 26 | 27 | // WHEN 28 | const wf = new Watchful(stack, 'watchful', { 29 | alarmActionArns: [ 30 | 'arn:of:custom:alarm:action', 31 | 'arn:2', 32 | ], 33 | }); 34 | 35 | wf.watchDynamoTable('MyTable', table); 36 | 37 | // THEN 38 | const template = Template.fromStack(stack); 39 | expect( 40 | template.hasResourceProperties('AWS::CloudWatch::Alarm', { 41 | AlarmActions: ['arn:of:custom:alarm:action', 'arn:2'], 42 | }), 43 | ); 44 | }); 45 | 46 | test('alarmActions can be used to specify a list of custom alarm actions', () => { 47 | // GIVEN 48 | const stack = new Stack(); 49 | const table = new ddb.Table(stack, 'Table', { 50 | partitionKey: { name: 'ID', type: ddb.AttributeType.STRING }, 51 | }); 52 | 53 | // WHEN 54 | const wf = new Watchful(stack, 'watchful', { 55 | alarmActions: [ 56 | { bind: (scope, alarm) => ({ alarmActionArn: `arn:phony:${scope.node.path}:${alarm.node.path}` }) }, 57 | ], 58 | }); 59 | 60 | wf.watchDynamoTable('MyTable', table); 61 | 62 | // THEN 63 | const template = Template.fromStack(stack); 64 | expect( 65 | template.hasResourceProperties('AWS::CloudWatch::Alarm', { 66 | AlarmActions: [ 67 | 'arn:phony:Default/watchful/Table/CapacityAlarm:write:Default/watchful/Table/CapacityAlarm:write', 68 | ], 69 | }), 70 | ); 71 | }); 72 | 73 | test('alarmActions AND alarmActionArns can be used to specify a list of custom alarm actions', () => { 74 | // GIVEN 75 | const stack = new Stack(); 76 | const table = new ddb.Table(stack, 'Table', { 77 | partitionKey: { name: 'ID', type: ddb.AttributeType.STRING }, 78 | }); 79 | 80 | // WHEN 81 | const wf = new Watchful(stack, 'watchful', { 82 | alarmActionArns: [ 83 | 'arn:of:custom:alarm:action', 84 | 'arn:2', 85 | ], 86 | alarmActions: [ 87 | { bind: (scope, alarm) => ({ alarmActionArn: `arn:phony:${scope.node.path}:${alarm.node.path}` }) }, 88 | ], 89 | }); 90 | 91 | wf.watchDynamoTable('MyTable', table); 92 | 93 | // THEN 94 | const template = Template.fromStack(stack); 95 | expect( 96 | template.hasResourceProperties('AWS::CloudWatch::Alarm', { 97 | AlarmActions: [ 98 | 'arn:of:custom:alarm:action', 99 | 'arn:2', 100 | 'arn:phony:Default/watchful/Table/CapacityAlarm:write:Default/watchful/Table/CapacityAlarm:write', 101 | ], 102 | }), 103 | ); 104 | }); 105 | 106 | test('composite alarms can be created from other alarms', ()=> { 107 | // GIVEN 108 | const stack = new Stack(); 109 | const table = new ddb.Table(stack, 'Table', { 110 | partitionKey: { name: 'ID', type: ddb.AttributeType.STRING }, 111 | }); 112 | 113 | // WHEN 114 | const wf = new Watchful(stack, 'watchful', { 115 | alarmActionArns: [ 116 | 'arn:of:custom:alarm:action', 117 | 'arn:2', 118 | ], 119 | }); 120 | 121 | const alarm1 = new cloudwatch.Alarm(stack, 'Alarm1', { 122 | evaluationPeriods: 1, 123 | metric: table.metricConsumedReadCapacityUnits(), 124 | threshold: 100, 125 | }); 126 | 127 | const alarm2 = new cloudwatch.Alarm(stack, 'Alarm2', { 128 | evaluationPeriods: 1, 129 | metric: table.metricConsumedWriteCapacityUnits(), 130 | threshold: 100, 131 | }); 132 | 133 | const compositeAlarm = new cloudwatch.CompositeAlarm(stack, 'CompositeAlarm', { 134 | alarmRule: cloudwatch.AlarmRule.allOf(alarm1, alarm2), 135 | }); 136 | 137 | wf.addAlarm(compositeAlarm); 138 | 139 | // THEN 140 | const template = Template.fromStack(stack); 141 | expect( 142 | template.hasResourceProperties('AWS::CloudWatch::CompositeAlarm', { 143 | AlarmActions: ['arn:of:custom:alarm:action', 'arn:2'], 144 | }), 145 | ); 146 | }); 147 | 148 | test('alarms that do not implement addAlarmAction will be wrapped in CompositeAlarm', () => { 149 | // GIVEN 150 | const stack = new Stack(); 151 | 152 | interface MyAlarmProps extends ResourceProps { 153 | alarmArn: string; 154 | alarmName: string; 155 | } 156 | class MyAlarm extends Resource implements cloudwatch.IAlarm { 157 | public alarmArn: string; 158 | public alarmName: string; 159 | 160 | constructor(scope: Construct, id: string, props: MyAlarmProps) { 161 | super(scope, id, props); 162 | this.alarmArn = props.alarmArn; 163 | this.alarmName = props.alarmName; 164 | } 165 | public renderAlarmRule(): string { 166 | return `ALARM("${this.alarmArn}")`; 167 | } 168 | } 169 | 170 | // WHEN 171 | const wf = new Watchful(stack, 'watchful', { 172 | alarmActionArns: [ 173 | 'arn:of:custom:alarm:action', 174 | 'arn:2', 175 | ], 176 | }); 177 | 178 | const alarmArn = 'arn:of:custom:external:my:alarm'; 179 | const alarm1 = new MyAlarm(stack, 'MyAlarm', { alarmArn, alarmName: 'MyAlarm' }); 180 | 181 | wf.addAlarm(alarm1); 182 | 183 | // THEN 184 | const template = Template.fromStack(stack); 185 | expect( 186 | template.hasResourceProperties('AWS::CloudWatch::CompositeAlarm', { 187 | AlarmActions: ['arn:of:custom:alarm:action', 'arn:2'], 188 | AlarmRule: `ALARM(\"${alarmArn}\")`, 189 | }), 190 | ); 191 | }); 192 | -------------------------------------------------------------------------------- /test/widget/section.test.ts: -------------------------------------------------------------------------------- 1 | import { SectionWidget } from '../../src/widget/section'; 2 | 3 | test('correct size and content generated with minimum properties', () => { 4 | // GIVEN 5 | const widget = new SectionWidget({ 6 | titleMarkdown: 'Dummy Title', 7 | }); 8 | 9 | // WHEN 10 | const code = widget.toJson(); 11 | 12 | // THEN 13 | expect(code).toHaveLength(1); 14 | expect(code[0].width).toStrictEqual(24); 15 | expect(code[0].height).toStrictEqual(2); 16 | expect(code[0].properties.markdown).toStrictEqual('# Dummy Title'); 17 | }); 18 | 19 | test('correct size and content generated with all properties', () => { 20 | // GIVEN 21 | const widget = new SectionWidget({ 22 | titleMarkdown: 'Dummy Title', 23 | width: 12, 24 | height: 3, 25 | titleLevel: 3, 26 | descriptionMarkdown: 'Dummy Description in *Markdown*', 27 | quicklinks: [ 28 | { url: 'https://github.com/awslabs/cdk-watchful', title: 'GitHub Repository' }, 29 | { url: 'https://github.com/awslabs', title: 'AWS Labs Homepage' }, 30 | ], 31 | }); 32 | 33 | // WHEN 34 | const code = widget.toJson(); 35 | 36 | // THEN 37 | expect(code).toHaveLength(1); 38 | expect(code[0].width).toStrictEqual(12); 39 | expect(code[0].height).toStrictEqual(3); 40 | expect(code[0].properties.markdown).toStrictEqual( 41 | '### Dummy Title\n\n' + 42 | 'Dummy Description in *Markdown*\n\n' + 43 | '[button:GitHub Repository](https://github.com/awslabs/cdk-watchful) [button:AWS Labs Homepage](https://github.com/awslabs)', 44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "rootDir": "src", 5 | "declarationMap": false, 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "alwaysStrict": true, 9 | "declaration": true, 10 | "experimentalDecorators": true, 11 | "incremental": true, 12 | "lib": [ 13 | "es2020" 14 | ], 15 | "module": "commonjs", 16 | "noEmitOnError": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "strictNullChecks": true, 27 | "strictPropertyInitialization": true, 28 | "stripInternal": false, 29 | "target": "es2020", 30 | "composite": false, 31 | "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo" 32 | }, 33 | "include": [ 34 | "src/**/*.ts" 35 | ], 36 | "exclude": [ 37 | "node_modules", 38 | "/home/runner/work/cdk-watchful/cdk-watchful/lib/.types-compat" 39 | ], 40 | "_generated_by_jsii_": "Generated by jsii - safe to delete, and ideally should be in .gitignore" 41 | } --------------------------------------------------------------------------------