Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for AppSync alarms and dashboard #104

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Automatic, best-practice CloudWatch **Dashboards** and **Alarms** for your SAM, CloudFormation, CDK and Serverless Framework applications.

SLIC Watch supports: _AWS Lambda, API Gateway, DynamoDB, Kinesis Data Streams, SQS Queues, Step Functions, ECS (Fargate or EC2), SNS, EventBridge and Application Load Balancer._
SLIC Watch supports: _AWS Lambda, API Gateway, DynamoDB, Kinesis Data Streams, SQS Queues, Step Functions, ECS (Fargate or EC2), SNS, EventBridge, Application Load Balancer and AppSync._

Supported tools include:
* ⚡️ **Serverless Framework** v2 and v3 via the [_SLIC Watch Serverless Plugin_](#getting-started-with-serverless-framework)
Expand All @@ -33,6 +33,7 @@ Supported tools include:
- [SNS](#sns)
- [EventBridge](#eventbridge)
- [Application Load Balancer](#application-load-balancer)
- [AppSync](#appsync)
- [Configuration](#configuration)
- [Top-level configuration](#top-level-configuration)
- [Function-level configuration](#function-level-configuration)
Expand Down Expand Up @@ -303,6 +304,18 @@ Application Load Balancer dashboard widgets show:
|**UnHealthy Host Count**|**Lambda User Error**|**Lambda Internal Error**|
|![UnHealthyHostCount](https://raw.githubusercontent.com/fourtheorem/slic-watch/main/docs/unHealthyHostCount.png) |![LambdaUserError](https://raw.githubusercontent.com/fourtheorem/slic-watch/main/docs/lambdaUserError.png)| |

### AppSync
AppSync alarms are created for:
1. 5XX Error
2. Latency

AppSync dashboard widgets show:

|5XX Error, Latency, 4XX Error, Request|
|--|
|![API Widget](https://raw.githubusercontent.com/fourtheorem/slic-watch/main/docs/appsyncAPI.png)|
|**Connect Server Error**, **Disconnect Server Error**, **Subscribe Server Error**, **Unsubscribe Server Error**,**PublishDataMessageServerError**|
|![Real-time Subscriptions Widget](https://raw.githubusercontent.com/fourtheorem/slic-watch/main/docs/appsyncRealTimeSubscriptions.png)|
## Configuration

Configuration is entirely optional - SLIC Watch provides defaults that work out of the box.
Expand Down
126 changes: 126 additions & 0 deletions core/alarms-appsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict'

const { makeResourceName, getStatisticName } = require('./util')

/**
* @param {object} appSyncAlarmConfig The fully resolved alarm configuration
*/
module.exports = function appSyncAlarms (appSyncAlarmConfig, context) {
return {
createAppSyncAlarms
}

/**
* Add all required AppSync alarms to the provided CloudFormation template
* based on the AppSync resources found within
*
* @param {CloudFormationTemplate} cfTemplate A CloudFormation template object
*/
function createAppSyncAlarms (cfTemplate) {
const appSyncResources = cfTemplate.getResourcesByType(
'AWS::AppSync::GraphQLApi'
)

for (const [appSyncResourceName, appSyncResource] of Object.entries(appSyncResources)) {
const alarms = []
if (appSyncAlarmConfig['5XXError'].enabled) {
alarms.push(create5XXAlarm(
appSyncResourceName,
appSyncResource,
appSyncAlarmConfig['5XXError']
))
}

if (appSyncAlarmConfig.Latency.enabled) {
alarms.push(createLatencyAlarm(
appSyncResourceName,
appSyncResource,
appSyncAlarmConfig.Latency
))
}
for (const alarm of alarms) {
cfTemplate.addResource(alarm.resourceName, alarm.resource)
}
}
}
function createAppSyncAlarm (
alarmName,
alarmDescription,
appSyncResourceName,
comparisonOperator,
threshold,
metricName,
statistic,
period,
extendedStatistic,
evaluationPeriods,
treatMissingData
) {
const graphQLAPIId = { 'Fn::GetAtt': [appSyncResourceName, 'ApiId'] }
const metricProperties = {
Dimensions: [{ Name: 'GraphQLAPIId', Value: graphQLAPIId }],
MetricName: metricName,
Namespace: 'AWS/AppSync',
Period: period,
Statistic: statistic,
ExtendedStatistic: extendedStatistic
}

return {
Type: 'AWS::CloudWatch::Alarm',
Properties: {
ActionsEnabled: true,
AlarmActions: context.alarmActions,
AlarmName: alarmName,
AlarmDescription: alarmDescription,
EvaluationPeriods: evaluationPeriods,
ComparisonOperator: comparisonOperator,
Threshold: threshold,
TreatMissingData: treatMissingData,
...metricProperties
}
}
}

function create5XXAlarm (appSyncResourceName, appSyncResource, config) {
const graphQLName = appSyncResource.Properties.Name
const threshold = config.Threshold
return {
resourceName: makeResourceName('AppSync', graphQLName, '5XXError'),
resource: createAppSyncAlarm(
`AppSync5XXErrorAlarm_${graphQLName}`,
`AppSync 5XX Error ${getStatisticName(config)} for ${graphQLName} breaches ${threshold}`,
appSyncResourceName,
config.ComparisonOperator,
threshold,
'5XXError',
config.Statistic,
config.Period,
config.ExtendedStatistic,
config.EvaluationPeriods,
config.TreatMissingData
)
}
}

function createLatencyAlarm (appSyncResourceName, appSyncResource, config) {
const graphQLName = appSyncResource.Properties.Name
const threshold = config.Threshold
return {
resourceName: makeResourceName('AppSync', graphQLName, 'Latency'),
resource: createAppSyncAlarm(
`AppSyncLatencyAlarm_${graphQLName}`,
`AppSync Latency ${getStatisticName(config)} for ${graphQLName} breaches ${threshold}`,
appSyncResourceName,
config.ComparisonOperator,
threshold,
'Latency',
config.Statistic,
config.Period,
config.ExtendedStatistic,
config.EvaluationPeriods,
config.TreatMissingData
)
}
}
}
7 changes: 5 additions & 2 deletions core/alarms.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const snsAlarms = require('./alarms-sns')
const ruleAlarms = require('./alarms-eventbridge')
const albAlarms = require('./alarms-alb')
const albTargetAlarms = require('./alarms-alb-target-group')
const appSyncAlarms = require('./alarms-appsync')

module.exports = function alarms (alarmConfig, functionAlarmConfigs, context) {
const {
Expand All @@ -27,7 +28,8 @@ module.exports = function alarms (alarmConfig, functionAlarmConfigs, context) {
SNS: snsConfig,
Events: ruleConfig,
ApplicationELB: albConfig,
ApplicationELBTarget: albTargetConfig
ApplicationELBTarget: albTargetConfig,
AppSync: appSyncConfig
} = cascade(alarmConfig)

const cascadedFunctionAlarmConfigs = applyAlarmConfig(lambdaConfig, functionAlarmConfigs)
Expand All @@ -42,7 +44,7 @@ module.exports = function alarms (alarmConfig, functionAlarmConfigs, context) {
const { createRuleAlarms } = ruleAlarms(ruleConfig, context)
const { createALBAlarms } = albAlarms(albConfig, context)
const { createALBTargetAlarms } = albTargetAlarms(albTargetConfig, context)

const { createAppSyncAlarms } = appSyncAlarms(appSyncConfig, context)
return {
addAlarms
}
Expand All @@ -66,6 +68,7 @@ module.exports = function alarms (alarmConfig, functionAlarmConfigs, context) {
ruleConfig.enabled && createRuleAlarms(cfTemplate)
albConfig.enabled && createALBAlarms(cfTemplate)
albTargetConfig.enabled && createALBTargetAlarms(cfTemplate)
appSyncConfig.enabled && createAppSyncAlarms(cfTemplate)
}
}
}
7 changes: 5 additions & 2 deletions core/config-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const supportedAlarms = {
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed'],
Events: ['FailedInvocations', 'ThrottledRules'],
ApplicationELB: ['HTTPCode_ELB_5XX_Count', 'RejectedConnectionCount'],
ApplicationELBTarget: ['HTTPCode_Target_5XX_Count', 'UnHealthyHostCount', 'LambdaInternalError', 'LambdaUserError']
ApplicationELBTarget: ['HTTPCode_Target_5XX_Count', 'UnHealthyHostCount', 'LambdaInternalError', 'LambdaUserError'],
AppSync: ['5XXError', 'Latency']
}

const supportedWidgets = {
Expand All @@ -36,7 +37,8 @@ const supportedWidgets = {
SNS: ['NumberOfNotificationsFilteredOut-InvalidAttributes', 'NumberOfNotificationsFailed'],
Events: ['FailedInvocations', 'ThrottledRules', 'Invocations'],
ApplicationELB: ['HTTPCode_ELB_5XX_Count', 'RejectedConnectionCount'],
ApplicationELBTarget: ['HTTPCode_Target_5XX_Count', 'UnHealthyHostCount', 'LambdaInternalError', 'LambdaUserError']
ApplicationELBTarget: ['HTTPCode_Target_5XX_Count', 'UnHealthyHostCount', 'LambdaInternalError', 'LambdaUserError'],
AppSync: ['5XXError', '4XXError', 'Latency', 'Requests', 'ConnectServerError', 'DisconnectServerError', 'SubscribeServerError', 'UnsubscribeServerError', 'PublishDataMessageServerError']
}

const commonAlarmProperties = {
Expand Down Expand Up @@ -113,6 +115,7 @@ const commonWidgetProperties = {
width: { type: ['integer', 'null'], minimum: 1, maximum: 24 },
height: { type: ['integer', 'null'], minimum: 1, maximum: 1000 },
metricPeriod: { type: ['integer', 'null'], minimum: 60, multipleOf: 60 },
yAxis: { type: ['string', 'null'], enum: ['left', 'right'] },
Statistic: {
type: 'array',
items: {
Expand Down
64 changes: 58 additions & 6 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 All @@ -33,7 +34,8 @@ module.exports = function dashboard (dashboardConfig, functionDashboardConfigs,
SNS: snsDashConfig,
Events: ruleDashConfig,
ApplicationELB: albDashConfig,
ApplicationELBTarget: albTargetDashConfig
ApplicationELBTarget: albTargetDashConfig,
AppSync: appSyncDashConfig
}
} = cascade(dashboardConfig)

Expand Down Expand Up @@ -83,6 +85,10 @@ module.exports = function dashboard (dashboardConfig, functionDashboardConfigs,
'AWS::ElasticLoadBalancingV2::TargetGroup'
)

const appSyncResources = cfTemplate.getResourcesByType(
'AWS::AppSync::GraphQLApi'
)

const eventSourceMappingFunctions = cfTemplate.getEventSourceMappingFunctions()
const apiWidgets = createApiWidgets(apiResources)
const stateMachineWidgets = createStateMachineWidgets(stateMachineResources)
Expand All @@ -98,6 +104,7 @@ module.exports = function dashboard (dashboardConfig, functionDashboardConfigs,
const ruleWidgets = createRuleWidgets(ruleResources)
const loadBalancerWidgets = createLoadBalancerWidgets(loadBalancerResources)
const targetGroupWidgets = createTargetGroupWidgets(targetGroupResources, cfTemplate)
const appSyncWidgets = createAppSyncWidgets(appSyncResources)

const positionedWidgets = layOutWidgets([
...apiWidgets,
Expand All @@ -110,7 +117,8 @@ module.exports = function dashboard (dashboardConfig, functionDashboardConfigs,
...topicWidgets,
...ruleWidgets,
...loadBalancerWidgets,
...targetGroupWidgets
...targetGroupWidgets,
...appSyncWidgets
])

if (positionedWidgets.length > 0) {
Expand Down Expand Up @@ -138,17 +146,16 @@ module.exports = function dashboard (dashboardConfig, functionDashboardConfigs,
*/
function createMetricWidget (title, metricDefs, config) {
const metrics = metricDefs.map(
({ namespace, metric, dimensions, stat }) => [
({ namespace, metric, dimensions, stat, yAxis }) => [
namespace,
metric,
...Object.entries(dimensions).reduce(
(acc, [name, value]) => [...acc, name, value],
[]
),
{ stat }
{ stat, yAxis }
]
)

return {
type: 'metric',
properties: {
Expand Down Expand Up @@ -638,6 +645,51 @@ module.exports = function dashboard (dashboardConfig, functionDashboardConfigs,
return targetGroupWidgets
}

/**
* Create a set of CloudWatch Dashboard widgets for AppSync services.
*
* @param {object} appSyncResources Object of AppSync Service resources by resource name
*/
function createAppSyncWidgets (appSyncResources) {
const appSyncWidgets = []
const metricGroups = {
API: ['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,
yAxis: metricConfig.yAxis
})
}
}
}
if (widgetMetrics.length > 0) {
appSyncWidgets.push(createMetricWidget(
`AppSync ${group} ${appSyncResourceName}`,
widgetMetrics,
sqsDashConfig
))
}
}
}
}
return appSyncWidgets
}

/**
* Set the location and dimension properties of each provided widget
*
Expand Down
Loading