Skip to content
Next Next commit
feat(block): Add cloudwatch publish operation
  • Loading branch information
Theodore Li authored and waleedlatif1 committed Apr 8, 2026
commit 044612def2edd78630130fe4ce131ebba33ba154
116 changes: 116 additions & 0 deletions apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
CloudWatchClient,
PutMetricDataCommand,
type StandardUnit,
} 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('CloudWatchPutMetricData')

const VALID_UNITS = [
'Seconds',
'Microseconds',
'Milliseconds',
'Bytes',
'Kilobytes',
'Megabytes',
'Gigabytes',
'Terabytes',
'Bits',
'Kilobits',
'Megabits',
'Gigabits',
'Terabits',
'Percent',
'Count',
'Bytes/Second',
'Kilobytes/Second',
'Megabytes/Second',
'Gigabytes/Second',
'Terabytes/Second',
'Bits/Second',
'Kilobits/Second',
'Megabits/Second',
'Gigabits/Second',
'Terabits/Second',
'Count/Second',
'None',
] as const

const PutMetricDataSchema = 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'),
value: z.number({ coerce: true }),
unit: z.enum(VALID_UNITS).optional(),
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 = PutMetricDataSchema.parse(body)

const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})

const timestamp = new Date()

const dimensions: { Name: string; Value: string }[] = []
if (validatedData.dimensions) {
const parsed = JSON.parse(validatedData.dimensions)
if (typeof parsed === 'object' && parsed !== null) {
for (const [name, value] of Object.entries(parsed)) {
dimensions.push({ Name: name, Value: String(value) })
}
}
}

const command = new PutMetricDataCommand({
Namespace: validatedData.namespace,
MetricData: [
{
MetricName: validatedData.metricName,
Value: validatedData.value,
Timestamp: timestamp,
...(validatedData.unit && { Unit: validatedData.unit as StandardUnit }),
...(dimensions.length > 0 && { Dimensions: dimensions }),
},
],
})

await client.send(command)

return NextResponse.json({
success: true,
output: {
success: true,
namespace: validatedData.namespace,
metricName: validatedData.metricName,
value: validatedData.value,
unit: validatedData.unit ?? 'None',
timestamp: timestamp.toISOString(),
},
})
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Failed to publish CloudWatch metric'
logger.error('PutMetricData failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}
116 changes: 110 additions & 6 deletions apps/sim/blocks/blocks/cloudwatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
CloudWatchGetLogEventsResponse,
CloudWatchGetMetricStatisticsResponse,
CloudWatchListMetricsResponse,
CloudWatchPutMetricDataResponse,
CloudWatchQueryLogsResponse,
} from '@/tools/cloudwatch/types'

Expand All @@ -19,6 +20,7 @@ export const CloudWatchBlock: BlockConfig<
| CloudWatchDescribeAlarmsResponse
| CloudWatchListMetricsResponse
| CloudWatchGetMetricStatisticsResponse
| CloudWatchPutMetricDataResponse
> = {
type: 'cloudwatch',
name: 'CloudWatch',
Expand All @@ -42,6 +44,7 @@ export const CloudWatchBlock: BlockConfig<
{ label: 'Describe Log Streams', id: 'describe_log_streams' },
{ label: 'List Metrics', id: 'list_metrics' },
{ label: 'Get Metric Statistics', id: 'get_metric_statistics' },
{ label: 'Publish Metric', id: 'put_metric_data' },
{ label: 'Describe Alarms', id: 'describe_alarms' },
],
value: () => 'query_logs',
Expand Down Expand Up @@ -203,24 +206,74 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
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' },
placeholder: 'e.g., AWS/EC2, AWS/Lambda, Custom/MyApp',
condition: {
field: 'operation',
value: ['list_metrics', 'get_metric_statistics', 'put_metric_data'],
},
required: {
field: 'operation',
value: ['get_metric_statistics', 'put_metric_data'],
},
},
{
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' },
placeholder: 'e.g., CPUUtilization, Invocations, ErrorCount',
condition: {
field: 'operation',
value: ['list_metrics', 'get_metric_statistics', 'put_metric_data'],
},
required: {
field: 'operation',
value: ['get_metric_statistics', 'put_metric_data'],
},
},
{
id: 'recentlyActive',
title: 'Recently Active Only',
type: 'switch',
condition: { field: 'operation', value: 'list_metrics' },
},
// Publish Metric fields
{
id: 'metricValue',
title: 'Value',
type: 'short-input',
placeholder: 'e.g., 1, 42.5',
condition: { field: 'operation', value: 'put_metric_data' },
required: { field: 'operation', value: 'put_metric_data' },
},
{
id: 'metricUnit',
title: 'Unit',
type: 'dropdown',
options: [
{ label: 'None', id: 'None' },
{ label: 'Count', id: 'Count' },
{ label: 'Percent', id: 'Percent' },
{ label: 'Seconds', id: 'Seconds' },
{ label: 'Milliseconds', id: 'Milliseconds' },
{ label: 'Microseconds', id: 'Microseconds' },
{ label: 'Bytes', id: 'Bytes' },
{ label: 'Kilobytes', id: 'Kilobytes' },
{ label: 'Megabytes', id: 'Megabytes' },
{ label: 'Gigabytes', id: 'Gigabytes' },
{ label: 'Bits', id: 'Bits' },
{ label: 'Bytes/Second', id: 'Bytes/Second' },
Comment thread
waleedlatif1 marked this conversation as resolved.
{ label: 'Count/Second', id: 'Count/Second' },
],
value: () => 'None',
condition: { field: 'operation', value: 'put_metric_data' },
},
{
id: 'publishDimensions',
title: 'Dimensions',
type: 'table',
columns: ['name', 'value'],
condition: { field: 'operation', value: 'put_metric_data' },
},
// Get Metric Statistics fields
{
id: 'metricPeriod',
Expand Down Expand Up @@ -309,6 +362,7 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
'cloudwatch_describe_log_streams',
'cloudwatch_list_metrics',
'cloudwatch_get_metric_statistics',
'cloudwatch_put_metric_data',
'cloudwatch_describe_alarms',
],
config: {
Expand All @@ -326,6 +380,8 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
return 'cloudwatch_list_metrics'
case 'get_metric_statistics':
return 'cloudwatch_get_metric_statistics'
case 'put_metric_data':
return 'cloudwatch_put_metric_data'
case 'describe_alarms':
return 'cloudwatch_describe_alarms'
default:
Expand Down Expand Up @@ -479,6 +535,44 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
}
}

case 'put_metric_data': {
if (!rest.metricNamespace) {
throw new Error('Namespace is required')
}
if (!rest.metricName) {
throw new Error('Metric name is required')
}
if (rest.metricValue === undefined || rest.metricValue === '') {
throw new Error('Metric value is required')
}
Comment thread
waleedlatif1 marked this conversation as resolved.

return {
awsRegion,
awsAccessKeyId,
awsSecretAccessKey,
namespace: rest.metricNamespace,
metricName: rest.metricName,
value: Number(rest.metricValue),
...(rest.metricUnit && rest.metricUnit !== 'None' && { unit: rest.metricUnit }),
...(rest.publishDimensions && {
dimensions: (() => {
const dims = rest.publishDimensions
if (typeof dims === 'string') return dims
if (Array.isArray(dims)) {
const obj: Record<string, string> = {}
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,
Expand Down Expand Up @@ -518,6 +612,12 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
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)' },
metricValue: { type: 'number', description: 'Metric value to publish' },
metricUnit: { type: 'string', description: 'Metric unit (Count, Seconds, Bytes, etc.)' },
publishDimensions: {
type: 'json',
description: 'Dimensions for published metric (Name/Value pairs)',
},
alarmNamePrefix: { type: 'string', description: 'Alarm name prefix filter' },
stateValue: {
type: 'string',
Expand Down Expand Up @@ -567,5 +667,9 @@ Return ONLY the query — no explanations, no markdown code blocks.`,
type: 'array',
description: 'CloudWatch alarms with state and configuration',
},
timestamp: {
type: 'string',
description: 'Timestamp when metric was published',
},
},
}
2 changes: 2 additions & 0 deletions apps/sim/tools/cloudwatch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { putMetricDataTool } from '@/tools/cloudwatch/put_metric_data'
import { queryLogsTool } from '@/tools/cloudwatch/query_logs'

export const cloudwatchDescribeAlarmsTool = describeAlarmsTool
Expand All @@ -12,4 +13,5 @@ export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool
export const cloudwatchGetLogEventsTool = getLogEventsTool
export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool
export const cloudwatchListMetricsTool = listMetricsTool
export const cloudwatchPutMetricDataTool = putMetricDataTool
export const cloudwatchQueryLogsTool = queryLogsTool
Loading