Skip to content

Commit

Permalink
Merge ba3df97 into 049c205
Browse files Browse the repository at this point in the history
  • Loading branch information
eoinsha committed Nov 14, 2023
2 parents 049c205 + ba3df97 commit abb5f17
Show file tree
Hide file tree
Showing 47 changed files with 1,208 additions and 992 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Expand Up @@ -17,8 +17,8 @@ jobs:
node-version: [18, 20]

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}.x
cache: 'npm'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Expand Up @@ -19,9 +19,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.x
registry-url: "https://registry.npmjs.org"
Expand Down
23 changes: 5 additions & 18 deletions cf-macro/index.ts
@@ -1,10 +1,9 @@
import _ from 'lodash'
import { type Template } from 'cloudform-types'
import pino from 'pino'

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

const logger = pino({ name: 'macroHandler' })
setLogger(logger)
Expand Down Expand Up @@ -37,23 +36,11 @@ export async function handler (event: Event): Promise<MacroResponse> {

const config = resolveSlicWatchConfig(slicWatchConfig)

const functionAlarmConfigs = {}
const functionDashboardConfigs = {}

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

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

_.merge(transformedTemplate)
addAlarms(config.alarms, functionAlarmConfigs, config.alarmActionsConfig, transformedTemplate)
addDashboard(config.dashboard, functionDashboardConfigs, transformedTemplate)
addAlarms(config.alarms, config.alarmActionsConfig, transformedTemplate)
addDashboard(config.dashboard, transformedTemplate)
outputFragment = transformedTemplate
} catch (err) {
logger.error(err)
logger.error({ err })
errorMessage = (err as Error).message
status = 'fail'
}
Expand Down
8 changes: 5 additions & 3 deletions cf-macro/tests/cdk-cf.test.ts
@@ -1,5 +1,5 @@
import { test } from 'tap'

import type { Template } from 'cloudform-types'
import { getResourcesByType } from 'slic-watch-core/cf-template'
import { handler } from '../index'
import cdkStack from './resources/cdk-ecs-cf.json'
Expand All @@ -9,11 +9,13 @@ import cdkStack from './resources/cdk-ecs-cf.json'
*/
test('ECS CDK stack', async (t) => {
const event = {
fragment: cdkStack
fragment: cdkStack as Template,
requestId: 'test'
}

const handlerResponse = await handler(event)
t.equal(handlerResponse.status, 'success')
const compiledTemplate = handlerResponse.fragment
const compiledTemplate = handlerResponse.fragment as Template

test('alarms are generated', (t) => {
const alarms = getResourcesByType('AWS::CloudWatch::Alarm', compiledTemplate)
Expand Down
19 changes: 11 additions & 8 deletions cf-macro/tests/index.test.ts
@@ -1,13 +1,14 @@
import { test } from 'tap'
import _ from 'lodash'
import type Template from 'cloudform-types/types/template'
import type { Template } from 'cloudform-types'
import type Resource from 'cloudform-types/types/resource'

import { handler } from '../index'
import _template from './event.json'

const template = _template as Template

const event = { fragment: template }
const event = { fragment: template, requestId: 'test' }

test('macro returns success', async t => {
const result = await handler(event)
Expand Down Expand Up @@ -37,7 +38,7 @@ test('macro uses topicArn if specified', async t => {
const result = await handler(eventWithTopic)
t.equal(result.status, 'success')
t.notOk(result.errorMessage)
t.same(result.fragment.Resources.slicWatchLambdaDurationAlarmHelloLambdaFunction.Properties.AlarmActions, [topicArn])
t.same(result?.fragment?.Resources?.slicWatchLambdaDurationAlarmHelloLambdaFunction?.Properties?.AlarmActions, [topicArn])
t.end()
})

Expand All @@ -51,21 +52,23 @@ test('Macro skips SLIC Watch if top-level enabled==false', async t => {
})

test('Macro adds dashboard and alarms if no function configuration is provided', async t => {
const functionResource: Resource = {
...event.fragment.Resources?.HelloLambdaFunction,
Metadata: {}
} as unknown as Resource

const testEvent = {
...event,
fragment: {
...event.fragment,
Resources: {
...event.fragment.Resources,
HelloLambdaFunction: {
...event.fragment.Resources?.HelloLambdaFunction,
Metadata: {}
}
HelloLambdaFunction: functionResource
}
}
}
const compiledTemplate = (await handler(testEvent)).fragment
t.same(compiledTemplate.Resources.Properties, template.Resources?.Properties)
t.same(compiledTemplate?.Resources?.Properties, template.Resources?.Properties)
t.end()
})

Expand Down
14 changes: 8 additions & 6 deletions core/alarms/alarm-types.ts
Expand Up @@ -23,29 +23,31 @@ export interface AlarmTemplate {
Properties: AlarmProperties
}

/**
* Alarm configuration type used *before* all mandatory fields have been applied
*/
export interface SlicWatchAlarmConfig extends Omit<AlarmProperties, OptionalAlarmProps> {
ComparisonOperator?: string
EvaluationPeriods?: number
enabled?: boolean
}

/**
* Alarm configuration type used *after* all mandatory fields have been applied
*/
export interface SlicWatchMergedConfig extends AlarmProperties {
enabled: boolean
}

export type InputOutput = SlicWatchAlarmConfig | SlicWatchMergedConfig

export interface ReturnAlarm {
resourceName: string
resource: Resource
}

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

export interface SlicWatchCascadedAlarmsConfig<T extends InputOutput> extends AlarmProperties {
export type SlicWatchCascadedAlarmsConfig<T extends InputOutput> = T & {
enabled: boolean
Lambda: SlicWatchLambdaAlarmsConfig<T>
ApiGateway: SlicWatchApiGwAlarmsConfig<T>
Expand Down
23 changes: 9 additions & 14 deletions core/alarms/alarm-utils.ts
Expand Up @@ -3,12 +3,7 @@ import type { AlarmProperties } from 'cloudform-types/types/cloudWatch/alarm'
import { pascal } from 'case'

import type { AlarmActionsConfig, AlarmTemplate, CloudFormationResources, OptionalAlarmProps, SlicWatchMergedConfig } from './alarm-types'
import { getResourcesByType } from '../cf-template'
import type { SlicWatchAlbAlarmsConfig } from './alb'
import type { SlicWatchDynamoDbAlarmsConfig } from './dynamodb'
import type { SlicWatchEventsAlarmsConfig } from './eventbridge'
import type { SlicWatchSnsAlarmsConfig } from './sns'
import type { SlicWatchSfAlarmsConfig } from './step-functions'
import { getResourceAlarmConfigurationsByType } from '../cf-template'

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

type CommonAlarmsConfigs = SlicWatchAlbAlarmsConfig<SlicWatchMergedConfig> | SlicWatchDynamoDbAlarmsConfig<SlicWatchMergedConfig> | SlicWatchEventsAlarmsConfig<SlicWatchMergedConfig> | SlicWatchSnsAlarmsConfig<SlicWatchMergedConfig> | SlicWatchSfAlarmsConfig<SlicWatchMergedConfig>

/**
* Create CloudFormation 'AWS::CloudWatch::Alarm' resources based on metrics for a specfic resources type
*
Expand All @@ -43,17 +36,18 @@ type CommonAlarmsConfigs = SlicWatchAlbAlarmsConfig<SlicWatchMergedConfig> | Sli
* @returns An object containing the alarm resources in CloudFormation syntax by logical ID
*/
export function createCfAlarms (
type: string, service: string, metrics: string[], config: CommonAlarmsConfigs, alarmActionsConfig: AlarmActionsConfig,
type: string, service: string, metrics: string[], config: SlicWatchMergedConfig, alarmActionsConfig: AlarmActionsConfig,
compiledTemplate: Template, genSpecificAlarmProps: SpecificAlarmPropertiesGeneratorFunction
): CloudFormationResources {
const resources: CloudFormationResources = {}
const resourcesOfType = getResourcesByType(type, compiledTemplate)
const resourceConfigs = getResourceAlarmConfigurationsByType(type, compiledTemplate, config)

for (const resourceLogicalId of Object.keys(resourcesOfType)) {
for (const resourceLogicalId of Object.keys(resourceConfigs.resources)) {
for (const metric of metrics) {
const { enabled, ...rest } = config[metric]
if (enabled !== false) {
const alarm = genSpecificAlarmProps(metric, resourceLogicalId, rest)
const mergedConfig = resourceConfigs.alarmConfigurations[resourceLogicalId][metric] as SlicWatchMergedConfig
const { enabled, ...rest } = mergedConfig
if (enabled) {
const alarm = genSpecificAlarmProps(metric, resourceLogicalId, mergedConfig)
const alarmLogicalId = makeAlarmLogicalId(service, pascal(resourceLogicalId), metric)
const resource = createAlarm({
MetricName: metric,
Expand All @@ -66,6 +60,7 @@ export function createCfAlarms (
}
return resources
}

/**
* Create a CloudFormation Alarm resourc
*
Expand Down
18 changes: 7 additions & 11 deletions core/alarms/alarms.ts
Expand Up @@ -2,14 +2,12 @@ import type Resource from 'cloudform-types/types/resource'
import type Template from 'cloudform-types/types/template'

import { cascade } from '../inputs/cascading-config'
import { applyAlarmConfig } from '../inputs/function-config'
import type {
AlarmActionsConfig, InputOutput,
AlarmActionsConfig,
SlicWatchAlarmConfig,
SlicWatchCascadedAlarmsConfig, SlicWatchMergedConfig
} from './alarm-types'

import type { FunctionAlarmProperties } from './lambda'
import createLambdaAlarms from './lambda'
import createApiGatewayAlarms from './api-gateway'
import createStatesAlarms from './step-functions'
Expand All @@ -34,7 +32,6 @@ import { addResource } from '../cf-template'
*/
export default function addAlarms (
alarmProperties: SlicWatchCascadedAlarmsConfig<SlicWatchAlarmConfig>,
functionAlarmProperties: FunctionAlarmProperties<InputOutput>,
alarmActionsConfig: AlarmActionsConfig,
compiledTemplate: Template
) {
Expand All @@ -50,12 +47,11 @@ export default function addAlarms (
Events: ruleConfig,
ApplicationELB: albConfig,
ApplicationELBTarget: albTargetConfig,
AppSync: appSyncConfig
AppSync: appSyncConfig,
enabled
} = cascade(alarmProperties) as SlicWatchCascadedAlarmsConfig<SlicWatchMergedConfig>

const cascadedFunctionAlarmProperties = applyAlarmConfig(lambdaConfig, functionAlarmProperties)

const funcsWithConfig: Array<{ config: SlicWatchAlarmConfig, alarmFunc: any }> = [
const funcsWithConfig: Array<{ config: SlicWatchMergedConfig, alarmFunc: any }> = [
{ config: apiGwConfig, alarmFunc: createApiGatewayAlarms },
{ config: sfConfig, alarmFunc: createStatesAlarms },
{ config: dynamoDbConfig, alarmFunc: createDynamoDbAlarms },
Expand All @@ -66,12 +62,12 @@ export default function addAlarms (
{ config: ruleConfig, alarmFunc: createRuleAlarms },
{ config: albConfig, alarmFunc: createAlbAlarms },
{ config: albTargetConfig, alarmFunc: createAlbTargetAlarms },
{ config: appSyncConfig, alarmFunc: createAppSyncAlarms }
{ config: appSyncConfig, alarmFunc: createAppSyncAlarms },
{ config: lambdaConfig, alarmFunc: createLambdaAlarms }
]
const resources = {}

if (alarmProperties.enabled) {
Object.assign(resources, createLambdaAlarms(cascadedFunctionAlarmProperties, alarmActionsConfig, compiledTemplate))
if (enabled) {
for (const { config, alarmFunc } of funcsWithConfig) {
Object.assign(resources, alarmFunc(config, alarmActionsConfig, compiledTemplate))
}
Expand Down
17 changes: 9 additions & 8 deletions core/alarms/alb-target-group.ts
Expand Up @@ -2,12 +2,12 @@ import type { AlarmProperties } from 'cloudform-types/types/cloudWatch/alarm'
import type Template from 'cloudform-types/types/template'
import { Fn } from 'cloudform'

import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchAlarmConfig, SlicWatchMergedConfig } from './alarm-types'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm, getStatisticName, makeAlarmLogicalId } from './alarm-utils'
import type { ResourceType } from '../cf-template'
import { getResourcesByType } from '../cf-template'
import { getResourceAlarmConfigurationsByType, getResourcesByType } from '../cf-template'

export interface SlicWatchAlbTargetAlarmsConfig<T extends InputOutput> extends SlicWatchAlarmConfig {
export type SlicWatchAlbTargetAlarmsConfig<T extends InputOutput> = T & {
HTTPCode_Target_5XX_Count: T
UnHealthyHostCount: T
LambdaInternalError: T
Expand Down Expand Up @@ -128,15 +128,16 @@ function createAlbTargetCfAlarm (
export default function createAlbTargetAlarms (
albTargetAlarmsConfig: SlicWatchAlbTargetAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const targetGroupResources = getResourcesByType('AWS::ElasticLoadBalancingV2::TargetGroup', compiledTemplate)
const resourceConfigs = getResourceAlarmConfigurationsByType('AWS::ElasticLoadBalancingV2::TargetGroup', compiledTemplate, albTargetAlarmsConfig)
const resources: CloudFormationResources = {}
for (const [targetGroupResourceName, targetGroupResource] of Object.entries(targetGroupResources)) {
const loadBalancerLogicalIds = findLoadBalancersForTargetGroup(targetGroupResourceName, compiledTemplate)
Object.assign(resources, createAlbTargetCfAlarm(targetGroupResourceName, executionMetrics, loadBalancerLogicalIds, albTargetAlarmsConfig, alarmActionsConfig))
for (const [targetGroupLogicalId, targetGroupResource] of Object.entries(resourceConfigs.resources)) {
const mergedConfig = resourceConfigs.alarmConfigurations[targetGroupLogicalId]
const loadBalancerLogicalIds = findLoadBalancersForTargetGroup(targetGroupLogicalId, compiledTemplate)
Object.assign(resources, createAlbTargetCfAlarm(targetGroupLogicalId, executionMetrics, loadBalancerLogicalIds, mergedConfig, alarmActionsConfig))

if (targetGroupResource.Properties?.TargetType === 'lambda') {
// Create additional alarms for Lambda-specific ALB metrics
Object.assign(resources, createAlbTargetCfAlarm(targetGroupResourceName, executionMetricsLambda, loadBalancerLogicalIds, albTargetAlarmsConfig, alarmActionsConfig))
Object.assign(resources, createAlbTargetCfAlarm(targetGroupLogicalId, executionMetricsLambda, loadBalancerLogicalIds, mergedConfig, alarmActionsConfig))
}
}
return resources
Expand Down
4 changes: 2 additions & 2 deletions core/alarms/alb.ts
Expand Up @@ -2,10 +2,10 @@ import type { AlarmProperties } from 'cloudform-types/types/cloudWatch/alarm'
import type Template from 'cloudform-types/types/template'
import { Fn } from 'cloudform'

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

export interface SlicWatchAlbAlarmsConfig<T extends InputOutput> extends SlicWatchAlarmConfig {
export type SlicWatchAlbAlarmsConfig<T extends InputOutput> = T & {
HTTPCode_ELB_5XX_Count: T
RejectedConnectionCount: T
}
Expand Down
18 changes: 9 additions & 9 deletions core/alarms/api-gateway.ts
Expand Up @@ -3,11 +3,11 @@ import type Resource from 'cloudform-types/types/resource'
import type Template from 'cloudform-types/types/template'
import { Fn } from 'cloudform'

import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchAlarmConfig, SlicWatchMergedConfig } from './alarm-types'
import type { AlarmActionsConfig, CloudFormationResources, InputOutput, SlicWatchMergedConfig } from './alarm-types'
import { createAlarm, getStatisticName, makeAlarmLogicalId } from './alarm-utils'
import { getResourcesByType } from '../cf-template'
import { getResourceAlarmConfigurationsByType } from '../cf-template'

export interface SlicWatchApiGwAlarmsConfig<T extends InputOutput> extends SlicWatchAlarmConfig {
export type SlicWatchApiGwAlarmsConfig<T extends InputOutput> = T & {
'5XXError': T
'4XXError': T
Latency: T
Expand Down Expand Up @@ -83,18 +83,18 @@ export default function createApiGatewayAlarms (
apiGwAlarmsConfig: SlicWatchApiGwAlarmsConfig<SlicWatchMergedConfig>, alarmActionsConfig: AlarmActionsConfig, compiledTemplate: Template
): CloudFormationResources {
const resources: CloudFormationResources = {}
const apiResources = getResourcesByType('AWS::ApiGateway::RestApi', compiledTemplate)
const configuredResources = getResourceAlarmConfigurationsByType('AWS::ApiGateway::RestApi', compiledTemplate, apiGwAlarmsConfig)

for (const [apiLogicalId, apiResource] of Object.entries(apiResources)) {
for (const [apiLogicalId, apiResource] of Object.entries(configuredResources.resources)) {
for (const metric of executionMetrics) {
const config: SlicWatchMergedConfig = apiGwAlarmsConfig[metric]
if (config.enabled) {
const { enabled, ...rest } = config
const mergedConfig: SlicWatchMergedConfig = configuredResources.alarmConfigurations[apiLogicalId][metric]
if (mergedConfig.enabled) {
const { enabled, ...rest } = mergedConfig
const apiName = resolveRestApiNameAsCfn(apiResource, apiLogicalId)
const apiNameForSub = resolveRestApiNameForSub(apiResource, apiLogicalId)
const apiAlarmProperties: AlarmProperties = {
AlarmName: Fn.Sub(`ApiGW_${metric}_${apiNameForSub}`, {}),
AlarmDescription: Fn.Sub(`API Gateway ${metric} ${getStatisticName(config)} for ${apiNameForSub} breaches ${config.Threshold}`, {}),
AlarmDescription: Fn.Sub(`API Gateway ${metric} ${getStatisticName(mergedConfig)} for ${apiNameForSub} breaches ${mergedConfig.Threshold}`, {}),
MetricName: metric,
Namespace: 'AWS/ApiGateway',
Dimensions: [{ Name: 'ApiName', Value: apiName }],
Expand Down

0 comments on commit abb5f17

Please sign in to comment.