Skip to content

feat: EarnFM token auth, Prometheus metrics, orchestrator resilience#62

Merged
GeiserX merged 7 commits into
mainfrom
fix/earnfm-and-privileged
May 23, 2026
Merged

feat: EarnFM token auth, Prometheus metrics, orchestrator resilience#62
GeiserX merged 7 commits into
mainfrom
fix/earnfm-and-privileged

Conversation

@GeiserX

@GeiserX GeiserX commented May 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • EarnFM rewrite: Drop broken Supabase email/password auth, replace with simple token-based API key (fixes BUG: drop privileged: true from earnfm service definition #58, BUG: use EARNFM_TOKEN instead of email/password for earnfm #59)
  • Drop privileged mode: Remove privileged: true from all 10 bandwidth service YAMLs (none actually need it; Mysterium uses cap_add: NET_ADMIN correctly)
  • Prometheus metrics: Rich /metrics endpoint gated behind CASHPILOT_METRICS_ENABLED=true — HTTP latency, container CPU/memory, earnings per platform, collection health, worker heartbeats, auth counters, health scores
  • Orchestrator resilience: Per-container try/except so corrupted Docker containers don't crash the entire status endpoint
  • UX improvements: Credential hint URLs are now clickable links with distinct accent color
  • Dead service docs: Updated Peer2Profit (domain unreachable) and SpeedShare (auth broken) guides

Test plan

  • All 820 tests pass (803 existing + 17 new metrics tests)
  • Ruff lint clean
  • EarnFM collector handles null API response, expired token, empty token
  • Metrics endpoint returns valid Prometheus format when enabled
  • Metrics module is zero-overhead when disabled (guard pattern on all hooks)
  • Path normalization covers all dynamic routes including sub-paths
  • 30s TTL cache prevents DB hammering on frequent scrapes
  • Deploy to watchtower, verify dashboard shows containers
  • Enable CASHPILOT_METRICS_ENABLED=true, verify /metrics returns data

Closes #58, closes #59

Summary by CodeRabbit

Release Notes

  • New Features

    • Added optional Prometheus metrics support—enable via environment variable to expose metrics at /metrics endpoint for monitoring and observability.
  • Bug Fixes

    • Improved error handling for Docker container operations to prevent disruptions from corrupted or inaccessible containers.
  • Documentation

    • Updated Earn.fm authentication guide to reflect new API key method.
    • Added comprehensive Prometheus metrics configuration guide with alerting examples.
    • Updated deprecation notices for Peer2Profit and SpeedShare services.
  • Style

    • Enhanced hyperlink styling in modals.
  • Chores

    • Disabled privileged container mode for bandwidth services to improve security posture.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@GeiserX, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 33 minutes and 4 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a084dca1-e004-40bb-a1b0-e9c7824a6396

📥 Commits

Reviewing files that changed from the base of the PR and between f12e1b0 and ab57e9b.

📒 Files selected for processing (12)
  • README.md
  • app/collectors/earnfm.py
  • app/main.py
  • app/metrics.py
  • app/orchestrator.py
  • app/static/css/style.css
  • app/static/js/app.js
  • docs/guides/peer2profit.md
  • docs/guides/prometheus-metrics.md
  • docs/guides/speedshare.md
  • requirements.txt
  • tests/test_metrics.py
📝 Walkthrough

Walkthrough

This PR migrates EarnFM to token-based authentication, reduces Docker privilege across bandwidth services, introduces a comprehensive Prometheus metrics system, hardens container status operations, and updates documentation and tests accordingly.

Changes

EarnFM and Service Configuration Updates

Layer / File(s) Summary
EarnFM token-based authentication
app/collectors/earnfm.py, app/collectors/__init__.py
EarnFMCollector replaces email/password Supabase flow with UUID token auth; __init__ accepts single token parameter, collect() returns error on missing token, and balance requests use X-API-Key header to api.earn.fm/v2 endpoint; config validation keys updated to require earnfm_token.
Service configuration and privilege reduction
services/bandwidth/earnfm.yml, services/bandwidth/bitping.yml, services/bandwidth/earnapp.yml, services/bandwidth/honeygain.yml, services/bandwidth/iproyal.yml, services/bandwidth/mysterium.yml, services/bandwidth/packetstream.yml, services/bandwidth/proxyrack.yml, services/bandwidth/repocket.yml, services/bandwidth/traffmonetizer.yml
EarnFM service replaces EARNFM_EMAIL/EARNFM_PASSWORD with EARNFM_TOKEN (required, secret); all bandwidth service YAML files change docker.privileged from true to false to align with worker security policy.
Documentation and tests
docs/guides/earnfm.md, tests/test_collectors_extended.py, tests/test_collectors_deep.py, tests/test_coverage_gaps.py
EarnFM guide updated to direct users to obtain UUID API key from dashboard; collector tests refactored to use token constructor and mock balance GET requests; gap coverage test changed to validate "no token" error path.

Prometheus Metrics System

Layer / File(s) Summary
Prometheus metrics foundation
app/metrics.py, requirements.txt
New module implements optional Prometheus instrumentation via CASHPILOT_METRICS_ENABLED with CollectorRegistry, metric definitions (info/counters/gauges/histograms) for system/HTTP/containers/workers/earnings/collection/health/auth, path normalization to reduce cardinality, and TTL-cached gauge refresh that pulls state from database and exchange rates.
Metrics wiring in app lifecycle
app/main.py
metrics.setup(app) initializes during startup; collection job records start timestamp, tracks platforms/errors, and reports end with success status; login handler records rate-limit and success/failure events; container lifecycle endpoints record deploy/stop/restart/remove actions; heartbeat registration is tracked; collector metadata hints updated.
Container status resilience
app/orchestrator.py
get_status() and get_status_light() wrap per-container Docker operations with exception handling, skipping corrupted containers and logging warnings instead of aborting status refresh; external container scan also guarded against listing failures.
Documentation and test coverage
README.md, docs/guides/prometheus-metrics.md, tests/test_metrics.py, app/static/css/style.css, app/static/js/app.js
README documents CASHPILOT_METRICS_ENABLED; new comprehensive metrics guide covers enablement, scrape config, metric catalog with types/labels, alerting rules, and Grafana dashboard panels; test suite validates instrumentation functions as no-ops when disabled and record correct metric increments when enabled, plus setup(), path normalization, and gauge refresh with mocked dependencies; modal link styling and hint rendering improved.
Service deprecation notices
docs/guides/peer2profit.md, docs/guides/speedshare.md
Peer2Profit and SpeedShare deprecation notices updated with clearer language indicating services are completely offline/broken and deployments are blocked.

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: EarnFM token-based auth (replacing email/password), Prometheus metrics implementation, and orchestrator resilience improvements.
Linked Issues check ✅ Passed The PR fully addresses both linked issues: #58 removes privileged: true from all 10 bandwidth services; #59 replaces EARNFM_EMAIL/EARNFM_PASSWORD with EARNFM_TOKEN throughout YAML, docs, collector code, and tests.
Out of Scope Changes check ✅ Passed Changes are within scope. Prometheus metrics (not explicitly in #58/#59) are a supporting instrumentation feature mentioned in PR objectives; orchestrator resilience is necessary context. Minor UX updates (CSS links, hint styling, dead-service notices) are harmless documentation adjustments.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/earnfm-and-privileged

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
app/metrics.py (1)

305-308: 💤 Low value

Avoid relying on prometheus_client private Gauge._metrics internals in app/metrics.py.

_refresh_gauges() clears label sets via m["..."]._metrics.clear() (lines 305-308, plus 356, 362-363, 387-388), which is an undocumented internal that can break on library updates. Prefer the public API (e.g., Gauge.clear() / remove() / remove_by_labels()) or document why internals are required.

🤖 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 `@app/metrics.py` around lines 305 - 308, _in _refresh_gauges(), stop using the
private _metrics dict and instead clear labelsets via the public API: for each
metric key (e.g., "containers_total", "container_info", "container_cpu_percent",
"container_memory_mb") either iterate m["..."].collect() to extract each
sample's label dict and call the Gauge.remove(...) for that labelset, or
unregister the entire metric from the CollectorRegistry and re-create it (using
Registry.unregister and re-instantiating the Gauge) to achieve a full reset;
update _refresh_gauges() to use one of these approaches rather than
m["..."]._metrics.clear().
docs/guides/prometheus-metrics.md (1)

14-14: ⚡ Quick win

Add an explicit “internal-only” exposure warning for /metrics.

The guide says /metrics is unauthenticated but doesn’t warn against public exposure. Add a short warning to keep it private (LAN/VPN/reverse-proxy auth/IP allowlist) to prevent leaking operational metadata.

🤖 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 `@docs/guides/prometheus-metrics.md` at line 14, Add an explicit internal-only
warning for the unauthenticated GET /metrics endpoint: update the documentation
text that mentions "Metrics are exposed at `GET /metrics`" to append a short
caution that `/metrics` must not be publicly accessible and should be restricted
to trusted networks (LAN/VPN) or protected via a reverse-proxy with
authentication or IP allowlist to avoid leaking operational metadata.
tests/test_metrics.py (1)

58-97: ⚡ Quick win

tests/test_metrics.py: reduce fragility from prometheus_client private metric access

The assertions read values via Prometheus client private internals (._value.get()), which isn’t part of the public API and can break with upgrades (even though the current test run succeeds). Consider asserting through a stable path (e.g., generate_latest() output or parsing metric collect() results) to make the tests upgrade-proof.

🤖 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 `@tests/test_metrics.py` around lines 58 - 97, The tests currently assert
metric values by touching Prometheus internals
(metrics._metrics["..."]._value.get()), which is fragile; update each test that
calls metrics.record_collection_end, record_collection_error,
record_container_lifecycle, record_login, record_rate_limit, and
record_heartbeat to assert via the public Prometheus API instead — e.g., call
prometheus_client.generate_latest(...) or use each metric's collect() and
inspect Sample.value for the matching label set (replace any usage of
metrics._metrics["..."]._value.get() or ._value with parsing of generate_latest
output or reading the sample values returned by metric.collect()) so assertions
no longer rely on private attributes.
🤖 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 `@app/main.py`:
- Line 939: The lifecycle metrics for stop/restart are recorded before the proxy
call, so failures are counted as successes; move the
metrics.record_container_lifecycle calls for "stop" and "restart" so they
execute after a successful _proxy_worker_command call (i.e., only record when
_proxy_worker_command returns/does not raise), updating the occurrences of
metrics.record_container_lifecycle("stop", slug) and
metrics.record_container_lifecycle("restart", slug) to run after the proxy call
in the same handler functions.

In `@app/metrics.py`:
- Around line 260-262: The except block increments
_metrics["http_requests_total"] for status "500" but fails to record latency;
update the exception path to also observe the request duration on the duration
histogram (e.g. _metrics["http_request_duration_seconds"].labels(method=method,
path=path, status="500").observe(duration)). Compute duration the same way the
success path does (use the existing start_time or timer used elsewhere, or
compute duration = time.time() - start_time before observing) and then re-raise
the exception; ensure you reference the same label keys (method, path, status)
as used for successful requests.

In `@app/static/js/app.js`:
- Around line 763-765: The code injects user-controlled `hint` directly into
`body.innerHTML` which creates an XSS vector; change the build to avoid raw HTML
insertion by creating a DOM element for the hint (e.g., create a <p> node and
set its textContent to `hint`) or sanitize `hint` with a strict allowlist before
adding it, then append that element to the container and combine with
`fieldsHtml` (or insert `fieldsHtml` into a separate container) instead of
interpolating `hint` into `innerHTML`; update the code paths that reference
`body.innerHTML`, `hint`, and `fieldsHtml` accordingly.

---

Nitpick comments:
In `@app/metrics.py`:
- Around line 305-308: _in _refresh_gauges(), stop using the private _metrics
dict and instead clear labelsets via the public API: for each metric key (e.g.,
"containers_total", "container_info", "container_cpu_percent",
"container_memory_mb") either iterate m["..."].collect() to extract each
sample's label dict and call the Gauge.remove(...) for that labelset, or
unregister the entire metric from the CollectorRegistry and re-create it (using
Registry.unregister and re-instantiating the Gauge) to achieve a full reset;
update _refresh_gauges() to use one of these approaches rather than
m["..."]._metrics.clear().

In `@docs/guides/prometheus-metrics.md`:
- Line 14: Add an explicit internal-only warning for the unauthenticated GET
/metrics endpoint: update the documentation text that mentions "Metrics are
exposed at `GET /metrics`" to append a short caution that `/metrics` must not be
publicly accessible and should be restricted to trusted networks (LAN/VPN) or
protected via a reverse-proxy with authentication or IP allowlist to avoid
leaking operational metadata.

In `@tests/test_metrics.py`:
- Around line 58-97: The tests currently assert metric values by touching
Prometheus internals (metrics._metrics["..."]._value.get()), which is fragile;
update each test that calls metrics.record_collection_end,
record_collection_error, record_container_lifecycle, record_login,
record_rate_limit, and record_heartbeat to assert via the public Prometheus API
instead — e.g., call prometheus_client.generate_latest(...) or use each metric's
collect() and inspect Sample.value for the matching label set (replace any usage
of metrics._metrics["..."]._value.get() or ._value with parsing of
generate_latest output or reading the sample values returned by
metric.collect()) so assertions no longer rely on private attributes.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa8df843-4c8d-438f-8dc9-a7d442b4e428

📥 Commits

Reviewing files that changed from the base of the PR and between b92c1ff and f12e1b0.

📒 Files selected for processing (27)
  • README.md
  • app/collectors/__init__.py
  • app/collectors/earnfm.py
  • app/main.py
  • app/metrics.py
  • app/orchestrator.py
  • app/static/css/style.css
  • app/static/js/app.js
  • docs/guides/earnfm.md
  • docs/guides/peer2profit.md
  • docs/guides/prometheus-metrics.md
  • docs/guides/speedshare.md
  • requirements.txt
  • services/bandwidth/bitping.yml
  • services/bandwidth/earnapp.yml
  • services/bandwidth/earnfm.yml
  • services/bandwidth/honeygain.yml
  • services/bandwidth/iproyal.yml
  • services/bandwidth/mysterium.yml
  • services/bandwidth/packetstream.yml
  • services/bandwidth/proxyrack.yml
  • services/bandwidth/repocket.yml
  • services/bandwidth/traffmonetizer.yml
  • tests/test_collectors_deep.py
  • tests/test_collectors_extended.py
  • tests/test_coverage_gaps.py
  • tests/test_metrics.py

Comment thread app/main.py
Comment thread app/metrics.py
Comment thread app/static/js/app.js
GeiserX added 6 commits May 23, 2026 15:09
Make orchestrator skip corrupted containers instead of crashing the
entire status endpoint. Add clickable URLs in credential hint text
with distinct accent color so users can quickly navigate to auth pages.
Rich observability when CASHPILOT_METRICS_ENABLED=true:
- HTTP request count/latency/in-flight by method/path/status
- Container count, CPU/memory per service, lifecycle events
- Earnings balance per platform in native currency and USD
- Collection duration histogram, per-platform errors, staleness
- Worker heartbeat tracking, Docker availability, container counts
- Health scores and uptime percentages per service
- Auth login attempts and rate-limit counters
- System uptime and build info

Gated behind env var so zero overhead when disabled.
Peer2Profit: domain completely unreachable, all services offline.
SpeedShare: authentication broken, confirmed dead in Discord Mar 2026.
- earnfm: handle null API response body (or {} fallback)
- metrics: 30s TTL cache on _refresh_gauges to avoid DB load per scrape
- metrics: remove image label from container_info (cardinality risk)
- metrics: precompile path regex, cover sub-routes (/services/{slug}/*)
- metrics: use .set() for containers_total instead of .inc() after clear
- metrics: fix timestamp handling for timezone-aware last_seen
- orchestrator: simplify redundant (APIError, Exception) to Exception
- Move stop/restart lifecycle metrics after proxy call (only count success)
- Record HTTP duration on 500 errors in metrics middleware
- Add security warning about /metrics endpoint exposure in docs
- Add sanitizeHint() to strip all tags except a/b/code with safe attrs
- Replace ._metrics.clear() with public .clear() method
@GeiserX GeiserX force-pushed the fix/earnfm-and-privileged branch from d84c3e8 to 70653a0 Compare May 23, 2026 13:11
@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.09607% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.18%. Comparing base (b92c1ff) to head (ab57e9b).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
app/metrics.py 82.50% 35 Missing ⚠️
app/main.py 77.77% 6 Missing ⚠️

❌ Your patch check has failed because the patch coverage (82.09%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #62      +/-   ##
==========================================
- Coverage   92.03%   91.18%   -0.86%     
==========================================
  Files          24       25       +1     
  Lines        2499     2722     +223     
==========================================
+ Hits         2300     2482     +182     
- Misses        199      240      +41     
Files with missing lines Coverage Δ
app/collectors/earnfm.py 96.55% <100.00%> (ø)
app/main.py 90.33% <77.77%> (-0.39%) ⬇️
app/metrics.py 82.50% <82.50%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@GeiserX GeiserX merged commit fdc1117 into main May 23, 2026
5 of 6 checks passed
@GeiserX GeiserX deleted the fix/earnfm-and-privileged branch May 23, 2026 13:20
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.

BUG: use EARNFM_TOKEN instead of email/password for earnfm BUG: drop privileged: true from earnfm service definition

1 participant