Skip to content

Commit 139213e

Browse files
TheodoreSpeaksTheodore Liwaleedlatif1claude
authored
feat(block): Add cloudwatch publish operation (#4027)
* feat(block): Add cloudwatch publish operation * fix(integrations): validate and fix cloudwatch, cloudformation, athena conventions - Update tool version strings from '1.0' to '1.0.0' across all three integrations - Add missing `export * from './types'` barrel re-exports (cloudwatch, cloudformation) - Add docsLink, wandConfig timestamps, mode: 'advanced' on optional fields (cloudwatch) - Add dropdown defaults, ZodError handling, docs intro section (cloudwatch) - Add mode: 'advanced' on limit field (cloudformation) - Alphabetize registry entries (cloudwatch, cloudformation) - Fix athena docs maxResults range (1-999) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cloudwatch): complete put_metric_data unit dropdown, add missing outputs, fix JSON error handling - Add all 27 valid CloudWatch StandardUnit values to metricUnit dropdown (was 13) - Add missing block outputs for put_metric_data: success, namespace, metricName, value, unit - Add try-catch around dimensions JSON.parse in put-metric-data route for proper 400 errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cloudwatch): fix DescribeAlarms returning only MetricAlarm when "All Types" selected Per AWS docs, omitting AlarmTypes returns only MetricAlarm. Now explicitly sends both MetricAlarm and CompositeAlarm when no filter is selected. Also fix dimensions JSON parse errors returning 500 instead of 400 in get-metric-statistics route. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cloudwatch): validate dimensions JSON at Zod schema level Move dimensions validation from runtime try-catch to Zod refinement, catching malformed JSON and arrays at schema validation time (400) instead of runtime (500). Also rejects JSON arrays that would produce meaningless numeric dimension names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cloudwatch): reject non-numeric metricValue instead of silently publishing 0 Add NaN guard in block config and .finite() refinement in Zod schema so "abc" → NaN is caught at both layers instead of coercing to 0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cloudwatch): use Number.isFinite to also reject Infinity in block config Aligns block-level validation with route's Zod .finite() refinement so Infinity/-Infinity are caught at the block config layer, not just the API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Theodore Li <teddy@zenobiapay.com> Co-authored-by: Waleed Latif <walif6@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a8468a6 commit 139213e

35 files changed

+522
-47
lines changed

apps/docs/content/docs/en/tools/athena.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Retrieve the results of a completed Athena query execution
113113
| `awsAccessKeyId` | string | Yes | AWS access key ID |
114114
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
115115
| `queryExecutionId` | string | Yes | Query execution ID to get results for |
116-
| `maxResults` | number | No | Maximum number of rows to return \(1-1000\) |
116+
| `maxResults` | number | No | Maximum number of rows to return \(1-999\) |
117117
| `nextToken` | string | No | Pagination token from a previous request |
118118

119119
#### Output

apps/docs/content/docs/en/tools/cloudwatch.mdx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
1010
color="linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)"
1111
/>
1212

13+
{/* MANUAL-CONTENT-START:intro */}
14+
[AWS CloudWatch](https://aws.amazon.com/cloudwatch/) is a monitoring and observability service that provides data and actionable insights for AWS resources, applications, and services. CloudWatch collects monitoring and operational data in the form of logs, metrics, and events, giving you a unified view of your AWS environment.
15+
16+
With the CloudWatch integration, you can:
17+
18+
- **Query Logs (Insights)**: Run CloudWatch Log Insights queries against one or more log groups to analyze log data with a powerful query language
19+
- **Describe Log Groups**: List available CloudWatch log groups in your account, optionally filtered by name prefix
20+
- **Get Log Events**: Retrieve log events from a specific log stream within a log group
21+
- **Describe Log Streams**: List log streams within a log group, ordered by last event time or filtered by name prefix
22+
- **List Metrics**: Browse available CloudWatch metrics, optionally filtered by namespace, metric name, or recent activity
23+
- **Get Metric Statistics**: Retrieve statistical data for a metric over a specified time range with configurable granularity
24+
- **Publish Metric**: Publish custom metric data points to CloudWatch for your own application monitoring
25+
- **Describe Alarms**: List and filter CloudWatch alarms by name prefix, state, or alarm type
26+
27+
In Sim, the CloudWatch integration enables your agents to monitor AWS infrastructure, analyze application logs, track custom metrics, and respond to alarm states as part of automated DevOps and SRE workflows. This is especially powerful when combined with other AWS integrations like CloudFormation and SNS for end-to-end infrastructure management.
28+
{/* MANUAL-CONTENT-END */}
29+
30+
1331
## Usage Instructions
1432

1533
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.
@@ -155,6 +173,34 @@ Get statistics for a CloudWatch metric over a time range
155173
| `label` | string | Metric label |
156174
| `datapoints` | array | Datapoints with timestamp and statistics values |
157175

176+
### `cloudwatch_put_metric_data`
177+
178+
Publish a custom metric data point to CloudWatch
179+
180+
#### Input
181+
182+
| Parameter | Type | Required | Description |
183+
| --------- | ---- | -------- | ----------- |
184+
| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) |
185+
| `awsAccessKeyId` | string | Yes | AWS access key ID |
186+
| `awsSecretAccessKey` | string | Yes | AWS secret access key |
187+
| `namespace` | string | Yes | Metric namespace \(e.g., Custom/MyApp\) |
188+
| `metricName` | string | Yes | Name of the metric |
189+
| `value` | number | Yes | Metric value to publish |
190+
| `unit` | string | No | Unit of the metric \(e.g., Count, Seconds, Bytes\) |
191+
| `dimensions` | string | No | JSON string of dimension name/value pairs |
192+
193+
#### Output
194+
195+
| Parameter | Type | Description |
196+
| --------- | ---- | ----------- |
197+
| `success` | boolean | Whether the metric was published successfully |
198+
| `namespace` | string | Metric namespace |
199+
| `metricName` | string | Metric name |
200+
| `value` | number | Published metric value |
201+
| `unit` | string | Metric unit |
202+
| `timestamp` | string | Timestamp when the metric was published |
203+
158204
### `cloudwatch_describe_alarms`
159205

160206
List and filter CloudWatch alarms

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2044,12 +2044,16 @@
20442044
"name": "Get Metric Statistics",
20452045
"description": "Get statistics for a CloudWatch metric over a time range"
20462046
},
2047+
{
2048+
"name": "Publish Metric",
2049+
"description": "Publish a custom metric data point to CloudWatch"
2050+
},
20472051
{
20482052
"name": "Describe Alarms",
20492053
"description": "List and filter CloudWatch alarms"
20502054
}
20512055
],
2052-
"operationCount": 7,
2056+
"operationCount": 8,
20532057
"triggers": [],
20542058
"triggerCount": 0,
20552059
"authType": "none",

apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ export async function POST(request: NextRequest) {
5151
const command = new DescribeAlarmsCommand({
5252
...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }),
5353
...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }),
54-
...(validatedData.alarmType && { AlarmTypes: [validatedData.alarmType as AlarmType] }),
54+
AlarmTypes: validatedData.alarmType
55+
? [validatedData.alarmType as AlarmType]
56+
: (['MetricAlarm', 'CompositeAlarm'] as AlarmType[]),
5557
...(validatedData.limit !== undefined && { MaxRecords: validatedData.limit }),
5658
})
5759

apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function POST(request: NextRequest) {
5353
}))
5454
}
5555
} catch {
56-
throw new Error('Invalid dimensions JSON')
56+
return NextResponse.json({ error: 'Invalid dimensions JSON format' }, { status: 400 })
5757
}
5858
}
5959

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {
2+
CloudWatchClient,
3+
PutMetricDataCommand,
4+
type StandardUnit,
5+
} from '@aws-sdk/client-cloudwatch'
6+
import { createLogger } from '@sim/logger'
7+
import { type NextRequest, NextResponse } from 'next/server'
8+
import { z } from 'zod'
9+
import { checkInternalAuth } from '@/lib/auth/hybrid'
10+
11+
const logger = createLogger('CloudWatchPutMetricData')
12+
13+
const VALID_UNITS = [
14+
'Seconds',
15+
'Microseconds',
16+
'Milliseconds',
17+
'Bytes',
18+
'Kilobytes',
19+
'Megabytes',
20+
'Gigabytes',
21+
'Terabytes',
22+
'Bits',
23+
'Kilobits',
24+
'Megabits',
25+
'Gigabits',
26+
'Terabits',
27+
'Percent',
28+
'Count',
29+
'Bytes/Second',
30+
'Kilobytes/Second',
31+
'Megabytes/Second',
32+
'Gigabytes/Second',
33+
'Terabytes/Second',
34+
'Bits/Second',
35+
'Kilobits/Second',
36+
'Megabits/Second',
37+
'Gigabits/Second',
38+
'Terabits/Second',
39+
'Count/Second',
40+
'None',
41+
] as const
42+
43+
const PutMetricDataSchema = z.object({
44+
region: z.string().min(1, 'AWS region is required'),
45+
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
46+
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
47+
namespace: z.string().min(1, 'Namespace is required'),
48+
metricName: z.string().min(1, 'Metric name is required'),
49+
value: z.number({ coerce: true }).refine((v) => Number.isFinite(v), {
50+
message: 'Metric value must be a finite number',
51+
}),
52+
unit: z.enum(VALID_UNITS).optional(),
53+
dimensions: z
54+
.string()
55+
.optional()
56+
.refine(
57+
(val) => {
58+
if (!val) return true
59+
try {
60+
const parsed = JSON.parse(val)
61+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
62+
} catch {
63+
return false
64+
}
65+
},
66+
{ message: 'dimensions must be a valid JSON object string' }
67+
),
68+
})
69+
70+
export async function POST(request: NextRequest) {
71+
try {
72+
const auth = await checkInternalAuth(request)
73+
if (!auth.success || !auth.userId) {
74+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
75+
}
76+
77+
const body = await request.json()
78+
const validatedData = PutMetricDataSchema.parse(body)
79+
80+
const client = new CloudWatchClient({
81+
region: validatedData.region,
82+
credentials: {
83+
accessKeyId: validatedData.accessKeyId,
84+
secretAccessKey: validatedData.secretAccessKey,
85+
},
86+
})
87+
88+
const timestamp = new Date()
89+
90+
const dimensions: { Name: string; Value: string }[] = []
91+
if (validatedData.dimensions) {
92+
const parsed = JSON.parse(validatedData.dimensions)
93+
for (const [name, value] of Object.entries(parsed)) {
94+
dimensions.push({ Name: name, Value: String(value) })
95+
}
96+
}
97+
98+
const command = new PutMetricDataCommand({
99+
Namespace: validatedData.namespace,
100+
MetricData: [
101+
{
102+
MetricName: validatedData.metricName,
103+
Value: validatedData.value,
104+
Timestamp: timestamp,
105+
...(validatedData.unit && { Unit: validatedData.unit as StandardUnit }),
106+
...(dimensions.length > 0 && { Dimensions: dimensions }),
107+
},
108+
],
109+
})
110+
111+
await client.send(command)
112+
113+
return NextResponse.json({
114+
success: true,
115+
output: {
116+
success: true,
117+
namespace: validatedData.namespace,
118+
metricName: validatedData.metricName,
119+
value: validatedData.value,
120+
unit: validatedData.unit ?? 'None',
121+
timestamp: timestamp.toISOString(),
122+
},
123+
})
124+
} catch (error) {
125+
if (error instanceof z.ZodError) {
126+
return NextResponse.json(
127+
{ error: error.errors[0]?.message ?? 'Invalid request' },
128+
{ status: 400 }
129+
)
130+
}
131+
const errorMessage =
132+
error instanceof Error ? error.message : 'Failed to publish CloudWatch metric'
133+
logger.error('PutMetricData failed', { error: errorMessage })
134+
return NextResponse.json({ error: errorMessage }, { status: 500 })
135+
}
136+
}

apps/sim/blocks/blocks/cloudformation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const CloudFormationBlock: BlockConfig<
117117
type: 'short-input',
118118
placeholder: '50',
119119
condition: { field: 'operation', value: 'describe_stack_events' },
120+
mode: 'advanced',
120121
},
121122
],
122123
tools: {

0 commit comments

Comments
 (0)