Skip to content

Commit

Permalink
Add alarms and dashboard widgets for AppSync
Browse files Browse the repository at this point in the history
  • Loading branch information
direnakkoc committed Nov 25, 2022
1 parent a4e9dcc commit 9396cf3
Show file tree
Hide file tree
Showing 11 changed files with 823 additions and 65 deletions.
14 changes: 7 additions & 7 deletions core/alarms-appsync.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ module.exports = function appSyncAlarms (appSyncAlarmConfig, context) {
const appSyncResources = cfTemplate.getResourcesByType(
'AWS::AppSync::GraphQLApi'
)
// When I use AWS::DynamoDB::Table it creates the alarms it only doesn't retrieve appSync
console.log(appSyncResources)

for (const [appSyncResourceName, appSyncResource] of Object.entries(appSyncResources)) {
const alarms = []
Expand All @@ -48,22 +46,24 @@ module.exports = function appSyncAlarms (appSyncAlarmConfig, context) {
function createAppSyncAlarm (
alarmName,
alarmDescription,
graphQLAPIId,
appSyncResourceName,
comparisonOperator,
threshold,
metricName,
statistic,
period,
extendedStatistic,
evaluationPeriods,
treatMissingData
) {
// const graphQLAPIId = { 'Fn::GetAtt': [appSyncResourceName, 'GraphQLAPIId'] }
const graphQLAPIId = { 'Fn::GetAtt': [appSyncResourceName, 'ApiId'] }
const metricProperties = {
Dimensions: [{ Name: 'GraphQLAPIId', Value: graphQLAPIId }],
MetricName: metricName,
Namespace: 'AWS/AppSync',
Period: period,
Statistic: statistic
Statistic: statistic,
ExtendedStatistic: extendedStatistic
}

return {
Expand All @@ -90,7 +90,7 @@ module.exports = function appSyncAlarms (appSyncAlarmConfig, context) {
resource: createAppSyncAlarm(
`AppSync5XXErrorAlarm_${graphQLName}`,
`AppSync 5XX Error ${getStatisticName(config)} for ${graphQLName} breaches ${threshold}`,
graphQLName,
appSyncResourceName,
config.ComparisonOperator,
threshold,
'5XXError',
Expand All @@ -111,7 +111,7 @@ module.exports = function appSyncAlarms (appSyncAlarmConfig, context) {
resource: createAppSyncAlarm(
`AppSyncLatencyAlarm_${graphQLName}`,
`AppSync Latency ${getStatisticName(config)} for ${graphQLName} breaches ${threshold}`,
graphQLName,
appSyncResourceName,
config.ComparisonOperator,
threshold,
'Latency',
Expand Down
2 changes: 1 addition & 1 deletion core/config-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const supportedWidgets = {
Events: ['FailedInvocations', 'ThrottledRules', 'Invocations'],
ApplicationELB: ['HTTPCode_ELB_5XX_Count', 'RejectedConnectionCount'],
ApplicationELBTarget: ['HTTPCode_Target_5XX_Count', 'UnHealthyHostCount', 'LambdaInternalError', 'LambdaUserError'],
AppSync: ['5XXError', 'Latency']
AppSync: ['5XXError', '4XXError', 'Latency', 'Requests', 'ConnectServerError', 'DisconnectServerError', 'SubscribeServerError', 'UnsubscribeServerError', 'PublishDataMessageServerError']
}

const commonAlarmProperties = {
Expand Down
53 changes: 32 additions & 21 deletions core/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const {
resolveRestApiNameForSub,
resolveLoadBalancerFullNameForSub,
resolveTargetGroupFullNameForSub,
findLoadBalancersForTargetGroup
findLoadBalancersForTargetGroup,
resolveGraphlQLId
} = require('./util')
const { getLogger } = require('./logging')

Expand Down Expand Up @@ -651,30 +652,40 @@ module.exports = function dashboard (dashboardConfig, functionDashboardConfigs,
* @param {object} appSyncResources Object of AppSync Service resources by resource name
*/
function createAppSyncWidgets (appSyncResources) {
const graphQLAPIId = { 'Fn::GetAtt': [appSyncResources, 'GraphQLAPIId'] }
const appSyncWidgets = []
for (const logicalId of Object.entries(appSyncResources)) {
const widgetMetrics = []
for (const [metric, metricConfig] of Object.entries(getConfiguredMetrics(appSyncDashConfig))) {
if (metricConfig.enabled) {
for (const stat of metricConfig.Statistic) {
widgetMetrics.push({
namespace: 'AWS/AppSync',
metric,
dimensions: { GraphQLAPIId: graphQLAPIId },
stat
})
const metricGroups = {
CloudWatch: ['5XXError', '4XXError', 'Latency', 'Requests'],
'Real-time Subscriptions': ['ConnectServerError', 'DisconnectServerError', 'SubscribeServerError', 'UnsubscribeServerError', 'PublishDataMessageServerError']
}
const metricConfigs = getConfiguredMetrics(appSyncDashConfig)
for (const res of Object.values(appSyncResources)) {
const appSyncResourceName = res.Properties.Name
for (const [logicalId] of Object.entries(appSyncResources)) {
const graphQLAPIId = resolveGraphlQLId(logicalId)
for (const [group, metrics] of Object.entries(metricGroups)) {
const widgetMetrics = []
for (const metric of metrics) {
const metricConfig = metricConfigs[metric]
if (metricConfig.enabled) {
for (const stat of metricConfig.Statistic) {
widgetMetrics.push({
namespace: 'AWS/AppSync',
metric,
dimensions: { GraphQLAPIId: graphQLAPIId },
stat
})
}
}
}
if (widgetMetrics.length > 0) {
appSyncWidgets.push(createMetricWidget(
`AppSync ${group} ${appSyncResourceName}`,
widgetMetrics,
sqsDashConfig
))
}
}
}
if (widgetMetrics.length > 0) {
const metricStatWidget = createMetricWidget(
`AppSync \${${logicalId}}`,
widgetMetrics,
appSyncDashConfig
)
appSyncWidgets.push(metricStatWidget)
}
}
return appSyncWidgets
}
Expand Down
21 changes: 21 additions & 0 deletions core/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,29 @@ const defaultConfig = {
'5XXError': {
Statistic: ['Sum']
},
'4XXError': {
Statistic: ['Sum']
},
Latency: {
Statistic: ['Average']
},
Requests: {
Statistic: ['Maximum']
},
ConnectServerError: {
Statistic: ['Sum']
},
DisconnectServerError: {
Statistic: ['Sum']
},
SubscribeServerError: {
Statistic: ['Sum']
},
UnsubscribeServerError: {
Statistic: ['Sum']
},
PublishDataMessageServerError: {
Statistic: ['Sum']
}
}
}
Expand Down
101 changes: 101 additions & 0 deletions core/tests/alarms-appsync.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict'

const appSyncAlarms = require('../alarms-appsync')
const { test } = require('tap')
const defaultConfig = require('../default-config')
const {
assertCommonAlarmProperties,
alarmNameToType,
createTestConfig,
createTestCloudFormationTemplate,
appSyncCfTemplate,
testContext
} = require('./testing-utils')

test('AppSync alarms are created', (t) => {
const alarmConfigAppSync = createTestConfig(
defaultConfig.alarms,
{
Period: 120,
EvaluationPeriods: 2,
TreatMissingData: 'breaching',
ComparisonOperator: 'GreaterThanOrEqualToThreshold',
AppSync: {
'5XXError': {
Threshold: 50
},
Latency: {
Threshold: 50
}
}
}

)
function createAlarmResources (appSyncAlarmConfig) {
const { createAppSyncAlarms } = appSyncAlarms(appSyncAlarmConfig, testContext)
const cfTemplate = createTestCloudFormationTemplate(appSyncCfTemplate)
createAppSyncAlarms(cfTemplate)
return cfTemplate.getResourcesByType('AWS::CloudWatch::Alarm')
}

const appSyncAlarmResources = createAlarmResources(alarmConfigAppSync.AppSync)

const expectedTypesAppSync = {
AppSync5XXErrorAlarm: '5XXError',
AppSyncLatencyAlarm: 'Latency'
}

t.equal(Object.keys(appSyncAlarmResources).length, Object.keys(expectedTypesAppSync).length)
for (const alarmResource of Object.values(appSyncAlarmResources)) {
const al = alarmResource.Properties
assertCommonAlarmProperties(t, al)
const alarmType = alarmNameToType(al.AlarmName)
const expectedMetric = expectedTypesAppSync[alarmType]
t.equal(al.MetricName, expectedMetric)
t.ok(al.Statistic)
t.equal(al.Threshold, alarmConfigAppSync.AppSync[expectedMetric].Threshold)
t.equal(al.EvaluationPeriods, 2)
t.equal(al.TreatMissingData, 'breaching')
t.equal(al.ComparisonOperator, 'GreaterThanOrEqualToThreshold')
t.equal(al.Namespace, 'AWS/AppSync')
t.equal(al.Period, 120)
t.same(al.Dimensions, [
{
Name: 'GraphQLAPIId',
Value: { 'Fn::GetAtt': ['AwesomeappsyncGraphQlApi', 'ApiId'] }
}
])
}

t.end()
})

test('AppSync alarms are not created when disabled globally', (t) => {
const alarmConfigAppSync = createTestConfig(
defaultConfig.alarms,
{
AppSync: {
enabled: false, // disabled globally
Period: 60,
'5XXError': {
Threshold: 50
},
Latency: {
Threshold: 50
}
}
}
)

function createAlarmResources (appSyncAlarmConfig) {
const { createAppSyncAlarms } = appSyncAlarms(appSyncAlarmConfig, testContext)
const cfTemplate = createTestCloudFormationTemplate(appSyncCfTemplate)
createAppSyncAlarms(cfTemplate)
return cfTemplate.getResourcesByType('AWS::CloudWatch::Alarm')
}

const appSyncAlarmResources = createAlarmResources(alarmConfigAppSync.AppSync)

t.same({}, appSyncAlarmResources)
t.end()
})
65 changes: 53 additions & 12 deletions core/tests/dashboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { test } = require('tap')
const dashboard = require('../dashboard')
const defaultConfig = require('../default-config')

const { createTestCloudFormationTemplate, defaultCfTemplate, albCfTemplate } = require('./testing-utils')
const { createTestCloudFormationTemplate, defaultCfTemplate, albCfTemplate, appSyncCfTemplate } = require('./testing-utils')

const context = {
stackName: 'testStack',
Expand Down Expand Up @@ -367,19 +367,60 @@ test('A dashboard includes metrics for ALB', (t) => {
t.end()
})

test('No widgets are created if all ALB metrics are disabled', (t) => {
const services = ['Lambda', 'ApiGateway', 'States', 'DynamoDB', 'SQS', 'Kinesis', 'ECS', 'SNS', 'Events', 'ApplicationELB', 'ApplicationELBTarget']
const dashConfig = cloneDeep(defaultConfig.dashboard)
for (const service of services) {
for (const metricConfig of Object.values(dashConfig.widgets[service])) {
metricConfig.enabled = false
}
}
const dash = dashboard(dashConfig, emptyFuncConfigs, context)
const cfTemplate = createTestCloudFormationTemplate(albCfTemplate)
test('A dashboard includes metrics for AppSync', (t) => {
const dash = dashboard(defaultConfig.dashboard, emptyFuncConfigs, context)
const cfTemplate = createTestCloudFormationTemplate(appSyncCfTemplate)
dash.addDashboard(cfTemplate)
const dashResources = cfTemplate.getResourcesByType('AWS::CloudWatch::Dashboard')
t.same(dashResources, {})
t.equal(Object.keys(dashResources).length, 1)
const [, dashResource] = Object.entries(dashResources)[0]
// eslint-disable-next-line no-template-curly-in-string
t.same(dashResource.Properties.DashboardName, { 'Fn::Sub': '${AWS::StackName}-${AWS::Region}-Dashboard' })
const dashBody = JSON.parse(dashResource.Properties.DashboardBody['Fn::Sub'])

t.ok(dashBody.start)

t.test('dashboard includes AppSync metrics', (t) => {
const widgets = dashBody.widgets.filter(({ properties: { title } }) =>
title.startsWith('AppSync')
)
t.equal(widgets.length, 2)
const namespaces = new Set()
for (const widget of widgets) {
for (const metric of widget.properties.metrics) {
namespaces.add(metric[0])
}
}
t.same(namespaces, new Set(['AWS/AppSync']))
const expectedTitles = new Set([
'AppSync CloudWatch awesome-appsync',
'AppSync Real-time Subscriptions awesome-appsync'
])
// eslint-disable-next-line no-template-curly-in-string

const actualTitles = new Set(
widgets.map((widget) => widget.properties.title)
)
t.same(actualTitles, expectedTitles)
t.end()
})

test('No widgets are created if all AppSync metrics are disabled', (t) => {
const services = ['Lambda', 'ApiGateway', 'States', 'DynamoDB', 'SQS', 'Kinesis', 'ECS', 'SNS', 'Events', 'ApplicationELB', 'ApplicationELBTarget', 'AppSync']
const dashConfig = cloneDeep(defaultConfig.dashboard)
for (const service of services) {
for (const metricConfig of Object.values(dashConfig.widgets[service])) {
metricConfig.enabled = false
}
}
const dash = dashboard(dashConfig, emptyFuncConfigs, context)
const cfTemplate = createTestCloudFormationTemplate(appSyncCfTemplate)
dash.addDashboard(cfTemplate)
const dashResources = cfTemplate.getResourcesByType('AWS::CloudWatch::Dashboard')
t.same(dashResources, {})
t.end()
})

t.end()
})

Expand Down
Loading

0 comments on commit 9396cf3

Please sign in to comment.