P1: Load dd-trace before other modules via datadog-init.ts entry point P1: Batch all CloudWatch metrics into single PutMetricDataCommand per request P2: Deduplicate warning logs with else-if for high latency vs error P3: Add response.ok check to Datadog log forwarding fetch P3: Update getSentryHub() to use getCurrentScope() for Sentry SDK 8.x Co-Authored-By: Paperclip <noreply@paperclip.ing>
127 lines
2.9 KiB
TypeScript
127 lines
2.9 KiB
TypeScript
import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from '@aws-sdk/client-cloudwatch';
|
|
import { getMonitoringConfig } from './config';
|
|
|
|
let client: CloudWatchClient | null = null;
|
|
|
|
function getClient(): CloudWatchClient | null {
|
|
if (client) return client;
|
|
|
|
const config = getMonitoringConfig();
|
|
const region = process.env.AWS_REGION || 'us-east-1';
|
|
|
|
try {
|
|
client = new CloudWatchClient({ region });
|
|
return client;
|
|
} catch {
|
|
console.warn('[CloudWatch] Metrics client initialization skipped');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export interface MetricDataPoint {
|
|
MetricName: string;
|
|
Dimensions?: { Name: string; Value: string }[];
|
|
Value: number;
|
|
Unit?: string;
|
|
Timestamp?: Date;
|
|
}
|
|
|
|
const NAMESPACE = 'ShieldAI';
|
|
|
|
export async function emitMetric(
|
|
serviceName: string,
|
|
metricName: string,
|
|
value: number,
|
|
unit: StandardUnit = 'Count',
|
|
dimensions?: Record<string, string>
|
|
) {
|
|
const cw = getClient();
|
|
if (!cw) return;
|
|
|
|
const dims: { Name: string; Value: string }[] = [
|
|
{ Name: 'service', Value: serviceName },
|
|
...(dimensions ? Object.entries(dimensions).map(([n, v]) => ({ Name: n, Value: v })) : []),
|
|
];
|
|
|
|
const command = new PutMetricDataCommand({
|
|
Namespace: NAMESPACE,
|
|
MetricData: [
|
|
{
|
|
MetricName: metricName,
|
|
Dimensions: dims,
|
|
Value: value,
|
|
Unit: unit,
|
|
},
|
|
],
|
|
});
|
|
|
|
try {
|
|
await cw.send(command);
|
|
} catch (err) {
|
|
console.warn('[CloudWatch] Metric emit failed:', (err as Error).message);
|
|
}
|
|
}
|
|
|
|
export async function emitBatchMetrics(metrics: {
|
|
serviceName: string;
|
|
data: { metricName: string; value: number; unit: StandardUnit; dimensions?: Record<string, string> }[];
|
|
}) {
|
|
const cw = getClient();
|
|
if (!cw) return;
|
|
|
|
const metricData = metrics.data.map((m) => ({
|
|
MetricName: m.metricName,
|
|
Dimensions: [
|
|
{ Name: 'service', Value: metrics.serviceName },
|
|
...(m.dimensions ? Object.entries(m.dimensions).map(([n, v]) => ({ Name: n, Value: v })) : []),
|
|
],
|
|
Value: m.value,
|
|
Unit: m.unit,
|
|
}));
|
|
|
|
const command = new PutMetricDataCommand({
|
|
Namespace: NAMESPACE,
|
|
MetricData: metricData,
|
|
});
|
|
|
|
try {
|
|
await cw.send(command);
|
|
} catch (err) {
|
|
console.warn('[CloudWatch] Batch metric emit failed:', (err as Error).message);
|
|
}
|
|
}
|
|
|
|
export async function emitLatency(
|
|
serviceName: string,
|
|
latencyMs: number,
|
|
percentile: 'p50' | 'p95' | 'p99'
|
|
) {
|
|
await emitMetric(
|
|
serviceName,
|
|
'api_latency',
|
|
latencyMs,
|
|
'Milliseconds' as StandardUnit,
|
|
{ percentile }
|
|
);
|
|
}
|
|
|
|
export async function emitRequestCount(serviceName: string, statusCode: number) {
|
|
await emitMetric(
|
|
serviceName,
|
|
'api_requests',
|
|
1,
|
|
'Count' as StandardUnit,
|
|
{ status_class: String(Math.floor(statusCode / 100)) + 'xx' }
|
|
);
|
|
}
|
|
|
|
export async function emitError(serviceName: string, errorType: string) {
|
|
await emitMetric(
|
|
serviceName,
|
|
'api_errors',
|
|
1,
|
|
'Count' as StandardUnit,
|
|
{ error_type: errorType }
|
|
);
|
|
}
|