Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/aws

src/logs.ts

137 lines · 3.5 KB · TypeScript
import { awsSignedFetch, parseJsonBody } from './client.ts'
import { redactUnknown } from './redact.ts'
import {
	boundedPageSize,
	inputRecord,
	optionalString,
	requiredString,
	type AwsAuthOptions,
} from './validation.ts'

function jsonTarget(target: string, body: Record<string, unknown>) {
	return {
		headers: {
			'content-type': 'application/x-amz-json-1.1',
			'x-amz-target': target,
		},
		body: JSON.stringify(body),
	}
}

/**
 * List CloudWatch log groups. Names and stored-byte metadata only.
 *
 * @example
 * import { listLogGroups } from 'kody:@kody/aws/logs'
 * const { items } = await listLogGroups({ prefix: '/aws/lambda/' })
 */
export async function listLogGroups(
	params: AwsAuthOptions & { prefix?: string; pageSize?: number } = {},
) {
	const input = inputRecord(params)
	const result = await awsSignedFetch({
		...params,
		service: 'logs',
		method: 'POST',
		path: '/',
		...jsonTarget('Logs_20140328.DescribeLogGroups', {
			logGroupNamePrefix: optionalString(input, 'prefix'),
			limit: boundedPageSize(input.pageSize, 20, 50),
		}),
	})
	const parsed = parseJsonBody(result.text) as {
		logGroups?: Array<Record<string, unknown>>
		nextToken?: string
	}
	const items = (parsed.logGroups ?? []).map((group) => ({
		name: typeof group.logGroupName === 'string' ? group.logGroupName : null,
		storedBytes:
			typeof group.storedBytes === 'number' ? group.storedBytes : null,
		retentionInDays:
			typeof group.retentionInDays === 'number'
				? group.retentionInDays
				: null,
		arn: typeof group.arn === 'string' ? group.arn : null,
	}))
	return {
		ok: true,
		region: result.region,
		nextToken: parsed.nextToken ?? null,
		items,
	}
}

/**
 * Read a short window of CloudWatch log events. Bodies that look like
 * credentials are redacted. Never dumps secret object contents.
 *
 * @example
 * import { getLogEvents } from 'kody:@kody/aws/logs'
 * const { events } = await getLogEvents({ logGroupName: '/aws/lambda/example' })
 */
export async function getLogEvents(
	params: AwsAuthOptions & {
		logGroupName: string
		logStreamName?: string
		filterPattern?: string
		pageSize?: number
	},
) {
	const input = inputRecord(params)
	const logGroupName = requiredString(input, 'logGroupName')
	const logStreamName = optionalString(input, 'logStreamName')
	const filterPattern = optionalString(input, 'filterPattern')
	const limit = boundedPageSize(input.pageSize, 20, 50)

	const target = logStreamName
		? 'Logs_20140328.GetLogEvents'
		: 'Logs_20140328.FilterLogEvents'
	const body = logStreamName
		? {
				logGroupName,
				logStreamName,
				limit,
				startFromHead: false,
			}
		: {
				logGroupName,
				filterPattern,
				limit,
			}

	const result = await awsSignedFetch({
		...params,
		service: 'logs',
		method: 'POST',
		path: '/',
		...jsonTarget(target, body),
	})
	const parsed = parseJsonBody(result.text) as {
		events?: Array<Record<string, unknown>>
		nextToken?: string
		nextForwardToken?: string
	}
	const rawEvents = parsed.events ?? []
	const events = rawEvents.map((event) => ({
		timestamp: typeof event.timestamp === 'number' ? event.timestamp : null,
		ingestionTime:
			typeof event.ingestionTime === 'number' ? event.ingestionTime : null,
		logStreamName:
			typeof event.logStreamName === 'string' ? event.logStreamName : null,
		message:
			typeof event.message === 'string'
				? redactUnknown(event.message)
				: null,
	}))

	return {
		ok: true,
		region: result.region,
		logGroupName,
		logStreamName: logStreamName ?? null,
		nextToken: parsed.nextToken ?? parsed.nextForwardToken ?? null,
		events,
	}
}

export default listLogGroups