-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathlogs-eval.ts
More file actions
167 lines (143 loc) · 5.25 KB
/
logs-eval.ts
File metadata and controls
167 lines (143 loc) · 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import { parseTimeString } from '../../../lib/utils';
import { getOnlineEvaluationConfig } from '../../aws/agentcore-control';
import { searchLogs, streamLogs } from '../../aws/cloudwatch';
import type { DeployedProjectConfig } from '../resolve-agent';
import { loadDeployedProjectConfig, resolveAgent } from '../resolve-agent';
export interface LogsEvalOptions {
agent?: string;
since?: string;
until?: string;
limit?: string;
json?: boolean;
follow?: boolean;
}
export interface LogsEvalResult {
success: boolean;
error?: string;
}
function formatLogLine(event: { timestamp: number; message: string }, json: boolean): string {
if (json) {
return JSON.stringify({ timestamp: new Date(event.timestamp).toISOString(), message: event.message });
}
const ts = new Date(event.timestamp).toISOString();
return `${ts} ${event.message}`;
}
interface ResolvedLogGroup {
logGroupName: string;
configName: string;
failureReason?: string;
}
/**
* Resolve the online eval config log group names.
* Fetches the actual log group from the API when possible, falls back to convention.
*/
async function resolveEvalLogGroups(
context: DeployedProjectConfig,
targetName: string,
region: string
): Promise<ResolvedLogGroup[]> {
const { project, deployedState } = context;
const targetResources = deployedState.targets[targetName]?.resources;
const matchingConfigs = project.onlineEvalConfigs ?? [];
const results: ResolvedLogGroup[] = [];
for (const config of matchingConfigs) {
const deployed = targetResources?.onlineEvalConfigs?.[config.name];
if (!deployed?.onlineEvaluationConfigId) continue;
const configId = deployed.onlineEvaluationConfigId;
const fallbackLogGroup = `/aws/bedrock-agentcore/evaluations/results/${configId}`;
try {
const apiConfig = await getOnlineEvaluationConfig({ region, configId });
results.push({
logGroupName: apiConfig.outputLogGroupName ?? fallbackLogGroup,
configName: config.name,
failureReason: apiConfig.failureReason,
});
} catch {
// API call failed — fall back to convention-based name
results.push({ logGroupName: fallbackLogGroup, configName: config.name });
}
}
return results;
}
export async function handleLogsEval(options: LogsEvalOptions): Promise<LogsEvalResult> {
const context = await loadDeployedProjectConfig();
const agentResult = resolveAgent(context, { agent: options.agent });
if (!agentResult.success) {
return { success: false, error: agentResult.error };
}
const { agent } = agentResult;
const resolvedLogGroups = await resolveEvalLogGroups(context, agent.targetName, agent.region);
if (resolvedLogGroups.length === 0) {
return {
success: false,
error: `No deployed online eval configs found. Add one with 'agentcore add online-eval' and deploy.`,
};
}
// Surface failure reasons from configs that are in a failed state
for (const lg of resolvedLogGroups) {
if (lg.failureReason) {
console.error(`Warning: Online eval config '${lg.configName}' has a failure: ${lg.failureReason}`);
}
}
const isJson = options.json ?? false;
const isFollow = options.follow ?? (!options.since && !options.until);
const ac = new AbortController();
const onSignal = () => ac.abort();
process.on('SIGINT', onSignal);
try {
// Query all matching log groups
for (const { logGroupName } of resolvedLogGroups) {
if (!isFollow) {
const startTimeMs = options.since ? parseTimeString(options.since) : Date.now() - 3_600_000;
const endTimeMs = options.until ? parseTimeString(options.until) : Date.now();
const limit = options.limit ? parseInt(options.limit, 10) : undefined;
try {
for await (const event of searchLogs({
logGroupName,
region: agent.region,
startTimeMs,
endTimeMs,
limit,
})) {
console.log(formatLogLine(event, isJson));
}
} catch (err: unknown) {
const errorName = (err as { name?: string })?.name;
if (errorName === 'ResourceNotFoundException') {
// Log group exists in config but not yet in CloudWatch — skip
continue;
}
throw err;
}
} else {
console.error(`Streaming eval logs for ${agent.agentName} from ${logGroupName}... (Ctrl+C to stop)`);
try {
for await (const event of streamLogs({
logGroupName,
region: agent.region,
accountId: agent.accountId,
abortSignal: ac.signal,
})) {
console.log(formatLogLine(event, isJson));
}
} catch (err: unknown) {
const errorName = (err as { name?: string })?.name;
if (errorName === 'ResourceNotFoundException') {
console.error(`Log group ${logGroupName} not found yet — waiting for online eval results...`);
continue;
}
throw err;
}
}
}
return { success: true };
} catch (err: unknown) {
const errorName = (err as { name?: string })?.name;
if (errorName === 'AbortError' || ac.signal.aborted) {
return { success: true };
}
throw err;
} finally {
process.removeListener('SIGINT', onSignal);
}
}