Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
62 changes: 62 additions & 0 deletions apps/sim/app/api/tools/cloudwatch/mute-alarm/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
)
}
})
62 changes: 62 additions & 0 deletions apps/sim/app/api/tools/cloudwatch/unmute-alarm/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
)
}
})
54 changes: 53 additions & 1 deletion apps/sim/blocks/blocks/cloudwatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import type {
CloudWatchGetLogEventsResponse,
CloudWatchGetMetricStatisticsResponse,
CloudWatchListMetricsResponse,
CloudWatchMuteAlarmResponse,
CloudWatchPutMetricDataResponse,
CloudWatchQueryLogsResponse,
CloudWatchUnmuteAlarmResponse,
} from '@/tools/cloudwatch/types'

export const CloudWatchBlock: BlockConfig<
Expand All @@ -21,6 +23,8 @@ export const CloudWatchBlock: BlockConfig<
| CloudWatchListMetricsResponse
| CloudWatchGetMetricStatisticsResponse
| CloudWatchPutMetricDataResponse
| CloudWatchMuteAlarmResponse
| CloudWatchUnmuteAlarmResponse
> = {
type: 'cloudwatch',
name: 'CloudWatch',
Expand All @@ -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',
},
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) => {
Expand All @@ -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}`)
}
Expand Down Expand Up @@ -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}`)
}
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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',
Expand Down
43 changes: 43 additions & 0 deletions apps/sim/lib/api/contracts/tools/aws/cloudwatch-mute-alarm.ts
Original file line number Diff line number Diff line change
@@ -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'),
})
Comment thread
TheodoreSpeaks marked this conversation as resolved.

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
>
45 changes: 45 additions & 0 deletions apps/sim/lib/api/contracts/tools/aws/cloudwatch-unmute-alarm.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions apps/sim/tools/cloudwatch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
Loading
Loading