diff --git a/cloudformation/__snapshots__/dashboard.test.js.snap b/cloudformation/__snapshots__/dashboard.test.js.snap new file mode 100644 index 0000000..6bdd678 --- /dev/null +++ b/cloudformation/__snapshots__/dashboard.test.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dashboard returns cloudformation dashboard body 1`] = ` +Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"widgets\\":[{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":1,\\"width\\":6,\\"height\\":3,\\"properties\\":{\\"view\\":\\"singleValue\\",\\"metrics\\":[[\\"\${self:custom.cloudWatchNamespace}\\",\\"BuyPrice\\",\\"CryptoCurrency\\",\\"\${self:provider.environment.PREFERRED_CRYPTO_CURRENCY}\\",\\"Stage\\",\\"\${self:custom.stage}\\",\\"LocalCurrency\\",\\"\${self:provider.environment.PREFERRED_LOCAL_CURRENCY}\\",{\\"label\\":\\"Buy\\"}],[\\".\\",\\"SellPrice\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"label\\":\\"Sell\\"}]],\\"region\\":\\"\${self:provider.region}\\",\\"title\\":\\"Current price in \${self:provider.environment.PREFERRED_LOCAL_CURRENCY}\\"}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":4,\\"width\\":24,\\"height\\":9,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"stacked\\":false,\\"metrics\\":[[\\"\${self:custom.cloudWatchNamespace}\\",\\"BuyPrice\\",\\"CryptoCurrency\\",\\"\${self:provider.environment.PREFERRED_CRYPTO_CURRENCY}\\",\\"Stage\\",\\"\${self:custom.stage}\\",\\"LocalCurrency\\",\\"\${self:provider.environment.PREFERRED_LOCAL_CURRENCY}\\",{\\"color\\":\\"#aec7e8\\",\\"label\\":\\"Buy actual\\"}],[\\".\\",\\"SellPrice\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"color\\":\\"#ffbb78\\",\\"label\\":\\"Sell actual\\"}],[\\".\\",\\"BuyPrice\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"period\\":3600,\\"color\\":\\"#1f77b4\\",\\"label\\":\\"Buy hourly average\\"}],[\\".\\",\\"SellPrice\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"period\\":3600,\\"color\\":\\"#ff7f0e\\",\\"label\\":\\"Sell hourly average\\"}],[\\".\\",\\"BuyPrice\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"period\\":86400,\\"label\\":\\"Buy daily average\\",\\"color\\":\\"#ff9896\\"}],[\\".\\",\\"SellPrice\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",\\".\\",{\\"period\\":86400,\\"label\\":\\"Sell daily average\\",\\"color\\":\\"#98df8a\\"}]],\\"region\\":\\"\${self:provider.region}\\",\\"title\\":\\"Buy / Sell \${self:provider.environment.PREFERRED_CRYPTO_CURRENCY}-\${self:provider.environment.PREFERRED_LOCAL_CURRENCY}\\"}},{\\"type\\":\\"metric\\",\\"x\\":0,\\"y\\":13,\\"width\\":6,\\"height\\":6,\\"properties\\":{\\"title\\":\\"Low Buy Price\\",\\"annotations\\":{\\"alarms\\":[\\"arn:aws:cloudwatch:\${self:provider.region}:", + Object { + "Ref": "AWS::AccountId", + }, + ":alarm:", + Object { + "Ref": "LowBuyPriceAlarm", + }, + "\\"]},\\"view\\":\\"timeSeries\\",\\"stacked\\":false}},{\\"type\\":\\"metric\\",\\"x\\":6,\\"y\\":13,\\"width\\":6,\\"height\\":6,\\"properties\\":{\\"title\\":\\"Low Sell Price\\",\\"annotations\\":{\\"alarms\\":[\\"arn:aws:cloudwatch:\${self:provider.region}:", + Object { + "Ref": "AWS::AccountId", + }, + ":alarm:", + Object { + "Ref": "LowSellPriceAlarm", + }, + "\\"]},\\"view\\":\\"timeSeries\\",\\"stacked\\":false}},{\\"type\\":\\"metric\\",\\"x\\":12,\\"y\\":13,\\"width\\":6,\\"height\\":6,\\"properties\\":{\\"title\\":\\"High Buy Price\\",\\"annotations\\":{\\"alarms\\":[\\"arn:aws:cloudwatch:\${self:provider.region}:", + Object { + "Ref": "AWS::AccountId", + }, + ":alarm:", + Object { + "Ref": "HighBuyPriceAlarm", + }, + "\\"]},\\"view\\":\\"timeSeries\\",\\"stacked\\":false}},{\\"type\\":\\"metric\\",\\"x\\":18,\\"y\\":13,\\"width\\":6,\\"height\\":6,\\"properties\\":{\\"title\\":\\"High Sell Price\\",\\"annotations\\":{\\"alarms\\":[\\"arn:aws:cloudwatch:\${self:provider.region}:", + Object { + "Ref": "AWS::AccountId", + }, + ":alarm:", + Object { + "Ref": "HighSellPriceAlarm", + }, + "\\"]},\\"view\\":\\"timeSeries\\",\\"stacked\\":false}}]}", + ], + ], +} +`; diff --git a/cloudformation/dashboard.js b/cloudformation/dashboard.js index 2e68f40..473acb5 100644 --- a/cloudformation/dashboard.js +++ b/cloudformation/dashboard.js @@ -1,20 +1,18 @@ -/* eslint no-template-curly-in-string: "off" */ +/* eslint-disable no-template-curly-in-string */ 'use strict'; +const label = (title, props) => ['.', title, '.', '.', '.', '.', '.', '.', props]; +const alarmArn = cfAlarmResource => [ + 'arn:aws:cloudwatch:${self:provider.region}', + '-SPLIT-Ref:AWS::AccountId-SPLIT-', + 'alarm', + `-SPLIT-Ref:${cfAlarmResource}-SPLIT-`, +].join(':'); + module.exports.dashboard = () => { - let dashboardTemplate = JSON.stringify({ + const json = JSON.stringify({ widgets: [ - { - type: 'text', - x: 0, - y: 0, - width: 24, - height: 1, - properties: { - markdown: '## ${self:service}', - }, - }, { type: 'metric', x: 0, @@ -25,7 +23,7 @@ module.exports.dashboard = () => { view: 'singleValue', metrics: [ [ - '${self:service}', + '${self:custom.cloudWatchNamespace}', 'BuyPrice', 'CryptoCurrency', '${self:provider.environment.PREFERRED_CRYPTO_CURRENCY}', @@ -37,19 +35,9 @@ module.exports.dashboard = () => { label: 'Buy', }, ], - [ - '.', - 'SellPrice', - '.', - '.', - '.', - '.', - '.', - '.', - { - label: 'Sell', - }, - ], + label('SellPrice', { + label: 'Sell', + }), ], region: '${self:provider.region}', title: 'Current price in ${self:provider.environment.PREFERRED_LOCAL_CURRENCY}', @@ -66,7 +54,7 @@ module.exports.dashboard = () => { stacked: false, metrics: [ [ - '${self:service}', + '${self:custom.cloudWatchNamespace}', 'BuyPrice', 'CryptoCurrency', '${self:provider.environment.PREFERRED_CRYPTO_CURRENCY}', @@ -79,80 +67,30 @@ module.exports.dashboard = () => { label: 'Buy actual', }, ], - [ - '.', - 'SellPrice', - '.', - '.', - '.', - '.', - '.', - '.', - { - color: '#ffbb78', - label: 'Sell actual', - }, - ], - [ - '.', - 'BuyPrice', - '.', - '.', - '.', - '.', - '.', - '.', - { - period: 3600, - color: '#1f77b4', - label: 'Buy hourly average', - }, - ], - [ - '.', - 'SellPrice', - '.', - '.', - '.', - '.', - '.', - '.', - { - period: 3600, - color: '#ff7f0e', - label: 'Sell hourly average', - }, - ], - [ - '.', - 'BuyPrice', - '.', - '.', - '.', - '.', - '.', - '.', - { - period: 86400, - label: 'Buy daily average', - color: '#ff9896', - }, - ], - [ - '.', - 'SellPrice', - '.', - '.', - '.', - '.', - '.', - '.', - { - period: 86400, - label: 'Sell daily average', - color: '#98df8a', - }, - ], + label('SellPrice', { + color: '#ffbb78', + label: 'Sell actual', + }), + label('BuyPrice', { + period: 3600, + color: '#1f77b4', + label: 'Buy hourly average', + }), + label('SellPrice', { + period: 3600, + color: '#ff7f0e', + label: 'Sell hourly average', + }), + label('BuyPrice', { + period: 86400, + label: 'Buy daily average', + color: '#ff9896', + }), + label('SellPrice', { + period: 86400, + label: 'Sell daily average', + color: '#98df8a', + }), ], region: '${self:provider.region}', title: 'Buy / Sell ${self:provider.environment.PREFERRED_CRYPTO_CURRENCY}-${self:provider.environment.PREFERRED_LOCAL_CURRENCY}', @@ -167,9 +105,7 @@ module.exports.dashboard = () => { properties: { title: 'Low Buy Price', annotations: { - alarms: [ - 'arn:aws:cloudwatch:${self:provider.region}:JOINREF:AWS::AccountIdJOIN:alarm:JOINREF:LowBuyPriceAlarmJOIN', - ], + alarms: [alarmArn('LowBuyPriceAlarm')], }, view: 'timeSeries', stacked: false, @@ -184,9 +120,7 @@ module.exports.dashboard = () => { properties: { title: 'Low Sell Price', annotations: { - alarms: [ - 'arn:aws:cloudwatch:${self:provider.region}:JOINREF:AWS::AccountIdJOIN:alarm:JOINREF:LowSellPriceAlarmJOIN', - ], + alarms: [alarmArn('LowSellPriceAlarm')], }, view: 'timeSeries', stacked: false, @@ -201,9 +135,7 @@ module.exports.dashboard = () => { properties: { title: 'High Buy Price', annotations: { - alarms: [ - 'arn:aws:cloudwatch:${self:provider.region}:JOINREF:AWS::AccountIdJOIN:alarm:JOINREF:HighBuyPriceAlarmJOIN', - ], + alarms: [alarmArn('HighBuyPriceAlarm')], }, view: 'timeSeries', stacked: false, @@ -218,9 +150,7 @@ module.exports.dashboard = () => { properties: { title: 'High Sell Price', annotations: { - alarms: [ - 'arn:aws:cloudwatch:${self:provider.region}:JOINREF:AWS::AccountIdJOIN:alarm:JOINREF:HighSellPriceAlarmJOIN', - ], + alarms: [alarmArn('HighSellPriceAlarm')], }, view: 'timeSeries', stacked: false, @@ -229,20 +159,19 @@ module.exports.dashboard = () => { ], }); - // because of variable collisions with serverless, we can't use fn:sub - // so we need to split the template to use fn:join to ref the alarms - dashboardTemplate = dashboardTemplate.split('JOIN'); - const lines = dashboardTemplate.map((line) => { - if (line.indexOf('REF:') > -1) { - return { Ref: line.replace('REF:', '') }; - } - return line; - }); return { - 'Fn::Join': - [ + 'Fn::Join': [ '', - lines, + /* + Because of variable collisions with serverless, we can't use Fn::Sub so we need to split the + template to use Fn::Join to Ref the alarms. + */ + json.split('-SPLIT-').map((line) => { + if (line.includes('Ref:')) { + return { Ref: line.replace('Ref:', '') }; + } + return line; + }) // eslint-disable-line ], }; }; diff --git a/cloudformation/dashboard.test.js b/cloudformation/dashboard.test.js new file mode 100644 index 0000000..0f68e6b --- /dev/null +++ b/cloudformation/dashboard.test.js @@ -0,0 +1,7 @@ +import { dashboard } from './dashboard'; + +describe('Dashboard', () => { + test('returns cloudformation dashboard body', () => { + expect(dashboard()).toMatchSnapshot(); + }); +}); diff --git a/cloudformation/template.yml b/cloudformation/template.yml index cd0219a..97d1ce0 100644 --- a/cloudformation/template.yml +++ b/cloudformation/template.yml @@ -41,7 +41,7 @@ Resources: Value: ${self:provider.environment.STAGE} EvaluationPeriods: 1 MetricName: BuyPrice - Namespace: ${self:service} + Namespace: ${self:custom.cloudWatchNamespace} Period: 60 Statistic: Average Threshold: ${self:provider.environment.LOW_BUY_PRICE_THRESHOLD} @@ -63,7 +63,7 @@ Resources: Value: ${self:provider.environment.STAGE} EvaluationPeriods: 1 MetricName: BuyPrice - Namespace: ${self:service} + Namespace: ${self:custom.cloudWatchNamespace} Period: 300 Statistic: Average Threshold: ${self:provider.environment.HIGH_BUY_PRICE_THRESHOLD} @@ -85,7 +85,7 @@ Resources: Value: ${self:provider.environment.STAGE} EvaluationPeriods: 1 MetricName: SellPrice - Namespace: ${self:service} + Namespace: ${self:custom.cloudWatchNamespace} Period: 300 Statistic: Average Threshold: ${self:provider.environment.LOW_SELL_PRICE_THRESHOLD} @@ -107,12 +107,13 @@ Resources: Value: ${self:provider.environment.STAGE} EvaluationPeriods: 1 MetricName: SellPrice - Namespace: ${self:service} + Namespace: ${self:custom.cloudWatchNamespace} Period: 60 Statistic: Average Threshold: ${self:provider.environment.HIGH_SELL_PRICE_THRESHOLD} Unit: Count + # CloudWatch Dashboard displaying custom metrics and alarm states Dashboard: Type: AWS::CloudWatch::Dashboard Properties: diff --git a/lib/cloudWatch.js b/lib/cloudWatch.js index ed20e95..2f3877e 100644 --- a/lib/cloudWatch.js +++ b/lib/cloudWatch.js @@ -20,7 +20,7 @@ export default class CloudWatch { constructor() { this.cloudwatch = new AWS.CloudWatch({ region: process.env.REGION || 'eu-central-1' }); - this.namespace = process.env.SERVICE_NAME || 'coinboss'; + this.namespace = process.env.CLOUDWATCH_NAMESPACE || 'Coinboss'; } putPriceMetric(args) { diff --git a/serverless.yml b/serverless.yml index 2daf218..7e30f29 100644 --- a/serverless.yml +++ b/serverless.yml @@ -5,6 +5,7 @@ plugins: - serverless-webpack custom: + cloudWatchNamespace: Coinboss highBuyPriceTopicName: ${self:service}-high-buy-price-${self:custom.stage} lowBuyPriceTopicName: ${self:service}-low-buy-price-${self:custom.stage} highSellPriceTopicName: ${self:service}-high-sell-price-${self:custom.stage} @@ -31,6 +32,7 @@ provider: BUY_AMOUNT: ${env:BUY_AMOUNT} SELL_AMOUNT: ${env:SELL_AMOUNT} # Alarm settings + CLOUDWATCH_NAMESPACE: ${self:custom.cloudWatchNamespace} LOW_BUY_PRICE_THRESHOLD: ${env:LOW_BUY_PRICE_THRESHOLD} HIGH_BUY_PRICE_THRESHOLD: ${env:HIGH_BUY_PRICE_THRESHOLD} LOW_SELL_PRICE_THRESHOLD: ${env:LOW_SELL_PRICE_THRESHOLD}