diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index 93f627e5b..b8daea9fa 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -390,6 +390,90 @@ def monitor( str, typer.Option(help="Log level (critical, error, warning, info, debug)"), ] = "error", + project_name: Annotated[ + str | None, typer.Option(help="Project name for the monitored run") + ] = None, + 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.""" @@ -399,6 +483,44 @@ def monitor( "api_call_interval": api_call_interval, "log_level": log_level, } + optional_tracker_args = { + "project_name": project_name, + "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 + } + ) # 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: @@ -414,8 +536,8 @@ def monitor( "region": region, } 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, diff --git a/tests/cli/test_cli_main.py b/tests/cli/test_cli_main.py index 8bb4d66f4..9b9f78d33 100644 --- a/tests/cli/test_cli_main.py +++ b/tests/cli/test_cli_main.py @@ -386,6 +386,177 @@ def stop(self): assert calls["kwargs"]["region"] == "IDF" +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) + + 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", + "region": None, + } + + +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(): + runner = CliRunner() + result = runner.invoke( + cli_main.codecarbon, ["monitor", "--help"], terminal_width=200 + ) + + 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 = {}