Skip to content

Commit

Permalink
feat: support OK actions for alarms
Browse files Browse the repository at this point in the history
Co-authored-by: David <[email protected]>
  • Loading branch information
eoinsha and David committed Nov 3, 2023
1 parent 327a6f3 commit 84bd817
Show file tree
Hide file tree
Showing 49 changed files with 370 additions and 230 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,5 @@ lerna-debug*

samconfig.toml
packaged.yaml
.tgz
*.tar.gz
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ so you can receive alarm notifications via email, Slack, etc.
```yaml
custom:
slicWatch:
topicArn: {'Fn::Ref': myTopic}
alarmActionsConfig: {
alarmActions: [{'Fn::Ref': myTopic}]
}
```
See the [Configuration](#configuration) section below for more detailed instructions on fine tuning SLIC Watch to your needs.

Expand Down Expand Up @@ -125,7 +127,9 @@ so you can receive alarm notifications via email, Slack, etc.
Metadata:
slicWatch:
enabled: true
topicArn: !Ref MonitoringTopic
alarmActionsConfig:
alarmActions:
- !Ref MonitoringTopic
```
See the [Configuration](#configuration) section below for more detailed instructions on fine tuning SLIC Watch to your needs.

Expand Down Expand Up @@ -166,7 +170,11 @@ this.addTransform("SlicWatch-v3");
this.templateOptions.metadata = {
slicWatch: {
enabled: true,
topicArn: "arn:aws:sns:eu-west-1:xxxxxxx:topic",
alarmActionsConfig: {
alarmActions: ["arn:aws:sns:eu-west-1:xxxxxxx:topic"],
okActions: ["arn:aws:sns:eu-west-1:xxxxxxx:topic"],
actionsEnabled: true
}
}
}
```
Expand Down Expand Up @@ -358,7 +366,17 @@ this.templateOptions.metadata = {
}
```

- The `topicArn` may be optionally provided as an SNS Topic destination for all alarms. If you omit the topic, alarms are still created but are not sent to any destination.
- The `alarmActionsConfig` may be optionally added to specifc one or more SNS Topic destinations for all alarm status changes to `ALARM` and `OK`. If you omit destination topics, alarms are still created but are not sent to any destination. For example:
```yaml
slicWatch:
alarmActionsConfig:
alarmActions: # Default to no actions
- arn:aws:sns:eu-west-1:123456789012
okActions: # Defaults to no actions
- arn:aws:sns:eu-west-1:123456789012
actionsEnabled:
- true # Defaults to true
```
- Alarms or dashboards can be disabled at any level in the configuration by adding `enabled: false`. You can even disable all plugin functionality by specifying `enabled: false` at the top-level plugin configuration.

A complete set of supported options along with their defaults are shown in [default-config.js](./core/default-config.js)
Expand Down
2 changes: 1 addition & 1 deletion cdk-test-project/source/ecs-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class CdkECSStack extends cdk.Stack {
this.templateOptions.metadata = {
slicWatch: {
enabled: true,
// "topicArn": "arn:aws:xxxxxx:mytopic",
// alarmActionsConfig: { alarmActions: ["arn:aws:xxxxxx:mytopic"] },
alarms: {
Lambda: {
Invocations: {
Expand Down
4 changes: 3 additions & 1 deletion cdk-test-project/source/general-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export class CdkTestGeneralStack extends cdk.Stack {
}

const topic = new sns.Topic(this, 'MyTopic')
this.templateOptions.metadata.slicWatch.topicArn = topic.topicArn
this.templateOptions.metadata.slicWatch.alarmActionsConfig = {
alarmActions: topic.topicArn
}

const helloFunction = new lambda.Function(this, 'HelloHandler',
{
Expand Down
61 changes: 20 additions & 41 deletions cf-macro/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import _ from 'lodash'
import Ajv from 'ajv'
import pino from 'pino'

import { addAlarms, addDashboard, defaultConfig, slicWatchSchema, getResourcesByType } from '../core/index'
import { addAlarms, addDashboard, getResourcesByType } from '../core/index'
import { setLogger } from 'slic-watch-core/logging'
import { type SlicWatchConfig, resolveSlicWatchConfig } from 'slic-watch-core/inputs/general-config'

const logger = pino({ name: 'macroHandler' })
setLogger(logger)
Expand All @@ -14,65 +14,44 @@ interface Event {
fragment
}

interface SlicWatchConfig {
topicArn?: string
enabled?: boolean
}

// macro requires handler to be async
export async function handler (event: Event) {
let status = 'success'
let errorMessage: string | undefined

logger.info({ event })
const outputFragment = event.fragment
try {
const slicWatchConfig: SlicWatchConfig = outputFragment.Metadata?.slicWatch ?? {}
if (slicWatchConfig.enabled !== false) {
const ajv = new Ajv({
unicodeRegExp: false
})
const config = _.merge(defaultConfig, slicWatchConfig)

const slicWatchValidate = ajv.compile(slicWatchSchema)
const slicWatchValid = slicWatchValidate(slicWatchConfig)

if (!slicWatchValid) {
throw new Error('SLIC Watch configuration is invalid: ' + ajv.errorsText(slicWatchValidate.errors))
}

const alarmActions: string[] = []
slicWatchConfig?.topicArn != null && alarmActions.push(slicWatchConfig.topicArn)
process.env.ALARM_SNS_TOPIC != null && alarmActions.push(process.env.ALARM_SNS_TOPIC)
const config = resolveSlicWatchConfig(slicWatchConfig)

const context = {
alarmActions
}
const functionAlarmConfigs = {}
const functionDashboardConfigs = {}
const functionAlarmConfigs = {}
const functionDashboardConfigs = {}

const lambdaResources = getResourcesByType(
'AWS::Lambda::Function',
outputFragment
)
const lambdaResources = getResourcesByType('AWS::Lambda::Function', outputFragment)

for (const [funcResourceName, funcResource] of Object.entries(lambdaResources)) {
const funcConfig = funcResource.Metadata?.slicWatch ?? {}
functionAlarmConfigs[funcResourceName] = funcConfig.alarms ?? {}
functionDashboardConfigs[funcResourceName] = funcConfig.dashboard
}

_.merge(outputFragment)
addAlarms(config.alarms, functionAlarmConfigs, context, outputFragment)
addDashboard(config.dashboard, functionDashboardConfigs, outputFragment)
for (const [funcResourceName, funcResource] of Object.entries(lambdaResources)) {
const funcConfig = funcResource.Metadata?.slicWatch ?? {}
functionAlarmConfigs[funcResourceName] = funcConfig.alarms ?? {}
functionDashboardConfigs[funcResourceName] = funcConfig.dashboard
}

_.merge(outputFragment)
addAlarms(config.alarms, functionAlarmConfigs, config.alarmActionsConfig, outputFragment)
addDashboard(config.dashboard, functionDashboardConfigs, outputFragment)
} catch (err) {
logger.error(err)
errorMessage = (err as Error).message
status = 'fail'
}

logger.info({ outputFragment })

return {
requestId: event.requestId,
status,
errorMessage,
requestId: event.requestId,
fragment: outputFragment
}
}
22 changes: 10 additions & 12 deletions cf-macro/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,9 @@ test('macro returns success', async t => {
t.end()
})

test('macro uses SNS Topic environment variable if specified', async t => {
process.env.ALARM_SNS_TOPIC = 'arn:aws:sns:eu-west-1:123456789123:TestTopic'
try {
const result = await handler(event)
t.equal(result.status, 'success')
} finally {
delete process.env.ALARM_SNS_TOPIC
}
t.end()
})

test('macro uses topicArn if specified', async t => {
const topicArn = 'arn:aws:sns:eu-west-1:123456789123:TestTopic'

const eventWithTopic = {
...event,
fragment: {
Expand All @@ -35,13 +26,18 @@ test('macro uses topicArn if specified', async t => {
...event.fragment.Metadata,
slicWatch: {
...event.fragment.Metadata?.slicWatch,
topicArn: 'arn:aws:sns:eu-west-1:123456789123:TestTopic'
alarmActionsConfig: {
alarmActions: [topicArn],
okActions: [topicArn]
}
}
}
}
}
const result = await handler(eventWithTopic)
t.equal(result.status, 'success')
t.notOk(result.errorMessage)
t.same(result.fragment.Resources.slicWatchLambdaDurationAlarmHelloLambdaFunction.Properties.AlarmActions, [topicArn])
t.end()
})

Expand Down Expand Up @@ -78,6 +74,7 @@ test('Macro execution fails if an invalid SLIC Watch config is provided', async
testevent.fragment.Metadata = { slicWatch: { topicArrrrn: 'pirateTopic' } }
const result = await handler(testevent)
t.equal(result.status, 'fail')
t.ok(result.errorMessage)
t.end()
})

Expand All @@ -91,6 +88,7 @@ test('Macro execution succeeds with no slicWatch config', t => {
test('Macro execution succeeds if no SNS Topic is provided', t => {
const testevent = _.cloneDeep(event)
delete testevent.fragment.Metadata?.slicWatch.topicArn
delete testevent.fragment.Metadata?.slicWatch.alarmActionsConfig
handler(testevent)
t.end()
})
Expand Down
4 changes: 3 additions & 1 deletion core/alarms/alarm-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export interface ReturnAlarm {
}

export interface AlarmActionsConfig {
alarmActions: string[]
actionsEnabled?: boolean
okActions?: string[]
alarmActions?: string[]
}

export interface SlicWatchCascadedAlarmsConfig<T extends InputOutput> extends AlarmProperties {
Expand Down
9 changes: 5 additions & 4 deletions core/alarms/alarm-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,18 @@ export function createCfAlarms (
* Create a CloudFormation Alarm resourc
*
* @param alarmProperties The alarm configuration for this specific resource type
* @param context Alarm actions
* @param alarmActionsConfig Alarm actions
*
* @returns An template object for the Cloudformation alarm
*/

export function createAlarm (alarmProperties: AlarmProperties, context?: AlarmActionsConfig): AlarmTemplate {
export function createAlarm (alarmProperties: AlarmProperties, alarmActionsConfig?: AlarmActionsConfig): AlarmTemplate {
return {
Type: 'AWS::CloudWatch::Alarm',
Properties: {
ActionsEnabled: true,
AlarmActions: context?.alarmActions,
ActionsEnabled: alarmActionsConfig?.actionsEnabled,
AlarmActions: alarmActionsConfig?.alarmActions,
OKActions: alarmActionsConfig?.okActions,
...alarmProperties
}
}
Expand Down
10 changes: 5 additions & 5 deletions core/alarms/alb-target-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function findLoadBalancersForTargetGroup (targetGroupLogicalId: string, c
* @param metrics The Target Group metric names
* @param loadBalancerLogicalIds The CloudFormation Logical IDs of the ALB resource
* @param albTargetAlarmsConfig The fully resolved alarm configuration
* @param alarmActionsConfig Deployment context (alarmActions)
* @param alarmActionsConfig Notification configuration for alarm status change events
*
* @returns ALB Target Group-specific CloudFormation Alarm resources
*/
Expand Down Expand Up @@ -120,23 +120,23 @@ function createAlbTargetCfAlarm (
* based on the resources found within
*
* @param albTargetAlarmsConfig The fully resolved alarm configuration
* @param context Deployment context (alarmActions)
* @param alarmActionsConfig Notification configuration for alarm status change events
* @param compiledTemplate A CloudFormation template object
*
* @returns ALB Target Group-specific CloudFormation Alarm resources
*/
export default function createAlbTargetAlarms (
albTargetAlarmsConfig: SlicWatchAlbTargetAlarmsConfig<SlicWatchMergedConfig>, context: AlarmActionsConfig, compiledTemplate: Template
albTargetAlarmsConfig: SlicWatchAlbTargetAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const targetGroupResources = getResourcesByType('AWS::ElasticLoadBalancingV2::TargetGroup', compiledTemplate)
const resources: CloudFormationResources = {}
for (const [targetGroupResourceName, targetGroupResource] of Object.entries(targetGroupResources)) {
const loadBalancerLogicalIds = findLoadBalancersForTargetGroup(targetGroupResourceName, compiledTemplate)
Object.assign(resources, createAlbTargetCfAlarm(targetGroupResourceName, executionMetrics, loadBalancerLogicalIds, albTargetAlarmsConfig, context))
Object.assign(resources, createAlbTargetCfAlarm(targetGroupResourceName, executionMetrics, loadBalancerLogicalIds, albTargetAlarmsConfig, alarmActionsConfig))

if (targetGroupResource.Properties?.TargetType === 'lambda') {
// Create additional alarms for Lambda-specific ALB metrics
Object.assign(resources, createAlbTargetCfAlarm(targetGroupResourceName, executionMetricsLambda, loadBalancerLogicalIds, albTargetAlarmsConfig, context))
Object.assign(resources, createAlbTargetCfAlarm(targetGroupResourceName, executionMetricsLambda, loadBalancerLogicalIds, albTargetAlarmsConfig, alarmActionsConfig))
}
}
return resources
Expand Down
6 changes: 3 additions & 3 deletions core/alarms/alb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,20 @@ function createAlbAlarmCfProperties (metric: string, albLogicalId: string, confi
* based on the resources found within
*
* @param albAlarmsConfig The fully resolved alarm configuration
* @param context Deployment context (alarmActions)
* @param alarmActionsConfig Notification configuration for alarm status change events
* @param compiledTemplate A CloudFormation template object
*
* @returns ALB-specific CloudFormation Alarm resources
*/
export default function createAlbAlarms (
albAlarmsConfig: SlicWatchAlbAlarmsConfig<SlicWatchMergedConfig>, context: AlarmActionsConfig, compiledTemplate: Template
albAlarmsConfig: SlicWatchAlbAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
return createCfAlarms(
'AWS::ElasticLoadBalancingV2::LoadBalancer',
'LoadBalancer',
executionMetrics,
albAlarmsConfig,
context,
alarmActionsConfig,
compiledTemplate,
createAlbAlarmCfProperties
)
Expand Down
2 changes: 1 addition & 1 deletion core/alarms/api-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const executionMetrics = ['5XXError', '4XXError', 'Latency']
* Add all required API Gateway REST API alarms to the provided CloudFormation template based on the resources found within
*
* @param apiGwAlarmsConfig The fully resolved alarm configuration
* @param alarmActionsConfig Deployment context (alarmActions)
* @param alarmActionsConfig Notification configuration for alarm status change events
* @param compiledTemplate A CloudFormation template object
*
* @returns API Gateway-specific CloudFormation Alarm resources
Expand Down
2 changes: 1 addition & 1 deletion core/alarms/appsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const executionMetrics = ['5XXError', 'Latency']
* based on the AppSync resources found within
*
* @param appSyncAlarmsConfig The fully resolved alarm configuration
* @param alarmActionsConfig Deployment context (alarmActions)
* @param alarmActionsConfig Notification configuration for alarm status change events
* @param compiledTemplate A CloudFormation template object
*
* @returns AppSync-specific CloudFormation Alarm resources
Expand Down
8 changes: 4 additions & 4 deletions core/alarms/dynamodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ const dynamoDbGsiMetrics = ['ReadThrottleEvents', 'WriteThrottleEvents']
* based on the tables and any global secondary indices (GSIs).
*
* @param dynamoDbAlarmsConfig The fully resolved alarm configuration
* @param context Deployment context (alarmActions)
* @param alarmActionsConfig Notification configuration for alarm status change events
* @param compiledTemplate A CloudFormation template object
*
* @returns DynamoDB-specific CloudFormation Alarm resources
*/
export default function createDynamoDbAlarms (
dynamoDbAlarmsConfig: SlicWatchDynamoDbAlarmsConfig<SlicWatchMergedConfig>, context: AlarmActionsConfig, compiledTemplate: Template
dynamoDbAlarmsConfig: SlicWatchDynamoDbAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const resources: CloudFormationResources = {}
const tableResources = getResourcesByType('AWS::DynamoDB::Table', compiledTemplate)
Expand All @@ -46,7 +46,7 @@ export default function createDynamoDbAlarms (
...rest
}
const alarmLogicalId = makeAlarmLogicalId('Table', tableLogicalId, metric)
const resource = createAlarm(dynamoDbAlarmProperties, context)
const resource = createAlarm(dynamoDbAlarmProperties, alarmActionsConfig)
resources[alarmLogicalId] = resource
}
}
Expand All @@ -66,7 +66,7 @@ export default function createDynamoDbAlarms (
...rest
}
const alarmLogicalId = makeAlarmLogicalId('GSI', `${tableLogicalId}${gsiName}`, metric)
const resource = createAlarm(dynamoDbAlarmsConfig, context)
const resource = createAlarm(dynamoDbAlarmsConfig, alarmActionsConfig)
resources[alarmLogicalId] = resource
}
}
Expand Down
Loading

0 comments on commit 84bd817

Please sign in to comment.