import { awsSignedFetch } from './client.ts' import { boundedPageSize, inputRecord, optionalString, requiredString, type AwsAuthOptions, } from './validation.ts' function formBody(fields: Record): string { const params = new URLSearchParams() for (const [key, value] of Object.entries(fields)) { if (value === undefined || value === '') continue params.set(key, value) } return params.toString() } /** * List CloudWatch metrics. Namespaces and metric names only. * * @example * import { listMetrics } from 'kody:@kody/aws/metrics' * const { items } = await listMetrics({ namespace: 'AWS/Lambda' }) */ export async function listMetrics( params: AwsAuthOptions & { namespace?: string metricName?: string pageSize?: number } = {}, ) { const input = inputRecord(params) const result = await awsSignedFetch({ ...params, service: 'monitoring', method: 'POST', path: '/', headers: { 'content-type': 'application/x-www-form-urlencoded', }, body: formBody({ Action: 'ListMetrics', Version: '2010-08-01', Namespace: optionalString(input, 'namespace'), MetricName: optionalString(input, 'metricName'), }), }) const names = Array.from( result.text.matchAll(/([\s\S]*?)<\/MetricName>/g), ).map((match) => match[1]?.trim() ?? '') const namespaces = Array.from( result.text.matchAll(/([\s\S]*?)<\/Namespace>/g), ).map((match) => match[1]?.trim() ?? '') const items = names.map((metricName, index) => ({ metricName, namespace: namespaces[index] ?? null, })) return { ok: true, region: result.region, items: items.slice(0, boundedPageSize(input.pageSize, 50, 200)), } } /** * Read one CloudWatch statistic for a metric. Pass namespace, metric name, * and a statistic — no baked-in account resources. * * @example * import { getMetricStatistics } from 'kody:@kody/aws/metrics' * const stats = await getMetricStatistics({ * namespace: 'AWS/S3', * metricName: 'NumberOfObjects', * }) */ export async function getMetricStatistics( params: AwsAuthOptions & { namespace: string metricName: string statistic?: string periodSeconds?: number hours?: number }, ) { const input = inputRecord(params) const namespace = requiredString(input, 'namespace') const metricName = requiredString(input, 'metricName') const statistic = optionalString(input, 'statistic') ?? 'Average' const period = Number(input.periodSeconds ?? 300) const hours = Number(input.hours ?? 1) if (!Number.isInteger(period) || period < 60) { throw new Error('periodSeconds must be an integer of at least 60.') } if (!Number.isFinite(hours) || hours <= 0 || hours > 24) { throw new Error('hours must be between 0 exclusive and 24 inclusive.') } const end = new Date() const start = new Date(end.getTime() - hours * 60 * 60 * 1000) const result = await awsSignedFetch({ ...params, service: 'monitoring', method: 'POST', path: '/', headers: { 'content-type': 'application/x-www-form-urlencoded', }, body: formBody({ Action: 'GetMetricStatistics', Version: '2010-08-01', Namespace: namespace, MetricName: metricName, StartTime: start.toISOString(), EndTime: end.toISOString(), Period: String(period), 'Statistics.member.1': statistic, }), }) const datapoints = Array.from( result.text.matchAll(/([\s\S]*?)<\/member>/g), ) .map((match) => match[1] ?? '') .filter((block) => block.includes('')) .map((block) => ({ timestamp: block.match(/([\s\S]*?)<\/Timestamp>/)?.[1] ?? null, average: numberTag(block, 'Average'), sum: numberTag(block, 'Sum'), maximum: numberTag(block, 'Maximum'), minimum: numberTag(block, 'Minimum'), sampleCount: numberTag(block, 'SampleCount'), unit: block.match(/([\s\S]*?)<\/Unit>/)?.[1] ?? null, })) return { ok: true, region: result.region, namespace, metricName, statistic, label: result.text.match(/