feat: add SDK telemetry into emissions tracker - #1200
davidberenstein1957 wants to merge 34 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1200 +/- ##
==========================================
+ Coverage 91.69% 92.30% +0.61%
==========================================
Files 49 54 +5
Lines 5152 5434 +282
==========================================
+ Hits 4724 5016 +292
+ Misses 428 418 -10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@inimaz, before merging, we should still add the hardcoded experiment and project for the codecarbon api. |
Add CLI helper and interactive-flow tests, cover coordinate rounding in ApiClient, and extend collect environment probes to satisfy Codecov patch and project thresholds on PR #1200. Co-authored-by: Cursor <cursoragent@cursor.com>
inimaz
left a comment
There was a problem hiding this comment.
Thanks for this PR @davidberenstein1957 . I have left some comments
| raise typer.BadParameter(str(error)) from error | ||
|
|
||
|
|
||
| def resolve_config_path(config: Optional[Path], *, create: bool = False) -> Path: |
There was a problem hiding this comment.
Maybe this is not needed. get_hierarchical_config handles it?
There was a problem hiding this comment.
Kept on purpose: set has to pick one file to write to, while get_hierarchical_config only merges what it reads.
506030a to
e102d91
Compare
Send product telemetry at tracker stop, with the tier resolved from config, environment, or the EmissionsTracker(telemetry_level=...) kwarg. The send runs on a daemon thread so stop() never blocks on the network, and every request of one send shares a single 2s wall-clock budget, so the extensive tier cannot cost more just because it makes two calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e102d91 to
fd7a63b
Compare
Verdict: 🔧 Request changesMaintainer direction on the telemetry policy (@benoit-cty): telemetry should be opt-out by default, and the user should be asked the question explicitly. A suggested implementation:
Must fix:
Housekeeping: Nits:
|
# Conflicts: # codecarbon/cli/main.py # codecarbon/core/config.py
- Revert the default experiment_id: telemetry no longer changes the experiment written to every CSV row or used by save_to_api. - stop() never blocks: the payload is built on the send thread, and cloud fields reuse what the tracker detected instead of probing the metadata endpoints again. - Opt-out by default, and ask: interactive `codecarbon config` / `monitor` ask once for a level and save it in ~/.codecarbon.config; non-interactive runs log an accurate notice once per machine (marker in ~/.codecarbon/). `telemetry status` says whether anything is actually sent. - Unknown coordinates are omitted instead of sent as 0,0; a config key holding None no longer masks the detected value. - Pending sends get an atexit join capped at 1 s; logs from the telemetry thread (ApiClient in extensive mode) drop to DEBUG. - telemetry_level no longer leaks into _conf; Telemetry imported at module level. - docs: list every field sent per tier, the prompt/notice flow and opt-out paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Made the changes in cd8ccf6: opt-out with a one-time prompt/notice, stop() non-blocking, experiment_id reverted, null coords, atexit join, docs list every field. Merged master, redid config.py on top of get_hierarchical_config, refreshed the description. |
# Conflicts: # tests/conftest.py
Telemetry is an open ingestion endpoint: remove the x-api-token check on POST /telemetry, the client-side telemetry api_key/experiment_id settings, the public run summary (post_public_summary) and the ApiClient deadline change that only existed for it. Remove the extensive tier from client, server, CLI and docs; levels left are minimal and disabled. Drop latitude and longitude from the payload, schemas and SQL model. Non-201 responses now log at debug: with no key gate every default install posts, so a 404 warning would reach every user of an older server. Tests default CODECARBON_TELEMETRY_LEVEL=disabled so the suite never posts to the real endpoint.
Remove every field the SDK never fills (run metrics, framework flags and versions, host/executable hashes, error counters, usage diagnostics) from the client schema, the server schema and the SQL model. extra="forbid" now does the job of the minimal-tier allowlist validator, and every free text field is capped at 256 characters. No list fields remain.
Create the trimmed telemetry table through alembic instead of a dedicated create_all call at startup.
The payload describes the environment, which does not change within a process, so only the first stop() of a run lasting at least one second sends it. Sub-second runs still do not count, so a quick smoke run does not use up the single send. POST /telemetry is open, so cap it per client IP (60 requests a minute, 429 beyond). The limit is in-process; with several API instances it needs a proxy or Redis limit instead.
Add tests for the environment detectors, install-method and cuDNN lookups, NVML failure, notice marker errors, exit join, invalid levels and the CLI prompt's no-op paths. Delete the unreachable empty-options branch in pick_config_path_interactive instead of testing it.
scripts/e2e_telemetry.sh starts the compose Postgres under its own project, migrates it, runs the API, then one library process (two stops) and one codecarbon monitor process with telemetry at minimal. An integ_test reads DATABASE_URL and checks one row per process, only allowed columns and no coordinates. Refuses to pull a missing postgres image unless E2E_ALLOW_PULL=1, and tears everything down on exit.
…ing it's absent Production DBs created the telemetry table via create_all (#1171) with the old, wider schema before this migration existed. upgrade() now adjusts that schema in place (drop old-only columns, add any missing new ones) instead of crashing on create_table; downgrade() restores the dropped columns as nullable rather than dropping all rows.
…evel Legacy 'extensive' still maps to minimal, but any other unparseable value (including privacy-intent strings like off/false/none/0) now falls back to disabled instead of minimal, so a typo or misunderstood value can never cause telemetry to be sent.
Wrap thread creation/start in try/except so stop() can never crash from telemetry; _sent is now only set once a thread has actually started (under the lock, so concurrent stop() calls stay atomic), and a failed start no longer permanently suppresses later sends. Also reset _sent and rebuild the lock after fork via os.register_at_fork, guarded for platforms without it, so a forked child can still send its own one-per-process telemetry.
Reduces precision of the one identifying field in the minimal payload; a row still shows usage trends over time without pinning a process to the exact second it ran.
FastAPI runs sync path operations in a threadpool, so concurrent requests could race past the len(hits) >= RATE_LIMIT check in _rate_limited and both get admitted. Guard the check-and-record with a lock. Also documents, next to --forwarded-allow-ips=*, that it trusts X-Forwarded-For from any peer and so the client IP the limiter keys on can be spoofed without a real reverse proxy in front (kept as-is; maintainer decision).
…roblems pg_isready timing out silently fell through to migrating a database that was never ready. The API startup loop could also silently succeed against something else already bound to :8008, or keep polling after uvicorn had already died. Now: fail if pg_isready never succeeds, fail if the port is already in use before starting uvicorn, and fail (with the log) if the API process exits or never comes up.
|
@benoit-cty @inimaz I'd like to move ahead with a simpler version of this PR and get it merged, then build on it. What changes:
Three things I need from you before this can be deployed:
Tier 2, the token question, retention and the endpoint hardening move to #1429. |
…f sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
|
Thanks @davidberenstein1957 ! Good idea to start small and iterate.
I did some fixes, with Opus 5.5. Review fixes: telemetry collection & CLIFour fixes on the package side of the telemetry feature. The server side (migration, rate limiter, schema, repository) needed no change. 1. Don't import
|
…-merge # Conflicts: # codecarbon/core/telemetry/collect.py # tests/test_telemetry_collect.py
|
@inimaz could you deploy the server side of this PR to api.codecarbon.io before the next package release? It needs the alembic migration Decisions made: telemetry stays on by default at |
Description
CodeCarbon can now send a small, anonymous report about the environment it runs in, so we can see which platforms and versions people use. It is a reduced first version of #1106. Tier 2 (
extensive) and endpoint hardening move to #1429.What users see
minimallevel. Until someone picks a level, every run prints a short notice to stderr explaining what is sent and how to turn it off. The notice stops once a level is chosen.codecarbon configandcodecarbon monitorask which level to use.codecarbon telemetry set disabled(applies in every directory),CODECARBON_TELEMETRY_LEVEL=disabled, ortelemetry_level=on the tracker.OfflineEmissionsTracker,codecarbon monitor --offline) never sends anything.What is sent
docs/how-to/telemetry.md. Rows contain no IP address and are deleted automatically after 3 years.When
Settings
CODECARBON_TELEMETRY_LEVEL, thentelemetry_levelin.codecarbon.config, then the default. An oldextensivevalue falls back tominimal; an empty or unknown value (off,false,0) meansdisabled.telemetry_api_url) and does not follow the dashboard'sapi_endpoint.Server
POST /telemetryneeds no token. It rejects unknown fields, caps strings at 256 characters, and returns 429 above 60 requests a minute per IP.20260927_add_telemetry. It creates the table, or, where the table from feat: first version of telemetry #1171 already exists, drops the removed columns, keeps existing rows and convertstimestampto a time-zone-aware column. It also adds an index ontimestampand a trigger that deletes rows older than 3 years on insert, so retention needs no separate job.Related Issue
Part of #1106. Follow-up: #1429.
Motivation and Context
The project has no view of how CodeCarbon is used. This sends the smallest useful set of environment fields, with a clear notice, a one-line opt-out and a documented field list. A token shipped inside an open-source package would be public and would also allow writes to
/runsand/emissions, so the endpoint takes no token and relies on validation and rate limiting.How Has This Been Tested?
uv run task test-packageanduv run task test-api-unit: all passed.codecarbon/core/telemetry/, the CLI, the router and the migration.scripts/e2e_telemetry.shagainst a real Postgres and a local API: two processes produce exactly two rows with only the documented columns.telemetrytable and a row: upgrade keeps the row, downgrade restores the old columns, upgrade again works.carbonserver/tests/database/test_telemetry_retention_db.pyrepeats this whenDATABASE_URLpoints at a Postgres.Screenshots (if appropriate):
N/A
Before deploying
--forwarded-allow-ips=*, so the per-IP limit can be bypassed with a forgedX-Forwarded-For, and the limit is per process. Tracked in Telemetry follow-up: tier 2, endpoint hardening, retention #1429.Types of changes
AI Usage Disclosure
Please refer to docs/how-to/ai-policy.md for detailed guidelines on how to disclose AI usage in your PR. Accurately completing this section is mandatory.
Checklist:
Commits since the original PR