Skip to content

Preserve OTEL resource attribution and normalize agent token counters#36450

Merged
pelikhan merged 7 commits into
mainfrom
copilot/token-consumption-report-2026-06-02
Jun 2, 2026
Merged

Preserve OTEL resource attribution and normalize agent token counters#36450
pelikhan merged 7 commits into
mainfrom
copilot/token-consumption-report-2026-06-02

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 2, 2026

Daily token reporting showed 151.3M tokens across 5,939 model calls, but workflow-level attribution was missing because token-bearing spans and workflow identity were emitted on disjoint span populations. In practice, gh-aw.workflow.name was absent from token-bearing spans, and the agent_usage.json roll-up path was too strict about token counter types.

  • Preserve workflow/run attribution on OTLP resource spans

    • Merge precomputed OTEL_RESOURCE_ATTRIBUTES into the JavaScript OTLP resource builder instead of rebuilding resource metadata from scratch.
    • Preserve escaped OpenTelemetry resource attribute values (\,, \=, \\) so workflow names and other contextual fields survive round-trip parsing.
    • Keep GitHub run metadata as authoritative by upserting github.* resource attributes after merging.
  • Harden token roll-up emission

    • Normalize agent_usage.json through the same token-usage path used for runtime log parsing before emitting gen_ai.usage.*.
    • Accept numeric-string counters in addition to numeric JSON values for input_tokens, output_tokens, cache_read_tokens, and cache_write_tokens.
    • Continue emitting gen_ai.usage.total_tokens from normalized input/output totals.
  • Add regressions for the observability gap

    • Cover merged OTEL_RESOURCE_ATTRIBUTES on emitted resource spans.
    • Cover string-valued token counters on the dedicated agent span.

Example of the new resource-attribute handling:

process.env.OTEL_RESOURCE_ATTRIBUTES =
  "gh-aw.workflow.name=Daily\\, Token\\= Report,gh-aw.run.id=987654321,gh-aw.engine.id=claude";

const resourceAttributes = buildGitHubActionsResourceAttributes({
  repository: "owner/repo",
  runId: "987654321",
  staged: false,
});

// Includes:
// - gh-aw.workflow.name = "Daily, Token= Report"
// - gh-aw.run.id = "987654321"
// - gh-aw.engine.id = "claude"
// - github.repository / github.run_id / github.run_attempt

Copilot AI and others added 3 commits June 2, 2026 13:51
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix token attribution for daily report Preserve OTEL resource attribution and normalize agent token counters Jun 2, 2026
Copilot AI requested a review from pelikhan June 2, 2026 14:00
@pelikhan pelikhan marked this pull request as ready for review June 2, 2026 14:02
Copilot AI review requested due to automatic review settings June 2, 2026 14:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an observability attribution gap by ensuring OTLP resource spans retain workflow/run identity from OTEL_RESOURCE_ATTRIBUTES, and hardens token roll-ups by normalizing agent_usage.json counters (including numeric strings) before emitting gen_ai.usage.* attributes.

Changes:

  • Merge and upsert OTEL_RESOURCE_ATTRIBUTES into emitted OTLP resource attributes (preserving escaping) while keeping GitHub github.* metadata authoritative.
  • Normalize token counters (including string-valued counters) via the shared runtime token normalization path before emitting gen_ai.usage.*.
  • Add regression tests for merged resource attributes and string token counters on the agent span.
Show a summary per file
File Description
actions/setup/js/send_otlp_span.cjs Adds parsing/merging of OTEL_RESOURCE_ATTRIBUTES into resource attributes and expands token normalization to accept numeric strings.
actions/setup/js/send_otlp_span.test.cjs Adds regression tests for resource-attribute merging and string-valued token counters.
.github/workflows/agentic-token-trend-audit.lock.yml Updates the workflow lock metadata hash.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment on lines +453 to +457
for (const char of raw) {
if (escaping) {
current += char;
escaping = false;
continue;
Comment on lines +1491 to +1502
it("merges OTEL_RESOURCE_ATTRIBUTES into resource attributes", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);
process.env.GITHUB_REPOSITORY = "owner/repo";
process.env.GITHUB_RUN_ID = "987654321";
process.env.OTEL_RESOURCE_ATTRIBUTES =
"gh-aw.workflow.name=Daily\\, Token\\= Report,gh-aw.run.id=987654321,gh-aw.engine.id=claude,service.version=1.2.3";

await sendJobConclusionSpan("gh-aw.job.conclusion");

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #36450 does not have the 'implementation' label (has_implementation_label=false) and has 0 new lines of code in business logic directories (requires_adr_by_default_volume=false, threshold=100). Neither Condition A nor Condition B is met.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions github-actions Bot mentioned this pull request Jun 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 2 test(s): 2 design, 0 implementation, 0 guideline violation(s).

📊 Metrics & Test Classification (2 tests analyzed)
Metric Value
New/modified tests analyzed 2
✅ Design tests (behavioral contracts) 2 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (100%)
Duplicate test clusters 0
Test inflation detected No (0.34:1 ratio)
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
merges OTEL_RESOURCE_ATTRIBUTES into resource attributes actions/setup/js/send_otlp_span.test.cjs:1490 ✅ Design None — covers escape-char parsing edge case
normalizes string token counters from agent_usage.json on the agent span actions/setup/js/send_otlp_span.test.cjs:4883 ✅ Design None — covers string→integer normalization edge case

Language Support

Tests analyzed:

  • 🟨 JavaScript (*.test.cjs): 2 tests (vitest)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). Both tests verify observable OTLP payload output against real behavioral contracts introduced by this PR.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 1.7M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). Both new tests enforce behavioral contracts on observable OTLP payloads.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnose and /tdd — changes are correct and targeted; three minor issues worth addressing before merge.

📋 Key Themes & Highlights

Key Themes

  • Test env pollution: OTEL_RESOURCE_ATTRIBUTES is set by the new integration test but not included in the envKeys save/restore list — will bleed into later tests in the same describe block.
  • Parser under-tested: parseOTELResourceAttributes implements non-trivial OTEL escape rules but lacks a dedicated unit test suite for edge cases.
  • Silent fallback change: Passing agent_usage.json through normalizeRuntimeTokenUsage changes the fallback behavior for files with unrecognized fields; the change is probably correct but untested.

Positive Highlights

  • ✅ Root cause correctly identified and fixed — merging OTEL_RESOURCE_ATTRIBUTES before upserting github.* attributes is exactly the right precedence model.
  • normalizeTokenCounter is clean and handles both numeric and string values without duplicating logic.
  • ✅ Two regression tests added that directly cover the two fixed gaps — good signal that future breakage will be caught.
  • ✅ OTEL escape spec (,, =, \) correctly implemented in parseOTELResourceAttributes.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 2.2M

Comments that could not be inline-anchored

actions/setup/js/send_otlp_span.test.cjs:1165

[/tdd] OTEL_RESOURCE_ATTRIBUTES is missing from the envKeys save/restore list, so the new test at line 1498 sets this env var without cleanup — it will leak into every subsequent test in this describe block.

<details>
<summary>💡 Fix</summary>

Add &quot;OTEL_RESOURCE_ATTRIBUTES&quot; to the envKeys array here (and to the equivalent array in the sendJobConclusionSpan describe block ~line 2480). Without this, tests that run after the new integration test will see unexpected resource attri…

actions/setup/js/send_otlp_span.cjs:470

[/tdd] parseOTELResourceAttributes is a non-trivial parser with custom escape rules but is only exercised through the high-level integration test. Consider adding a small dedicated describe(&#39;parseOTELResourceAttributes&#39;) block to cover edge cases directly.

<details>
<summary>💡 Uncovered edge cases</summary>

  • \\ (literal backslash in a value) — e.g. key=a\\b should yield a\b
  • Pair with no = — e.g. noequals,key=val — should the keyless token be silently dropped?
  • Trailing …
actions/setup/js/send_otlp_span.cjs:2202

[/diagnose] Wrapping readJSONIfExists(...) in normalizeRuntimeTokenUsage introduces a subtle fallback: if agent_usage.json exists but contains an object with no recognized token fields (e.g. {&quot;model&quot;:&quot;claude-3&quot;} only), the function returns undefined and execution falls through to runtimeMetrics.tokenUsage. Previously the raw file object would have been used directly.

This is likely fine, but the behavior change is unintentional and untested — a test with a file containing only …

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three issues found across the two changed files. None are data-loss or security bugs, but two will cause test environment pollution and the third is a silent protocol-conformance bug.

### Findings summary

1. OTEL_RESOURCE_ATTRIBUTES not cleaned up in afterEach (test.cjs:1498) — env var leaks into all subsequent tests in the describe block; test results become execution-order dependent.

2. parseOTELResourceAttributes two-pass design breaks \= in key names (cjs:479) — the outer loop strips backslashes before splitPair runs, making splitPair's own escape handling dead code. A key like my\=key=value is silently mis-parsed as key my, value key=value. Fix: collapse both passes into one.

3. New test placed inside wrong describe block (test.cjs:1491) — calls sendJobConclusionSpan while scoped to sendJobSetupSpan hooks; conclusion-span env keys are not reset, creating potential for cross-test state bleed.

The production-side logic changes (normalizeRuntimeTokenUsage string-normalization, upsertResourceAttr for OTEL attribute merging) look correct.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 254K

process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);
process.env.GITHUB_REPOSITORY = "owner/repo";
process.env.GITHUB_RUN_ID = "987654321";
process.env.OTEL_RESOURCE_ATTRIBUTES =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTEL_RESOURCE_ATTRIBUTES leaks into subsequent tests: this env var is set here but not included in the envKeys cleanup array, so afterEach will not delete it.

💡 Suggested fix

Add "OTEL_RESOURCE_ATTRIBUTES" to the envKeys array at line ~1142:

const envKeys = [
  "GH_AW_OTLP_ENDPOINTS",
  // ... existing keys ...
  "OTEL_RESOURCE_ATTRIBUTES",  // add this
];

Without this, every test that runs after "merges OTEL_RESOURCE_ATTRIBUTES" will see gh-aw.workflow.name, gh-aw.engine.id, and service.version injected into resource attributes. Tests that use toContainEqual will likely still pass (they only assert presence), but the env pollution makes test results non-deterministic with respect to execution order and can mask regressions.

Comment thread actions/setup/js/send_otlp_span.cjs Outdated
pairs.push(current);
}

const splitPair = pair => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

splitPair's escape-handling is dead code and masks a real parsing bug for \\= in key names: the outer loop strips all backslashes before pairs reach splitPair, so the escaped flag inside splitPair can never be set, and \\= in a key position is silently mis-parsed.

💡 Details and fix

What the outer loop does

For input key\\=1=value, the outer loop (lines 455–471) sets escaping=true on \\, then appends = literally and resets escaping. The resulting pair string is key=1=valuethe backslash is gone.

splitPair then sees key=1=value, finds the first = and produces:

  • key: "key"
  • value: "1=value"

The correct result per the OTel spec is key: "key=1", value: "value".

The escape-handling code inside splitPair (the escaped variable and its branches) is therefore unreachable dead code — it will never fire because the outer loop already consumed every backslash.

Fix

Do the whole parse in a single pass so escape context is shared between comma-splitting and equals-splitting:

function parseOTELResourceAttributes(raw) {
  if (!raw || !raw.trim()) return [];
  const result = [];
  let key = "", value = "", seenEq = false, escaped = false;
  const flush = () => {
    const k = key.trim();
    if (k) result.push(buildAttr(k, value.trim()));
    key = ""; value = ""; seenEq = false;
  };
  for (const ch of raw) {
    if (escaped) { (seenEq ? (value += ch) : (key += ch)); escaped = false; continue; }
    if (ch === "\\") { escaped = true; continue; }
    if (ch === ",") { flush(); continue; }
    if (ch === "=" && !seenEq) { seenEq = true; continue; }
    seenEq ? (value += ch) : (key += ch);
  }
  flush();
  return result;
}

This correctly handles \\,, \\=, and \\\\ in a single pass and eliminates the dead code.

expect(resourceAttrs).toContainEqual({ key: "github.run_id", value: { stringValue: "987654321" } });
});

it("merges OTEL_RESOURCE_ATTRIBUTES into resource attributes", async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test placed in the wrong describe block: this test is inside sendJobSetupSpan but exercises sendJobConclusionSpan.

💡 Details

The sendJobSetupSpan describe block wraps this test, but line 1501 calls sendJobConclusionSpan("gh-aw.job.conclusion"). The beforeEach/afterEach hooks for sendJobSetupSpan save/restore env vars that are relevant to setup spans, not conclusion spans — e.g., GH_AW_INFO_STAGED, GH_AW_SETUP_WORKFLOW_NAME. Conclusion-span-specific keys like GH_AW_AGENT_CONCLUSION or GH_AW_EFFECTIVE_TOKENS are not reset, so this test can see state leaked by earlier conclusion-span tests.

Move this test into the sendJobConclusionSpan describe block and ensure OTEL_RESOURCE_ATTRIBUTES is added to that block's envKeys array as well.

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 2, 2026

@copilot review all comments and reviews. fix https://github.com/github/gh-aw/actions/runs/26824926231/job/79092092107?pr=36450 . apply copilot-review skill

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 2, 2026

@mnkiefer please review

Copilot AI and others added 2 commits June 2, 2026 14:56
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 2, 2026

@copilot review all comments and reviews. fix https://github.com/github/gh-aw/actions/runs/26824926231/job/79092092107?pr=36450 . apply copilot-review skill

Addressed in e894421. I reviewed the in-scope feedback, fixed OTEL_RESOURCE_ATTRIBUTES parsing for escaped \= in keys, cleaned up the OTEL env var in the relevant test scopes, scoped the resource-attribute regression to sendJobSetupSpan, and added regression coverage for parser escaping plus the agent_usage.json fallback path. Local send_otlp_span.test.cjs, npm run typecheck, and npm run lint:cjs now pass; the earlier js-typecheck failure from that run does not reproduce on the current branch head.

@pelikhan pelikhan merged commit 7a97944 into main Jun 2, 2026
12 checks passed
@pelikhan pelikhan deleted the copilot/token-consumption-report-2026-06-02 branch June 2, 2026 16:58
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

pr-sous-chef
@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · gpt54mini 4.1M ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[token-consumption] Daily Token Consumption Report - 2026-06-02

3 participants