Skip to content

Commit

Permalink
fix: cascade correctly with resource-level overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
eoinsha committed Dec 4, 2023
1 parent 16d93d5 commit 4988f57
Show file tree
Hide file tree
Showing 31 changed files with 959 additions and 769 deletions.
6 changes: 2 additions & 4 deletions cdk-test-project/source/general-stack.ts
Expand Up @@ -55,10 +55,8 @@ export class CdkTestGeneralStack extends cdk.Stack {
cfnFuncHello.cfnOptions.metadata = {
slicWatch: {
alarms: {
Lambda: {
Invocations: {
Threshold: 4
}
Invocations: {
Threshold: 4
}
}
}
Expand Down
6 changes: 2 additions & 4 deletions cdk-test-project/source/sfn-stack.ts
Expand Up @@ -51,10 +51,8 @@ export class CdkSFNStack extends cdk.Stack {
cfnFuncHello.cfnOptions.metadata = {
slicWatch: {
alarms: {
Lambda: {
Invocations: {
Threshold: 4
}
Invocations: {
Threshold: 4
}
}
}
Expand Down
9 changes: 5 additions & 4 deletions core/alarms/alarm-utils.ts
Expand Up @@ -4,6 +4,7 @@ import { pascal } from 'case'

import type { AlarmActionsConfig, AlarmTemplate, CloudFormationResources, OptionalAlarmProps, SlicWatchMergedConfig } from './alarm-types'
import { getResourceAlarmConfigurationsByType } from '../cf-template'
import { type ConfigType } from '../inputs/config-types'

/*
* RegEx to filter out invalid CloudFormation Logical ID characters
Expand All @@ -23,9 +24,9 @@ const LOGICAL_ID_FILTER_REGEX = /[^a-z0-9]/gi
type SpecificAlarmPropertiesGeneratorFunction = (metric: string, resourceName: string, config: SlicWatchMergedConfig) => Omit<AlarmProperties, OptionalAlarmProps>

/**
* Create CloudFormation 'AWS::CloudWatch::Alarm' resources based on metrics for a specfic resources type
* Create CloudFormation 'AWS::CloudWatch::Alarm' resources based on metrics for a specific resources type
*
* @param type The resource CloudFormation type, e.g., `AWS::Lambda::Function`
* @param type The resource config type
* @param service A human readable name for the service, e.g., 'Lambda'
* @param metrics A list of metric names to use in the alarms
* @param config The alarm configuration for this specific resource type
Expand All @@ -36,7 +37,7 @@ type SpecificAlarmPropertiesGeneratorFunction = (metric: string, resourceName: s
* @returns An object containing the alarm resources in CloudFormation syntax by logical ID
*/
export function createCfAlarms (
type: string, service: string, metrics: string[], config: SlicWatchMergedConfig, alarmActionsConfig: AlarmActionsConfig,
type: ConfigType, service: string, metrics: string[], config: SlicWatchMergedConfig, alarmActionsConfig: AlarmActionsConfig,
compiledTemplate: Template, genSpecificAlarmProps: SpecificAlarmPropertiesGeneratorFunction
): CloudFormationResources {
const resources: CloudFormationResources = {}
Expand All @@ -62,7 +63,7 @@ export function createCfAlarms (
}

/**
* Create a CloudFormation Alarm resourc
* Create a CloudFormation Alarm resource
*
* @param alarmProperties The alarm configuration for this specific resource type
* @param alarmActionsConfig Alarm actions
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/alb-target-group.ts
Expand Up @@ -6,6 +6,7 @@ import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatc
import { createAlarm, getStatisticName, makeAlarmLogicalId } from './alarm-utils'
import type { ResourceType } from '../cf-template'
import { getResourceAlarmConfigurationsByType, getResourcesByType } from '../cf-template'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchAlbTargetAlarmsConfig<T extends InputOutput> = T & {
HTTPCode_Target_5XX_Count: T
Expand Down Expand Up @@ -128,7 +129,7 @@ function createAlbTargetCfAlarm (
export default function createAlbTargetAlarms (
albTargetAlarmsConfig: SlicWatchAlbTargetAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const resourceConfigs = getResourceAlarmConfigurationsByType('AWS::ElasticLoadBalancingV2::TargetGroup', compiledTemplate, albTargetAlarmsConfig)
const resourceConfigs = getResourceAlarmConfigurationsByType(ConfigType.ApplicationELBTarget, compiledTemplate, albTargetAlarmsConfig)
const resources: CloudFormationResources = {}
for (const [targetGroupLogicalId, targetGroupResource] of Object.entries(resourceConfigs.resources)) {
const mergedConfig = resourceConfigs.alarmConfigurations[targetGroupLogicalId]
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/alb.ts
Expand Up @@ -4,6 +4,7 @@ import { Fn } from 'cloudform'

import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createCfAlarms, getStatisticName } from './alarm-utils'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchAlbAlarmsConfig<T extends InputOutput> = T & {
HTTPCode_ELB_5XX_Count: T
Expand Down Expand Up @@ -43,7 +44,7 @@ export default function createAlbAlarms (
albAlarmsConfig: SlicWatchAlbAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
return createCfAlarms(
'AWS::ElasticLoadBalancingV2::LoadBalancer',
ConfigType.ApplicationELB,
'LoadBalancer',
executionMetrics,
albAlarmsConfig,
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/api-gateway.ts
Expand Up @@ -6,6 +6,7 @@ import { Fn } from 'cloudform'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm, getStatisticName, makeAlarmLogicalId } from './alarm-utils'
import { getResourceAlarmConfigurationsByType } from '../cf-template'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchApiGwAlarmsConfig<T extends InputOutput> = T & {
'5XXError': T
Expand Down Expand Up @@ -84,7 +85,7 @@ export default function createApiGatewayAlarms (
): CloudFormationResources {
const resources: CloudFormationResources = {}
const configuredResources = getResourceAlarmConfigurationsByType(
'AWS::ApiGateway::RestApi', compiledTemplate, apiGwAlarmsConfig
ConfigType.ApiGateway, compiledTemplate, apiGwAlarmsConfig
)

for (const [apiLogicalId, apiResource] of Object.entries(configuredResources.resources)) {
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/appsync.ts
Expand Up @@ -5,6 +5,7 @@ import { Fn } from 'cloudform'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm, getStatisticName, makeAlarmLogicalId } from './alarm-utils'
import { getResourceAlarmConfigurationsByType } from '../cf-template'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchAppSyncAlarmsConfig<T extends InputOutput> = T & {
'5XXError': T
Expand All @@ -27,7 +28,7 @@ export default function createAppSyncAlarms (
appSyncAlarmsConfig: SlicWatchAppSyncAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const resources = {}
const configuredResources = getResourceAlarmConfigurationsByType('AWS::AppSync::GraphQLApi', compiledTemplate, appSyncAlarmsConfig)
const configuredResources = getResourceAlarmConfigurationsByType(ConfigType.AppSync, compiledTemplate, appSyncAlarmsConfig)

for (const [appSyncLogicalId, appSyncResource] of Object.entries(configuredResources.resources)) {
for (const metric of executionMetrics) {
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/dynamodb.ts
Expand Up @@ -5,6 +5,7 @@ import { Fn } from 'cloudform'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm, makeAlarmLogicalId } from './alarm-utils'
import { getResourceAlarmConfigurationsByType } from '../cf-template'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchDynamoDbAlarmsConfig<T extends InputOutput> = T & {
ReadThrottleEvents: T
Expand Down Expand Up @@ -32,7 +33,7 @@ export default function createDynamoDbAlarms (
compiledTemplate: Template
): CloudFormationResources {
const resources: CloudFormationResources = {}
const configuredResources = getResourceAlarmConfigurationsByType('AWS::DynamoDB::Table', compiledTemplate, dynamoDbAlarmsConfig)
const configuredResources = getResourceAlarmConfigurationsByType(ConfigType.DynamoDB, compiledTemplate, dynamoDbAlarmsConfig)

for (const [tableLogicalId, tableResource] of Object.entries(configuredResources.resources)) {
for (const metric of dynamoDbMetrics) {
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/ecs.ts
Expand Up @@ -5,6 +5,7 @@ import { Fn } from 'cloudform'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm } from './alarm-utils'
import { getResourceAlarmConfigurationsByType } from '../cf-template'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchEcsAlarmsConfig<T extends InputOutput> = T & {
MemoryUtilization: T
Expand Down Expand Up @@ -49,7 +50,7 @@ export default function createECSAlarms (
ecsAlarmsConfig: SlicWatchEcsAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const resources: CloudFormationResources = {}
const configuredResources = getResourceAlarmConfigurationsByType('AWS::ECS::Service', compiledTemplate, ecsAlarmsConfig)
const configuredResources = getResourceAlarmConfigurationsByType(ConfigType.ECS, compiledTemplate, ecsAlarmsConfig)

for (const [serviceLogicalId, serviceResource] of Object.entries(configuredResources.resources)) {
for (const metric of executionMetrics) {
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/eventbridge.ts
Expand Up @@ -3,6 +3,7 @@ import { Fn } from 'cloudform'

import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createCfAlarms } from './alarm-utils'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchEventsAlarmsConfig<T extends InputOutput> = T & {
FailedInvocations: T
Expand Down Expand Up @@ -43,7 +44,7 @@ export default function createRuleAlarms (
eventsAlarmsConfig: SlicWatchEventsAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
return createCfAlarms(
'AWS::Events::Rule',
ConfigType.Events,
'Events',
executionMetrics,
eventsAlarmsConfig,
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/kinesis.ts
Expand Up @@ -6,6 +6,7 @@ import { pascal } from 'case'
import { getResourceAlarmConfigurationsByType } from '../cf-template'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm, getStatisticName, makeAlarmLogicalId } from './alarm-utils'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchKinesisAlarmsConfig<T extends InputOutput> = T & {
'GetRecords.IteratorAgeMilliseconds': T
Expand Down Expand Up @@ -39,7 +40,7 @@ export default function createKinesisAlarms (
kinesisAlarmsConfig: SlicWatchKinesisAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const resources: CloudFormationResources = {}
const configuredResources = getResourceAlarmConfigurationsByType('AWS::Kinesis::Stream', compiledTemplate, kinesisAlarmsConfig)
const configuredResources = getResourceAlarmConfigurationsByType(ConfigType.Kinesis, compiledTemplate, kinesisAlarmsConfig)

for (const [streamLogicalId] of Object.entries(configuredResources.resources)) {
for (const [type, metric] of Object.entries(kinesisAlarmTypes)) {
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/lambda.ts
Expand Up @@ -5,6 +5,7 @@ import { Fn } from 'cloudform'
import { getEventSourceMappingFunctions, getResourceAlarmConfigurationsByType } from '../cf-template'
import type { AlarmActionsConfig, InputOutput, Value, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm } from './alarm-utils'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchLambdaAlarmsConfig<T extends InputOutput> = T & {
Errors: T
Expand All @@ -30,7 +31,7 @@ export default function createLambdaAlarms (
) {
const resources = {}

const configuredLambdaResources = getResourceAlarmConfigurationsByType('AWS::Lambda::Function', compiledTemplate, lambdaAlarmConfig)
const configuredLambdaResources = getResourceAlarmConfigurationsByType(ConfigType.Lambda, compiledTemplate, lambdaAlarmConfig)
for (const [funcLogicalId, funcResource] of Object.entries(configuredLambdaResources.resources)) {
const mergedConfig = configuredLambdaResources.alarmConfigurations[funcLogicalId]

Expand Down
3 changes: 2 additions & 1 deletion core/alarms/sns.ts
Expand Up @@ -3,6 +3,7 @@ import { Fn } from 'cloudform'

import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createCfAlarms } from './alarm-utils'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchSnsAlarmsConfig<T extends InputOutput> = T & {
'NumberOfNotificationsFilteredOut-InvalidAttributes': T
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function createSnsAlarms (
snsAlarmsConfig: SlicWatchSnsAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
return createCfAlarms(
'AWS::SNS::Topic',
ConfigType.SNS,
'SNS',
executionMetrics,
snsAlarmsConfig,
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/sqs.ts
Expand Up @@ -5,6 +5,7 @@ import { Fn } from 'cloudform'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm } from './alarm-utils'
import { getResourceAlarmConfigurationsByType } from '../cf-template'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchSqsAlarmsConfig<T extends InputOutput> = T & {
InFlightMessagesPc: T
Expand All @@ -25,7 +26,7 @@ export default function createSQSAlarms (
sqsAlarmsConfig: SlicWatchSqsAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const resources: CloudFormationResources = {}
const configuredResources = getResourceAlarmConfigurationsByType('AWS::SQS::Queue', compiledTemplate, sqsAlarmsConfig)
const configuredResources = getResourceAlarmConfigurationsByType(ConfigType.SQS, compiledTemplate, sqsAlarmsConfig)

for (const [queueLogicalId, queueResource] of Object.entries(configuredResources.resources)) {
const mergedConfig = configuredResources.alarmConfigurations[queueLogicalId]
Expand Down
3 changes: 2 additions & 1 deletion core/alarms/step-functions.ts
Expand Up @@ -3,6 +3,7 @@ import { Fn } from 'cloudform'

import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createCfAlarms } from './alarm-utils'
import { ConfigType } from '../inputs/config-types'

export type SlicWatchSfAlarmsConfig<T extends InputOutput> = T & {
ExecutionThrottled: T
Expand Down Expand Up @@ -43,7 +44,7 @@ export default function createStatesAlarms (
sfAlarmProperties: SlicWatchSfAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
return createCfAlarms(
'AWS::StepFunctions::StateMachine',
ConfigType.States,
'States',
executionMetrics,
sfAlarmProperties,
Expand Down
31 changes: 22 additions & 9 deletions core/cf-template.ts
@@ -1,3 +1,5 @@

import { merge } from 'lodash'
import type Resource from 'cloudform-types/types/resource'
import type Template from 'cloudform-types/types/template'

Expand All @@ -6,7 +8,8 @@ import { getLogger } from './logging'
import { cascade } from './inputs/cascading-config'
import { type SlicWatchMergedConfig } from './alarms/alarm-types'
import { type WidgetMetricProperties } from './dashboards/dashboard-types'
import { merge } from 'lodash'
import { defaultConfig } from './inputs/default-config'
import { type ConfigType, cfTypeByConfigType } from './inputs/config-types'

const logger = getLogger()

Expand Down Expand Up @@ -55,18 +58,23 @@ export interface ResourceDashboardConfigurations<T extends WidgetMetricPropertie
* Find all resources of a given type and merge any resource-specific SLIC Watch configuration with
* the global alarm configuration for resources of that type
*
* @param type The CloudFormation resource type
* @param type The resource type
* @param template The CloudFormation template
* @param config The global alarm configuration for resources of this type
* @returns The resources along with the merged configuration for each resource by logical ID
*/
export function getResourceAlarmConfigurationsByType<M extends SlicWatchMergedConfig> (
type: string, template: Template, config: M
type: ConfigType, template: Template, config: M
): ResourceAlarmConfigurations<M> {
const alarmConfigurations: Record<string, M> = {}
const resources = getResourcesByType(type, template)
const resources = getResourcesByType(cfTypeByConfigType[type], template)
for (const [funcLogicalId, resource] of Object.entries(resources)) {
alarmConfigurations[funcLogicalId] = merge({}, config, cascade(resource?.Metadata?.slicWatch?.alarms ?? {}) as M)
const resourceConfig = resource?.Metadata?.slicWatch?.alarms // Resource-specific overrides
const defaultResourceConfig = defaultConfig.alarms?.[type] // Default configuration for the type's alarms
// Cascade the default resource's configuration into the resource-specific overrides
const cascadedResourceConfig = resourceConfig !== undefined ? cascade(merge({}, defaultResourceConfig, resourceConfig)) : {}
// Lastly, cascade the full SLIC Watch config for any properties not yet set in the widget's config
alarmConfigurations[funcLogicalId] = cascade(merge({}, config, cascadedResourceConfig)) as M
}
return {
resources,
Expand All @@ -78,18 +86,23 @@ export function getResourceAlarmConfigurationsByType<M extends SlicWatchMergedCo
* Find all resources of a given type and merge any resource-specific SLIC Watch configuration with
* the global dashboard configuration for resources of that type
*
* @param type The CloudFormation resource type
* @param type The resource type
* @param template The CloudFormation template
* @param config The global dashboard configuration for resources of this type
* @returns The resources along with the merged configuration for each resource by logical ID
*/
export function getResourceDashboardConfigurationsByType<T extends WidgetMetricProperties> (
type: string, template: Template, config: T
type: ConfigType, template: Template, config: T
): ResourceDashboardConfigurations<T> {
const dashConfigurations: Record<string, T> = {}
const resources = getResourcesByType(type, template)
const resources = getResourcesByType(cfTypeByConfigType[type], template)
for (const [logicalId, resource] of Object.entries(resources)) {
dashConfigurations[logicalId] = cascade(merge({}, config, cascade(resource?.Metadata?.slicWatch?.dashboard ?? {}))) as T
const resourceConfig = resource?.Metadata?.slicWatch?.dashboard // Resource-specific overrides
const defaultResourceConfig = defaultConfig.dashboard?.widgets?.[type] // Default configuration for the widget
// Cascade the default resource's configuration into the resource-specific overrides
const cascadedResourceConfig = resourceConfig !== undefined ? cascade(merge({}, defaultResourceConfig, resourceConfig)) : {}
// Lastly, cascade the full SLIC Watch config for any properties not yet set in the widget's config
dashConfigurations[logicalId] = cascade(merge({}, config, cascadedResourceConfig)) as T
}
return {
resources,
Expand Down
25 changes: 13 additions & 12 deletions core/dashboards/dashboard-types.ts
@@ -1,4 +1,5 @@
import type { Widget } from 'cloudwatch-dashboard-types'
import { type ConfigType } from '../inputs/config-types'

export type YAxisPos = 'left' | 'right'

Expand Down Expand Up @@ -30,18 +31,18 @@ export interface WidgetMetricProperties {
}

export interface Widgets extends WidgetMetricProperties {
Lambda: LambdaDashboardProperties
ApiGateway: ApiGwDashboardProperties
States: SfDashboardProperties
DynamoDB: DynamoDbDashboardProperties
Kinesis: KinesisDashboardProperties
SQS: SqsDashboardProperties
ECS: EcsDashboardProperties
SNS: SnsDashboardProperties
Events: RuleDashboardProperties
ApplicationELB: AlbDashboardProperties
ApplicationELBTarget: AlbTargetDashboardProperties
AppSync: AppSyncDashboardProperties
[ConfigType.Lambda]: LambdaDashboardProperties
[ConfigType.ApiGateway]: ApiGwDashboardProperties
[ConfigType.States]: SfDashboardProperties
[ConfigType.DynamoDB]: DynamoDbDashboardProperties
[ConfigType.Kinesis]: KinesisDashboardProperties
[ConfigType.SQS]: SqsDashboardProperties
[ConfigType.ECS]: EcsDashboardProperties
[ConfigType.SNS]: SnsDashboardProperties
[ConfigType.Events]: RuleDashboardProperties
[ConfigType.ApplicationELB]: AlbDashboardProperties
[ConfigType.ApplicationELBTarget]: AlbTargetDashboardProperties
[ConfigType.AppSync]: AppSyncDashboardProperties
}

type NestedPartial<T> = {
Expand Down

0 comments on commit 4988f57

Please sign in to comment.