Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 7 additions & 56 deletions apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
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'
import { createCloudWatchLogsClient, describeLogGroups } 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)
Expand All @@ -39,58 +32,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})

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
const result = await describeLogGroups(client, {
prefix: validatedData.prefix,
limit: validatedData.limit,
})

logger.info(`Successfully described ${cappedLogGroups.length} log groups`)
logger.info(`Successfully described ${result.logGroups.length} log groups`)

return NextResponse.json({
success: true,
output: { logGroups: cappedLogGroups },
output: { logGroups: result.logGroups },
})
} finally {
client.destroy()
Expand Down
63 changes: 63 additions & 0 deletions apps/sim/app/api/tools/cloudwatch/selector-log-groups/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import {
cloudwatchLogGroupsBodySchema,
cloudwatchSelectorLogGroupsContract,
} from '@/lib/api/contracts/selectors/cloudwatch'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
authenticateSelectorRequest,
resolveAuthorizedSelectorContext,
} from '@/lib/selectors/server/resolve-authorized-context'
import { createCloudWatchLogsClient, describeLogGroups } from '@/app/api/tools/cloudwatch/utils'

const logger = createLogger('CloudWatchSelectorLogGroupsAPI')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authentication = await authenticateSelectorRequest(request)
if (!authentication.ok) {
return NextResponse.json({ error: authentication.error }, { status: authentication.status })
}
const parsed = await parseRequest(cloudwatchSelectorLogGroupsContract, request, {})
if (!parsed.success) return parsed.response

const { workflowId, prefix, limit, ...context } = parsed.data.body
const resolution = await resolveAuthorizedSelectorContext(authentication.principal, {
workflowId,
context,
})
if (!resolution.ok) {
return NextResponse.json({ error: resolution.error }, { status: resolution.status })
}

const validated = cloudwatchLogGroupsBodySchema.safeParse({
...resolution.context,
prefix,
limit,
})
if (!validated.success) {
return NextResponse.json(
{ error: 'Invalid CloudWatch selector configuration' },
{ status: 400 }
)
}

const client = createCloudWatchLogsClient(validated.data)
try {
const result = await describeLogGroups(client, {
prefix: validated.data.prefix,
limit: validated.data.limit,
})
return NextResponse.json({
logGroups: result.logGroups.map(({ logGroupName }) => ({ logGroupName })),
})
} finally {
client.destroy()
}
} catch {
logger.error('CloudWatch selector log-group request failed')
return NextResponse.json({ error: 'Failed to retrieve CloudWatch log groups' }, { status: 500 })
}
})
67 changes: 67 additions & 0 deletions apps/sim/app/api/tools/cloudwatch/selector-log-streams/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import {
cloudwatchLogStreamsBodySchema,
cloudwatchSelectorLogStreamsContract,
} from '@/lib/api/contracts/selectors/cloudwatch'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
authenticateSelectorRequest,
resolveAuthorizedSelectorContext,
} from '@/lib/selectors/server/resolve-authorized-context'
import { createCloudWatchLogsClient, describeLogStreams } from '@/app/api/tools/cloudwatch/utils'

const logger = createLogger('CloudWatchSelectorLogStreamsAPI')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authentication = await authenticateSelectorRequest(request)
if (!authentication.ok) {
return NextResponse.json({ error: authentication.error }, { status: authentication.status })
}
const parsed = await parseRequest(cloudwatchSelectorLogStreamsContract, request, {})
if (!parsed.success) return parsed.response

const { workflowId, prefix, limit, logGroupName, ...context } = parsed.data.body
const resolution = await resolveAuthorizedSelectorContext(authentication.principal, {
workflowId,
context,
})
if (!resolution.ok) {
return NextResponse.json({ error: resolution.error }, { status: resolution.status })
}

const validated = cloudwatchLogStreamsBodySchema.safeParse({
...resolution.context,
prefix,
limit,
logGroupName,
})
if (!validated.success) {
return NextResponse.json(
{ error: 'Invalid CloudWatch selector configuration' },
{ status: 400 }
)
}

const client = createCloudWatchLogsClient(validated.data)
try {
const result = await describeLogStreams(client, validated.data.logGroupName, {
prefix: validated.data.prefix,
limit: validated.data.limit,
})
return NextResponse.json({
logStreams: result.logStreams.map(({ logStreamName }) => ({ logStreamName })),
})
} finally {
client.destroy()
}
} catch {
logger.error('CloudWatch selector log-stream request failed')
return NextResponse.json(
{ error: 'Failed to retrieve CloudWatch log streams' },
{ status: 500 }
)
}
})
Loading
Loading