diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 4d204f92a792..39cc55f7d4c3 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -14,6 +14,8 @@ - [Kimai](https://www.kimai.org/), a web-based multi-user time-tracking application. Available as [services.kimai](option.html#opt-services.kimai). +- [Amazon CloudWatch Agent](https://github.com/aws/amazon-cloudwatch-agent), the official telemetry collector for AWS CloudWatch and AWS X-Ray. Available as [services.amazon-cloudwatch-agent](#opt-services.amazon-cloudwatch-agent.enable). + ## Backward Incompatibilities {#sec-release-25.05-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5b85b0c5fad7..7b61c4f50b76 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -877,6 +877,7 @@ ./services/misc/zookeeper.nix ./services/monitoring/alerta.nix ./services/monitoring/alloy.nix + ./services/monitoring/amazon-cloudwatch-agent.nix ./services/monitoring/apcupsd.nix ./services/monitoring/arbtt.nix ./services/monitoring/below.nix diff --git a/nixos/modules/services/monitoring/amazon-cloudwatch-agent.nix b/nixos/modules/services/monitoring/amazon-cloudwatch-agent.nix new file mode 100644 index 000000000000..fef2cfdd6fb5 --- /dev/null +++ b/nixos/modules/services/monitoring/amazon-cloudwatch-agent.nix @@ -0,0 +1,179 @@ +{ + lib, + pkgs, + config, + ... +}: +let + cfg = config.services.amazon-cloudwatch-agent; + + tomlFormat = pkgs.formats.toml { }; + jsonFormat = pkgs.formats.json { }; + + commonConfigurationFile = tomlFormat.generate "common-config.toml" cfg.commonConfiguration; + configurationFile = jsonFormat.generate "amazon-cloudwatch-agent.json" cfg.configuration; + # See https://docs.aws.amazon.com/prescriptive-guidance/latest/implementing-logging-monitoring-cloudwatch/create-store-cloudwatch-configurations.html#store-cloudwatch-configuration-s3. + # + # We don't use the multiple JSON configuration files feature, + # but "config-translator" will log a benign error if the "-input-dir" option is omitted or is a non-existent directory. + # + # Create an empty directory to hide this benign error log. This prevents false-positives if users filter for "error" in the agent logs. + configurationDirectory = pkgs.runCommand "amazon-cloudwatch-agent.d" { } "mkdir $out"; +in +{ + options.services.amazon-cloudwatch-agent = { + enable = lib.mkEnableOption "Amazon CloudWatch Agent"; + package = lib.mkPackageOption pkgs "amazon-cloudwatch-agent" { }; + commonConfiguration = lib.mkOption { + type = tomlFormat.type; + default = { }; + description = '' + Amazon CloudWatch Agent common configuration. See + + for supported values. + ''; + example = { + credentials = { + shared_credential_profile = "profile_name"; + shared_credential_file = "/path/to/credentials"; + }; + proxy = { + http_proxy = "http_url"; + https_proxy = "https_url"; + no_proxy = "domain"; + }; + }; + }; + configuration = lib.mkOption { + type = jsonFormat.type; + default = { }; + description = '' + Amazon CloudWatch Agent configuration. See + + for supported values. + ''; + # Subset of "CloudWatch agent configuration file: Complete examples" and "CloudWatch agent configuration file: Traces section" in the description link. + # + # Log file path changed from "/opt/aws/amazon-cloudwatch-agent/logs" to "/var/log/amazon-cloudwatch-agent" to follow the FHS. + example = { + agent = { + metrics_collection_interval = 10; + logfile = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log"; + }; + metrics = { + namespace = "MyCustomNamespace"; + metrics_collected = { + cpu = { + resource = [ "*" ]; + measurement = [ + { + name = "cpu_usage_idle"; + rename = "CPU_USAGE_IDLE"; + unit = "Percent"; + } + { + name = "cpu_usage_nice"; + unit = "Percent"; + } + "cpu_usage_guest" + ]; + totalcpu = false; + metrics_collection_interval = 10; + append_dimensions = { + customized_dimension_key_1 = "customized_dimension_value_1"; + customized_dimension_key_2 = "customized_dimension_value_2"; + }; + }; + }; + }; + logs = { + logs_collected = { + files = { + collect_list = [ + { + file_path = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log"; + log_group_name = "amazon-cloudwatch-agent.log"; + log_stream_name = "{instance_id}"; + timezone = "UTC"; + } + ]; + }; + }; + log_stream_name = "log_stream_name"; + force_flush_interval = 15; + }; + traces = { + traces_collected = { + xray = { }; + oltp = { }; + }; + }; + }; + }; + mode = lib.mkOption { + type = lib.types.str; + default = "auto"; + description = '' + Amazon CloudWatch Agent mode. Indicates whether the agent is running in EC2 ("ec2"), on-premises ("onPremise"), + or if it should guess based on metadata endpoints like IMDS or the ECS task metadata endpoint ("auto"). + ''; + example = "onPremise"; + }; + }; + + config = lib.mkIf cfg.enable { + # See https://github.com/aws/amazon-cloudwatch-agent/blob/v1.300048.1/packaging/dependencies/amazon-cloudwatch-agent.service. + systemd.services.amazon-cloudwatch-agent = { + description = "Amazon CloudWatch Agent"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + # "start-amazon-cloudwatch-agent" assumes the package is installed at "/opt/aws/amazon-cloudwatch-agent" so we can't use it. + # + # See https://github.com/aws/amazon-cloudwatch-agent/issues/1319. + # + # This program: + # 1. Switches to a non-root user if configured. + # 2. Runs "config-translator" to translate the input JSON configuration files into separate TOML (for CloudWatch Logs + Metrics), + # YAML (for X-Ray + OpenTelemetry), and JSON (for environment variables) configuration files. + # 3. Runs "amazon-cloudwatch-agent" with the paths to these generated files. + # + # Re-implementing with systemd options. + User = lib.attrByPath [ + "agent" + "run_as_user" + ] "root" cfg.configuration; + RuntimeDirectory = "amazon-cloudwatch-agent"; + LogsDirectory = "amazon-cloudwatch-agent"; + ExecStartPre = '' + ${cfg.package}/bin/config-translator \ + -config ${commonConfigurationFile} \ + -input ${configurationFile} \ + -input-dir ${configurationDirectory} \ + -mode ${cfg.mode} \ + -output ''${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.toml + ''; + ExecStart = '' + ${cfg.package}/bin/amazon-cloudwatch-agent \ + -config ''${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.toml \ + -envconfig ''${RUNTIME_DIRECTORY}/env-config.json \ + -otelconfig ''${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.yaml \ + -pidfile ''${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.pid + ''; + KillMode = "process"; + Restart = "on-failure"; + RestartSec = 60; + }; + restartTriggers = [ + cfg.package + commonConfigurationFile + configurationFile + configurationDirectory + cfg.mode + ]; + }; + }; + + meta.maintainers = pkgs.amazon-cloudwatch-agent.meta.maintainers; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index eb15a5f874e1..d2ff01f9c7bb 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -119,6 +119,7 @@ in { alloy = handleTest ./alloy.nix {}; allTerminfo = handleTest ./all-terminfo.nix {}; alps = handleTest ./alps.nix {}; + amazon-cloudwatch-agent = handleTest ./amazon-cloudwatch-agent.nix {}; amazon-init-shell = handleTest ./amazon-init-shell.nix {}; amazon-ssm-agent = handleTest ./amazon-ssm-agent.nix {}; amd-sev = runTest ./amd-sev.nix; diff --git a/nixos/tests/amazon-cloudwatch-agent.nix b/nixos/tests/amazon-cloudwatch-agent.nix new file mode 100644 index 000000000000..2810ac0a72ea --- /dev/null +++ b/nixos/tests/amazon-cloudwatch-agent.nix @@ -0,0 +1,93 @@ +import ./make-test-python.nix ( + { lib, pkgs, ... }: + let + # See https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html. + iniFormat = pkgs.formats.ini { }; + + region = "ap-northeast-1"; + sharedConfigurationDefaultProfile = "default"; + sharedConfigurationFile = iniFormat.generate "config" { + "${sharedConfigurationDefaultProfile}" = { + region = region; + }; + }; + sharedCredentialsFile = iniFormat.generate "credentials" { + "${sharedConfigurationDefaultProfile}" = { + aws_access_key_id = "placeholder"; + aws_secret_access_key = "placeholder"; + aws_session_token = "placeholder"; + }; + }; + sharedConfigurationDirectory = pkgs.runCommand ".aws" { } '' + mkdir $out + + cp ${sharedConfigurationFile} $out/config + cp ${sharedCredentialsFile} $out/credentials + ''; + in + { + name = "amazon-cloudwatch-agent"; + meta.maintainers = pkgs.amazon-cloudwatch-agent.meta.maintainers; + + nodes.machine = + { config, pkgs, ... }: + { + services.amazon-cloudwatch-agent = { + enable = true; + commonConfiguration = { + credentials = { + shared_credential_profile = sharedConfigurationDefaultProfile; + shared_credential_file = "${sharedConfigurationDirectory}/credentials"; + }; + }; + configuration = { + agent = { + # Required despite documentation saying the agent ignores it in "onPremise" mode. + region = region; + + # Show debug logs and write to a file for interactive debugging. + debug = true; + logfile = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log"; + }; + logs = { + logs_collected = { + files = { + collect_list = [ + { + file_path = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log"; + log_group_name = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log"; + log_stream_name = "{local_hostname}"; + } + ]; + }; + }; + }; + traces = { + local_mode = true; + traces_collected = { + xray = { }; + }; + }; + }; + mode = "onPremise"; + }; + + # Keep the runtime directory for interactive debugging. + systemd.services.amazon-cloudwatch-agent.serviceConfig.RuntimeDirectoryPreserve = true; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("amazon-cloudwatch-agent.service") + + machine.wait_for_file("/run/amazon-cloudwatch-agent/amazon-cloudwatch-agent.pid") + machine.wait_for_file("/run/amazon-cloudwatch-agent/amazon-cloudwatch-agent.toml") + # "config-translator" omits this file if no trace configurations are specified. + # + # See https://github.com/aws/amazon-cloudwatch-agent/issues/1320. + machine.wait_for_file("/run/amazon-cloudwatch-agent/amazon-cloudwatch-agent.yaml") + machine.wait_for_file("/run/amazon-cloudwatch-agent/env-config.json") + ''; + } +) diff --git a/pkgs/by-name/am/amazon-cloudwatch-agent/package.nix b/pkgs/by-name/am/amazon-cloudwatch-agent/package.nix new file mode 100644 index 000000000000..38d8136f127a --- /dev/null +++ b/pkgs/by-name/am/amazon-cloudwatch-agent/package.nix @@ -0,0 +1,63 @@ +{ + lib, + amazon-cloudwatch-agent, + buildGoModule, + fetchFromGitHub, + nix-update-script, + nixosTests, + stdenv, + versionCheckHook, +}: + +buildGoModule rec { + pname = "amazon-cloudwatch-agent"; + version = "1.300049.1"; + + src = fetchFromGitHub { + owner = "aws"; + repo = "amazon-cloudwatch-agent"; + rev = "refs/tags/v${version}"; + hash = "sha256-/VzLSHlBT40h7iErBisfSp7cTAm3L4vmZP03UiDmBaE="; + }; + + vendorHash = "sha256-zsASHuTXL3brRlgLPNb4wFPHkYpUWbOdRDCXQUwZjIY="; + + # See the list in https://github.com/aws/amazon-cloudwatch-agent/blob/v1.300048.1/Makefile#L68-L77. + subPackages = [ + "cmd/config-downloader" + "cmd/config-translator" + "cmd/amazon-cloudwatch-agent" + # Broken since it hardcodes the package install path. See https://github.com/aws/amazon-cloudwatch-agent/issues/1319. + # "cmd/start-amazon-cloudwatch-agent" + "cmd/amazon-cloudwatch-agent-config-wizard" + ]; + + # See https://github.com/aws/amazon-cloudwatch-agent/blob/v1.300048.1/Makefile#L57-L64. + # + # Needed for "amazon-cloudwatch-agent -version" to not show "Unknown". + postInstall = '' + echo v${version} > $out/bin/CWAGENT_VERSION + ''; + + doInstallCheck = true; + + nativeInstallCheckInputs = [ versionCheckHook ]; + + versionCheckProgramArg = "-version"; + + passthru = { + tests = lib.optionalAttrs stdenv.isLinux { + inherit (nixosTests) amazon-cloudwatch-agent; + }; + + updateScript = nix-update-script { }; + }; + + meta = { + description = "CloudWatch Agent enables you to collect and export host-level metrics and logs on instances running Linux or Windows server"; + homepage = "https://github.com/aws/amazon-cloudwatch-agent"; + license = lib.licenses.mit; + mainProgram = "amazon-cloudwatch-agent"; + maintainers = with lib.maintainers; [ pmw ]; + }; +}