Skip to content

Commit 28f761c

Browse files
committed
feat: Implement runtime observability metrics and dashboard specifications
1 parent 3d1140a commit 28f761c

7 files changed

Lines changed: 968 additions & 119 deletions
Lines changed: 3 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,19 @@
11
// @ts-check
22
/// <reference types="@actions/github-script" />
33

4-
const fs = require("fs");
54
const { main: exportCopilotOtelTraces } = require("./export_copilot_otel_traces.cjs");
6-
7-
const AW_INFO_PATH = "/tmp/gh-aw/aw_info.json";
8-
const AGENT_OUTPUT_PATH = "/tmp/gh-aw/agent_output.json";
9-
const gatewayEventPaths = ["/tmp/gh-aw/mcp-logs/gateway.jsonl", "/tmp/gh-aw/mcp-logs/rpc-messages.jsonl"];
10-
11-
function readJSONIfExists(path) {
12-
if (!fs.existsSync(path)) {
13-
return null;
14-
}
15-
16-
try {
17-
return JSON.parse(fs.readFileSync(path, "utf8"));
18-
} catch {
19-
return null;
20-
}
21-
}
22-
23-
function countBlockedRequests() {
24-
let total = 0;
25-
26-
for (const path of gatewayEventPaths) {
27-
if (!fs.existsSync(path)) {
28-
continue;
29-
}
30-
31-
const lines = fs.readFileSync(path, "utf8").split("\n");
32-
for (const raw of lines) {
33-
const line = raw.trim();
34-
if (!line) continue;
35-
try {
36-
const entry = JSON.parse(line);
37-
if (entry && entry.type === "DIFC_FILTERED") total++;
38-
} catch {
39-
// skip malformed lines
40-
}
41-
}
42-
}
43-
44-
return total;
45-
}
46-
47-
function uniqueCreatedItemTypes(items) {
48-
const types = new Set();
49-
50-
for (const item of items) {
51-
if (item && typeof item.type === "string" && item.type.trim() !== "") {
52-
types.add(item.type);
53-
}
54-
}
55-
56-
return [...types].sort();
57-
}
58-
59-
function collectObservabilityData() {
60-
const awInfo = readJSONIfExists(AW_INFO_PATH) || {};
61-
const agentOutput = readJSONIfExists(AGENT_OUTPUT_PATH) || { items: [], errors: [] };
62-
const items = Array.isArray(agentOutput.items) ? agentOutput.items : [];
63-
const errors = Array.isArray(agentOutput.errors) ? agentOutput.errors : [];
64-
// Prefer GITHUB_AW_OTEL_TRACE_ID (written to GITHUB_ENV by action_setup_otlp.cjs)
65-
// so the summary always shows the trace ID that is actually present in the OTLP backend.
66-
// Fall back to context.otel_trace_id for cross-workflow traces propagated from a parent.
67-
// Do NOT fall back to workflow_call_id — it is not a valid OTLP trace ID.
68-
const traceId = process.env.GITHUB_AW_OTEL_TRACE_ID || (awInfo.context ? awInfo.context.otel_trace_id || "" : "");
69-
70-
return {
71-
workflowName: awInfo.workflow_name || "",
72-
engineId: awInfo.engine_id || "",
73-
traceId,
74-
staged: awInfo.staged === true,
75-
firewallEnabled: awInfo.firewall_enabled === true,
76-
createdItemCount: items.length,
77-
createdItemTypes: uniqueCreatedItemTypes(items),
78-
outputErrorCount: errors.length,
79-
blockedRequests: countBlockedRequests(),
80-
};
81-
}
82-
83-
function buildObservabilitySummary(data) {
84-
const posture = data.createdItemCount > 0 ? "write-capable" : "read-only";
85-
const lines = [];
86-
87-
lines.push("<details>");
88-
lines.push("<summary>Observability</summary>");
89-
lines.push("");
90-
91-
if (data.workflowName) {
92-
lines.push(`- **workflow**: ${data.workflowName}`);
93-
}
94-
if (data.engineId) {
95-
lines.push(`- **engine**: ${data.engineId}`);
96-
}
97-
if (data.traceId) {
98-
lines.push(`- **trace id**: ${data.traceId}`);
99-
}
100-
101-
lines.push(`- **posture**: ${posture}`);
102-
lines.push(`- **created items**: ${data.createdItemCount}`);
103-
lines.push(`- **blocked requests**: ${data.blockedRequests}`);
104-
lines.push(`- **agent output errors**: ${data.outputErrorCount}`);
105-
lines.push(`- **firewall enabled**: ${data.firewallEnabled}`);
106-
lines.push(`- **staged**: ${data.staged}`);
107-
108-
if (data.createdItemTypes.length > 0) {
109-
lines.push("- **item types**:");
110-
for (const itemType of data.createdItemTypes) {
111-
lines.push(` - ${itemType}`);
112-
}
113-
}
114-
115-
lines.push("");
116-
lines.push("</details>");
117-
118-
return lines.join("\n") + "\n";
119-
}
5+
const { buildObservabilitySummary, collectRuntimeObservabilityData } = require("./runtime_observability.cjs");
1206

1217
async function main(core) {
1228
await exportCopilotOtelTraces(core);
123-
const data = collectObservabilityData();
9+
const data = collectRuntimeObservabilityData();
12410
const markdown = buildObservabilitySummary(data);
12511
await core.summary.addRaw(markdown).write();
12612
core.info("Generated observability summary in step summary");
12713
}
12814

12915
module.exports = {
13016
buildObservabilitySummary,
131-
collectObservabilityData,
17+
collectObservabilityData: collectRuntimeObservabilityData,
13218
main,
13319
};

actions/setup/js/generate_observability_summary.test.cjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("generate_observability_summary.cjs", () => {
2121
});
2222

2323
afterEach(() => {
24-
for (const path of ["/tmp/gh-aw/aw_info.json", "/tmp/gh-aw/agent_output.json", "/tmp/gh-aw/mcp-logs/gateway.jsonl", "/tmp/gh-aw/mcp-logs/rpc-messages.jsonl"]) {
24+
for (const path of ["/tmp/gh-aw/aw_info.json", "/tmp/gh-aw/agent_output.json", "/tmp/gh-aw/agent_usage.json", "/tmp/gh-aw/agent-stdio.log", "/tmp/gh-aw/mcp-logs/gateway.jsonl", "/tmp/gh-aw/mcp-logs/rpc-messages.jsonl"]) {
2525
if (fs.existsSync(path)) {
2626
fs.unlinkSync(path);
2727
}
@@ -46,6 +46,8 @@ describe("generate_observability_summary.cjs", () => {
4646
errors: ["validation failed"],
4747
})
4848
);
49+
fs.writeFileSync("/tmp/gh-aw/agent_usage.json", JSON.stringify({ input_tokens: 1000, output_tokens: 200, cache_read_tokens: 1000, cache_write_tokens: 50, effective_tokens: 500 }));
50+
fs.writeFileSync("/tmp/gh-aw/agent-stdio.log", '[WARN] first warning\n{"type":"result","num_turns":7,"total_cost_usd":1.75}\n');
4951
fs.writeFileSync("/tmp/gh-aw/mcp-logs/gateway.jsonl", [JSON.stringify({ type: "DIFC_FILTERED" }), JSON.stringify({ type: "REQUEST" })].join("\n"));
5052

5153
await module.main(mockCore);
@@ -58,6 +60,13 @@ describe("generate_observability_summary.cjs", () => {
5860
expect(summary).toContain("- **trace id**: a3f2c8d1e4b7091f6a5c2e3d8f401b72");
5961
expect(summary).not.toContain("12345678901-1");
6062
expect(summary).toContain("- **posture**: write-capable");
63+
expect(summary).toContain("- **runtime status**: error");
64+
expect(summary).toContain("- **total tokens**: 500");
65+
expect(summary).toContain("- **estimated cost usd**: 1.75");
66+
expect(summary).toContain("- **turns**: 7");
67+
expect(summary).toContain("- **cache efficiency**: 50%");
68+
expect(summary).toContain("- **runtime risk score**: 31");
69+
expect(summary).toContain("- **optimization score**: 31");
6170
expect(summary).toContain("- **created items**: 2");
6271
expect(summary).toContain("- **blocked requests**: 1");
6372
expect(summary).toContain("- **agent output errors**: 1");

0 commit comments

Comments
 (0)