Skip to content

Commit 044612d

Browse files
Theodore Liwaleedlatif1
authored andcommitted
feat(block): Add cloudwatch publish operation
1 parent c833492 commit 044612d

File tree

6 files changed

+355
-6
lines changed

6 files changed

+355
-6
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 }),
50+
unit: z.enum(VALID_UNITS).optional(),
51+
dimensions: z.string().optional(),
52+
})
53+
54+
export async function POST(request: NextRequest) {
55+
try {
56+
const auth = await checkInternalAuth(request)
57+
if (!auth.success || !auth.userId) {
58+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
59+
}
60+
61+
const body = await request.json()
62+
const validatedData = PutMetricDataSchema.parse(body)
63+
64+
const client = new CloudWatchClient({
65+
region: validatedData.region,
66+
credentials: {
67+
accessKeyId: validatedData.accessKeyId,
68+
secretAccessKey: validatedData.secretAccessKey,
69+
},
70+
})
71+
72+
const timestamp = new Date()
73+
74+
const dimensions: { Name: string; Value: string }[] = []
75+
if (validatedData.dimensions) {
76+
const parsed = JSON.parse(validatedData.dimensions)
77+
if (typeof parsed === 'object' && parsed !== null) {
78+
for (const [name, value] of Object.entries(parsed)) {
79+
dimensions.push({ Name: name, Value: String(value) })
80+
}
81+
}
82+
}
83+
84+
const command = new PutMetricDataCommand({
85+
Namespace: validatedData.namespace,
86+
MetricData: [
87+
{
88+
MetricName: validatedData.metricName,
89+
Value: validatedData.value,
90+
Timestamp: timestamp,
91+
...(validatedData.unit && { Unit: validatedData.unit as StandardUnit }),
92+
...(dimensions.length > 0 && { Dimensions: dimensions }),
93+
},
94+
],
95+
})
96+
97+
await client.send(command)
98+
99+
return NextResponse.json({
100+
success: true,
101+
output: {
102+
success: true,
103+
namespace: validatedData.namespace,
104+
metricName: validatedData.metricName,
105+
value: validatedData.value,
106+
unit: validatedData.unit ?? 'None',
107+
timestamp: timestamp.toISOString(),
108+
},
109+
})
110+
} catch (error) {
111+
const errorMessage =
112+
error instanceof Error ? error.message : 'Failed to publish CloudWatch metric'
113+
logger.error('PutMetricData failed', { error: errorMessage })
114+
return NextResponse.json({ error: errorMessage }, { status: 500 })
115+
}
116+
}

apps/sim/blocks/blocks/cloudwatch.ts

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
CloudWatchGetLogEventsResponse,
99
CloudWatchGetMetricStatisticsResponse,
1010
CloudWatchListMetricsResponse,
11+
CloudWatchPutMetricDataResponse,
1112
CloudWatchQueryLogsResponse,
1213
} from '@/tools/cloudwatch/types'
1314

@@ -19,6 +20,7 @@ export const CloudWatchBlock: BlockConfig<
1920
| CloudWatchDescribeAlarmsResponse
2021
| CloudWatchListMetricsResponse
2122
| CloudWatchGetMetricStatisticsResponse
23+
| CloudWatchPutMetricDataResponse
2224
> = {
2325
type: 'cloudwatch',
2426
name: 'CloudWatch',
@@ -42,6 +44,7 @@ export const CloudWatchBlock: BlockConfig<
4244
{ label: 'Describe Log Streams', id: 'describe_log_streams' },
4345
{ label: 'List Metrics', id: 'list_metrics' },
4446
{ label: 'Get Metric Statistics', id: 'get_metric_statistics' },
47+
{ label: 'Publish Metric', id: 'put_metric_data' },
4548
{ label: 'Describe Alarms', id: 'describe_alarms' },
4649
],
4750
value: () => 'query_logs',
@@ -203,24 +206,74 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
203206
id: 'metricNamespace',
204207
title: 'Namespace',
205208
type: 'short-input',
206-
placeholder: 'e.g., AWS/EC2, AWS/Lambda, AWS/RDS',
207-
condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] },
208-
required: { field: 'operation', value: 'get_metric_statistics' },
209+
placeholder: 'e.g., AWS/EC2, AWS/Lambda, Custom/MyApp',
210+
condition: {
211+
field: 'operation',
212+
value: ['list_metrics', 'get_metric_statistics', 'put_metric_data'],
213+
},
214+
required: {
215+
field: 'operation',
216+
value: ['get_metric_statistics', 'put_metric_data'],
217+
},
209218
},
210219
{
211220
id: 'metricName',
212221
title: 'Metric Name',
213222
type: 'short-input',
214-
placeholder: 'e.g., CPUUtilization, Invocations',
215-
condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] },
216-
required: { field: 'operation', value: 'get_metric_statistics' },
223+
placeholder: 'e.g., CPUUtilization, Invocations, ErrorCount',
224+
condition: {
225+
field: 'operation',
226+
value: ['list_metrics', 'get_metric_statistics', 'put_metric_data'],
227+
},
228+
required: {
229+
field: 'operation',
230+
value: ['get_metric_statistics', 'put_metric_data'],
231+
},
217232
},
218233
{
219234
id: 'recentlyActive',
220235
title: 'Recently Active Only',
221236
type: 'switch',
222237
condition: { field: 'operation', value: 'list_metrics' },
223238
},
239+
// Publish Metric fields
240+
{
241+
id: 'metricValue',
242+
title: 'Value',
243+
type: 'short-input',
244+
placeholder: 'e.g., 1, 42.5',
245+
condition: { field: 'operation', value: 'put_metric_data' },
246+
required: { field: 'operation', value: 'put_metric_data' },
247+
},
248+
{
249+
id: 'metricUnit',
250+
title: 'Unit',
251+
type: 'dropdown',
252+
options: [
253+
{ label: 'None', id: 'None' },
254+
{ label: 'Count', id: 'Count' },
255+
{ label: 'Percent', id: 'Percent' },
256+
{ label: 'Seconds', id: 'Seconds' },
257+
{ label: 'Milliseconds', id: 'Milliseconds' },
258+
{ label: 'Microseconds', id: 'Microseconds' },
259+
{ label: 'Bytes', id: 'Bytes' },
260+
{ label: 'Kilobytes', id: 'Kilobytes' },
261+
{ label: 'Megabytes', id: 'Megabytes' },
262+
{ label: 'Gigabytes', id: 'Gigabytes' },
263+
{ label: 'Bits', id: 'Bits' },
264+
{ label: 'Bytes/Second', id: 'Bytes/Second' },
265+
{ label: 'Count/Second', id: 'Count/Second' },
266+
],
267+
value: () => 'None',
268+
condition: { field: 'operation', value: 'put_metric_data' },
269+
},
270+
{
271+
id: 'publishDimensions',
272+
title: 'Dimensions',
273+
type: 'table',
274+
columns: ['name', 'value'],
275+
condition: { field: 'operation', value: 'put_metric_data' },
276+
},
224277
// Get Metric Statistics fields
225278
{
226279
id: 'metricPeriod',
@@ -309,6 +362,7 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
309362
'cloudwatch_describe_log_streams',
310363
'cloudwatch_list_metrics',
311364
'cloudwatch_get_metric_statistics',
365+
'cloudwatch_put_metric_data',
312366
'cloudwatch_describe_alarms',
313367
],
314368
config: {
@@ -326,6 +380,8 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
326380
return 'cloudwatch_list_metrics'
327381
case 'get_metric_statistics':
328382
return 'cloudwatch_get_metric_statistics'
383+
case 'put_metric_data':
384+
return 'cloudwatch_put_metric_data'
329385
case 'describe_alarms':
330386
return 'cloudwatch_describe_alarms'
331387
default:
@@ -479,6 +535,44 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
479535
}
480536
}
481537

538+
case 'put_metric_data': {
539+
if (!rest.metricNamespace) {
540+
throw new Error('Namespace is required')
541+
}
542+
if (!rest.metricName) {
543+
throw new Error('Metric name is required')
544+
}
545+
if (rest.metricValue === undefined || rest.metricValue === '') {
546+
throw new Error('Metric value is required')
547+
}
548+
549+
return {
550+
awsRegion,
551+
awsAccessKeyId,
552+
awsSecretAccessKey,
553+
namespace: rest.metricNamespace,
554+
metricName: rest.metricName,
555+
value: Number(rest.metricValue),
556+
...(rest.metricUnit && rest.metricUnit !== 'None' && { unit: rest.metricUnit }),
557+
...(rest.publishDimensions && {
558+
dimensions: (() => {
559+
const dims = rest.publishDimensions
560+
if (typeof dims === 'string') return dims
561+
if (Array.isArray(dims)) {
562+
const obj: Record<string, string> = {}
563+
for (const row of dims) {
564+
const name = row.cells?.name
565+
const value = row.cells?.value
566+
if (name && value !== undefined) obj[name] = String(value)
567+
}
568+
return JSON.stringify(obj)
569+
}
570+
return JSON.stringify(dims)
571+
})(),
572+
}),
573+
}
574+
}
575+
482576
case 'describe_alarms':
483577
return {
484578
awsRegion,
@@ -518,6 +612,12 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
518612
metricPeriod: { type: 'number', description: 'Granularity in seconds' },
519613
metricStatistics: { type: 'string', description: 'Statistic type (Average, Sum, etc.)' },
520614
metricDimensions: { type: 'json', description: 'Metric dimensions (Name/Value pairs)' },
615+
metricValue: { type: 'number', description: 'Metric value to publish' },
616+
metricUnit: { type: 'string', description: 'Metric unit (Count, Seconds, Bytes, etc.)' },
617+
publishDimensions: {
618+
type: 'json',
619+
description: 'Dimensions for published metric (Name/Value pairs)',
620+
},
521621
alarmNamePrefix: { type: 'string', description: 'Alarm name prefix filter' },
522622
stateValue: {
523623
type: 'string',
@@ -567,5 +667,9 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
567667
type: 'array',
568668
description: 'CloudWatch alarms with state and configuration',
569669
},
670+
timestamp: {
671+
type: 'string',
672+
description: 'Timestamp when metric was published',
673+
},
570674
},
571675
}

apps/sim/tools/cloudwatch/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describeLogStreamsTool } from '@/tools/cloudwatch/describe_log_streams'
44
import { getLogEventsTool } from '@/tools/cloudwatch/get_log_events'
55
import { getMetricStatisticsTool } from '@/tools/cloudwatch/get_metric_statistics'
66
import { listMetricsTool } from '@/tools/cloudwatch/list_metrics'
7+
import { putMetricDataTool } from '@/tools/cloudwatch/put_metric_data'
78
import { queryLogsTool } from '@/tools/cloudwatch/query_logs'
89

910
export const cloudwatchDescribeAlarmsTool = describeAlarmsTool
@@ -12,4 +13,5 @@ export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool
1213
export const cloudwatchGetLogEventsTool = getLogEventsTool
1314
export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool
1415
export const cloudwatchListMetricsTool = listMetricsTool
16+
export const cloudwatchPutMetricDataTool = putMetricDataTool
1517
export const cloudwatchQueryLogsTool = queryLogsTool

0 commit comments

Comments
 (0)