Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
5c568e7
feat: first version of telemetry
inimaz May 3, 2026
39eef0b
tests: add a test so that client and server do not differ
inimaz May 3, 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
eaa75f1
merge: rebase feat/add-telemetry onto origin/master
davidberenstein1957 Jun 15, 2026
74eed94
Align SDK telemetry with server schema and simplify collection
davidberenstein1957 Jun 15, 2026
29664d7
Apply pre-commit formatting for telemetry files
davidberenstein1957 Jun 17, 2026
a231750
Fix CodeQL logging alert and raise telemetry test coverage
davidberenstein1957 Jun 17, 2026
edb2d03
Apply black formatting to telemetry schema tests
davidberenstein1957 Jun 17, 2026
13cde15
Potential fix for pull request finding 'CodeQL / Clear-text logging o…
davidberenstein1957 Jun 17, 2026
c5e6c4a
Merge branch 'master' into feat/add-telemetry
davidberenstein1957 Jun 17, 2026
2145b61
Raise PR coverage for telemetry CLI and API helpers
davidberenstein1957 Jun 17, 2026
ea4f275
Refactor telemetry schema docstring
davidberenstein1957 Jul 18, 2026
8b995d3
Update telemetry configuration in emissions tracker
davidberenstein1957 Jul 18, 2026
08cc67c
Add telemetry experiment ID and default settings integration
davidberenstein1957 Jul 18, 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 @@ -136,3 +136,6 @@ tests/test_data/rapl/*
credentials*
.codecarbon.config*
scripts/agent-vm.personal.config.sh

# Added by ggshield

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe this is not needed as discussed

.cache_ggshield
14 changes: 13 additions & 1 deletion carbonserver/carbonserver/api/routers/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
from uuid import UUID

from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Header
from starlette import status

from carbonserver.api.schemas import AccessLevel
from carbonserver.api.schemas_telemetry import TelemetryCreate
from carbonserver.api.services.project_token_service import ProjectTokenService
from carbonserver.api.services.telemetry_service import TelemetryService
from carbonserver.config import settings
from carbonserver.container import ServerContainer

TELEMETRY_ROUTER_TAGS = ["Telemetry"]
Expand All @@ -27,5 +30,14 @@ def add_telemetry(
telemetry_service: TelemetryService = Depends(
Provide[ServerContainer.telemetry_service]
),
project_token_service: ProjectTokenService = Depends(
Provide[ServerContainer.project_token_service]
),
x_api_token: str = Header(None),
) -> UUID:
project_token_service.project_token_has_access(
AccessLevel.WRITE.value,
experiment_id=settings.telemetry_experiment_id,
project_token=x_api_token,
)
return telemetry_service.add_telemetry(telemetry)
8 changes: 8 additions & 0 deletions carbonserver/carbonserver/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

from carbonserver.telemetry_defaults import DEFAULT_TELEMETRY_EXPERIMENT_ID


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="", extra="ignore")
Expand Down Expand Up @@ -42,6 +44,12 @@ class Settings(BaseSettings):
server_host: str = Field(
"0.0.0.0", validation_alias=AliasChoices("SERVER_HOST", "server_host")
)
telemetry_experiment_id: str = Field(
DEFAULT_TELEMETRY_EXPERIMENT_ID,
validation_alias=AliasChoices(
"TELEMETRY_EXPERIMENT_ID", "telemetry_experiment_id"
),
)


settings = Settings()
1 change: 1 addition & 0 deletions carbonserver/carbonserver/telemetry_defaults.py
164 changes: 164 additions & 0 deletions carbonserver/tests/api/integration/test_telemetry_local_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Integration tests for telemetry against a running local carbonserver API."""

import os
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path

import pytest
import requests

REPO_ROOT = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(REPO_ROOT))

from codecarbon.core.telemetry.client import post_private, post_public_summary
from codecarbon.core.telemetry.collect import TelemetryContext, build_payload
from codecarbon.core.telemetry.defaults import (
DEFAULT_TELEMETRY_API_KEY,
DEFAULT_TELEMETRY_EXPERIMENT_ID,
)
from codecarbon.core.telemetry.schemas import TelemetryLevel
from codecarbon.core.telemetry.settings import TelemetrySettings
from codecarbon.output_methods.emissions_data import EmissionsData

URL = os.getenv("CODECARBON_API_URL")
if URL is None:
pytest.exit("CODECARBON_API_URL is not defined (e.g. http://localhost:8008)")


def _api_url(path: str) -> str:
base = URL.rstrip("/")
if not base.endswith("/api"):
base = f"{base}/api"
return f"{base}{path}"


def _local_settings() -> TelemetrySettings:
return TelemetrySettings.resolve(
external_conf={
"telemetry_level": "extensive",
"telemetry_api_url": URL.rstrip("/"),
"telemetry_api_key": DEFAULT_TELEMETRY_API_KEY,
"telemetry_experiment_id": DEFAULT_TELEMETRY_EXPERIMENT_ID,
}
)


def _sample_emissions() -> EmissionsData:
return EmissionsData(
timestamp="2026-01-01T00:00:00",
project_name="telemetry-local",
run_id="local-run",
experiment_id=DEFAULT_TELEMETRY_EXPERIMENT_ID,
duration=10.0,
emissions=0.001,
emissions_rate=0.0001,
cpu_power=0.0,
gpu_power=0.0,
ram_power=0.0,
cpu_energy=0.0,
gpu_energy=0.0,
ram_energy=0.0,
energy_consumed=0.01,
water_consumed=0.0,
country_name="France",
country_iso_code="FRA",
region="idf",
cloud_provider="",
cloud_region="",
os="Linux",
python_version="3.12",
codecarbon_version="3.2.8",
cpu_count=4,
cpu_model="test-cpu",
gpu_count=0,
gpu_model="",
longitude=0.0,
latitude=0.0,
ram_total_size=16.0,
tracking_mode="process",
)


def test_local_api_is_up():
response = requests.get(URL.rstrip("/") + "/", timeout=5)
assert response.status_code == 200
assert response.json()["status"] == "OK"


def test_local_telemetry_post_accepts_sdk_payload():
settings = _local_settings()
payload = build_payload(
TelemetryContext(
conf={
"os": "Linux-5.10.0-x86_64",
"codecarbon_version": "3.2.8",
"cpu_count": 4,
"python_version": "3.12",
"tracking_mode": "process",
},
emissions=_sample_emissions(),
hardware=[],
resource_tracker=None,
output_methods=[],
tasks={},
measure_power_secs=15,
integration="library",
),
level=TelemetryLevel.minimal,
)
assert post_private(settings, payload) is True


def test_local_extensive_run_and_emission_flow():
settings = _local_settings()
run_payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"experiment_id": settings.experiment_id,
"os": "Linux",
"python_version": "3.12",
"codecarbon_version": "3.2.8",
"cpu_count": 4,
"tracking_mode": "process",
}
run_response = requests.post(
_api_url("/runs"),
json=run_payload,
headers={"x-api-token": settings.api_key},
timeout=10,
)
assert run_response.status_code == 201, run_response.text
run_id = run_response.json()["id"]

emission_payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"run_id": run_id,
"duration": 10,
"emissions_sum": 0.001,
"emissions_rate": 0.0001,
"cpu_power": 0.0,
"gpu_power": 0.0,
"ram_power": 0.0,
"cpu_energy": 0.0,
"gpu_energy": 0.0,
"ram_energy": 0.0,
"energy_consumed": 0.01,
"wue": 0,
}
emission_response = requests.post(
_api_url("/emissions"),
json=emission_payload,
headers={"x-api-token": settings.api_key},
timeout=10,
)
assert emission_response.status_code == 201, emission_response.text
assert uuid.UUID(emission_response.json())


def test_local_post_public_summary_helper():
settings = _local_settings()
assert (
post_public_summary(settings, {"os": "Linux", "tracking_mode": "process"}, _sample_emissions())
is True
)
89 changes: 82 additions & 7 deletions carbonserver/tests/api/routers/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
from fastapi.testclient import TestClient
from starlette import status

from carbonserver.api.infra.repositories.repository_projects_tokens import (
SqlAlchemyRepository as ProjectTokenRepository,
)
from carbonserver.api.infra.repositories.repository_telemetry import (
SqlAlchemyRepository as TelemetryRepository,
)
from carbonserver.api.routers import telemetry
from carbonserver.api.schemas import AccessLevel, ProjectToken
from carbonserver.config import settings
from carbonserver.container import ServerContainer

TELEMETRY_ID = "f52fe339-164d-4c2b-a8c0-f562dfce066d"
Expand Down Expand Up @@ -44,37 +49,107 @@ def client(custom_test_server):
def test_add_telemetry(client, custom_test_server):
repository_mock = mock.Mock(spec=TelemetryRepository)
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)
project_tokens_repository_mock = mock.Mock(spec=ProjectTokenRepository)
project_tokens_repository_mock.get_project_token_by_experiment_id_and_token.return_value = ProjectToken(
id=UUID("e60afb92-17b7-4720-91a0-1ae91e409ba7"),
project_id=UUID("f52fe339-164d-4c2b-a8c0-f562dfce066d"),
name="Telemetry",
token="token",
access=AccessLevel.WRITE.value,
)

with custom_test_server.container.telemetry_repository.override(
repository_mock
) and custom_test_server.container.project_token_repository.override(
project_tokens_repository_mock
):
response = client.post(
"/telemetry",
json=MINIMAL_TELEMETRY_TO_CREATE,
headers={"x-api-token": "token"},
)

assert response.status_code == status.HTTP_201_CREATED
assert response.json() == TELEMETRY_ID
project_tokens_repository_mock.get_project_token_by_experiment_id_and_token.assert_called_once_with(
settings.telemetry_experiment_id, "token"
)


def test_add_telemetry_rejects_missing_token(client, custom_test_server):
repository_mock = mock.Mock(spec=TelemetryRepository)
project_tokens_repository_mock = mock.Mock(spec=ProjectTokenRepository)

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

assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json() == {
"detail": "Not allowed to perform this action. Missing project token"
}
repository_mock.add_telemetry.assert_not_called()
project_tokens_repository_mock.get_project_token_by_experiment_id_and_token.assert_not_called()


def test_minimal_telemetry_rejects_extensive_fields(client, custom_test_server):
repository_mock = mock.Mock(spec=TelemetryRepository)
project_tokens_repository_mock = mock.Mock(spec=ProjectTokenRepository)
project_tokens_repository_mock.get_project_token_by_experiment_id_and_token.return_value = ProjectToken(
id=UUID("e60afb92-17b7-4720-91a0-1ae91e409ba7"),
project_id=UUID("f52fe339-164d-4c2b-a8c0-f562dfce066d"),
name="Telemetry",
token="token",
access=AccessLevel.WRITE.value,
)
telemetry_with_extensive_field = {
**MINIMAL_TELEMETRY_TO_CREATE,
"total_emissions_kg": 0.42,
}

with custom_test_server.container.telemetry_repository.override(repository_mock):
response = client.post("/telemetry", json=telemetry_with_extensive_field)
with custom_test_server.container.telemetry_repository.override(
repository_mock
) and custom_test_server.container.project_token_repository.override(
project_tokens_repository_mock
):
response = client.post(
"/telemetry",
json=telemetry_with_extensive_field,
headers={"x-api-token": "token"},
)

assert response.status_code == 422
repository_mock.add_telemetry.assert_not_called()


def test_disabled_telemetry_is_rejected(client, custom_test_server):
repository_mock = mock.Mock(spec=TelemetryRepository)
project_tokens_repository_mock = mock.Mock(spec=ProjectTokenRepository)
project_tokens_repository_mock.get_project_token_by_experiment_id_and_token.return_value = ProjectToken(
id=UUID("e60afb92-17b7-4720-91a0-1ae91e409ba7"),
project_id=UUID("f52fe339-164d-4c2b-a8c0-f562dfce066d"),
name="Telemetry",
token="token",
access=AccessLevel.WRITE.value,
)
disabled_telemetry = {
**MINIMAL_TELEMETRY_TO_CREATE,
"telemetry_level": "disabled",
}

with custom_test_server.container.telemetry_repository.override(repository_mock):
response = client.post("/telemetry", json=disabled_telemetry)
with custom_test_server.container.telemetry_repository.override(
repository_mock
) and custom_test_server.container.project_token_repository.override(
project_tokens_repository_mock
):
response = client.post(
"/telemetry",
json=disabled_telemetry,
headers={"x-api-token": "token"},
)

assert response.status_code == 422
repository_mock.add_telemetry.assert_not_called()
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
Loading