feat(cloudwatch): add mute and unmute alarm operations (#4602)

This commit is contained in:
Theodore Li
2026-05-14 14:54:35 -04:00
committed by GitHub
parent 044e034719
commit 4a9e248eac
11 changed files with 450 additions and 3 deletions
@@ -0,0 +1,62 @@
import { CloudWatchClient, DisableAlarmActionsCommand } from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchMuteAlarmContract } from '@/lib/api/contracts/tools/aws/cloudwatch-mute-alarm'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchMuteAlarm')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchMuteAlarmContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`Muting ${validatedData.alarmNames.length} CloudWatch alarm(s)`)
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const command = new DisableAlarmActionsCommand({
AlarmNames: validatedData.alarmNames,
})
await client.send(command)
logger.info(`Successfully muted ${validatedData.alarmNames.length} alarm(s)`)
return NextResponse.json({
success: true,
output: {
success: true,
alarmNames: validatedData.alarmNames,
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('MuteAlarm failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to mute CloudWatch alarm: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,62 @@
import { CloudWatchClient, EnableAlarmActionsCommand } from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchUnmuteAlarmContract } from '@/lib/api/contracts/tools/aws/cloudwatch-unmute-alarm'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchUnmuteAlarm')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchUnmuteAlarmContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`Unmuting ${validatedData.alarmNames.length} CloudWatch alarm(s)`)
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const command = new EnableAlarmActionsCommand({
AlarmNames: validatedData.alarmNames,
})
await client.send(command)
logger.info(`Successfully unmuted ${validatedData.alarmNames.length} alarm(s)`)
return NextResponse.json({
success: true,
output: {
success: true,
alarmNames: validatedData.alarmNames,
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('UnmuteAlarm failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to unmute CloudWatch alarm: ${toError(error).message}` },
{ status: 500 }
)
}
})
+53 -1
View File
@@ -8,8 +8,10 @@ import type {
CloudWatchGetLogEventsResponse,
CloudWatchGetMetricStatisticsResponse,
CloudWatchListMetricsResponse,
CloudWatchMuteAlarmResponse,
CloudWatchPutMetricDataResponse,
CloudWatchQueryLogsResponse,
CloudWatchUnmuteAlarmResponse,
} from '@/tools/cloudwatch/types'
export const CloudWatchBlock: BlockConfig<
@@ -21,6 +23,8 @@ export const CloudWatchBlock: BlockConfig<
| CloudWatchListMetricsResponse
| CloudWatchGetMetricStatisticsResponse
| CloudWatchPutMetricDataResponse
| CloudWatchMuteAlarmResponse
| CloudWatchUnmuteAlarmResponse
> = {
type: 'cloudwatch',
name: 'CloudWatch',
@@ -47,6 +51,8 @@ export const CloudWatchBlock: BlockConfig<
{ label: 'Get Metric Statistics', id: 'get_metric_statistics' },
{ label: 'Publish Metric', id: 'put_metric_data' },
{ label: 'Describe Alarms', id: 'describe_alarms' },
{ label: 'Mute Alarm', id: 'mute_alarm' },
{ label: 'Unmute Alarm', id: 'unmute_alarm' },
],
value: () => 'query_logs',
},
@@ -360,6 +366,14 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
value: () => '',
condition: { field: 'operation', value: 'describe_alarms' },
},
{
id: 'alarmNames',
title: 'Alarm Names',
type: 'short-input',
placeholder: 'my-alarm-1, my-alarm-2',
condition: { field: 'operation', value: ['mute_alarm', 'unmute_alarm'] },
required: { field: 'operation', value: ['mute_alarm', 'unmute_alarm'] },
},
{
id: 'limit',
title: 'Limit',
@@ -389,6 +403,8 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
'cloudwatch_get_metric_statistics',
'cloudwatch_put_metric_data',
'cloudwatch_describe_alarms',
'cloudwatch_mute_alarm',
'cloudwatch_unmute_alarm',
],
config: {
tool: (params) => {
@@ -409,6 +425,10 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
return 'cloudwatch_put_metric_data'
case 'describe_alarms':
return 'cloudwatch_describe_alarms'
case 'mute_alarm':
return 'cloudwatch_mute_alarm'
case 'unmute_alarm':
return 'cloudwatch_unmute_alarm'
default:
throw new Error(`Invalid CloudWatch operation: ${params.operation}`)
}
@@ -613,6 +633,33 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
...(parsedLimit !== undefined && { limit: parsedLimit }),
}
case 'mute_alarm':
case 'unmute_alarm': {
const alarmNames = rest.alarmNames
if (!alarmNames) {
throw new Error('Alarm names are required')
}
const names =
typeof alarmNames === 'string'
? alarmNames
.split(',')
.map((n: string) => n.trim())
.filter(Boolean)
: alarmNames
if (!Array.isArray(names) || names.length === 0) {
throw new Error('At least one alarm name is required')
}
return {
awsRegion,
awsAccessKeyId,
awsSecretAccessKey,
alarmNames: names,
}
}
default:
throw new Error(`Invalid CloudWatch operation: ${operation}`)
}
@@ -653,6 +700,7 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
description: 'Alarm state filter (OK, ALARM, INSUFFICIENT_DATA)',
},
alarmType: { type: 'string', description: 'Alarm type filter (MetricAlarm, CompositeAlarm)' },
alarmNames: { type: 'string', description: 'Comma-separated alarm names to mute or unmute' },
limit: { type: 'number', description: 'Maximum number of results' },
},
outputs: {
@@ -696,9 +744,13 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
type: 'array',
description: 'CloudWatch alarms with state and configuration',
},
alarmNames: {
type: 'array',
description: 'Names of the alarms that were muted or unmuted',
},
success: {
type: 'boolean',
description: 'Whether the published metric was successful',
description: 'Whether the operation completed successfully',
},
namespace: {
type: 'string',
@@ -0,0 +1,43 @@
import { z } from 'zod'
import type {
ContractBody,
ContractBodyInput,
ContractJsonResponse,
} from '@/lib/api/contracts/types'
import { defineRouteContract } from '@/lib/api/contracts/types'
import { validateAwsRegion } from '@/lib/core/security/input-validation'
const MuteAlarmSchema = z.object({
region: z
.string()
.min(1, 'AWS region is required')
.refine((v) => validateAwsRegion(v).isValid, {
message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)',
}),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
alarmNames: z
.array(z.string().min(1, 'Alarm name cannot be empty'))
.min(1, 'At least one alarm name is required')
.max(100, 'At most 100 alarm names are allowed per request'),
})
const MuteAlarmResponseSchema = z.object({
success: z.literal(true),
output: z.object({
success: z.literal(true),
alarmNames: z.array(z.string()),
}),
})
export const awsCloudwatchMuteAlarmContract = defineRouteContract({
method: 'POST',
path: '/api/tools/cloudwatch/mute-alarm',
body: MuteAlarmSchema,
response: { mode: 'json', schema: MuteAlarmResponseSchema },
})
export type AwsCloudwatchMuteAlarmRequest = ContractBodyInput<typeof awsCloudwatchMuteAlarmContract>
export type AwsCloudwatchMuteAlarmBody = ContractBody<typeof awsCloudwatchMuteAlarmContract>
export type AwsCloudwatchMuteAlarmResponse = ContractJsonResponse<
typeof awsCloudwatchMuteAlarmContract
>
@@ -0,0 +1,45 @@
import { z } from 'zod'
import type {
ContractBody,
ContractBodyInput,
ContractJsonResponse,
} from '@/lib/api/contracts/types'
import { defineRouteContract } from '@/lib/api/contracts/types'
import { validateAwsRegion } from '@/lib/core/security/input-validation'
const UnmuteAlarmSchema = z.object({
region: z
.string()
.min(1, 'AWS region is required')
.refine((v) => validateAwsRegion(v).isValid, {
message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)',
}),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
alarmNames: z
.array(z.string().min(1, 'Alarm name cannot be empty'))
.min(1, 'At least one alarm name is required')
.max(100, 'At most 100 alarm names are allowed per request'),
})
const UnmuteAlarmResponseSchema = z.object({
success: z.literal(true),
output: z.object({
success: z.literal(true),
alarmNames: z.array(z.string()),
}),
})
export const awsCloudwatchUnmuteAlarmContract = defineRouteContract({
method: 'POST',
path: '/api/tools/cloudwatch/unmute-alarm',
body: UnmuteAlarmSchema,
response: { mode: 'json', schema: UnmuteAlarmResponseSchema },
})
export type AwsCloudwatchUnmuteAlarmRequest = ContractBodyInput<
typeof awsCloudwatchUnmuteAlarmContract
>
export type AwsCloudwatchUnmuteAlarmBody = ContractBody<typeof awsCloudwatchUnmuteAlarmContract>
export type AwsCloudwatchUnmuteAlarmResponse = ContractJsonResponse<
typeof awsCloudwatchUnmuteAlarmContract
>
+4
View File
@@ -4,8 +4,10 @@ import { describeLogStreamsTool } from '@/tools/cloudwatch/describe_log_streams'
import { getLogEventsTool } from '@/tools/cloudwatch/get_log_events'
import { getMetricStatisticsTool } from '@/tools/cloudwatch/get_metric_statistics'
import { listMetricsTool } from '@/tools/cloudwatch/list_metrics'
import { muteAlarmTool } from '@/tools/cloudwatch/mute_alarm'
import { putMetricDataTool } from '@/tools/cloudwatch/put_metric_data'
import { queryLogsTool } from '@/tools/cloudwatch/query_logs'
import { unmuteAlarmTool } from '@/tools/cloudwatch/unmute_alarm'
export * from './types'
@@ -15,5 +17,7 @@ export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool
export const cloudwatchGetLogEventsTool = getLogEventsTool
export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool
export const cloudwatchListMetricsTool = listMetricsTool
export const cloudwatchMuteAlarmTool = muteAlarmTool
export const cloudwatchPutMetricDataTool = putMetricDataTool
export const cloudwatchQueryLogsTool = queryLogsTool
export const cloudwatchUnmuteAlarmTool = unmuteAlarmTool
+75
View File
@@ -0,0 +1,75 @@
import type {
CloudWatchMuteAlarmParams,
CloudWatchMuteAlarmResponse,
} from '@/tools/cloudwatch/types'
import type { ToolConfig } from '@/tools/types'
export const muteAlarmTool: ToolConfig<CloudWatchMuteAlarmParams, CloudWatchMuteAlarmResponse> = {
id: 'cloudwatch_mute_alarm',
name: 'CloudWatch Mute Alarm',
description: 'Disable notification actions on one or more CloudWatch alarms',
version: '1.0.0',
params: {
awsRegion: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
awsAccessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
awsSecretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
alarmNames: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'Names of the CloudWatch alarms to mute',
},
},
request: {
url: '/api/tools/cloudwatch/mute-alarm',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.awsRegion,
accessKeyId: params.awsAccessKeyId,
secretAccessKey: params.awsSecretAccessKey,
alarmNames: params.alarmNames,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to mute CloudWatch alarm')
}
return {
success: true,
output: data.output,
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the alarms were muted successfully' },
alarmNames: {
type: 'array',
description: 'Names of the alarms that were muted',
items: { type: 'string' },
},
},
}
+22
View File
@@ -163,3 +163,25 @@ export interface CloudWatchPutMetricDataResponse extends ToolResponse {
timestamp: string
}
}
export interface CloudWatchMuteAlarmParams extends CloudWatchConnectionConfig {
alarmNames: string[]
}
export interface CloudWatchMuteAlarmResponse extends ToolResponse {
output: {
success: boolean
alarmNames: string[]
}
}
export interface CloudWatchUnmuteAlarmParams extends CloudWatchConnectionConfig {
alarmNames: string[]
}
export interface CloudWatchUnmuteAlarmResponse extends ToolResponse {
output: {
success: boolean
alarmNames: string[]
}
}
+78
View File
@@ -0,0 +1,78 @@
import type {
CloudWatchUnmuteAlarmParams,
CloudWatchUnmuteAlarmResponse,
} from '@/tools/cloudwatch/types'
import type { ToolConfig } from '@/tools/types'
export const unmuteAlarmTool: ToolConfig<
CloudWatchUnmuteAlarmParams,
CloudWatchUnmuteAlarmResponse
> = {
id: 'cloudwatch_unmute_alarm',
name: 'CloudWatch Unmute Alarm',
description: 'Re-enable notification actions on one or more CloudWatch alarms',
version: '1.0.0',
params: {
awsRegion: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
awsAccessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
awsSecretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
alarmNames: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'Names of the CloudWatch alarms to unmute',
},
},
request: {
url: '/api/tools/cloudwatch/unmute-alarm',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.awsRegion,
accessKeyId: params.awsAccessKeyId,
secretAccessKey: params.awsSecretAccessKey,
alarmNames: params.alarmNames,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to unmute CloudWatch alarm')
}
return {
success: true,
output: data.output,
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the alarms were unmuted successfully' },
alarmNames: {
type: 'array',
description: 'Names of the alarms that were unmuted',
items: { type: 'string' },
},
},
}
+4
View File
@@ -350,8 +350,10 @@ import {
cloudwatchGetLogEventsTool,
cloudwatchGetMetricStatisticsTool,
cloudwatchListMetricsTool,
cloudwatchMuteAlarmTool,
cloudwatchPutMetricDataTool,
cloudwatchQueryLogsTool,
cloudwatchUnmuteAlarmTool,
} from '@/tools/cloudwatch'
import {
confluenceAddLabelTool,
@@ -3813,8 +3815,10 @@ export const tools: Record<string, ToolConfig> = {
cloudwatch_get_log_events: cloudwatchGetLogEventsTool,
cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool,
cloudwatch_list_metrics: cloudwatchListMetricsTool,
cloudwatch_mute_alarm: cloudwatchMuteAlarmTool,
cloudwatch_put_metric_data: cloudwatchPutMetricDataTool,
cloudwatch_query_logs: cloudwatchQueryLogsTool,
cloudwatch_unmute_alarm: cloudwatchUnmuteAlarmTool,
crowdstrike_get_sensor_aggregates: crowdstrikeGetSensorAggregatesTool,
crowdstrike_get_sensor_details: crowdstrikeGetSensorDetailsTool,
crowdstrike_query_sensors: crowdstrikeQuerySensorsTool,
+2 -2
View File
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
const BASELINE = {
totalRoutes: 736,
zodRoutes: 736,
totalRoutes: 738,
zodRoutes: 738,
nonZodRoutes: 0,
} as const