Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0eea792
feat: add telemetry module with Tier 1 payload builder
davidberenstein1957 Apr 29, 2026
0347f01
fix: improve code quality for Task 1 (security, docs, types)
davidberenstein1957 Apr 29, 2026
0ae7783
feat: add Tier 1 telemetry send with session dedup and silent fail
davidberenstein1957 Apr 29, 2026
ed2d3d2
feat: wire Tier 1 telemetry into BaseEmissionsTracker (opt-out via se…
davidberenstein1957 Apr 29, 2026
f2ada76
merge: integrate telemetry backend from PR #1171
davidberenstein1957 May 19, 2026
9e97561
refactor: use TelemetryClient for minimal tracker telemetry
davidberenstein1957 May 19, 2026
453860f
feat: enhance telemetry module with minimal payload builder and impro…
davidberenstein1957 May 19, 2026
954910e
feat: enhance telemetry functionality with new configuration and sess…
davidberenstein1957 May 19, 2026
2a865f6
refactor: enhance telemetry level resolution and configuration handling
davidberenstein1957 May 19, 2026
1ffc45d
Delete docs/plans/2026-05-19-telemetry-configuration.md
davidberenstein1957 May 19, 2026
0a319bc
fix: improve error handling and logging for AMD GPU metrics
davidberenstein1957 May 19, 2026
dcec0c4
feat: enhance telemetry schema and collection methods
davidberenstein1957 May 19, 2026
1ecc237
refactor: update telemetry schema and enhance data collection
davidberenstein1957 May 19, 2026
6c873eb
refactor: streamline telemetry schema and enhance privacy measures
davidberenstein1957 May 19, 2026
e3dca3f
refactor: simplify telemetry framework detection and enhance privacy
davidberenstein1957 May 20, 2026
de3fff1
refactor: simplify telemetry client and enhance telemetry handling
davidberenstein1957 May 20, 2026
5a65220
refactor: overhaul telemetry handling and structure
davidberenstein1957 May 20, 2026
aefea4c
refactor: improve telemetry imports and code formatting
davidberenstein1957 May 20, 2026
adb50c3
feat: introduce telemetry module and enhance telemetry context manage…
davidberenstein1957 May 20, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,6 @@ tests/test_data/rapl/*
credentials*
.codecarbon.config*
scripts/agent-vm.personal.config.sh

# Added by ggshield
.cache_ggshield
61 changes: 6 additions & 55 deletions carbonserver/carbonserver/api/schemas_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ class TelemetryBase(BaseModel):
region: Optional[str] = None
cloud_provider: Optional[str] = None
cloud_region: Optional[str] = None
longitude: Optional[float] = Field(default=None, ge=-180, le=180)
latitude: Optional[float] = Field(default=None, ge=-90, le=90)
on_cloud: Optional[bool] = None

cpu_count: Optional[int] = Field(default=None, ge=0)
cpu_physical_count: Optional[int] = Field(default=None, ge=0)
Expand All @@ -58,12 +57,10 @@ class TelemetryBase(BaseModel):

python_version: Optional[str] = None
python_implementation: Optional[str] = None
python_executable_hash: Optional[str] = Field(
default=None, min_length=64, max_length=64
)
python_env_type: Optional[str] = None
codecarbon_version: Optional[str] = None
codecarbon_install_method: Optional[str] = None
python_package_manager: Optional[str] = None

total_emissions_kg: Optional[float] = Field(default=None, ge=0)
emissions_rate_kg_per_sec: Optional[float] = Field(default=None, ge=0)
Expand All @@ -81,21 +78,18 @@ class TelemetryBase(BaseModel):
output_methods: Optional[List[str]] = None
hardware_tracked: Optional[List[str]] = None
task_tracking_used: Optional[bool] = None
decorator_vs_context: Optional[str] = None
measure_power_interval_secs: Optional[float] = Field(default=None, ge=0)
integration_surface: Optional[str] = None
offline_mode: Optional[bool] = None
save_to_api_enabled: Optional[bool] = None

hardware_detection_success: Optional[bool] = None
rapl_available: Optional[bool] = None
gpu_detection_method: Optional[str] = None
first_measurement_time_ms: Optional[float] = Field(default=None, ge=0)
tracking_overhead_percent: Optional[float] = Field(default=None, ge=0)
errors_encountered: Optional[List[str]] = None
warning_count: Optional[int] = Field(default=None, ge=0)

ide_used: Optional[str] = None
notebook_environment: Optional[str] = None
ci_environment: Optional[str] = None
python_package_manager: Optional[str] = None
framework_detected: Optional[str] = None

has_torch: Optional[bool] = None
Expand All @@ -116,58 +110,15 @@ class TelemetryBase(BaseModel):

container_runtime: Optional[str] = None
in_container: Optional[bool] = None
host_machine_hash: Optional[str] = None

@model_validator(mode="after")
def validate_telemetry_level(self):
if self.telemetry_level == TelemetryLevel.disabled:
raise ValueError("Disabled telemetry must not be submitted")

if self.telemetry_level == TelemetryLevel.minimal:
extensive_fields = set(type(self).model_fields) - MINIMAL_TELEMETRY_FIELDS
submitted_extensive_fields = [
field
for field in extensive_fields
if getattr(self, field) not in (None, [], {})
]
if submitted_extensive_fields:
fields = ", ".join(sorted(submitted_extensive_fields))
raise ValueError(
f"Minimal telemetry cannot include extensive fields: {fields}"
)

return self


MINIMAL_TELEMETRY_FIELDS = {
"timestamp",
"telemetry_level",
"os",
"country_name",
"country_iso_code",
"region",
"cloud_provider",
"cloud_region",
"longitude",
"latitude",
"cpu_count",
"cpu_physical_count",
"cpu_model",
"cpu_architecture",
"gpu_count",
"gpu_model",
"gpu_driver_version",
"gpu_memory_total_gb",
"ram_total_size_gb",
"cuda_version",
"cudnn_version",
"python_version",
"python_implementation",
"python_executable_hash",
"python_env_type",
"codecarbon_version",
"codecarbon_install_method",
}
PRIVATE_TELEMETRY_FIELDS = frozenset(TelemetryBase.model_fields)


class TelemetryCreate(TelemetryBase):
Expand Down
31 changes: 23 additions & 8 deletions carbonserver/tests/api/routers/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

TELEMETRY_ID = "f52fe339-164d-4c2b-a8c0-f562dfce066d"

MINIMAL_TELEMETRY_TO_CREATE = {
SAMPLE_PRIVATE_TELEMETRY = {
"timestamp": "2026-05-03T12:00:00+00:00",
"telemetry_level": "minimal",
"os": "Linux-5.10.0-x86_64",
Expand Down Expand Up @@ -46,21 +46,36 @@ def test_add_telemetry(client, custom_test_server):
repository_mock.add_telemetry.return_value = UUID(TELEMETRY_ID)

with custom_test_server.container.telemetry_repository.override(repository_mock):
response = client.post("/telemetry", json=MINIMAL_TELEMETRY_TO_CREATE)
response = client.post("/telemetry", json=SAMPLE_PRIVATE_TELEMETRY)

assert response.status_code == status.HTTP_201_CREATED
assert response.json() == TELEMETRY_ID


def test_minimal_telemetry_rejects_extensive_fields(client, custom_test_server):
def test_minimal_telemetry_accepts_framework_versions(client, custom_test_server):
repository_mock = mock.Mock(spec=TelemetryRepository)
telemetry_with_extensive_field = {
**MINIMAL_TELEMETRY_TO_CREATE,
"total_emissions_kg": 0.42,
repository_mock.add_telemetry.return_value = UUID(TELEMETRY_ID)
telemetry_with_framework_version = {
**SAMPLE_PRIVATE_TELEMETRY,
"torch_version": "2.2.0",
}

with custom_test_server.container.telemetry_repository.override(repository_mock):
response = client.post("/telemetry", json=telemetry_with_framework_version)

assert response.status_code == status.HTTP_201_CREATED
repository_mock.add_telemetry.assert_called_once()


def test_telemetry_rejects_unknown_fields(client, custom_test_server):
repository_mock = mock.Mock(spec=TelemetryRepository)
telemetry_with_unknown_field = {
**SAMPLE_PRIVATE_TELEMETRY,
"unknown_field": "value",
}

with custom_test_server.container.telemetry_repository.override(repository_mock):
response = client.post("/telemetry", json=telemetry_with_extensive_field)
response = client.post("/telemetry", json=telemetry_with_unknown_field)

assert response.status_code == 422
repository_mock.add_telemetry.assert_not_called()
Expand All @@ -69,7 +84,7 @@ def test_minimal_telemetry_rejects_extensive_fields(client, custom_test_server):
def test_disabled_telemetry_is_rejected(client, custom_test_server):
repository_mock = mock.Mock(spec=TelemetryRepository)
disabled_telemetry = {
**MINIMAL_TELEMETRY_TO_CREATE,
**SAMPLE_PRIVATE_TELEMETRY,
"telemetry_level": "disabled",
}

Expand Down
4 changes: 3 additions & 1 deletion carbonserver/tests/api/test_telemetry_schema_drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from carbonserver.api.schemas_telemetry import TelemetryCreate as ServerTelemetryCreate

REPO_ROOT = Path(__file__).resolve().parents[3]
CORE_TELEMETRY_SCHEMA_PATH = REPO_ROOT / "codecarbon" / "core" / "telemetry_schemas.py"
CORE_TELEMETRY_SCHEMA_PATH = (
REPO_ROOT / "codecarbon" / "core" / "telemetry" / "schemas.py"
)


def _load_core_telemetry_create():
Expand Down
13 changes: 13 additions & 0 deletions codecarbon/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
overwrite_local_config,
)
from codecarbon.cli.monitor import run_and_monitor
from codecarbon.cli.telemetry_cli import normalize_telemetry_level, telemetry_app
from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone
from codecarbon.core.schemas import ExperimentCreate, OrganizationCreate, ProjectCreate
from codecarbon.emissions_tracker import EmissionsTracker, OfflineEmissionsTracker
Expand All @@ -32,6 +33,7 @@
DEFAULT_ORGANIzATION_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1"

codecarbon = typer.Typer(no_args_is_help=True)
codecarbon.add_typer(telemetry_app, name="telemetry")


def main():
Expand Down Expand Up @@ -342,6 +344,15 @@ def monitor(
str,
typer.Option(help="Region/province for offline mode"),
] = None,
telemetry_level: Annotated[
Optional[str],
typer.Option(
help=(
"Override telemetry tier for this run only "
"(disabled, minimal, or extensive)."
),
),
] = None,
):
"""Monitor your machine's carbon emissions."""

Expand All @@ -350,6 +361,8 @@ def monitor(
"measure_power_secs": measure_power_secs,
"api_call_interval": api_call_interval,
}
if telemetry_level is not None:
tracker_args["telemetry_level"] = normalize_telemetry_level(telemetry_level)
# Set up the tracker arguments based on mode (offline vs online) and validate required args for each mode
if offline:
if not country_iso_code:
Expand Down
Loading
Loading