1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7let
8 cfg = config.services.amazon-cloudwatch-agent;
9
10 tomlFormat = pkgs.formats.toml { };
11 jsonFormat = pkgs.formats.json { };
12
13 # See https://docs.aws.amazon.com/prescriptive-guidance/latest/implementing-logging-monitoring-cloudwatch/create-store-cloudwatch-configurations.html#store-cloudwatch-configuration-s3.
14 #
15 # We don't use the multiple JSON configuration files feature,
16 # but "config-translator" will log a benign error if the "-input-dir" option is omitted or is a non-existent directory.
17 #
18 # Create an empty directory to hide this benign error log. This prevents false-positives if users filter for "error" in the agent logs.
19 configurationDirectory = pkgs.runCommand "amazon-cloudwatch-agent.d" { } "mkdir $out";
20in
21{
22 options.services.amazon-cloudwatch-agent = {
23 enable = lib.mkEnableOption "Amazon CloudWatch Agent";
24 package = lib.mkPackageOption pkgs "amazon-cloudwatch-agent" { };
25 commonConfigurationFile = lib.mkOption {
26 type = lib.types.path;
27 default = tomlFormat.generate "common-config.toml" cfg.commonConfiguration;
28 defaultText = lib.literalExpression ''tomlFormat.generate "common-config.toml" cfg.commonConfiguration'';
29 description = ''
30 Amazon CloudWatch Agent common configuration. See
31 <https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Agent-commandline-fleet.html#CloudWatch-Agent-profile-instance-first>
32 for supported values.
33
34 {option}`commonConfigurationFile` takes precedence over {option}`commonConfiguration`.
35
36 Note: Restricted evaluation blocks access to paths outside the Nix store.
37 This means detecting content changes for mutable paths (i.e. not input or content-addressed) can't be done.
38 As a result, `nixos-rebuild` won't reload/restart the systemd unit when mutable path contents change.
39 `systemctl restart amazon-cloudwatch-agent.service` must be used instead.
40 '';
41 example = "/etc/amazon-cloudwatch-agent/amazon-cloudwatch-agent.json";
42 };
43 commonConfiguration = lib.mkOption {
44 type = tomlFormat.type;
45 default = { };
46 description = ''
47 See {option}`commonConfigurationFile`.
48
49 {option}`commonConfigurationFile` takes precedence over {option}`commonConfiguration`.
50 '';
51 example = {
52 credentials = {
53 shared_credential_profile = "profile_name";
54 shared_credential_file = "/path/to/credentials";
55 };
56 proxy = {
57 http_proxy = "http_url";
58 https_proxy = "https_url";
59 no_proxy = "domain";
60 };
61 };
62 };
63 configurationFile = lib.mkOption {
64 type = lib.types.path;
65 default = jsonFormat.generate "amazon-cloudwatch-agent.json" cfg.configuration;
66 defaultText = lib.literalExpression ''jsonFormat.generate "amazon-cloudwatch-agent.json" cfg.configuration'';
67 description = ''
68 Amazon CloudWatch Agent configuration file. See
69 <https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html>
70 for supported values.
71
72 The following options aren't supported:
73 * `agent.run_as_user`
74 * Use {option}`user` instead.
75
76 {option}`configurationFile` takes precedence over {option}`configuration`.
77
78 Note: Restricted evaluation blocks access to paths outside the Nix store.
79 This means detecting content changes for mutable paths (i.e. not input or content-addressed) can't be done.
80 As a result, `nixos-rebuild` won't reload/restart the systemd unit when mutable path contents change.
81 `systemctl restart amazon-cloudwatch-agent.service` must be used instead.
82 '';
83 example = "/etc/amazon-cloudwatch-agent/amazon-cloudwatch-agent.json";
84 };
85 configuration = lib.mkOption {
86 type = jsonFormat.type;
87 default = { };
88 description = ''
89 See {option}`configurationFile`.
90
91 {option}`configurationFile` takes precedence over {option}`configuration`.
92 '';
93 # Subset of "CloudWatch agent configuration file: Complete examples" and "CloudWatch agent configuration file: Traces section" in the description link.
94 #
95 # Log file path changed from "/opt/aws/amazon-cloudwatch-agent/logs" to "/var/log/amazon-cloudwatch-agent" to follow the FHS.
96 example = {
97 agent = {
98 metrics_collection_interval = 10;
99 logfile = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log";
100 };
101 metrics = {
102 namespace = "MyCustomNamespace";
103 metrics_collected = {
104 cpu = {
105 resource = [ "*" ];
106 measurement = [
107 {
108 name = "cpu_usage_idle";
109 rename = "CPU_USAGE_IDLE";
110 unit = "Percent";
111 }
112 {
113 name = "cpu_usage_nice";
114 unit = "Percent";
115 }
116 "cpu_usage_guest"
117 ];
118 totalcpu = false;
119 metrics_collection_interval = 10;
120 append_dimensions = {
121 customized_dimension_key_1 = "customized_dimension_value_1";
122 customized_dimension_key_2 = "customized_dimension_value_2";
123 };
124 };
125 };
126 };
127 logs = {
128 logs_collected = {
129 files = {
130 collect_list = [
131 {
132 file_path = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log";
133 log_group_name = "amazon-cloudwatch-agent.log";
134 log_stream_name = "{instance_id}";
135 timezone = "UTC";
136 }
137 ];
138 };
139 };
140 log_stream_name = "log_stream_name";
141 force_flush_interval = 15;
142 };
143 traces = {
144 traces_collected = {
145 xray = { };
146 oltp = { };
147 };
148 };
149 };
150 };
151 # Replaces "agent.run_as_user" from the configuration file.
152 user = lib.mkOption {
153 type = lib.types.str;
154 default = "root";
155 description = ''
156 The user that runs the Amazon CloudWatch Agent.
157 '';
158 example = "amazon-cloudwatch-agent";
159 };
160 mode = lib.mkOption {
161 type = lib.types.str;
162 default = "auto";
163 description = ''
164 Amazon CloudWatch Agent mode. Indicates whether the agent is running in EC2 ("ec2"), on-premises ("onPremise"),
165 or if it should guess based on metadata endpoints like IMDS or the ECS task metadata endpoint ("auto").
166 '';
167 example = "onPremise";
168 };
169 };
170
171 config = lib.mkIf cfg.enable {
172 # See https://github.com/aws/amazon-cloudwatch-agent/blob/v1.300049.1/packaging/dependencies/amazon-cloudwatch-agent.service.
173 systemd.services.amazon-cloudwatch-agent = {
174 description = "Amazon CloudWatch Agent";
175 after = [ "network.target" ];
176 wantedBy = [ "multi-user.target" ];
177 serviceConfig = {
178 Type = "simple";
179 # "start-amazon-cloudwatch-agent" assumes the package is installed at "/opt/aws/amazon-cloudwatch-agent" so we can't use it.
180 #
181 # See https://github.com/aws/amazon-cloudwatch-agent/issues/1319.
182 #
183 # This program:
184 # 1. Switches to a non-root user if configured.
185 # 2. Runs "config-translator" to translate the input JSON configuration files into separate TOML (for CloudWatch Logs + Metrics),
186 # YAML (for X-Ray + OpenTelemetry), and JSON (for environment variables) configuration files.
187 # 3. Runs "amazon-cloudwatch-agent" with the paths to these generated files.
188 #
189 # Re-implementing with systemd options.
190 User = cfg.user;
191 RuntimeDirectory = "amazon-cloudwatch-agent";
192 LogsDirectory = "amazon-cloudwatch-agent";
193 ExecStartPre = builtins.concatStringsSep " " [
194 "${cfg.package}/bin/config-translator"
195 "-config ${cfg.commonConfigurationFile}"
196 "-input ${cfg.configurationFile}"
197 "-input-dir ${configurationDirectory}"
198 "-mode ${cfg.mode}"
199 "-output \${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.toml"
200 ];
201 ExecStart = builtins.concatStringsSep " " [
202 "${cfg.package}/bin/amazon-cloudwatch-agent"
203 "-config \${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.toml"
204 "-envconfig \${RUNTIME_DIRECTORY}/env-config.json"
205 "-otelconfig \${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.yaml"
206 "-pidfile \${RUNTIME_DIRECTORY}/amazon-cloudwatch-agent.pid"
207 ];
208 KillMode = "process";
209 Restart = "on-failure";
210 RestartSec = 60;
211 };
212 };
213 };
214}