Skip to content

perf: reduce OpenTelemetry metric volume - #895

Merged
chrisdoc merged 2 commits into
mainfrom
feat/metrics-volume-reduction-20260804
Aug 4, 2026
Merged

perf: reduce OpenTelemetry metric volume#895
chrisdoc merged 2 commits into
mainfrom
feat/metrics-volume-reduction-20260804

Conversation

@chrisdoc

@chrisdoc chrisdoc commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • reduce the Node metric export interval from 10s to 30s while preserving force-flush behavior
  • remove result-shape dimensions from the tool-duration histogram; detailed result attributes remain on traces
  • normalize dynamic Hevy API endpoint labels for metrics while retaining raw trace diagnostics
  • add a patch changeset for hevy-mcp

Validation

  • full npm run build
  • full npm run check
  • full npm run check:types
  • full npm run check:changeset
  • full npm run test:pr
  • focused telemetry suite: 34 tests passed
  • git diff --check

Follow-up

This is the first source-instrumentation slice of the metrics-volume plan. Collector filtering, Grafana review, and staged live measurement remain separate follow-up work. No production deployment is included in this PR.

Summary by CodeRabbit

  • Improvements
    • Reduced telemetry volume by exporting metrics every 30 seconds instead of every 10 seconds.
    • Simplified tool-duration metrics by excluding result content and item counts.
    • Standardized API endpoint labels for more consistent metrics while retaining detailed trace information.
    • Unrecognized API paths are now grouped under an unknown label.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Worker preview

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

MCP tool token cost

Measured with o200k_base over the complete json-serialized mcp tools/list result payload: { tools }.
Targets are advisory except the enforced total-token budget.

Metric Current Target Status
Tools 26 ≤ 20 Above target
Total tokens 7964 ≤ 8900 Within target
Average tokens/tool 306.31 < 600 Within target

Component totals

Component Tokens
name 153
description 560
inputSchema 3400
outputSchema 3051
annotations 606

Change from baseline

Metric Baseline Current Delta
Tools 26 26 0
Total tokens 7964 7964 0
Average tokens/tool 306.31 306.31 0

Per-tool changes

Tool Baseline Current Delta
create-body-measurement 328 328 0
create-exercise-template 354 354 0
create-routine 420 420 0
create-routine-folder 109 109 0
create-workout 532 532 0
get-body-measurement 300 300 0
get-body-measurements 372 372 0
get-exercise-history 247 247 0
get-exercise-template 172 172 0
get-exercise-templates 254 254 0
get-routine 332 332 0
get-routine-folder 142 142 0
get-routine-folders 224 224 0
get-routines 277 277 0
get-training-summary 624 624 0
get-user-info 133 133 0
get-workout 322 322 0
get-workout-count 109 109 0
get-workout-events 499 499 0
get-workouts 273 273 0
replace-workout-exercises 416 416 0
search-exercise-templates 258 258 0
search-routines 280 280 0
update-body-measurement 328 328 0
update-routine 423 423 0
update-workout 232 232 0

Component changes

Component Delta
name 0
description 0
inputSchema 0
outputSchema 0
annotations 0

Per-tool breakdown

Tool name description inputSchema outputSchema annotations Total Share of total
get-training-summary 5 30 31 531 19 624 7.84%
create-workout 5 20 470 0 31 532 6.68%
get-workout-events 6 22 84 360 19 499 6.27%
update-routine 5 18 363 0 31 423 5.31%
create-routine 5 20 358 0 31 420 5.27%
replace-workout-exercises 7 17 354 0 32 416 5.22%
get-body-measurements 7 24 62 252 19 372 4.67%
create-exercise-template 6 18 292 0 32 354 4.45%
get-routine 5 23 31 247 18 332 4.17%
create-body-measurement 7 24 259 0 32 328 4.12%
update-body-measurement 7 24 259 0 32 328 4.12%
get-workout 5 23 33 235 18 322 4.04%
get-body-measurement 7 25 43 198 19 300 3.77%
search-routines 5 24 42 182 19 280 3.52%
get-routines 5 23 62 160 19 277 3.48%
get-workouts 5 25 47 169 19 273 3.43%
search-exercise-templates 7 25 113 86 19 258 3.24%
get-exercise-templates 7 21 62 137 19 254 3.19%
get-exercise-history 6 22 58 134 19 247 3.1%
update-workout 5 17 173 0 31 232 2.91%
get-routine-folders 7 15 62 112 20 224 2.81%
get-exercise-template 6 23 33 83 19 172 2.16%
get-routine-folder 6 22 31 56 19 142 1.78%
get-user-info 5 20 14 67 19 133 1.67%
create-routine-folder 6 15 50 0 32 109 1.37%
get-workout-count 6 20 14 42 19 109 1.37%

Per-component counts are diagnostic and non-additive because keys and separators live in complete tool objects. Per-tool counts encode each complete tool object independently. The total encodes the complete { tools } envelope, so punctuation and separators mean the per-tool values need not sum exactly to the total.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 241 bytes (0.1%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
hevy-mcp-esm 232.6kB 241 bytes (0.1%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: hevy-mcp-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
src-C_UE843W.mjs (New) 230.43kB 230.43kB 100.0% 🚀
src-DUdA1jj-.mjs (Deleted) -230.19kB 0 bytes -100.0% 🗑️

Files in src-C_UE843W.mjs:

  • ./src/utils/telemetry.ts → Total Size: 6.63kB

  • ./src/utils/hevy-client-observability.ts → Total Size: 5.65kB

  • ./src/utils/tool-observer.ts → Total Size: 9.32kB

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.75%. Comparing base (f96e205) to head (2f312c5).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ckages/node/src/utils/hevy-client-observability.ts 85.71% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #895      +/-   ##
==========================================
+ Coverage   80.70%   80.75%   +0.05%     
==========================================
  Files          70       70              
  Lines        4058     4054       -4     
  Branches     1149     1147       -2     
==========================================
- Hits         3275     3274       -1     
+ Misses        400      398       -2     
+ Partials      383      382       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Unit Test Results

  1 files   66 suites   8s ⏱️
716 tests 716 ✅ 0 💤 0 ❌
723 runs  723 ✅ 0 💤 0 ❌

Results for commit 2f312c5.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR normalizes API endpoint labels for metrics, removes result-derived dimensions from tool metrics, and changes metric export from 10 to 30 seconds. Tests cover endpoint labeling, trace preservation, removed attributes, and the new interval.

Changes

Telemetry metrics

Layer / File(s) Summary
API endpoint metric normalization
packages/node/src/utils/hevy-client-observability.ts, packages/node/src/utils/hevy-client-observability.test.ts
API metrics now use normalized static, dynamic, or unknown endpoint labels. Trace attributes retain raw endpoints.
Tool metric dimension reduction
packages/node/src/utils/tool-observer.ts, packages/node/src/utils/tool-observer.test.ts
Tool outcome metrics no longer include result content or item-count attributes.
Metric export interval update
packages/node/src/utils/telemetry.ts, packages/node/src/utils/telemetry.test.ts, .changeset/quiet-metrics-volume.md
Metric export changes from 10 seconds to 30 seconds. The Changeset documents the patch release and telemetry changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: 30 min review, safe-changes, review-pending

Poem

A rabbit trims metrics, neat and light,
Labels hop into paths just right.
Traces keep their wild tracks,
Results shed extra packs,
And exports wait thirty seconds tonight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's primary change: reducing OpenTelemetry metric volume.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/metrics-volume-reduction-20260804

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chrisdoc
chrisdoc marked this pull request as ready for review August 4, 2026 10:48
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@mergify

mergify Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

perf: reduce OpenTelemetry metric volume

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Increase Node metrics export interval from 10s to 30s (force-flush still supported).
• Reduce metric cardinality by removing tool result-shape dimensions from duration histograms.
• Normalize Hevy API endpoint labels in metrics while keeping raw endpoints on spans.
Diagram

graph TD
A["Node service"] --> B["telemetry.ts (export interval)"] --> C[("OTel Collector")]
A --> D["tool-observer.ts (tool metrics)"] --> C
A --> E["hevy-client-observability.ts (API metrics)"] --> C
E --> F{{"Hevy API"}}
E --> G["Trace spans (raw endpoint)"]
subgraph Legend
direction LR
_mod["Module"] ~~~ _col[("Collector") ] ~~~ _api{{"External API"}}
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Collector-side attribute dropping (processor rules)
  • ➕ Centralized control across all services/sdks
  • ➕ No code changes required once configured
  • ➕ Can be tuned/rolled back quickly
  • ➖ Harder to validate with unit tests
  • ➖ Risk of over-dropping useful dimensions if rules are too broad
  • ➖ Doesn’t reduce client-side aggregation/memory footprint
2. OTel Views to drop/transform metric attributes
  • ➕ SDK-native way to control cardinality per instrument
  • ➕ Keeps instrumentation code simpler over time
  • ➖ View support/ergonomics vary by JS OTel versions
  • ➖ More opaque than explicit normalization logic in code
  • ➖ Still requires careful rollout/testing for each instrument name
3. Hash high-cardinality dimensions instead of templating
  • ➕ Keeps some ability to correlate distinct endpoints without templates
  • ➕ Bounds cardinality (if truncated/partitioned)
  • ➖ Still produces large unique-series counts at scale
  • ➖ Harder for humans to interpret dashboards
  • ➖ Can create accidental high-cardinality if not carefully bounded

Recommendation: The PR’s source-instrumentation approach is a good first slice: it reduces series cardinality at the source (endpoint templating, removing result-shape metric dimensions) while preserving debugging fidelity in traces. Collector-side filtering and/or OTel Views are still worth adding later for defense-in-depth and central tuning, but doing the normalization in code makes the intended semantics explicit and testable.

Files changed (7) +101 / -30

Enhancement (2) +31 / -26
hevy-client-observability.tsNormalize Hevy API endpoint labels for metrics +31/-2

Normalize Hevy API endpoint labels for metrics

• Introduces allowlisted static endpoints and prefix-based dynamic endpoint templates for use in metric dimensions. Updates apiCalls/apiDuration metrics to use the normalized endpoint label while leaving span attributes/debug logs unchanged.

packages/node/src/utils/hevy-client-observability.ts

tool-observer.tsRemove result-shape dimensions from tool duration metrics +0/-24

Remove result-shape dimensions from tool duration metrics

• Deletes the helper that attached result-shape attributes to duration metrics and stops including those attributes when recording toolDuration. Span result attributes remain intact for trace-level diagnostics.

packages/node/src/utils/tool-observer.ts

Tests (3) +62 / -3
hevy-client-observability.test.tsTest metric endpoint normalization vs raw trace endpoint retention +53/-0

Test metric endpoint normalization vs raw trace endpoint retention

• Adds coverage ensuring dynamic Hevy API endpoints are templated for metrics while spans still record the raw endpoint. Also verifies unknown paths are mapped to an "unknown" metric endpoint label.

packages/node/src/utils/hevy-client-observability.test.ts

telemetry.test.tsUpdate export interval expectation to 30s +1/-1

Update export interval expectation to 30s

• Adjusts the telemetry initialization test to assert a 30s PeriodicExportingMetricReader interval.

packages/node/src/utils/telemetry.test.ts

tool-observer.test.tsAssert tool duration metrics omit result-shape attributes +8/-2

Assert tool duration metrics omit result-shape attributes

• Updates tests to ensure toolDuration metric attributes no longer include result bucket dimensions (e.g., content/item count buckets) while other bounded attributes remain.

packages/node/src/utils/tool-observer.test.ts

Documentation (1) +7 / -0
quiet-metrics-volume.mdAdd patch changeset documenting telemetry volume reduction +7/-0

Add patch changeset documenting telemetry volume reduction

• Adds a patch changeset for the hevy-mcp package describing the reduced metrics export frequency and cardinality reductions.

.changeset/quiet-metrics-volume.md

Other (1) +1 / -1
telemetry.tsIncrease OTEL metric export interval to 30 seconds +1/-1

Increase OTEL metric export interval to 30 seconds

• Changes PeriodicExportingMetricReader exportIntervalMillis from 10s to 30s, reducing steady-state metrics export volume while keeping explicit forceFlush support via flushTelemetry.

packages/node/src/utils/telemetry.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/node/src/utils/hevy-client-observability.ts`:
- Around line 37-43: Update normalizeHevyMetricEndpoint to accept a dynamic
prefix only when it is followed by one non-empty path segment and no trailing
slash or additional “/...” segments; otherwise return “unknown”. Add regression
tests covering the collection path ending in “/” and a path with an extra
segment, while preserving valid single-resource identifier matching.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b22e434f-d28f-4610-8a2f-a56d03e3bf8f

📥 Commits

Reviewing files that changed from the base of the PR and between f96e205 and 2f312c5.

📒 Files selected for processing (7)
  • .changeset/quiet-metrics-volume.md
  • packages/node/src/utils/hevy-client-observability.test.ts
  • packages/node/src/utils/hevy-client-observability.ts
  • packages/node/src/utils/telemetry.test.ts
  • packages/node/src/utils/telemetry.ts
  • packages/node/src/utils/tool-observer.test.ts
  • packages/node/src/utils/tool-observer.ts
💤 Files with no reviewable changes (1)
  • packages/node/src/utils/tool-observer.ts

Comment on lines +37 to +43
function normalizeHevyMetricEndpoint(endpoint: string): string {
const path = endpoint.split("?")[0] ?? "";
if (SAFE_STATIC_ENDPOINTS.has(path)) return path;
return (
SAFE_DYNAMIC_ENDPOINTS.find(([prefix]) => path.startsWith(prefix))?.[1] ??
"unknown"
);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '!**/node_modules/**' \
  '(/v1/(body_measurements|exercise_history|exercise_templates|routine_folders|routines|workouts)|endpoint[[:space:]]*[:=])' \
  packages

Repository: chrisdoc/hevy-mcp

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- packages/node/src/utils/hevy-client-observability.ts ---\n'
sed -n '1,180p' packages/node/src/utils/hevy-client-observability.ts

printf '\n--- packages/node/src/utils/hevy-client-observability.test.ts relevant tests ---\n'
sed -n '115,175p' packages/node/src/utils/hevy-client-observability.test.ts

printf '\n--- packages/hevy-client/src/hevy-client-kubb.ts normalization duplicate ---\n'
sed -n '200,235p' packages/hevy-client/src/hevy-client-kubb.ts

printf '\n--- static normalization behavior for dynamic prefix inputs ---\n'
python3 - <<'PY'
from pathlib import Path
import re, ast, json

path = Path('packages/node/src/utils/hevy-client-observability.ts')
text = path.read_text()
m = re.search(r'(?ms)(\n const |function) normalizeHevyMetricEndpoint\([\s\S]*?\n\)(?:\s*=>\s*[^{]+)?\s*\{([\s\S]*?)\n}', text)
if not m:
    print('function not found')
    raise SystemExit(1)
func = m.group(0)
start, end = text[:m.start()].count('\n')+1, text[:m.end()].count('\n')+1
print(f'normalizeHevyMetricEndpoint definition lines {start}-{end}')
print(func)

# Extract the arrays by parsing the TS snippet with JS semantic-ish extraction from source text.
static_set = set()
dynamic = []
for block in text.split('const '):
    if 'SAFE_STATIC_ENDPOINTS' in block or 'SAFE_DYNAMIC_ENDPOINTS' in block:
        src = block[:block.index(';')+1]
        # strip const/assign and use Node eval safely? Actually contains Set constructor syntax not plain JS?
        # We can manually convert relevant TypeScript into JS by replacing Set literals.
        js = src
        js = re.sub(r'const\s+\w+\s*=', '', js)
        # Replace Set([...]) around the whole expression or inside assignment with array.
        # We'll use a more permissive extraction approach:
        pass

PY

Repository: chrisdoc/hevy-mcp

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- packages/node/src/utils/hevy-client-observability.ts ---'
sed -n '1,180p' packages/node/src/utils/hevy-client-observability.ts

printf '%s\n' ''
printf '%s\n' '--- packages/node/src/utils/hevy-client-observability.test.ts relevant tests ---'
sed -n '115,175p' packages/node/src/utils/hevy-client-observability.test.ts

printf '%s\n' ''
printf '%s\n' '--- packages/hevy-client/src/hevy-client-kubb.ts normalization duplicate ---'
sed -n '200,235p' packages/hevy-client/src/hevy-client-kubb.ts

printf '%s\n' ''
printf '%s\n' '--- generated resource endpoints under /v1/... ---'
rg -n '"url": "/v1/[^"]*" url:`/v1/|function .*Url\(\) =>' packages/hevy-client/src/generated/client/api -g '*.ts' | head -200

printf '%s\n' ''
printf '%s\n' '--- deterministic dynamic prefix behavior from source constants ---'
python3 - <<'PY'
import re, ast
from pathlib import Path

path = Path('packages/node/src/utils/hevy-client-observability.ts')
text = path.read_text()
m = re.search(r'(?ms)function normalizeHevyMetricEndpoint\(endpoint: string\): string \{([\s\S]*?)\n\}', text)
if not m:
    raise SystemExit('normalizeHevyMetricEndpoint not found')
body = m.group(1)
print(body)

static_m = re.search(r'new Set\(\[(?P<body>[\s\S]*?)\]\s*,', text)
dynamic_m = re.search(r'SAFE_DYNAMIC_ENDPOINTS\s*(?:=|as\s)+\[(?P<body>[\s\S]*?)\]\s*as const;', text)
print('static count', len(ast.literal_eval('[' + static_m.group('body') + ']')) if static_m else '?')
print('dynamic entries')
for raw in re.split(r',\s*(?=/\')', dynamic_m.group('body')) if dynamic_m else []:
    raw = raw.strip()
    match = re.match(r'\["([^"]+)", "([^"]+)"\]', raw)
    if match:
        print((match.group(1), match.group(2)))

static = set(ast.literal_eval('[' + static_m.group('body') + ']'))
dynamics = [(match.group(1), match.group(2)) for match in (re.finditer(r'\["([^"]+)", "([^"]+)"\]', dynamic_m.group('body')) or [])]
cases = ['/v1/workouts/', '/v1/workouts/id/extra', '/v1/workouts/id?foo=bar']
for case in cases:
    path_part = case.split('?', 1)[0] or ''
    out = path_part if path_part in static else next(((found, templ) for prefix, templ in dynamics if path_part.startswith(prefix)), 'unknown')
    print({'input': case, 'normalized': out})
PY

Repository: chrisdoc/hevy-mcp

Length of output: 8806


Bound dynamic metric matching to single resource paths.

startsWith(prefix) also matches /v1/workouts/ and /v1/workouts/id/extra, but the generated endpoint contract treats these collection IDs as :workoutId rather than unknown. If these labels model one resource identifier, add a non-empty segment check without a trailing / or /..., and add regression tests for both edge cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/node/src/utils/hevy-client-observability.ts` around lines 37 - 43,
Update normalizeHevyMetricEndpoint to accept a dynamic prefix only when it is
followed by one non-empty path segment and no trailing slash or additional
“/...” segments; otherwise return “unknown”. Add regression tests covering the
collection path ending in “/” and a path with an extra segment, while preserving
valid single-resource identifier matching.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

@chrisdoc
chrisdoc merged commit d51bf30 into main Aug 4, 2026
38 checks passed
@chrisdoc
chrisdoc deleted the feat/metrics-volume-reduction-20260804 branch August 4, 2026 10:53
@github-actions github-actions Bot mentioned this pull request Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant