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
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:
@@ -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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user