Skip to content
Open
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
122 changes: 120 additions & 2 deletions codecarbon/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,87 @@ def monitor(
str,
typer.Option(help="Log level (critical, error, warning, info, debug)"),
] = "error",
api_endpoint: Annotated[
str | None, typer.Option(help="CodeCarbon API endpoint")
] = None,
api_key: Annotated[str | None, typer.Option(help="CodeCarbon API key")] = None,
output_dir: Annotated[
str | None, typer.Option(help="Directory for output files")
] = None,
output_file: Annotated[str | None, typer.Option(help="Output CSV filename")] = None,
output_methods: Annotated[
str | None,
typer.Option(help="Comma-separated output methods (csv, api, logger, ...)"),
] = None,
save_to_file: Annotated[
bool | None,
typer.Option(help="Deprecated: save emissions to a CSV file"),
] = None,
save_to_logger: Annotated[
bool | None,
typer.Option(help="Deprecated: write emissions to a logger"),
] = None,
save_to_prometheus: Annotated[
bool | None,
typer.Option(help="Deprecated: push emissions to Prometheus"),
] = None,
save_to_logfire: Annotated[
bool | None,
typer.Option(help="Deprecated: write emissions to Logfire"),
] = None,
prometheus_url: Annotated[
str | None, typer.Option(help="Prometheus server URL")
] = None,
gpu_ids: Annotated[
str | None, typer.Option(help="Comma-separated GPU IDs to track")
] = None,
emissions_endpoint: Annotated[
str | None, typer.Option(help="HTTP endpoint for emissions data")
] = None,
experiment_id: Annotated[str | None, typer.Option(help="Experiment ID")] = None,
experiment_name: Annotated[str | None, typer.Option(help="Experiment name")] = None,
electricitymaps_api_token: Annotated[
str | None, typer.Option(help="Electricity Maps API token")
] = None,
co2_signal_api_token: Annotated[
str | None,
typer.Option(help="Deprecated: use --electricitymaps-api-token"),
] = None,
tracking_mode: Annotated[
str | None, typer.Option(help="Tracking mode: process or machine")
] = None,
on_csv_write: Annotated[
str | None, typer.Option(help="CSV write mode: append or update")
] = None,
logger_preamble: Annotated[
str | None, typer.Option(help="Text prefixed to tracker log messages")
] = None,
force_cpu_power: Annotated[
int | None, typer.Option(help="Override CPU power in watts")
] = None,
force_ram_power: Annotated[
int | None, typer.Option(help="Override RAM power in watts")
] = None,
pue: Annotated[float | None, typer.Option(help="Power Usage Effectiveness")] = None,
wue: Annotated[
float | None, typer.Option(help="Water Usage Effectiveness in L/kWh")
] = None,
force_carbon_intensity_g_co2e_kwh: Annotated[
float | None,
typer.Option(help="Override carbon intensity in gCO2e/kWh"),
] = None,
force_mode_cpu_load: Annotated[
bool | None, typer.Option(help="Force CPU load estimation mode")
] = None,
allow_multiple_runs: Annotated[
bool | None, typer.Option(help="Allow concurrent CodeCarbon instances")
] = None,
rapl_include_dram: Annotated[
bool | None, typer.Option(help="Include DRAM power in RAPL measurements")
] = None,
rapl_prefer_psys: Annotated[
bool | None, typer.Option(help="Prefer the RAPL platform power domain")
] = None,
):
"""Monitor your machine's carbon emissions."""

Expand All @@ -439,6 +520,43 @@ def monitor(
tracker_args = {
name: value for name, value in cli_defaults if _cli_provided(ctx, name)
}
optional_tracker_args = {
"api_endpoint": api_endpoint,
"api_key": api_key,
"output_dir": output_dir,
"output_file": output_file,
"output_methods": output_methods,
"save_to_file": save_to_file,
"save_to_logger": save_to_logger,
"save_to_prometheus": save_to_prometheus,
"save_to_logfire": save_to_logfire,
"prometheus_url": prometheus_url,
"gpu_ids": gpu_ids,
"emissions_endpoint": emissions_endpoint,
"experiment_id": experiment_id,
"experiment_name": experiment_name,
"electricitymaps_api_token": electricitymaps_api_token,
"co2_signal_api_token": co2_signal_api_token,
"tracking_mode": tracking_mode,
"on_csv_write": on_csv_write,
"logger_preamble": logger_preamble,
"force_cpu_power": force_cpu_power,
"force_ram_power": force_ram_power,
"pue": pue,
"wue": wue,
"force_carbon_intensity_g_co2e_kwh": force_carbon_intensity_g_co2e_kwh,
"force_mode_cpu_load": force_mode_cpu_load,
"allow_multiple_runs": allow_multiple_runs,
"rapl_include_dram": rapl_include_dram,
"rapl_prefer_psys": rapl_prefer_psys,
}
tracker_args.update(
{
name: value
for name, value in optional_tracker_args.items()
if value is not None
}
)
for name, value in cli_defaults:
if name not in tracker_args and name not in external_conf:
# Nothing configures it: keep the defaults advertised by `--help`
Expand All @@ -460,8 +578,8 @@ def monitor(
**({"region": region} if region else {}),
}
else:
experiment_id = get_existing_exp_id()
if api and experiment_id is None:
configured_experiment_id = experiment_id or get_existing_exp_id()
if api and configured_experiment_id is None:
print(
"ERROR: No experiment id. Set CODECARBON_EXPERIMENT_ID, call 'codecarbon config' first, or run in offline mode with `--offline --country-iso-code FRA`.",
file=sys.stderr,
Expand Down
175 changes: 175 additions & 0 deletions tests/cli/test_cli_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
import requests
import typer
from typer import rich_utils
from typer.testing import CliRunner

from codecarbon.cli import main as cli_main
Expand Down Expand Up @@ -542,6 +543,180 @@ def test_monitor_offline_accepts_country_iso_code_from_config(monkeypatch, tmp_p
assert "country_iso_code" not in calls["kwargs"]


def test_monitor_forwards_explicit_tracker_options(monkeypatch):
captured = {}

def fake_run_and_monitor(ctx, offline=False, **kwargs):
captured["args"] = list(ctx.args)
captured["offline"] = offline
captured["kwargs"] = kwargs

monkeypatch.setattr("codecarbon.cli.monitor.run_and_monitor", fake_run_and_monitor)
# Keep the user's own config from deciding which defaults get forwarded.
monkeypatch.setattr(cli_main, "_external_config", lambda: {})

runner = CliRunner()
result = runner.invoke(
cli_main.codecarbon,
[
"monitor",
"--offline",
"--country-iso-code",
"FRA",
"--project-name",
"cli-project",
"--api-endpoint",
"https://api.example.com",
"--api-key",
"secret",
"--output-dir",
"/tmp/emissions",
"--output-file",
"custom.csv",
"--output-methods",
"csv,logger",
"--no-save-to-file",
"--save-to-logger",
"--save-to-prometheus",
"--no-save-to-logfire",
"--prometheus-url",
"http://localhost:9091",
"--gpu-ids",
"0,2",
"--emissions-endpoint",
"https://emissions.example.com",
"--experiment-id",
"exp-123",
"--experiment-name",
"cli-experiment",
"--electricitymaps-api-token",
"electricity-token",
"--co2-signal-api-token",
"legacy-token",
"--tracking-mode",
"process",
"--on-csv-write",
"update",
"--logger-preamble",
"monitor:",
"--force-cpu-power",
"75",
"--force-ram-power",
"20",
"--pue",
"1.25",
"--wue",
"0.4",
"--force-carbon-intensity-g-co2e-kwh",
"42.5",
"--force-mode-cpu-load",
"--no-allow-multiple-runs",
"--rapl-include-dram",
"--no-rapl-prefer-psys",
"--",
"python",
"train.py",
],
)

assert result.exit_code == 0
assert captured["args"] == ["python", "train.py"]
assert captured["offline"] is True
assert captured["kwargs"] == {
"measure_power_secs": 10,
"api_call_interval": 30,
"log_level": "error",
"project_name": "cli-project",
"api_endpoint": "https://api.example.com",
"api_key": "secret",
"output_dir": "/tmp/emissions",
"output_file": "custom.csv",
"output_methods": "csv,logger",
"save_to_file": False,
"save_to_logger": True,
"save_to_prometheus": True,
"save_to_logfire": False,
"prometheus_url": "http://localhost:9091",
"gpu_ids": "0,2",
"emissions_endpoint": "https://emissions.example.com",
"experiment_id": "exp-123",
"experiment_name": "cli-experiment",
"electricitymaps_api_token": "electricity-token",
"co2_signal_api_token": "legacy-token",
"tracking_mode": "process",
"on_csv_write": "update",
"logger_preamble": "monitor:",
"force_cpu_power": 75,
"force_ram_power": 20,
"pue": 1.25,
"wue": 0.4,
"force_carbon_intensity_g_co2e_kwh": 42.5,
"force_mode_cpu_load": True,
"allow_multiple_runs": False,
"rapl_include_dram": True,
"rapl_prefer_psys": False,
"country_iso_code": "FRA",
}


def test_monitor_omits_unspecified_optional_tracker_options(monkeypatch):
captured = {}

def fake_run_and_monitor(ctx, offline=False, **kwargs):
captured["kwargs"] = kwargs

monkeypatch.setattr("codecarbon.cli.monitor.run_and_monitor", fake_run_and_monitor)

ctx = SimpleNamespace(args=["python", "train.py"])
cli_main.monitor(ctx=ctx, offline=True, country_iso_code="FRA")

assert "pue" not in captured["kwargs"]
assert "allow_multiple_runs" not in captured["kwargs"]
assert "output_methods" not in captured["kwargs"]


def test_monitor_explicit_experiment_id_satisfies_online_validation(monkeypatch):
captured = {}

def fake_run_and_monitor(ctx, offline=False, **kwargs):
captured["kwargs"] = kwargs

monkeypatch.setattr("codecarbon.cli.monitor.run_and_monitor", fake_run_and_monitor)
monkeypatch.setattr(cli_main, "get_existing_exp_id", lambda: None)

runner = CliRunner()
result = runner.invoke(
cli_main.codecarbon,
[
"monitor",
"--experiment-id",
"exp-from-cli",
"--",
"python",
"train.py",
],
)

assert result.exit_code == 0
assert captured["kwargs"]["experiment_id"] == "exp-from-cli"


def test_monitor_help_lists_explicit_tracker_options(monkeypatch):
# Typer's rich help reads TERMINAL_WIDTH at import time, so CliRunner's
# terminal_width has no effect and a narrow CI console wraps option names.
monkeypatch.setattr(rich_utils, "MAX_WIDTH", 200)
monkeypatch.setattr(rich_utils, "FORCE_TERMINAL", False)
runner = CliRunner()
result = runner.invoke(cli_main.codecarbon, ["monitor", "--help"])

assert result.exit_code == 0
assert "--output-methods" in result.output
assert "--pue" in result.output
assert "--wue" in result.output
assert "--force-cpu-power" in result.output
assert "--rapl-include-dram" in result.output


def test_monitor_delegates_offline_flag_to_run_and_monitor(monkeypatch):
captured = {}

Expand Down
Loading