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

feat: support OK actions for alarms #112

Merged
merged 2 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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