Backend Architecture

The carbon monitoring backend is implemented in ada-carbon-monitoring-api, a FastAPI service that calculates carbon footprint from CPU usage data.

Overview

ada-carbon-monitoring-api/
├── main.py                 # FastAPI application entry point
├── src/
│   ├── api/carbon/         # API route handlers
│   │   ├── carbon_route.py     # Carbon calculation endpoints
│   │   ├── workspace_route.py  # Workspace/user/group endpoints
│   │   ├── history_route.py    # Historical data endpoints
│   │   └── carbon_schema.py    # Pydantic models
│   ├── calculators/        # Calculation logic
│   │   ├── electricity_estimator.py  # kWh from CPU seconds
│   │   ├── carbon_calculator.py      # gCO2eq from kWh
│   │   └── carbon_equivalency.py     # Real-world equivalencies
│   ├── clients/            # External service clients
│   │   ├── prometheus_client.py      # Prometheus queries
│   │   ├── mongodb_client.py         # MongoDB queries
│   │   ├── register_client.py        # ada-db-interface client
│   │   └── carbon_intensity_client.py # UK Grid API
│   ├── models/             # Domain models
│   │   ├── workspace_tracker.py      # Main tracking logic
│   │   └── workspace_usage.py        # Usage data model
│   └── config.py           # Configuration management
├── tests/                  # Test suite
└── ada-carbon-monitoring-api.ini  # Configuration file

Configuration

The API is configured via ada-carbon-monitoring-api.ini:

[GENERAL]
version = 1.0.0
port = 8000
cors_allowed_origins = http://localhost:3000,https://ada.stfc.ac.uk

[PROMETHEUS]
url = https://host-172-16-100-248.nubes.stfc.ac.uk/
timeout = 120

[CARBON_INTENSITY]
api_url = https://api.carbonintensity.org.uk/intensity

[POWER]
busy_power_w = 12.0
idle_power_w = 1.0
cpu_tdp_w = 100.0

[TESTING]
use_fake_prometheus = false
use_fake_mongodb = false
use_fake_carbon_intensity = false

API Routers

Carbon Router (/carbon)

Core carbon calculation endpoints:

Endpoint Method Description
/carbon/intensity/current GET Current UK grid carbon intensity
/carbon/intensity/forecast GET 24/48 hour intensity forecast
/carbon/calculate POST Calculate carbon from CPU seconds
/carbon/equivalencies POST Calculate equivalencies
/carbon/equivalencies/{gco2eq} GET Get equivalencies for value
/carbon/electricity/estimate GET Estimate electricity usage

Workspace Router (/workspaces)

Workspace carbon tracking:

Endpoint Method Description
/workspaces/active GET List active workspaces
/workspaces/track POST Track all active workspaces
/workspaces/summary GET Get summary statistics
/workspaces/{id} GET Get specific workspace usage

User Router (/users)

Per-user carbon attribution:

Endpoint Method Description
/users/{username}/usage GET User’s workspace carbon usage
/users/{username}/summary GET User’s aggregated carbon summary

Group Router (/groups)

Per-group carbon attribution:

Endpoint Method Description
/groups GET List all groups (cloud_project + machine_name)
/groups/{name}/usage GET Group’s carbon usage
/groups/{name}/summary GET Group’s aggregated carbon summary

History Router (/carbon)

Historical data for charts:

Endpoint Method Description
/carbon/history GET Historical data (day/month/year view)
/carbon/heatmap GET Heatmap data (year of daily values)

Calculators

ElectricityEstimator

Converts CPU seconds to energy (kWh):

class ElectricityEstimator:
    def __init__(self, busy_power_w=12.0, idle_power_w=1.0):
        self.busy_power_w = busy_power_w
        self.idle_power_w = idle_power_w

    def estimate_total_kwh(self, busy_seconds, idle_seconds):
        watt_seconds = (self.busy_power_w * busy_seconds +
                       self.idle_power_w * idle_seconds)
        return watt_seconds / 3_600_000  # Convert to kWh

CarbonCalculator

Converts energy to carbon emissions:

class CarbonCalculator:
    def estimate_carbon_footprint(self, kwh, carbon_intensity_g_per_kwh):
        return kwh * carbon_intensity_g_per_kwh  # Returns gCO2eq

CarbonEquivalencyCalculator

Converts gCO2eq to relatable equivalencies:

EQUIVALENCIES = {
    "miles_driven_car": 400.0,      # gCO2eq per mile
    "smartphone_charge": 8.22,       # gCO2eq per charge
    "tree_day": 59.6,               # gCO2eq absorbed per tree per day
    "streaming_hour": 55.0,         # gCO2eq per hour of HD streaming
    "kettle_boil": 70.0,            # gCO2eq per liter boiled
    # ... 18+ total equivalencies
}

Clients

PrometheusAPIClient

Queries CPU metrics from Prometheus:

client = PrometheusAPIClient(prometheus_url="https://...")

# Get CPU usage breakdown
cpu_data = client.get_cpu_usage_breakdown(
    timestamp=datetime.now(),
    cloud_project_name="IDAaaS",
    machine_name="Muon",
    host="172.16.100.50"
)
# Returns: {"busy": 1234.5, "idle": 56789.0}

Query used:

increase(node_cpu_seconds_total{
    cloud_project_name="IDAaaS",
    machine_name="Muon",
    instance=~"172.16.100.50.*"
}[1h])

MongoDBClient

Queries workspace and user data:

client = MongoDBClient(
    mongo_uri="mongodb://...",
    database_name="ada"
)

# Get active workspaces
workspaces = client.get_active_workspaces(timestamp=datetime.now())

# Get user by platform name
user = client.get_user_by_platform_name("aa123456")

CarbonIntensityAPIClient

Queries UK Grid carbon intensity:

client = CarbonIntensityAPIClient()

# Current intensity
current = client.get_current_intensity()
# Returns: {"intensity": 185, "index": "moderate"}

# Forecast
forecast = client.get_forecast(hours=24)
# Returns: {"forecasts": [...]}

WorkspaceTracker

Main orchestration class that combines all components:

class WorkspaceTracker:
    def __init__(self, ...):
        self.data_client = MongoDBClient(...)
        self.prometheus_client = PrometheusAPIClient(...)
        self.carbon_client = CarbonIntensityAPIClient()
        self.electricity_estimator = ElectricityEstimator(...)
        self.carbon_calculator = CarbonCalculator(...)

    def track_workspace(self, workspace, timestamp):
        # 1. Get CPU data from Prometheus
        cpu_data = self.prometheus_client.get_cpu_usage_breakdown(...)

        # 2. Calculate energy
        energy_kwh = self.electricity_estimator.estimate_total_kwh(...)

        # 3. Get carbon intensity
        intensity = self.carbon_client.get_current_intensity()

        # 4. Calculate carbon
        carbon_gco2eq = self.carbon_calculator.estimate_carbon_footprint(...)

        # 5. Get equivalencies
        equivalencies = self.equivalency_calc.get_top_equivalencies(...)

        return WorkspaceUsageEntry(...)

    def track_group(self, cloud_project_name, machine_name, timestamp):
        # Track aggregated usage for a group (no host filter)
        ...

Data Models

WorkspaceUsageEntry

class WorkspaceUsageEntry:
    workspace_id: str
    hostname: str
    owner: Optional[str]
    timestamp: datetime

    # CPU metrics
    busy_cpu_seconds_total: float
    idle_cpu_seconds_total: float

    # Energy (kWh)
    busy_usage_kwh: float
    idle_usage_kwh: float
    total_usage_kwh: float

    # Carbon (gCO2eq)
    busy_usage_gco2eq: float
    idle_usage_gco2eq: float
    total_usage_gco2eq: float
    carbon_intensity_g_per_kwh: float

    # Equivalencies
    carbon_equivalencies: dict

    # Status
    status: str  # "initialized", "downloaded", "processed", "complete"

Running the API

# Development
python main.py

# Production with uvicorn
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

API documentation available at:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc

Testing

# Run all tests
pytest tests/ -v

# Run with coverage
pytest tests/ --cov=src --cov-report=html

Environment Variables

Variable Description Default
PROMETHEUS_URL Prometheus server URL localhost:9090
MONGO_URI MongoDB connection URI localhost:27017
MONGO_DATABASE MongoDB database name ada
USE_FAKE_PROMETHEUS Use fake Prometheus data false
USE_FAKE_MONGODB Use fake MongoDB data false

Table of contents