Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions carbonserver/carbonserver/api/domain/telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import abc
from uuid import UUID

from carbonserver.api import schemas_telemetry


class Telemetry(abc.ABC):
@abc.abstractmethod
def add_telemetry(self, telemetry: schemas_telemetry.TelemetryCreate) -> UUID:
raise NotImplementedError
104 changes: 104 additions & 0 deletions carbonserver/carbonserver/api/infra/database/telemetry_sql_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""SQLAlchemy models for telemetry data in the CarbonServer API."""

import uuid

from sqlalchemy import JSON, Boolean, Column, DateTime, Float, Integer, String
from sqlalchemy.dialects.postgresql import UUID

from carbonserver.database.database import Base


class Telemetry(Base):
__tablename__ = "telemetry"

id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
timestamp = Column(DateTime, nullable=False)
telemetry_level = Column(String, nullable=False)

os = Column(String, nullable=True)
country_name = Column(String, nullable=True)
country_iso_code = Column(String, nullable=True)
region = Column(String, nullable=True)
cloud_provider = Column(String, nullable=True)
cloud_region = Column(String, nullable=True)
longitude = Column(Float, nullable=True)
latitude = Column(Float, nullable=True)

cpu_count = Column(Integer, nullable=True)
cpu_physical_count = Column(Integer, nullable=True)
cpu_model = Column(String, nullable=True)
cpu_architecture = Column(String, nullable=True)
gpu_count = Column(Integer, nullable=True)
gpu_model = Column(String, nullable=True)
gpu_driver_version = Column(String, nullable=True)
gpu_memory_total_gb = Column(Float, nullable=True)
ram_total_size_gb = Column(Float, nullable=True)
cuda_version = Column(String, nullable=True)
cudnn_version = Column(String, nullable=True)

python_version = Column(String, nullable=True)
python_implementation = Column(String, nullable=True)
python_executable_hash = Column(String, nullable=True)
python_env_type = Column(String, nullable=True)
codecarbon_version = Column(String, nullable=True)
codecarbon_install_method = Column(String, nullable=True)

total_emissions_kg = Column(Float, nullable=True)
emissions_rate_kg_per_sec = Column(Float, nullable=True)
energy_consumed_kwh = Column(Float, nullable=True)
cpu_energy_kwh = Column(Float, nullable=True)
gpu_energy_kwh = Column(Float, nullable=True)
ram_energy_kwh = Column(Float, nullable=True)
duration_seconds = Column(Float, nullable=True)
cpu_utilization_avg = Column(Float, nullable=True)
gpu_utilization_avg = Column(Float, nullable=True)
ram_utilization_avg = Column(Float, nullable=True)

tracking_mode = Column(String, nullable=True)
api_mode = Column(String, nullable=True)
output_methods = Column(JSON, nullable=True)
hardware_tracked = Column(JSON, nullable=True)
task_tracking_used = Column(Boolean, nullable=True)
decorator_vs_context = Column(String, nullable=True)
measure_power_interval_secs = Column(Float, nullable=True)

hardware_detection_success = Column(Boolean, nullable=True)
rapl_available = Column(Boolean, nullable=True)
gpu_detection_method = Column(String, nullable=True)
first_measurement_time_ms = Column(Float, nullable=True)
tracking_overhead_percent = Column(Float, nullable=True)
errors_encountered = Column(JSON, nullable=True)
warning_count = Column(Integer, nullable=True)

ide_used = Column(String, nullable=True)
notebook_environment = Column(String, nullable=True)
ci_environment = Column(String, nullable=True)
python_package_manager = Column(String, nullable=True)
framework_detected = Column(String, nullable=True)

has_torch = Column(Boolean, nullable=True)
torch_version = Column(String, nullable=True)
has_transformers = Column(Boolean, nullable=True)
transformers_version = Column(String, nullable=True)
has_diffusers = Column(Boolean, nullable=True)
diffusers_version = Column(String, nullable=True)
has_tensorflow = Column(Boolean, nullable=True)
tensorflow_version = Column(String, nullable=True)
has_keras = Column(Boolean, nullable=True)
keras_version = Column(String, nullable=True)
has_pytorch_lightning = Column(Boolean, nullable=True)
pytorch_lightning_version = Column(String, nullable=True)
has_fastai = Column(Boolean, nullable=True)
fastai_version = Column(String, nullable=True)
ml_framework_primary = Column(String, nullable=True)

container_runtime = Column(String, nullable=True)
in_container = Column(Boolean, nullable=True)
host_machine_hash = Column(String, nullable=True)

def __repr__(self):
return (
f'<Telemetry(id="{self.id}", '
f'timestamp="{self.timestamp}", '
f'telemetry_level="{self.telemetry_level}")>'
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Repository implementation for telemetry data using SQLAlchemy."""

import uuid
from contextlib import AbstractContextManager
from uuid import UUID

from dependency_injector.providers import Callable

from carbonserver.api.domain.telemetry import Telemetry
from carbonserver.api.infra.database.telemetry_sql_models import (
Telemetry as SqlModelTelemetry,
)
from carbonserver.api.schemas_telemetry import TelemetryCreate


class SqlAlchemyRepository(Telemetry):
def __init__(self, session_factory) -> Callable[..., AbstractContextManager]:
self.session_factory = session_factory

def add_telemetry(self, telemetry: TelemetryCreate) -> UUID:
with self.session_factory() as session:
db_telemetry = SqlModelTelemetry(
id=uuid.uuid4(),
**telemetry.model_dump(),
)
session.add(db_telemetry)
session.commit()
return db_telemetry.id
31 changes: 31 additions & 0 deletions carbonserver/carbonserver/api/routers/telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""API router for handling telemetry data in the CarbonServer API."""

from uuid import UUID

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

from carbonserver.api.schemas_telemetry import TelemetryCreate
from carbonserver.api.services.telemetry_service import TelemetryService
from carbonserver.container import ServerContainer

TELEMETRY_ROUTER_TAGS = ["Telemetry"]

router = APIRouter()


@router.post(
"/telemetry",
tags=TELEMETRY_ROUTER_TAGS,
status_code=status.HTTP_201_CREATED,
response_model=UUID,
)
@inject
def add_telemetry(
telemetry: TelemetryCreate,
telemetry_service: TelemetryService = Depends(
Provide[ServerContainer.telemetry_service]
),
) -> UUID:
return telemetry_service.add_telemetry(telemetry)
178 changes: 178 additions & 0 deletions carbonserver/carbonserver/api/schemas_telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Schemas for telemetry data submitted to the CarbonServer API."""

from datetime import datetime
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, ConfigDict, Field, model_validator


class TelemetryLevel(str, Enum):
disabled = "disabled"
minimal = "minimal"
extensive = "extensive"


class TelemetryBase(BaseModel):
model_config = ConfigDict(
extra="forbid",
use_enum_values=True,
json_schema_extra={
"example": {
"timestamp": "2026-05-03T12:00:00+00:00",
"telemetry_level": "minimal",
"os": "Linux-5.10.0-x86_64",
"country_name": "France",
"country_iso_code": "FRA",
"cpu_count": 12,
"cpu_model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
"python_version": "3.11.5",
"codecarbon_version": "3.0.0",
}
},
)

timestamp: datetime
telemetry_level: TelemetryLevel

os: Optional[str] = None
country_name: Optional[str] = None
country_iso_code: Optional[str] = Field(default=None, min_length=2, max_length=3)
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)

cpu_count: Optional[int] = Field(default=None, ge=0)
cpu_physical_count: Optional[int] = Field(default=None, ge=0)
cpu_model: Optional[str] = None
cpu_architecture: Optional[str] = None
gpu_count: Optional[int] = Field(default=None, ge=0)
gpu_model: Optional[str] = None
gpu_driver_version: Optional[str] = None
gpu_memory_total_gb: Optional[float] = Field(default=None, ge=0)
ram_total_size_gb: Optional[float] = Field(default=None, ge=0)
cuda_version: Optional[str] = None
cudnn_version: Optional[str] = None

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

total_emissions_kg: Optional[float] = Field(default=None, ge=0)
emissions_rate_kg_per_sec: Optional[float] = Field(default=None, ge=0)
energy_consumed_kwh: Optional[float] = Field(default=None, ge=0)
cpu_energy_kwh: Optional[float] = Field(default=None, ge=0)
gpu_energy_kwh: Optional[float] = Field(default=None, ge=0)
ram_energy_kwh: Optional[float] = Field(default=None, ge=0)
duration_seconds: Optional[float] = Field(default=None, ge=0)
cpu_utilization_avg: Optional[float] = Field(default=None, ge=0, le=100)
gpu_utilization_avg: Optional[float] = Field(default=None, ge=0, le=100)
ram_utilization_avg: Optional[float] = Field(default=None, ge=0, le=100)

tracking_mode: Optional[str] = None
api_mode: Optional[str] = None
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)

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
torch_version: Optional[str] = None
has_transformers: Optional[bool] = None
transformers_version: Optional[str] = None
has_diffusers: Optional[bool] = None
diffusers_version: Optional[str] = None
has_tensorflow: Optional[bool] = None
tensorflow_version: Optional[str] = None
has_keras: Optional[bool] = None
keras_version: Optional[str] = None
has_pytorch_lightning: Optional[bool] = None
pytorch_lightning_version: Optional[str] = None
has_fastai: Optional[bool] = None
fastai_version: Optional[str] = None
ml_framework_primary: Optional[str] = None

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",
}


class TelemetryCreate(TelemetryBase):
pass


class Telemetry(TelemetryBase):
id: str
16 changes: 16 additions & 0 deletions carbonserver/carbonserver/api/services/telemetry_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Service layer for handling telemetry data in the CarbonServer API."""

from uuid import UUID

from carbonserver.api.infra.repositories.repository_telemetry import (
SqlAlchemyRepository as TelemetrySqlRepository,
)
from carbonserver.api.schemas_telemetry import TelemetryCreate


class TelemetryService:
def __init__(self, telemetry_repository: TelemetrySqlRepository):
self._repository = telemetry_repository

def add_telemetry(self, telemetry: TelemetryCreate) -> UUID:
return self._repository.add_telemetry(telemetry)
Loading
Loading