chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,123 @@
import {
type AlarmType,
CloudWatchClient,
DescribeAlarmHistoryCommand,
} from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchDescribeAlarmHistoryContract } from '@/lib/api/contracts/tools/aws/cloudwatch-describe-alarm-history'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchDescribeAlarmHistory')
/** AWS DescribeAlarmHistory caps `MaxRecords` at 100 items per page. */
const ALARM_HISTORY_PAGE_SIZE = 100
/** Upper bound on pages drained to avoid unbounded loops on long-lived alarms. */
const MAX_ALARM_HISTORY_PAGES = 20
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchDescribeAlarmHistoryContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info('Describing CloudWatch alarm history')
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const totalLimit = validatedData.limit
const alarmHistoryItems: {
alarmName: string | undefined
alarmType: string | undefined
timestamp: number | undefined
historyItemType: string | undefined
historySummary: string | undefined
}[] = []
let nextToken: string | undefined
for (let page = 0; page < MAX_ALARM_HISTORY_PAGES; page++) {
const pageLimit =
totalLimit !== undefined
? Math.min(ALARM_HISTORY_PAGE_SIZE, totalLimit - alarmHistoryItems.length)
: ALARM_HISTORY_PAGE_SIZE
const command = new DescribeAlarmHistoryCommand({
...(validatedData.alarmName && { AlarmName: validatedData.alarmName }),
// AWS defaults AlarmTypes to MetricAlarm-only, so always request both kinds explicitly.
AlarmTypes: ['MetricAlarm', 'CompositeAlarm'] as AlarmType[],
...(validatedData.historyItemType && {
HistoryItemType: validatedData.historyItemType,
}),
...(validatedData.startDate !== undefined && {
StartDate: new Date(validatedData.startDate * 1000),
}),
...(validatedData.endDate !== undefined && {
EndDate: new Date(validatedData.endDate * 1000),
}),
ScanBy: validatedData.scanBy ?? 'TimestampDescending',
MaxRecords: pageLimit,
...(nextToken && { NextToken: nextToken }),
})
const response = await client.send(command)
for (const item of response.AlarmHistoryItems ?? []) {
alarmHistoryItems.push({
alarmName: item.AlarmName,
alarmType: item.AlarmType,
timestamp: item.Timestamp?.getTime(),
historyItemType: item.HistoryItemType,
historySummary: item.HistorySummary,
})
}
nextToken = response.NextToken
if (!nextToken) break
if (totalLimit !== undefined && alarmHistoryItems.length >= totalLimit) break
if (page === MAX_ALARM_HISTORY_PAGES - 1) {
logger.warn(
`DescribeAlarmHistory hit pagination cap of ${MAX_ALARM_HISTORY_PAGES} pages; history may be incomplete`
)
}
}
const cappedItems =
totalLimit !== undefined ? alarmHistoryItems.slice(0, totalLimit) : alarmHistoryItems
logger.info(`Successfully described ${cappedItems.length} alarm history items`)
return NextResponse.json({
success: true,
output: { alarmHistoryItems: cappedItems },
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('DescribeAlarmHistory failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to describe CloudWatch alarm history: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import {
type AlarmType,
CloudWatchClient,
DescribeAlarmsCommand,
type StateValue,
} from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchDescribeAlarmsContract } from '@/lib/api/contracts/tools/aws/cloudwatch-describe-alarms'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchDescribeAlarms')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchDescribeAlarmsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info('Describing CloudWatch alarms')
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const command = new DescribeAlarmsCommand({
...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }),
...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }),
AlarmTypes: validatedData.alarmType
? [validatedData.alarmType as AlarmType]
: (['MetricAlarm', 'CompositeAlarm'] 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(),
}))
const alarms = [...metricAlarms, ...compositeAlarms]
logger.info(`Successfully described ${alarms.length} alarms`)
return NextResponse.json({
success: true,
output: { alarms },
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('DescribeAlarms failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to describe CloudWatch alarms: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,105 @@
import { DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { cloudwatchLogGroupsSelectorContract } from '@/lib/api/contracts/selectors/cloudwatch'
import { parseToolRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createCloudWatchLogsClient } from '@/app/api/tools/cloudwatch/utils'
const logger = createLogger('CloudWatchDescribeLogGroups')
/** AWS DescribeLogGroups caps `limit` at 50 items per page. */
const LOG_GROUPS_PAGE_SIZE = 50
/** Upper bound on pages drained to avoid unbounded loops on very large accounts. */
const MAX_LOG_GROUPS_PAGES = 20
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(cloudwatchLogGroupsSelectorContract, request, {
errorFormat: 'firstError',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info('Describing CloudWatch log groups')
const client = createCloudWatchLogsClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
try {
const totalLimit = validatedData.limit
const logGroups: {
logGroupName: string
arn: string
storedBytes: number
retentionInDays: number | undefined
creationTime: number | undefined
}[] = []
let nextToken: string | undefined
for (let page = 0; page < MAX_LOG_GROUPS_PAGES; page++) {
const pageLimit =
totalLimit !== undefined
? Math.min(LOG_GROUPS_PAGE_SIZE, totalLimit - logGroups.length)
: LOG_GROUPS_PAGE_SIZE
const command = new DescribeLogGroupsCommand({
...(validatedData.prefix && { logGroupNamePrefix: validatedData.prefix }),
limit: pageLimit,
...(nextToken && { nextToken }),
})
const response = await client.send(command)
for (const lg of response.logGroups ?? []) {
logGroups.push({
logGroupName: lg.logGroupName ?? '',
arn: lg.arn ?? '',
storedBytes: lg.storedBytes ?? 0,
retentionInDays: lg.retentionInDays,
creationTime: lg.creationTime,
})
}
nextToken = response.nextToken
if (!nextToken) break
if (totalLimit !== undefined && logGroups.length >= totalLimit) break
if (page === MAX_LOG_GROUPS_PAGES - 1) {
logger.warn(
`DescribeLogGroups hit pagination cap of ${MAX_LOG_GROUPS_PAGES} pages; log group list may be incomplete`
)
}
}
const cappedLogGroups = totalLimit !== undefined ? logGroups.slice(0, totalLimit) : logGroups
logger.info(`Successfully described ${cappedLogGroups.length} log groups`)
return NextResponse.json({
success: true,
output: { logGroups: cappedLogGroups },
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('DescribeLogGroups failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to describe CloudWatch log groups: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,56 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { cloudwatchLogStreamsSelectorContract } from '@/lib/api/contracts/selectors/cloudwatch'
import { parseToolRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createCloudWatchLogsClient, describeLogStreams } from '@/app/api/tools/cloudwatch/utils'
const logger = createLogger('CloudWatchDescribeLogStreams')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(cloudwatchLogStreamsSelectorContract, request, {
errorFormat: 'firstError',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`Describing log streams for group: ${validatedData.logGroupName}`)
const client = createCloudWatchLogsClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
try {
const result = await describeLogStreams(client, validatedData.logGroupName, {
prefix: validatedData.prefix,
limit: validatedData.limit,
})
logger.info(`Successfully described ${result.logStreams.length} log streams`)
return NextResponse.json({
success: true,
output: { logStreams: result.logStreams },
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('DescribeLogStreams failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to describe CloudWatch log streams: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,62 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchFilterLogEventsContract } from '@/lib/api/contracts/tools/aws/cloudwatch-filter-log-events'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createCloudWatchLogsClient, filterLogEvents } from '@/app/api/tools/cloudwatch/utils'
const logger = createLogger('CloudWatchFilterLogEvents')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchFilterLogEventsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`Filtering log events in ${validatedData.logGroupName}`)
const client = createCloudWatchLogsClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
try {
const result = await filterLogEvents(client, validatedData.logGroupName, {
filterPattern: validatedData.filterPattern,
logStreamNamePrefix: validatedData.logStreamNamePrefix,
// CloudWatch Logs timestamps are epoch milliseconds; our params are epoch seconds.
startTime:
validatedData.startTime !== undefined ? validatedData.startTime * 1000 : undefined,
endTime: validatedData.endTime !== undefined ? validatedData.endTime * 1000 : undefined,
startFromHead: validatedData.startFromHead,
limit: validatedData.limit,
})
logger.info(`Successfully filtered ${result.events.length} log events`)
return NextResponse.json({
success: true,
output: { events: result.events },
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('FilterLogEvents failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to filter CloudWatch log events: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,64 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchGetLogEventsContract } from '@/lib/api/contracts/tools/aws/cloudwatch-get-log-events'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createCloudWatchLogsClient, getLogEvents } from '@/app/api/tools/cloudwatch/utils'
const logger = createLogger('CloudWatchGetLogEvents')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchGetLogEventsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(
`Getting log events from ${validatedData.logGroupName}/${validatedData.logStreamName}`
)
const client = createCloudWatchLogsClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
try {
const result = await getLogEvents(
client,
validatedData.logGroupName,
validatedData.logStreamName,
{
startTime: validatedData.startTime,
endTime: validatedData.endTime,
limit: validatedData.limit,
}
)
logger.info(`Successfully retrieved ${result.events.length} log events`)
return NextResponse.json({
success: true,
output: { events: result.events },
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('GetLogEvents failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to get CloudWatch log events: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,102 @@
import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchGetMetricStatisticsContract } from '@/lib/api/contracts/tools/aws/cloudwatch-get-metric-statistics'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchGetMetricStatistics')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchGetMetricStatisticsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(
`Getting metric statistics for ${validatedData.namespace}/${validatedData.metricName}`
)
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
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<string, string>) => ({
Name: d.name,
Value: d.value,
}))
} else if (typeof dims === 'object') {
parsedDimensions = Object.entries(dims).map(([name, value]) => ({
Name: name,
Value: String(value),
}))
}
} catch {
return NextResponse.json({ error: 'Invalid dimensions JSON format' }, { status: 400 })
}
}
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 ? dp.Timestamp.getTime() : 0,
average: dp.Average,
sum: dp.Sum,
minimum: dp.Minimum,
maximum: dp.Maximum,
sampleCount: dp.SampleCount,
unit: dp.Unit,
}))
logger.info(`Successfully retrieved ${datapoints.length} datapoints`)
return NextResponse.json({
success: true,
output: {
label: response.Label ?? validatedData.metricName,
datapoints,
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('GetMetricStatistics failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to get CloudWatch metric statistics: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,101 @@
import { CloudWatchClient, ListMetricsCommand } from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchListMetricsContract } from '@/lib/api/contracts/tools/aws/cloudwatch-list-metrics'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchListMetrics')
/** AWS ListMetrics returns up to 500 results per page. */
const METRICS_PAGE_SIZE = 500
/** Upper bound on pages drained to avoid unbounded loops on accounts with many metrics. */
const MAX_METRICS_PAGES = 20
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchListMetricsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info('Listing CloudWatch metrics')
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const totalLimit = validatedData.limit ?? METRICS_PAGE_SIZE
const metrics: {
namespace: string
metricName: string
dimensions: { name: string; value: string }[]
}[] = []
let nextToken: string | undefined
for (let page = 0; page < MAX_METRICS_PAGES; page++) {
const command = new ListMetricsCommand({
...(validatedData.namespace && { Namespace: validatedData.namespace }),
...(validatedData.metricName && { MetricName: validatedData.metricName }),
...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }),
...(nextToken && { NextToken: nextToken }),
})
const response = await client.send(command)
for (const m of response.Metrics ?? []) {
metrics.push({
namespace: m.Namespace ?? '',
metricName: m.MetricName ?? '',
dimensions: (m.Dimensions ?? []).map((d) => ({
name: d.Name ?? '',
value: d.Value ?? '',
})),
})
}
nextToken = response.NextToken
if (!nextToken) break
if (metrics.length >= totalLimit) break
if (page === MAX_METRICS_PAGES - 1) {
logger.warn(
`ListMetrics hit pagination cap of ${MAX_METRICS_PAGES} pages; metric list may be incomplete`
)
}
}
const cappedMetrics = metrics.slice(0, totalLimit)
logger.info(`Successfully listed ${cappedMetrics.length} metrics`)
return NextResponse.json({
success: true,
output: { metrics: cappedMetrics },
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('ListMetrics failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to list CloudWatch metrics: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,100 @@
import { CloudWatchClient, PutAlarmMuteRuleCommand } from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchMuteAlarmContract } from '@/lib/api/contracts/tools/aws/cloudwatch-mute-alarm'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchMuteAlarm')
function toAtExpression(date: Date): string {
const yyyy = date.getUTCFullYear()
const mm = String(date.getUTCMonth() + 1).padStart(2, '0')
const dd = String(date.getUTCDate()).padStart(2, '0')
const hh = String(date.getUTCHours()).padStart(2, '0')
const min = String(date.getUTCMinutes()).padStart(2, '0')
return `at(${yyyy}-${mm}-${dd}T${hh}:${min})`
}
function toIsoDuration(value: number, unit: 'minutes' | 'hours' | 'days'): string {
switch (unit) {
case 'minutes':
return `PT${value}M`
case 'hours':
return `PT${value}H`
case 'days':
return `P${value}D`
}
}
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchMuteAlarmContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
const startDate =
validatedData.startDate !== undefined ? new Date(validatedData.startDate * 1000) : new Date()
const expression = toAtExpression(startDate)
const duration = toIsoDuration(validatedData.durationValue, validatedData.durationUnit)
logger.info(
`Creating CloudWatch alarm mute rule "${validatedData.muteRuleName}" for ${validatedData.alarmNames.length} alarm(s) (${expression}, duration ${duration})`
)
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const command = new PutAlarmMuteRuleCommand({
Name: validatedData.muteRuleName,
...(validatedData.description && { Description: validatedData.description }),
Rule: {
Schedule: {
Expression: expression,
Duration: duration,
},
},
MuteTargets: { AlarmNames: validatedData.alarmNames },
})
await client.send(command)
logger.info(`Successfully created mute rule "${validatedData.muteRuleName}"`)
return NextResponse.json({
success: true,
output: {
success: true,
muteRuleName: validatedData.muteRuleName,
alarmNames: validatedData.alarmNames,
expression,
duration,
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('MuteAlarm failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to create CloudWatch alarm mute rule: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,74 @@
import {
DeleteRetentionPolicyCommand,
PutRetentionPolicyCommand,
} from '@aws-sdk/client-cloudwatch-logs'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchPutLogGroupRetentionContract } from '@/lib/api/contracts/tools/aws/cloudwatch-put-log-group-retention'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createCloudWatchLogsClient } from '@/app/api/tools/cloudwatch/utils'
const logger = createLogger('CloudWatchPutLogGroupRetention')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchPutLogGroupRetentionContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
const client = createCloudWatchLogsClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
try {
if (validatedData.retentionInDays !== undefined) {
logger.info(
`Setting retention for log group "${validatedData.logGroupName}" to ${validatedData.retentionInDays} days`
)
await client.send(
new PutRetentionPolicyCommand({
logGroupName: validatedData.logGroupName,
retentionInDays: validatedData.retentionInDays,
})
)
} else {
logger.info(
`Removing retention policy for log group "${validatedData.logGroupName}" (events never expire)`
)
await client.send(
new DeleteRetentionPolicyCommand({ logGroupName: validatedData.logGroupName })
)
}
return NextResponse.json({
success: true,
output: {
success: true,
logGroupName: validatedData.logGroupName,
retentionInDays: validatedData.retentionInDays ?? null,
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('PutLogGroupRetention failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to set CloudWatch log group retention: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,89 @@
import {
CloudWatchClient,
PutMetricDataCommand,
type StandardUnit,
} from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchPutMetricDataContract } from '@/lib/api/contracts/tools/aws/cloudwatch-put-metric-data'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchPutMetricData')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchPutMetricDataContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`Publishing metric ${validatedData.namespace}/${validatedData.metricName}`)
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const timestamp = new Date()
const dimensions: { Name: string; Value: string }[] = []
if (validatedData.dimensions) {
const parsed = JSON.parse(validatedData.dimensions)
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)
logger.info('Successfully published metric')
return NextResponse.json({
success: true,
output: {
success: true,
namespace: validatedData.namespace,
metricName: validatedData.metricName,
value: validatedData.value,
unit: validatedData.unit ?? 'None',
timestamp: timestamp.toISOString(),
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('PutMetricData failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to publish CloudWatch metric: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,73 @@
import { StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchQueryLogsContract } from '@/lib/api/contracts/tools/aws/cloudwatch-query-logs'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createCloudWatchLogsClient, pollQueryResults } from '@/app/api/tools/cloudwatch/utils'
const logger = createLogger('CloudWatchQueryLogs')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchQueryLogsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info('Running CloudWatch Log Insights query')
const client = createCloudWatchLogsClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
try {
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)
logger.info(`Query completed with status: ${result.status}`)
return NextResponse.json({
success: true,
output: {
results: result.results,
statistics: result.statistics,
status: result.status,
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('QueryLogs failed', { error: toError(error).message })
return NextResponse.json(
{ error: `CloudWatch Log Insights query failed: ${toError(error).message}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,62 @@
import { CloudWatchClient, DeleteAlarmMuteRuleCommand } from '@aws-sdk/client-cloudwatch'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudwatchUnmuteAlarmContract } from '@/lib/api/contracts/tools/aws/cloudwatch-unmute-alarm'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
const logger = createLogger('CloudWatchUnmuteAlarm')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsCloudwatchUnmuteAlarmContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`Deleting CloudWatch alarm mute rule "${validatedData.muteRuleName}"`)
const client = new CloudWatchClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})
try {
const command = new DeleteAlarmMuteRuleCommand({
AlarmMuteRuleName: validatedData.muteRuleName,
})
await client.send(command)
logger.info(`Successfully deleted mute rule "${validatedData.muteRuleName}"`)
return NextResponse.json({
success: true,
output: {
success: true,
muteRuleName: validatedData.muteRuleName,
},
})
} finally {
client.destroy()
}
} catch (error) {
logger.error('UnmuteAlarm failed', { error: toError(error).message })
return NextResponse.json(
{ error: `Failed to delete CloudWatch alarm mute rule: ${toError(error).message}` },
{ status: 500 }
)
}
})
+291
View File
@@ -0,0 +1,291 @@
import {
CloudWatchLogsClient,
DescribeLogStreamsCommand,
FilterLogEventsCommand,
GetLogEventsCommand,
GetQueryResultsCommand,
type ResultField,
} from '@aws-sdk/client-cloudwatch-logs'
import { createLogger } from '@sim/logger'
import { sleep } from '@sim/utils/helpers'
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<string, string>[]
statistics: {
bytesScanned: number
recordsMatched: number
recordsScanned: number
}
status: string
}
function parseResultFields(fields: ResultField[] | undefined): Record<string, string> {
const record: Record<string, string> = {}
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<PollResult> {
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 sleep(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'})`,
}
}
/** AWS DescribeLogStreams caps `limit` at 50 items per page. */
const LOG_STREAMS_PAGE_SIZE = 50
/** Upper bound on pages drained to avoid unbounded loops on log groups with many streams. */
const MAX_LOG_STREAMS_PAGES = 20
const logger = createLogger('CloudWatchUtils')
interface DescribedLogStream {
logStreamName: string
lastEventTimestamp: number | undefined
firstEventTimestamp: number | undefined
creationTime: number | undefined
storedBytes: number
}
/**
* Lists log streams for a log group, following `nextToken` so the complete set
* is returned rather than just the first page. Bounded by
* `MAX_LOG_STREAMS_PAGES`; logs a warning rather than silently dropping streams
* when the cap is hit. Ordering/prefix inputs are preserved across all pages.
*
* When `limit` is provided it is treated as a total result cap: draining stops
* once enough streams have been collected. When omitted, every page is drained.
*/
export async function describeLogStreams(
client: CloudWatchLogsClient,
logGroupName: string,
options?: { prefix?: string; limit?: number }
): Promise<{ logStreams: DescribedLogStream[] }> {
const hasPrefix = Boolean(options?.prefix)
const totalLimit = options?.limit
const logStreams: DescribedLogStream[] = []
let nextToken: string | undefined
for (let page = 0; page < MAX_LOG_STREAMS_PAGES; page++) {
const pageLimit =
totalLimit !== undefined
? Math.min(LOG_STREAMS_PAGE_SIZE, totalLimit - logStreams.length)
: LOG_STREAMS_PAGE_SIZE
const command = new DescribeLogStreamsCommand({
logGroupName,
...(hasPrefix
? { orderBy: 'LogStreamName', logStreamNamePrefix: options!.prefix }
: { orderBy: 'LastEventTime', descending: true }),
limit: pageLimit,
...(nextToken && { nextToken }),
})
const response = await client.send(command)
for (const ls of response.logStreams ?? []) {
logStreams.push({
logStreamName: ls.logStreamName ?? '',
lastEventTimestamp: ls.lastEventTimestamp,
firstEventTimestamp: ls.firstEventTimestamp,
creationTime: ls.creationTime,
storedBytes: ls.storedBytes ?? 0,
})
}
nextToken = response.nextToken
if (!nextToken) break
if (totalLimit !== undefined && logStreams.length >= totalLimit) break
if (page === MAX_LOG_STREAMS_PAGES - 1) {
logger.warn(
`DescribeLogStreams hit pagination cap of ${MAX_LOG_STREAMS_PAGES} pages; log stream list may be incomplete`,
{ logGroupName }
)
}
}
return {
logStreams: totalLimit !== undefined ? logStreams.slice(0, totalLimit) : logStreams,
}
}
/** AWS FilterLogEvents caps `limit` at 10,000 events per page. */
const FILTER_LOG_EVENTS_PAGE_SIZE = 10_000
/** Upper bound on pages drained to avoid unbounded loops on very active log groups. */
const MAX_FILTER_LOG_EVENTS_PAGES = 20
interface FilteredLogEventResult {
logStreamName: string | undefined
timestamp: number | undefined
message: string | undefined
ingestionTime: number | undefined
}
/**
* Searches log events across all streams (or a prefix-matched subset) in a log
* group, following `nextToken` so the complete matching set is returned rather
* than just the first page. Bounded by `MAX_FILTER_LOG_EVENTS_PAGES`.
*
* When `limit` is provided it is treated as a total result cap: draining stops
* once enough events have been collected. When omitted, every page is drained.
*/
export async function filterLogEvents(
client: CloudWatchLogsClient,
logGroupName: string,
options?: {
filterPattern?: string
logStreamNamePrefix?: string
startTime?: number
endTime?: number
startFromHead?: boolean
limit?: number
}
): Promise<{ events: FilteredLogEventResult[] }> {
const totalLimit = options?.limit
const events: FilteredLogEventResult[] = []
let nextToken: string | undefined
for (let page = 0; page < MAX_FILTER_LOG_EVENTS_PAGES; page++) {
const pageLimit =
totalLimit !== undefined
? Math.min(FILTER_LOG_EVENTS_PAGE_SIZE, totalLimit - events.length)
: FILTER_LOG_EVENTS_PAGE_SIZE
const command = new FilterLogEventsCommand({
logGroupName,
...(options?.filterPattern && { filterPattern: options.filterPattern }),
...(options?.logStreamNamePrefix && { logStreamNamePrefix: options.logStreamNamePrefix }),
...(options?.startTime !== undefined && { startTime: options.startTime }),
...(options?.endTime !== undefined && { endTime: options.endTime }),
...(options?.startFromHead !== undefined && { startFromHead: options.startFromHead }),
limit: pageLimit,
...(nextToken && { nextToken }),
})
const response = await client.send(command)
for (const e of response.events ?? []) {
events.push({
logStreamName: e.logStreamName,
timestamp: e.timestamp,
message: e.message,
ingestionTime: e.ingestionTime,
})
}
nextToken = response.nextToken
if (!nextToken) break
if (totalLimit !== undefined && events.length >= totalLimit) break
if (page === MAX_FILTER_LOG_EVENTS_PAGES - 1) {
logger.warn(
`FilterLogEvents hit pagination cap of ${MAX_FILTER_LOG_EVENTS_PAGES} pages; event list may be incomplete`,
{ logGroupName }
)
}
}
return {
events: totalLimit !== undefined ? events.slice(0, totalLimit) : events,
}
}
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({
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,
})),
}
}