Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions actions/setup/js/send_otlp_span.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ const { getErrorMessage } = require("./error_helpers.cjs");
* - No third-party dependencies: uses only Node built-ins + native fetch.
*/

// ---------------------------------------------------------------------------
// OTel GenAI engine-to-system mapping
// ---------------------------------------------------------------------------

/**
* Maps gh-aw internal engine IDs to the OTel GenAI semantic-convention
* `gen_ai.system` values expected by Grafana, Datadog, Honeycomb, and Sentry.
* Unknown engines fall back to the engine ID as-is.
Comment on lines +29 to +30
* @type {Record<string, string>}
*/
const ENGINE_TO_SYSTEM_MAP = {
copilot: "github_models",
claude: "anthropic",
codex: "openai",
gemini: "google_vertex_ai",
};

// ---------------------------------------------------------------------------
// Low-level helpers
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -913,9 +930,14 @@ async function sendJobConclusionSpan(spanName, options = {}) {
// All gh-aw agent executions are chat-style LLM completions.
agentAttributes.push(buildAttr("gen_ai.operation.name", "chat"));
if (model) agentAttributes.push(buildAttr("gen_ai.request.model", model));
// Emit gen_ai.provider.name when engineId is available; it may be omitted when
// engine metadata is unavailable, so this span does not guarantee full GenAI spec compliance.
if (engineId) agentAttributes.push(buildAttr("gen_ai.provider.name", engineId));
// gen_ai.system is the OTel GenAI standard attribute for the LLM system/provider.
// Map the gh-aw internal engine ID to the standardized value so backends can apply
// native GenAI dashboard detection. The original engine ID is preserved in gh-aw.engine.
if (engineId) {
const genAiSystem = ENGINE_TO_SYSTEM_MAP[engineId] || engineId;
agentAttributes.push(buildAttr("gen_ai.system", genAiSystem));
agentAttributes.push(buildAttr("gh-aw.engine", engineId));
}
// gen_ai.workflow.name identifies the agentic workflow, matching the OTel spec example
// use-cases (e.g. "multi_agent_rag", "customer_support_pipeline").
if (workflowName) agentAttributes.push(buildAttr("gen_ai.workflow.name", workflowName));
Expand Down
10 changes: 6 additions & 4 deletions actions/setup/js/send_otlp_span.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1887,7 +1887,7 @@ describe("sendJobConclusionSpan", () => {
expect(agentSpan.kind).toBe(3); // SPAN_KIND_CLIENT
});

it("includes gen_ai.request.model, gen_ai.provider.name, gen_ai.operation.name and gen_ai.workflow.name on the agent span from aw_info.json", async () => {
it("includes gen_ai.request.model, gen_ai.system, gh-aw.engine, gen_ai.operation.name and gen_ai.workflow.name on the agent span from aw_info.json", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

Expand Down Expand Up @@ -1915,11 +1915,12 @@ describe("sendJobConclusionSpan", () => {
const attrs = Object.fromEntries(agentSpan.attributes.map(a => [a.key, a.value.stringValue ?? a.value.intValue]));
expect(attrs["gen_ai.operation.name"]).toBe("chat");
expect(attrs["gen_ai.request.model"]).toBe("claude-3-5-sonnet-20241022");
expect(attrs["gen_ai.provider.name"]).toBe("claude");
expect(attrs["gen_ai.system"]).toBe("anthropic");
expect(attrs["gh-aw.engine"]).toBe("claude");
expect(attrs["gen_ai.workflow.name"]).toBe("otel-advisor");
});

it("omits gen_ai.request.model, gen_ai.provider.name and gen_ai.workflow.name from the agent span when model, engine_id and workflow_name are absent", async () => {
it("omits gen_ai.request.model, gen_ai.system, gh-aw.engine and gen_ai.workflow.name from the agent span when model, engine_id and workflow_name are absent", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

Expand All @@ -1945,7 +1946,8 @@ describe("sendJobConclusionSpan", () => {
expect(attrs["gen_ai.operation.name"]).toBe("chat");
const keys = agentSpan.attributes.map(a => a.key);
expect(keys).not.toContain("gen_ai.request.model");
expect(keys).not.toContain("gen_ai.provider.name");
expect(keys).not.toContain("gen_ai.system");
expect(keys).not.toContain("gh-aw.engine");
expect(keys).not.toContain("gen_ai.workflow.name");
});

Expand Down
Loading