diff --git a/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts new file mode 100644 index 00000000000..0ab6c4aad8d --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts @@ -0,0 +1,96 @@ +import { + type AlarmType, + CloudWatchClient, + DescribeAlarmsCommand, + type StateValue, +} from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchDescribeAlarms') + +const DescribeAlarmsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + alarmNamePrefix: z.string().optional(), + stateValue: z.preprocess( + (v) => (v === '' ? undefined : v), + z.enum(['OK', 'ALARM', 'INSUFFICIENT_DATA']).optional() + ), + alarmType: z.preprocess( + (v) => (v === '' ? undefined : v), + z.enum(['MetricAlarm', 'CompositeAlarm']).optional() + ), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = DescribeAlarmsSchema.parse(body) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const command = new DescribeAlarmsCommand({ + ...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }), + ...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }), + ...(validatedData.alarmType && { AlarmTypes: [validatedData.alarmType as AlarmType] }), + ...(validatedData.limit !== undefined && { MaxRecords: validatedData.limit }), + }) + + const response = await client.send(command) + + const metricAlarms = (response.MetricAlarms ?? []).map((a) => ({ + alarmName: a.AlarmName ?? '', + alarmArn: a.AlarmArn ?? '', + stateValue: a.StateValue ?? 'UNKNOWN', + stateReason: a.StateReason ?? '', + metricName: a.MetricName, + namespace: a.Namespace, + comparisonOperator: a.ComparisonOperator, + threshold: a.Threshold, + evaluationPeriods: a.EvaluationPeriods, + stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(), + })) + + const compositeAlarms = (response.CompositeAlarms ?? []).map((a) => ({ + alarmName: a.AlarmName ?? '', + alarmArn: a.AlarmArn ?? '', + stateValue: a.StateValue ?? 'UNKNOWN', + stateReason: a.StateReason ?? '', + metricName: undefined, + namespace: undefined, + comparisonOperator: undefined, + threshold: undefined, + evaluationPeriods: undefined, + stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(), + })) + + return NextResponse.json({ + success: true, + output: { alarms: [...metricAlarms, ...compositeAlarms] }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to describe CloudWatch alarms' + logger.error('DescribeAlarms failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts new file mode 100644 index 00000000000..a10f46c4efa --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts @@ -0,0 +1,62 @@ +import { DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { createCloudWatchLogsClient } from '@/app/api/tools/cloudwatch/utils' + +const logger = createLogger('CloudWatchDescribeLogGroups') + +const DescribeLogGroupsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + prefix: z.string().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = DescribeLogGroupsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const command = new DescribeLogGroupsCommand({ + ...(validatedData.prefix && { logGroupNamePrefix: validatedData.prefix }), + ...(validatedData.limit !== undefined && { limit: validatedData.limit }), + }) + + const response = await client.send(command) + + const logGroups = (response.logGroups ?? []).map((lg) => ({ + logGroupName: lg.logGroupName ?? '', + arn: lg.arn ?? '', + storedBytes: lg.storedBytes ?? 0, + retentionInDays: lg.retentionInDays, + creationTime: lg.creationTime, + })) + + return NextResponse.json({ + success: true, + output: { logGroups }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to describe CloudWatch log groups' + logger.error('DescribeLogGroups failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/describe-log-streams/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-log-streams/route.ts new file mode 100644 index 00000000000..5c9c69d7a8b --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/describe-log-streams/route.ts @@ -0,0 +1,53 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchDescribeLogStreams') + +import { createCloudWatchLogsClient, describeLogStreams } from '@/app/api/tools/cloudwatch/utils' + +const DescribeLogStreamsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupName: z.string().min(1, 'Log group name is required'), + prefix: z.string().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = DescribeLogStreamsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const result = await describeLogStreams(client, validatedData.logGroupName, { + prefix: validatedData.prefix, + limit: validatedData.limit, + }) + + return NextResponse.json({ + success: true, + output: { logStreams: result.logStreams }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to describe CloudWatch log streams' + logger.error('DescribeLogStreams failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts b/apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts new file mode 100644 index 00000000000..78534999b7b --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts @@ -0,0 +1,61 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchGetLogEvents') + +import { createCloudWatchLogsClient, getLogEvents } from '@/app/api/tools/cloudwatch/utils' + +const GetLogEventsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupName: z.string().min(1, 'Log group name is required'), + logStreamName: z.string().min(1, 'Log stream name is required'), + startTime: z.number({ coerce: true }).int().optional(), + endTime: z.number({ coerce: true }).int().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = GetLogEventsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const result = await getLogEvents( + client, + validatedData.logGroupName, + validatedData.logStreamName, + { + startTime: validatedData.startTime, + endTime: validatedData.endTime, + limit: validatedData.limit, + } + ) + + return NextResponse.json({ + success: true, + output: { events: result.events }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to get CloudWatch log events' + logger.error('GetLogEvents failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts b/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts new file mode 100644 index 00000000000..1a510d3f12f --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts @@ -0,0 +1,97 @@ +import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchGetMetricStatistics') + +const GetMetricStatisticsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + namespace: z.string().min(1, 'Namespace is required'), + metricName: z.string().min(1, 'Metric name is required'), + startTime: z.number({ coerce: true }).int(), + endTime: z.number({ coerce: true }).int(), + period: z.number({ coerce: true }).int().min(1), + statistics: z.array(z.enum(['Average', 'Sum', 'Minimum', 'Maximum', 'SampleCount'])).min(1), + dimensions: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = GetMetricStatisticsSchema.parse(body) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + let parsedDimensions: { Name: string; Value: string }[] | undefined + if (validatedData.dimensions) { + try { + const dims = JSON.parse(validatedData.dimensions) + if (Array.isArray(dims)) { + parsedDimensions = dims.map((d: Record) => ({ + Name: d.name, + Value: d.value, + })) + } else if (typeof dims === 'object') { + parsedDimensions = Object.entries(dims).map(([name, value]) => ({ + Name: name, + Value: String(value), + })) + } + } catch { + throw new Error('Invalid dimensions JSON') + } + } + + const command = new GetMetricStatisticsCommand({ + Namespace: validatedData.namespace, + MetricName: validatedData.metricName, + StartTime: new Date(validatedData.startTime * 1000), + EndTime: new Date(validatedData.endTime * 1000), + Period: validatedData.period, + Statistics: validatedData.statistics, + ...(parsedDimensions && { Dimensions: parsedDimensions }), + }) + + const response = await client.send(command) + + const datapoints = (response.Datapoints ?? []) + .sort((a, b) => (a.Timestamp?.getTime() ?? 0) - (b.Timestamp?.getTime() ?? 0)) + .map((dp) => ({ + timestamp: dp.Timestamp ? Math.floor(dp.Timestamp.getTime() / 1000) : 0, + average: dp.Average, + sum: dp.Sum, + minimum: dp.Minimum, + maximum: dp.Maximum, + sampleCount: dp.SampleCount, + unit: dp.Unit, + })) + + return NextResponse.json({ + success: true, + output: { + label: response.Label ?? validatedData.metricName, + datapoints, + }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to get CloudWatch metric statistics' + logger.error('GetMetricStatistics failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts b/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts new file mode 100644 index 00000000000..ce2cbf80f9f --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts @@ -0,0 +1,67 @@ +import { CloudWatchClient, ListMetricsCommand } from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchListMetrics') + +const ListMetricsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + namespace: z.string().optional(), + metricName: z.string().optional(), + recentlyActive: z.boolean().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = ListMetricsSchema.parse(body) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const command = new ListMetricsCommand({ + ...(validatedData.namespace && { Namespace: validatedData.namespace }), + ...(validatedData.metricName && { MetricName: validatedData.metricName }), + ...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }), + }) + + const response = await client.send(command) + + const metrics = (response.Metrics ?? []).slice(0, validatedData.limit ?? 500).map((m) => ({ + namespace: m.Namespace ?? '', + metricName: m.MetricName ?? '', + dimensions: (m.Dimensions ?? []).map((d) => ({ + name: d.Name ?? '', + value: d.Value ?? '', + })), + })) + + return NextResponse.json({ + success: true, + output: { metrics }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to list CloudWatch metrics' + logger.error('ListMetrics failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/query-logs/route.ts b/apps/sim/app/api/tools/cloudwatch/query-logs/route.ts new file mode 100644 index 00000000000..d5aa13e05c2 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/query-logs/route.ts @@ -0,0 +1,72 @@ +import { StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchQueryLogs') + +import { createCloudWatchLogsClient, pollQueryResults } from '@/app/api/tools/cloudwatch/utils' + +const QueryLogsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupNames: z.array(z.string().min(1)).min(1, 'At least one log group name is required'), + queryString: z.string().min(1, 'Query string is required'), + startTime: z.number({ coerce: true }).int(), + endTime: z.number({ coerce: true }).int(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = QueryLogsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const startQueryCommand = new StartQueryCommand({ + logGroupNames: validatedData.logGroupNames, + queryString: validatedData.queryString, + startTime: validatedData.startTime, + endTime: validatedData.endTime, + ...(validatedData.limit !== undefined && { limit: validatedData.limit }), + }) + + const startQueryResponse = await client.send(startQueryCommand) + const queryId = startQueryResponse.queryId + + if (!queryId) { + throw new Error('Failed to start CloudWatch Log Insights query: no queryId returned') + } + + const result = await pollQueryResults(client, queryId) + + return NextResponse.json({ + success: true, + output: { + results: result.results, + statistics: result.statistics, + status: result.status, + }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'CloudWatch Log Insights query failed' + logger.error('QueryLogs failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/utils.ts b/apps/sim/app/api/tools/cloudwatch/utils.ts new file mode 100644 index 00000000000..47db3b541da --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/utils.ts @@ -0,0 +1,161 @@ +import { + CloudWatchLogsClient, + DescribeLogStreamsCommand, + GetLogEventsCommand, + GetQueryResultsCommand, + type ResultField, +} from '@aws-sdk/client-cloudwatch-logs' +import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' + +interface AwsCredentials { + region: string + accessKeyId: string + secretAccessKey: string +} + +export function createCloudWatchLogsClient(config: AwsCredentials): CloudWatchLogsClient { + return new CloudWatchLogsClient({ + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + }) +} + +interface PollOptions { + maxWaitMs?: number + pollIntervalMs?: number +} + +interface PollResult { + results: Record[] + statistics: { + bytesScanned: number + recordsMatched: number + recordsScanned: number + } + status: string +} + +function parseResultFields(fields: ResultField[] | undefined): Record { + const record: Record = {} + if (!fields) return record + for (const field of fields) { + if (field.field && field.value !== undefined) { + record[field.field] = field.value ?? '' + } + } + return record +} + +export async function pollQueryResults( + client: CloudWatchLogsClient, + queryId: string, + options: PollOptions = {} +): Promise { + const { maxWaitMs = DEFAULT_EXECUTION_TIMEOUT_MS, pollIntervalMs = 1_000 } = options + const startTime = Date.now() + + while (Date.now() - startTime < maxWaitMs) { + const command = new GetQueryResultsCommand({ queryId }) + const response = await client.send(command) + + const status = response.status ?? 'Unknown' + + if (status === 'Complete') { + return { + results: (response.results ?? []).map(parseResultFields), + statistics: { + bytesScanned: response.statistics?.bytesScanned ?? 0, + recordsMatched: response.statistics?.recordsMatched ?? 0, + recordsScanned: response.statistics?.recordsScanned ?? 0, + }, + status, + } + } + + if (status === 'Failed' || status === 'Cancelled') { + throw new Error(`CloudWatch Log Insights query ${status.toLowerCase()}`) + } + + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) + } + + // Timeout -- fetch one last time for partial results + const finalResponse = await client.send(new GetQueryResultsCommand({ queryId })) + return { + results: (finalResponse.results ?? []).map(parseResultFields), + statistics: { + bytesScanned: finalResponse.statistics?.bytesScanned ?? 0, + recordsMatched: finalResponse.statistics?.recordsMatched ?? 0, + recordsScanned: finalResponse.statistics?.recordsScanned ?? 0, + }, + status: `Timeout (last status: ${finalResponse.status ?? 'Unknown'})`, + } +} + +export async function describeLogStreams( + client: CloudWatchLogsClient, + logGroupName: string, + options?: { prefix?: string; limit?: number } +): Promise<{ + logStreams: { + logStreamName: string + lastEventTimestamp: number | undefined + firstEventTimestamp: number | undefined + creationTime: number | undefined + storedBytes: number + }[] +}> { + const hasPrefix = Boolean(options?.prefix) + const command = new DescribeLogStreamsCommand({ + logGroupName, + ...(hasPrefix + ? { orderBy: 'LogStreamName', logStreamNamePrefix: options!.prefix } + : { orderBy: 'LastEventTime', descending: true }), + ...(options?.limit !== undefined && { limit: options.limit }), + }) + + const response = await client.send(command) + return { + logStreams: (response.logStreams ?? []).map((ls) => ({ + logStreamName: ls.logStreamName ?? '', + lastEventTimestamp: ls.lastEventTimestamp, + firstEventTimestamp: ls.firstEventTimestamp, + creationTime: ls.creationTime, + storedBytes: ls.storedBytes ?? 0, + })), + } +} + +export async function getLogEvents( + client: CloudWatchLogsClient, + logGroupName: string, + logStreamName: string, + options?: { startTime?: number; endTime?: number; limit?: number } +): Promise<{ + events: { + timestamp: number | undefined + message: string | undefined + ingestionTime: number | undefined + }[] +}> { + const command = new GetLogEventsCommand({ + logGroupIdentifier: logGroupName, + logStreamName, + ...(options?.startTime !== undefined && { startTime: options.startTime * 1000 }), + ...(options?.endTime !== undefined && { endTime: options.endTime * 1000 }), + ...(options?.limit !== undefined && { limit: options.limit }), + startFromHead: true, + }) + + const response = await client.send(command) + return { + events: (response.events ?? []).map((e) => ({ + timestamp: e.timestamp, + message: e.message, + ingestionTime: e.ingestionTime, + })), + } +} diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts new file mode 100644 index 00000000000..c68bcf29430 --- /dev/null +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -0,0 +1,571 @@ +import { CloudWatchIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { IntegrationType } from '@/blocks/types' +import type { + CloudWatchDescribeAlarmsResponse, + CloudWatchDescribeLogGroupsResponse, + CloudWatchDescribeLogStreamsResponse, + CloudWatchGetLogEventsResponse, + CloudWatchGetMetricStatisticsResponse, + CloudWatchListMetricsResponse, + CloudWatchQueryLogsResponse, +} from '@/tools/cloudwatch/types' + +export const CloudWatchBlock: BlockConfig< + | CloudWatchQueryLogsResponse + | CloudWatchDescribeLogGroupsResponse + | CloudWatchDescribeLogStreamsResponse + | CloudWatchGetLogEventsResponse + | CloudWatchDescribeAlarmsResponse + | CloudWatchListMetricsResponse + | CloudWatchGetMetricStatisticsResponse +> = { + type: 'cloudwatch', + name: 'CloudWatch', + description: 'Query and monitor AWS CloudWatch logs, metrics, and alarms', + longDescription: + 'Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key.', + category: 'tools', + integrationType: IntegrationType.Analytics, + tags: ['cloud', 'monitoring'], + bgColor: 'linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)', + icon: CloudWatchIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Query Logs (Insights)', id: 'query_logs' }, + { label: 'Describe Log Groups', id: 'describe_log_groups' }, + { label: 'Get Log Events', id: 'get_log_events' }, + { label: 'Describe Log Streams', id: 'describe_log_streams' }, + { label: 'List Metrics', id: 'list_metrics' }, + { label: 'Get Metric Statistics', id: 'get_metric_statistics' }, + { label: 'Describe Alarms', id: 'describe_alarms' }, + ], + value: () => 'query_logs', + }, + { + id: 'awsRegion', + title: 'AWS Region', + type: 'short-input', + placeholder: 'us-east-1', + required: true, + }, + { + id: 'awsAccessKeyId', + title: 'AWS Access Key ID', + type: 'short-input', + placeholder: 'AKIA...', + password: true, + required: true, + }, + { + id: 'awsSecretAccessKey', + title: 'AWS Secret Access Key', + type: 'short-input', + placeholder: 'Your secret access key', + password: true, + required: true, + }, + // Query Logs fields + { + id: 'logGroupSelector', + title: 'Log Group', + type: 'file-selector', + canonicalParamId: 'logGroupNames', + selectorKey: 'cloudwatch.logGroups', + dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion'], + placeholder: 'Select a log group', + condition: { field: 'operation', value: 'query_logs' }, + required: { field: 'operation', value: 'query_logs' }, + mode: 'basic', + }, + { + id: 'logGroupNamesInput', + title: 'Log Group Names', + type: 'short-input', + canonicalParamId: 'logGroupNames', + placeholder: '/aws/lambda/my-func, /aws/ecs/my-service', + condition: { field: 'operation', value: 'query_logs' }, + required: { field: 'operation', value: 'query_logs' }, + mode: 'advanced', + }, + { + id: 'queryString', + title: 'Query', + type: 'code', + placeholder: 'fields @timestamp, @message\n| sort @timestamp desc\n| limit 20', + condition: { field: 'operation', value: 'query_logs' }, + required: { field: 'operation', value: 'query_logs' }, + wandConfig: { + enabled: true, + prompt: `Generate a CloudWatch Log Insights query based on the user's description. +The query language supports: fields, filter, stats, sort, limit, parse, display. +Common patterns: +- fields @timestamp, @message | sort @timestamp desc | limit 20 +- filter @message like /ERROR/ | stats count(*) by bin(1h) +- stats avg(duration) as avgDuration by functionName | sort avgDuration desc +- filter @message like /Exception/ | parse @message "* Exception: *" as prefix, errorMsg +- stats count(*) as requestCount by status | sort requestCount desc + +Return ONLY the query — no explanations, no markdown code blocks.`, + placeholder: 'Describe what you want to find in the logs...', + }, + }, + { + id: 'startTime', + title: 'Start Time (Unix epoch seconds)', + type: 'short-input', + placeholder: 'e.g., 1711900800', + condition: { + field: 'operation', + value: ['query_logs', 'get_log_events', 'get_metric_statistics'], + }, + required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, + }, + { + id: 'endTime', + title: 'End Time (Unix epoch seconds)', + type: 'short-input', + placeholder: 'e.g., 1711987200', + condition: { + field: 'operation', + value: ['query_logs', 'get_log_events', 'get_metric_statistics'], + }, + required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, + }, + // Describe Log Groups fields + { + id: 'prefix', + title: 'Log Group Name Prefix', + type: 'short-input', + placeholder: '/aws/lambda/', + condition: { field: 'operation', value: 'describe_log_groups' }, + }, + // Get Log Events / Describe Log Streams — shared log group selector + { + id: 'logGroupNameSelector', + title: 'Log Group', + type: 'file-selector', + canonicalParamId: 'logGroupName', + selectorKey: 'cloudwatch.logGroups', + dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion'], + placeholder: 'Select a log group', + condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + mode: 'basic', + }, + { + id: 'logGroupNameInput', + title: 'Log Group Name', + type: 'short-input', + canonicalParamId: 'logGroupName', + placeholder: '/aws/lambda/my-func', + condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + mode: 'advanced', + }, + // Describe Log Streams — stream prefix filter + { + id: 'streamPrefix', + title: 'Stream Name Prefix', + type: 'short-input', + placeholder: '2024/03/31/', + condition: { field: 'operation', value: 'describe_log_streams' }, + }, + // Get Log Events — log stream selector (cascading: depends on log group) + { + id: 'logStreamNameSelector', + title: 'Log Stream', + type: 'file-selector', + canonicalParamId: 'logStreamName', + selectorKey: 'cloudwatch.logStreams', + dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion', 'logGroupNameSelector'], + placeholder: 'Select a log stream', + condition: { field: 'operation', value: 'get_log_events' }, + required: { field: 'operation', value: 'get_log_events' }, + mode: 'basic', + }, + { + id: 'logStreamNameInput', + title: 'Log Stream Name', + type: 'short-input', + canonicalParamId: 'logStreamName', + placeholder: '2024/03/31/[$LATEST]abc123', + condition: { field: 'operation', value: 'get_log_events' }, + required: { field: 'operation', value: 'get_log_events' }, + mode: 'advanced', + }, + // List Metrics fields + { + id: 'metricNamespace', + title: 'Namespace', + type: 'short-input', + placeholder: 'e.g., AWS/EC2, AWS/Lambda, AWS/RDS', + condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'metricName', + title: 'Metric Name', + type: 'short-input', + placeholder: 'e.g., CPUUtilization, Invocations', + condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'recentlyActive', + title: 'Recently Active Only', + type: 'switch', + condition: { field: 'operation', value: 'list_metrics' }, + }, + // Get Metric Statistics fields + { + id: 'metricPeriod', + title: 'Period (seconds)', + type: 'short-input', + placeholder: 'e.g., 60, 300, 3600', + condition: { field: 'operation', value: 'get_metric_statistics' }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'metricStatistics', + title: 'Statistics', + type: 'dropdown', + options: [ + { label: 'Average', id: 'Average' }, + { label: 'Sum', id: 'Sum' }, + { label: 'Minimum', id: 'Minimum' }, + { label: 'Maximum', id: 'Maximum' }, + { label: 'Sample Count', id: 'SampleCount' }, + ], + condition: { field: 'operation', value: 'get_metric_statistics' }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'metricDimensions', + title: 'Dimensions', + type: 'table', + columns: ['name', 'value'], + condition: { field: 'operation', value: 'get_metric_statistics' }, + }, + // Describe Alarms fields + { + id: 'alarmNamePrefix', + title: 'Alarm Name Prefix', + type: 'short-input', + placeholder: 'my-service-', + condition: { field: 'operation', value: 'describe_alarms' }, + }, + { + id: 'stateValue', + title: 'State', + type: 'dropdown', + options: [ + { label: 'All States', id: '' }, + { label: 'OK', id: 'OK' }, + { label: 'ALARM', id: 'ALARM' }, + { label: 'INSUFFICIENT_DATA', id: 'INSUFFICIENT_DATA' }, + ], + condition: { field: 'operation', value: 'describe_alarms' }, + }, + { + id: 'alarmType', + title: 'Alarm Type', + type: 'dropdown', + options: [ + { label: 'All Types', id: '' }, + { label: 'Metric Alarm', id: 'MetricAlarm' }, + { label: 'Composite Alarm', id: 'CompositeAlarm' }, + ], + condition: { field: 'operation', value: 'describe_alarms' }, + }, + // Shared limit field + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { + field: 'operation', + value: [ + 'query_logs', + 'describe_log_groups', + 'get_log_events', + 'describe_log_streams', + 'list_metrics', + 'describe_alarms', + ], + }, + }, + ], + tools: { + access: [ + 'cloudwatch_query_logs', + 'cloudwatch_describe_log_groups', + 'cloudwatch_get_log_events', + 'cloudwatch_describe_log_streams', + 'cloudwatch_list_metrics', + 'cloudwatch_get_metric_statistics', + 'cloudwatch_describe_alarms', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'query_logs': + return 'cloudwatch_query_logs' + case 'describe_log_groups': + return 'cloudwatch_describe_log_groups' + case 'get_log_events': + return 'cloudwatch_get_log_events' + case 'describe_log_streams': + return 'cloudwatch_describe_log_streams' + case 'list_metrics': + return 'cloudwatch_list_metrics' + case 'get_metric_statistics': + return 'cloudwatch_get_metric_statistics' + case 'describe_alarms': + return 'cloudwatch_describe_alarms' + default: + throw new Error(`Invalid CloudWatch operation: ${params.operation}`) + } + }, + params: (params) => { + const { operation, startTime, endTime, limit, ...rest } = params + + const awsRegion = rest.awsRegion + const awsAccessKeyId = rest.awsAccessKeyId + const awsSecretAccessKey = rest.awsSecretAccessKey + const parsedLimit = limit ? Number.parseInt(String(limit), 10) : undefined + + switch (operation) { + case 'query_logs': { + const logGroupNames = rest.logGroupNames + if (!logGroupNames) { + throw new Error('Log group names are required') + } + if (!startTime) { + throw new Error('Start time is required') + } + if (!endTime) { + throw new Error('End time is required') + } + + const groupNames = + typeof logGroupNames === 'string' + ? logGroupNames + .split(',') + .map((n: string) => n.trim()) + .filter(Boolean) + : logGroupNames + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupNames: groupNames, + queryString: rest.queryString, + startTime: Number(startTime), + endTime: Number(endTime), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + } + + case 'describe_log_groups': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.prefix && { prefix: rest.prefix }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + + case 'get_log_events': { + if (!rest.logGroupName) { + throw new Error('Log group name is required') + } + if (!rest.logStreamName) { + throw new Error('Log stream name is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupName: rest.logGroupName, + logStreamName: rest.logStreamName, + ...(startTime && { startTime: Number(startTime) }), + ...(endTime && { endTime: Number(endTime) }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + } + + case 'describe_log_streams': { + if (!rest.logGroupName) { + throw new Error('Log group name is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupName: rest.logGroupName, + ...(rest.streamPrefix && { prefix: rest.streamPrefix }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + } + + case 'list_metrics': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.metricNamespace && { namespace: rest.metricNamespace }), + ...(rest.metricName && { metricName: rest.metricName }), + ...(rest.recentlyActive && { recentlyActive: true }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + + case 'get_metric_statistics': { + if (!rest.metricNamespace) { + throw new Error('Namespace is required') + } + if (!rest.metricName) { + throw new Error('Metric name is required') + } + if (!startTime) { + throw new Error('Start time is required') + } + if (!endTime) { + throw new Error('End time is required') + } + if (!rest.metricPeriod) { + throw new Error('Period is required') + } + + const stat = rest.metricStatistics + if (!stat) { + throw new Error('Statistics is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + namespace: rest.metricNamespace, + metricName: rest.metricName, + startTime: Number(startTime), + endTime: Number(endTime), + period: Number(rest.metricPeriod), + statistics: Array.isArray(stat) ? stat : [stat], + ...(rest.metricDimensions && { + dimensions: (() => { + const dims = rest.metricDimensions + if (typeof dims === 'string') return dims + if (Array.isArray(dims)) { + const obj: Record = {} + for (const row of dims) { + const name = row.cells?.name + const value = row.cells?.value + if (name && value !== undefined) obj[name] = String(value) + } + return JSON.stringify(obj) + } + return JSON.stringify(dims) + })(), + }), + } + } + + case 'describe_alarms': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.alarmNamePrefix && { alarmNamePrefix: rest.alarmNamePrefix }), + ...(rest.stateValue && { stateValue: rest.stateValue }), + ...(rest.alarmType && { alarmType: rest.alarmType }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + + default: + throw new Error(`Invalid CloudWatch operation: ${operation}`) + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'CloudWatch operation to perform' }, + awsRegion: { type: 'string', description: 'AWS region' }, + awsAccessKeyId: { type: 'string', description: 'AWS access key ID' }, + awsSecretAccessKey: { type: 'string', description: 'AWS secret access key' }, + logGroupNames: { type: 'string', description: 'Log group name(s) for query' }, + queryString: { type: 'string', description: 'CloudWatch Log Insights query string' }, + startTime: { type: 'string', description: 'Start time as Unix epoch seconds' }, + endTime: { type: 'string', description: 'End time as Unix epoch seconds' }, + prefix: { type: 'string', description: 'Log group name prefix filter' }, + logGroupName: { + type: 'string', + description: 'Log group name for get events / describe streams', + }, + logStreamName: { type: 'string', description: 'Log stream name for get events' }, + streamPrefix: { type: 'string', description: 'Log stream name prefix filter' }, + metricNamespace: { type: 'string', description: 'Metric namespace (e.g., AWS/EC2)' }, + metricName: { type: 'string', description: 'Metric name (e.g., CPUUtilization)' }, + recentlyActive: { type: 'boolean', description: 'Only show recently active metrics' }, + metricPeriod: { type: 'number', description: 'Granularity in seconds' }, + metricStatistics: { type: 'string', description: 'Statistic type (Average, Sum, etc.)' }, + metricDimensions: { type: 'json', description: 'Metric dimensions (Name/Value pairs)' }, + alarmNamePrefix: { type: 'string', description: 'Alarm name prefix filter' }, + stateValue: { + type: 'string', + description: 'Alarm state filter (OK, ALARM, INSUFFICIENT_DATA)', + }, + alarmType: { type: 'string', description: 'Alarm type filter (MetricAlarm, CompositeAlarm)' }, + limit: { type: 'number', description: 'Maximum number of results' }, + }, + outputs: { + results: { + type: 'array', + description: 'Log Insights query result rows', + }, + statistics: { + type: 'json', + description: 'Query statistics (bytesScanned, recordsMatched, recordsScanned)', + }, + status: { + type: 'string', + description: 'Query completion status', + }, + logGroups: { + type: 'array', + description: 'List of CloudWatch log groups', + }, + events: { + type: 'array', + description: 'Log events with timestamp and message', + }, + logStreams: { + type: 'array', + description: 'Log streams with metadata', + }, + metrics: { + type: 'array', + description: 'List of available metrics', + }, + label: { + type: 'string', + description: 'Metric label', + }, + datapoints: { + type: 'array', + description: 'Metric datapoints with timestamps and values', + }, + alarms: { + type: 'array', + description: 'CloudWatch alarms with state and configuration', + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 574fdd000eb..6d6fcedbbb0 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -23,6 +23,7 @@ import { CirclebackBlock } from '@/blocks/blocks/circleback' import { ClayBlock } from '@/blocks/blocks/clay' import { ClerkBlock } from '@/blocks/blocks/clerk' import { CloudflareBlock } from '@/blocks/blocks/cloudflare' +import { CloudWatchBlock } from '@/blocks/blocks/cloudwatch' import { ConditionBlock } from '@/blocks/blocks/condition' import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence' import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor' @@ -237,6 +238,7 @@ export const registry: Record = { chat_trigger: ChatTriggerBlock, circleback: CirclebackBlock, cloudflare: CloudflareBlock, + cloudwatch: CloudWatchBlock, clay: ClayBlock, clerk: ClerkBlock, condition: ConditionBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 6614c9f6036..549c73fe8dd 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4603,6 +4603,33 @@ export function SQSIcon(props: SVGProps) { ) } +export function CloudWatchIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function TextractIcon(props: SVGProps) { return ( = { })) }, }, + 'cloudwatch.logGroups': { + key: 'cloudwatch.logGroups', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'cloudwatch.logGroups', + context.awsAccessKeyId ?? 'none', + context.awsRegion ?? 'none', + ], + enabled: ({ context }) => + Boolean(context.awsAccessKeyId && context.awsSecretAccessKey && context.awsRegion), + fetchList: async ({ context, search }: SelectorQueryArgs) => { + const body = JSON.stringify({ + accessKeyId: context.awsAccessKeyId, + secretAccessKey: context.awsSecretAccessKey, + region: context.awsRegion, + ...(search && { prefix: search }), + }) + const data = await fetchJson<{ + output: { logGroups: { logGroupName: string }[] } + }>('/api/tools/cloudwatch/describe-log-groups', { + method: 'POST', + body, + }) + return (data.output?.logGroups || []).map((lg) => ({ + id: lg.logGroupName, + label: lg.logGroupName, + })) + }, + fetchById: async ({ detailId }: SelectorQueryArgs) => { + if (!detailId) return null + return { id: detailId, label: detailId } + }, + }, + 'cloudwatch.logStreams': { + key: 'cloudwatch.logStreams', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'cloudwatch.logStreams', + context.awsAccessKeyId ?? 'none', + context.awsRegion ?? 'none', + context.logGroupName ?? 'none', + ], + enabled: ({ context }) => + Boolean( + context.awsAccessKeyId && + context.awsSecretAccessKey && + context.awsRegion && + context.logGroupName + ), + fetchList: async ({ context, search }: SelectorQueryArgs) => { + const body = JSON.stringify({ + accessKeyId: context.awsAccessKeyId, + secretAccessKey: context.awsSecretAccessKey, + region: context.awsRegion, + logGroupName: context.logGroupName, + ...(search && { prefix: search }), + }) + const data = await fetchJson<{ + output: { logStreams: { logStreamName: string; lastEventTimestamp?: number }[] } + }>('/api/tools/cloudwatch/describe-log-streams', { + method: 'POST', + body, + }) + return (data.output?.logStreams || []).map((ls) => ({ + id: ls.logStreamName, + label: ls.logStreamName, + })) + }, + fetchById: async ({ detailId }: SelectorQueryArgs) => { + if (!detailId) return null + return { id: detailId, label: detailId } + }, + }, 'sim.workflows': { key: 'sim.workflows', staleTime: SELECTOR_STALE, diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index 5668d245226..bd5bcac547b 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -49,6 +49,8 @@ export type SelectorKey = | 'webflow.sites' | 'webflow.collections' | 'webflow.items' + | 'cloudwatch.logGroups' + | 'cloudwatch.logStreams' | 'sim.workflows' export interface SelectorOption { @@ -78,6 +80,10 @@ export interface SelectorContext { datasetId?: string serviceDeskId?: string impersonateUserEmail?: string + awsAccessKeyId?: string + awsSecretAccessKey?: string + awsRegion?: string + logGroupName?: string } export interface SelectorQueryArgs { diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index 4a1c22039a8..6f41759cffa 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -22,6 +22,10 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ 'datasetId', 'serviceDeskId', 'impersonateUserEmail', + 'awsAccessKeyId', + 'awsSecretAccessKey', + 'awsRegion', + 'logGroupName', ]) /** diff --git a/apps/sim/package.json b/apps/sim/package.json index 2b2ebded398..82d58472888 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -37,6 +37,8 @@ "@a2a-js/sdk": "0.3.7", "@anthropic-ai/sdk": "0.71.2", "@aws-sdk/client-bedrock-runtime": "3.940.0", + "@aws-sdk/client-cloudwatch": "3.940.0", + "@aws-sdk/client-cloudwatch-logs": "3.940.0", "@aws-sdk/client-dynamodb": "3.940.0", "@aws-sdk/client-rds-data": "3.940.0", "@aws-sdk/client-s3": "^3.779.0", diff --git a/apps/sim/tools/cloudwatch/describe_alarms.ts b/apps/sim/tools/cloudwatch/describe_alarms.ts new file mode 100644 index 00000000000..75913e76933 --- /dev/null +++ b/apps/sim/tools/cloudwatch/describe_alarms.ts @@ -0,0 +1,99 @@ +import type { + CloudWatchDescribeAlarmsParams, + CloudWatchDescribeAlarmsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const describeAlarmsTool: ToolConfig< + CloudWatchDescribeAlarmsParams, + CloudWatchDescribeAlarmsResponse +> = { + id: 'cloudwatch_describe_alarms', + name: 'CloudWatch Describe Alarms', + description: 'List and filter CloudWatch alarms', + version: '1.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', + }, + alarmNamePrefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter alarms by name prefix', + }, + stateValue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by alarm state (OK, ALARM, INSUFFICIENT_DATA)', + }, + alarmType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by alarm type (MetricAlarm, CompositeAlarm)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of alarms to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/describe-alarms', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.alarmNamePrefix && { alarmNamePrefix: params.alarmNamePrefix }), + ...(params.stateValue && { stateValue: params.stateValue }), + ...(params.alarmType && { alarmType: params.alarmType }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudWatch alarms') + } + + return { + success: true, + output: { + alarms: data.output.alarms, + }, + } + }, + + outputs: { + alarms: { + type: 'array', + description: 'List of CloudWatch alarms with state and configuration', + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/describe_log_groups.ts b/apps/sim/tools/cloudwatch/describe_log_groups.ts new file mode 100644 index 00000000000..8cd885e039a --- /dev/null +++ b/apps/sim/tools/cloudwatch/describe_log_groups.ts @@ -0,0 +1,82 @@ +import type { + CloudWatchDescribeLogGroupsParams, + CloudWatchDescribeLogGroupsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const describeLogGroupsTool: ToolConfig< + CloudWatchDescribeLogGroupsParams, + CloudWatchDescribeLogGroupsResponse +> = { + id: 'cloudwatch_describe_log_groups', + name: 'CloudWatch Describe Log Groups', + description: 'List available CloudWatch log groups', + version: '1.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', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter log groups by name prefix', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of log groups to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/describe-log-groups', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.prefix && { prefix: params.prefix }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudWatch log groups') + } + + return { + success: true, + output: { + logGroups: data.output.logGroups, + }, + } + }, + + outputs: { + logGroups: { type: 'array', description: 'List of CloudWatch log groups with metadata' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/describe_log_streams.ts b/apps/sim/tools/cloudwatch/describe_log_streams.ts new file mode 100644 index 00000000000..4508704b0a2 --- /dev/null +++ b/apps/sim/tools/cloudwatch/describe_log_streams.ts @@ -0,0 +1,92 @@ +import type { + CloudWatchDescribeLogStreamsParams, + CloudWatchDescribeLogStreamsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const describeLogStreamsTool: ToolConfig< + CloudWatchDescribeLogStreamsParams, + CloudWatchDescribeLogStreamsResponse +> = { + id: 'cloudwatch_describe_log_streams', + name: 'CloudWatch Describe Log Streams', + description: 'List log streams within a CloudWatch log group', + version: '1.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', + }, + logGroupName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log group name', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter log streams by name prefix', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of log streams to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/describe-log-streams', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupName: params.logGroupName, + ...(params.prefix && { prefix: params.prefix }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudWatch log streams') + } + + return { + success: true, + output: { + logStreams: data.output.logStreams, + }, + } + }, + + outputs: { + logStreams: { + type: 'array', + description: 'List of log streams with metadata', + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/get_log_events.ts b/apps/sim/tools/cloudwatch/get_log_events.ts new file mode 100644 index 00000000000..0844feba02e --- /dev/null +++ b/apps/sim/tools/cloudwatch/get_log_events.ts @@ -0,0 +1,106 @@ +import type { + CloudWatchGetLogEventsParams, + CloudWatchGetLogEventsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const getLogEventsTool: ToolConfig< + CloudWatchGetLogEventsParams, + CloudWatchGetLogEventsResponse +> = { + id: 'cloudwatch_get_log_events', + name: 'CloudWatch Get Log Events', + description: 'Retrieve log events from a specific CloudWatch log stream', + version: '1.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', + }, + logGroupName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log group name', + }, + logStreamName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log stream name', + }, + startTime: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start time as Unix epoch seconds', + }, + endTime: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End time as Unix epoch seconds', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of events to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/get-log-events', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupName: params.logGroupName, + logStreamName: params.logStreamName, + ...(params.startTime !== undefined && { startTime: params.startTime }), + ...(params.endTime !== undefined && { endTime: params.endTime }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to get CloudWatch log events') + } + + return { + success: true, + output: { + events: data.output.events, + }, + } + }, + + outputs: { + events: { + type: 'array', + description: 'Log events with timestamp, message, and ingestion time', + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/get_metric_statistics.ts b/apps/sim/tools/cloudwatch/get_metric_statistics.ts new file mode 100644 index 00000000000..d9c4e2c59c0 --- /dev/null +++ b/apps/sim/tools/cloudwatch/get_metric_statistics.ts @@ -0,0 +1,119 @@ +import type { + CloudWatchGetMetricStatisticsParams, + CloudWatchGetMetricStatisticsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const getMetricStatisticsTool: ToolConfig< + CloudWatchGetMetricStatisticsParams, + CloudWatchGetMetricStatisticsResponse +> = { + id: 'cloudwatch_get_metric_statistics', + name: 'CloudWatch Get Metric Statistics', + description: 'Get statistics for a CloudWatch metric over a time range', + version: '1.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', + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Metric namespace (e.g., AWS/EC2, AWS/Lambda)', + }, + metricName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Metric name (e.g., CPUUtilization, Invocations)', + }, + startTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Start time as Unix epoch seconds', + }, + endTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'End time as Unix epoch seconds', + }, + period: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Granularity in seconds (e.g., 60, 300, 3600)', + }, + statistics: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Statistics to retrieve (Average, Sum, Minimum, Maximum, SampleCount)', + }, + dimensions: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Dimensions as JSON (e.g., {"InstanceId": "i-1234"})', + }, + }, + + request: { + url: '/api/tools/cloudwatch/get-metric-statistics', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + namespace: params.namespace, + metricName: params.metricName, + startTime: params.startTime, + endTime: params.endTime, + period: params.period, + statistics: params.statistics, + ...(params.dimensions && { dimensions: params.dimensions }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to get CloudWatch metric statistics') + } + + return { + success: true, + output: { + label: data.output.label, + datapoints: data.output.datapoints, + }, + } + }, + + outputs: { + label: { type: 'string', description: 'Metric label' }, + datapoints: { type: 'array', description: 'Datapoints with timestamp and statistics values' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/index.ts b/apps/sim/tools/cloudwatch/index.ts new file mode 100644 index 00000000000..4ce796e168d --- /dev/null +++ b/apps/sim/tools/cloudwatch/index.ts @@ -0,0 +1,15 @@ +import { describeAlarmsTool } from '@/tools/cloudwatch/describe_alarms' +import { describeLogGroupsTool } from '@/tools/cloudwatch/describe_log_groups' +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 { queryLogsTool } from '@/tools/cloudwatch/query_logs' + +export const cloudwatchDescribeAlarmsTool = describeAlarmsTool +export const cloudwatchDescribeLogGroupsTool = describeLogGroupsTool +export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool +export const cloudwatchGetLogEventsTool = getLogEventsTool +export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool +export const cloudwatchListMetricsTool = listMetricsTool +export const cloudwatchQueryLogsTool = queryLogsTool diff --git a/apps/sim/tools/cloudwatch/list_metrics.ts b/apps/sim/tools/cloudwatch/list_metrics.ts new file mode 100644 index 00000000000..eb4754c0459 --- /dev/null +++ b/apps/sim/tools/cloudwatch/list_metrics.ts @@ -0,0 +1,96 @@ +import type { + CloudWatchListMetricsParams, + CloudWatchListMetricsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const listMetricsTool: ToolConfig< + CloudWatchListMetricsParams, + CloudWatchListMetricsResponse +> = { + id: 'cloudwatch_list_metrics', + name: 'CloudWatch List Metrics', + description: 'List available CloudWatch metrics', + version: '1.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', + }, + namespace: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by namespace (e.g., AWS/EC2, AWS/Lambda)', + }, + metricName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by metric name', + }, + recentlyActive: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Only show metrics active in the last 3 hours', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of metrics to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/list-metrics', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.namespace && { namespace: params.namespace }), + ...(params.metricName && { metricName: params.metricName }), + ...(params.recentlyActive && { recentlyActive: params.recentlyActive }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to list CloudWatch metrics') + } + + return { + success: true, + output: { + metrics: data.output.metrics, + }, + } + }, + + outputs: { + metrics: { type: 'array', description: 'List of metrics with namespace, name, and dimensions' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/query_logs.ts b/apps/sim/tools/cloudwatch/query_logs.ts new file mode 100644 index 00000000000..3031ff471db --- /dev/null +++ b/apps/sim/tools/cloudwatch/query_logs.ts @@ -0,0 +1,107 @@ +import type { + CloudWatchQueryLogsParams, + CloudWatchQueryLogsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const queryLogsTool: ToolConfig = { + id: 'cloudwatch_query_logs', + name: 'CloudWatch Query Logs', + description: 'Run a CloudWatch Log Insights query against one or more log groups', + version: '1.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', + }, + logGroupNames: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Log group names to query', + }, + queryString: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch Log Insights query string', + }, + startTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Start time as Unix epoch seconds', + }, + endTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'End time as Unix epoch seconds', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/query-logs', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupNames: params.logGroupNames, + queryString: params.queryString, + startTime: params.startTime, + endTime: params.endTime, + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'CloudWatch Log Insights query failed') + } + + return { + success: true, + output: { + results: data.output.results, + statistics: data.output.statistics, + status: data.output.status, + }, + } + }, + + outputs: { + results: { type: 'array', description: 'Query result rows' }, + statistics: { + type: 'object', + description: 'Query statistics (bytesScanned, recordsMatched, recordsScanned)', + }, + status: { type: 'string', description: 'Query completion status' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/types.ts b/apps/sim/tools/cloudwatch/types.ts new file mode 100644 index 00000000000..32259fa5a08 --- /dev/null +++ b/apps/sim/tools/cloudwatch/types.ts @@ -0,0 +1,146 @@ +import type { ToolResponse } from '@/tools/types' + +export interface CloudWatchConnectionConfig { + awsRegion: string + awsAccessKeyId: string + awsSecretAccessKey: string +} + +export interface CloudWatchQueryLogsParams extends CloudWatchConnectionConfig { + logGroupNames: string[] + queryString: string + startTime: number + endTime: number + limit?: number +} + +export interface CloudWatchDescribeLogGroupsParams extends CloudWatchConnectionConfig { + prefix?: string + limit?: number +} + +export interface CloudWatchGetLogEventsParams extends CloudWatchConnectionConfig { + logGroupName: string + logStreamName: string + startTime?: number + endTime?: number + limit?: number +} + +export interface CloudWatchQueryLogsResponse extends ToolResponse { + output: { + results: Record[] + statistics: { + bytesScanned: number + recordsMatched: number + recordsScanned: number + } + status: string + } +} + +export interface CloudWatchDescribeLogGroupsResponse extends ToolResponse { + output: { + logGroups: { + logGroupName: string + arn: string + storedBytes: number + retentionInDays: number | undefined + creationTime: number | undefined + }[] + } +} + +export interface CloudWatchGetLogEventsResponse extends ToolResponse { + output: { + events: { + timestamp: number | undefined + message: string | undefined + ingestionTime: number | undefined + }[] + } +} + +export interface CloudWatchDescribeLogStreamsParams extends CloudWatchConnectionConfig { + logGroupName: string + prefix?: string + limit?: number +} + +export interface CloudWatchDescribeLogStreamsResponse extends ToolResponse { + output: { + logStreams: { + logStreamName: string + lastEventTimestamp: number | undefined + firstEventTimestamp: number | undefined + creationTime: number | undefined + storedBytes: number + }[] + } +} + +export interface CloudWatchListMetricsParams extends CloudWatchConnectionConfig { + namespace?: string + metricName?: string + recentlyActive?: boolean + limit?: number +} + +export interface CloudWatchListMetricsResponse extends ToolResponse { + output: { + metrics: { + namespace: string + metricName: string + dimensions: { name: string; value: string }[] + }[] + } +} + +export interface CloudWatchGetMetricStatisticsParams extends CloudWatchConnectionConfig { + namespace: string + metricName: string + startTime: number + endTime: number + period: number + statistics: string[] + dimensions?: string +} + +export interface CloudWatchGetMetricStatisticsResponse extends ToolResponse { + output: { + label: string + datapoints: { + timestamp: number + average?: number + sum?: number + minimum?: number + maximum?: number + sampleCount?: number + unit?: string + }[] + } +} + +export interface CloudWatchDescribeAlarmsParams extends CloudWatchConnectionConfig { + alarmNamePrefix?: string + stateValue?: string + alarmType?: string + limit?: number +} + +export interface CloudWatchDescribeAlarmsResponse extends ToolResponse { + output: { + alarms: { + alarmName: string + alarmArn: string + stateValue: string + stateReason: string + metricName: string | undefined + namespace: string | undefined + comparisonOperator: string | undefined + threshold: number | undefined + evaluationPeriods: number | undefined + stateUpdatedTimestamp: number | undefined + }[] + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 09da0ce4bf5..2f5f68ad211 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -252,6 +252,15 @@ import { cloudflareUpdateDnsRecordTool, cloudflareUpdateZoneSettingTool, } from '@/tools/cloudflare' +import { + cloudwatchDescribeAlarmsTool, + cloudwatchDescribeLogGroupsTool, + cloudwatchDescribeLogStreamsTool, + cloudwatchGetLogEventsTool, + cloudwatchGetMetricStatisticsTool, + cloudwatchListMetricsTool, + cloudwatchQueryLogsTool, +} from '@/tools/cloudwatch' import { confluenceAddLabelTool, confluenceCreateBlogPostTool, @@ -3303,6 +3312,13 @@ export const tools: Record = { rds_delete: rdsDeleteTool, rds_execute: rdsExecuteTool, rds_introspect: rdsIntrospectTool, + cloudwatch_query_logs: cloudwatchQueryLogsTool, + cloudwatch_describe_log_groups: cloudwatchDescribeLogGroupsTool, + cloudwatch_describe_alarms: cloudwatchDescribeAlarmsTool, + cloudwatch_describe_log_streams: cloudwatchDescribeLogStreamsTool, + cloudwatch_get_log_events: cloudwatchGetLogEventsTool, + cloudwatch_list_metrics: cloudwatchListMetricsTool, + cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool, dynamodb_get: dynamodbGetTool, dynamodb_put: dynamodbPutTool, dynamodb_query: dynamodbQueryTool, diff --git a/bun.lock b/bun.lock index a9edfffb951..9a62972f278 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,8 @@ "@a2a-js/sdk": "0.3.7", "@anthropic-ai/sdk": "0.71.2", "@aws-sdk/client-bedrock-runtime": "3.940.0", + "@aws-sdk/client-cloudwatch": "3.940.0", + "@aws-sdk/client-cloudwatch-logs": "3.940.0", "@aws-sdk/client-dynamodb": "3.940.0", "@aws-sdk/client-rds-data": "3.940.0", "@aws-sdk/client-s3": "^3.779.0", @@ -414,6 +416,10 @@ "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/eventstream-handler-node": "3.936.0", "@aws-sdk/middleware-eventstream": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/middleware-websocket": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/token-providers": "3.940.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Gs6UUQP1zt8vahOxJ3BADcb3B+2KldUNA3bKa+KdK58de7N7tLJFJfZuXhFGGtwyNPh1aw6phtdP6dauq3OLWA=="], + "@aws-sdk/client-cloudwatch": ["@aws-sdk/client-cloudwatch@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-compression": "^4.3.12", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-C35xpPntRAGdEg3X5iKpSUCBaP3yxYNo1U95qipN/X1e0/TYIDWHwGt8Z1ntRafK19jp5oVzhRQ+PD1JAPSEzA=="], + + "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7dEIO3D98IxA9IhqixPJbzQsBkk4TchHHpFdd0JOhlSlihWhiwbf3ijUePJVXYJxcpRRtMmAMtDRLDzCSO+ZHg=="], + "@aws-sdk/client-dynamodb": ["@aws-sdk/client-dynamodb@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-endpoint-discovery": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-u2sXsNJazJbuHeWICvsj6RvNyJh3isedEfPvB21jK/kxcriK+dE/izlKC2cyxUjERCmku0zTFNzY9FhrLbYHjQ=="], "@aws-sdk/client-rds-data": ["@aws-sdk/client-rds-data@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-68NH61MvS48CVPfzBNCPdCG4KnNjM+Uj/3DSw7rT9PJvdML9ARS4M2Uqco9POPw+Aj20KBumsEUd6FMVcYBXAA=="], @@ -1348,6 +1354,8 @@ "@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="], + "@smithy/middleware-compression": ["@smithy/middleware-compression@4.3.42", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "fflate": "0.8.1", "tslib": "^2.6.2" } }, "sha512-Ys2R8N7oZ3b6p063lhk7paRbX1F9Ju8a8Bsrw2nFfsG8iHYpgfW6ijd7hJKqRe+Wq9ABfcmX3luBlEd+B5/jVA=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="], @@ -4122,6 +4130,10 @@ "@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], + "@smithy/middleware-compression/@smithy/core": ["@smithy/core@3.23.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="], + + "@smithy/middleware-compression/fflate": ["fflate@0.8.1", "", {}, "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ=="], + "@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -4662,6 +4674,8 @@ "@shikijs/rehype/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + "@smithy/middleware-compression/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], + "@trigger.dev/core/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], @@ -5130,6 +5144,8 @@ "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "@smithy/middleware-compression/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.1", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw=="], + "@trigger.dev/core/socket.io-client/engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], "@trigger.dev/core/socket.io-client/engine.io-client/xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.0.0", "", {}, "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="],