modules/nixos/goaccess.nix (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | {
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
cfg = config.services.goaccess;
# Helper function to generate a configuration file from the settings module
settingsFormat = pkgs.formats.keyValue {
mkKeyValue = pkgs.lib.generators.mkKeyValueDefault {} " ";
listsAsDuplicateKeys = true;
};
instanceName = name: "goaccess@${name}";
enabledInstances = lib.filterAttrs (name: conf: conf.enable) config.services.goaccess.instances;
in
{
options.services.goaccess = {
package = lib.mkPackageOption pkgs "goaccess" { };
user = lib.mkOption {
type = types.str;
default = "goaccess";
description = "User to run goaccess service as";
};
group = lib.mkOption {
type = types.str;
default = "goaccess";
description = "Group to run goaccess service as";
};
instances = lib.mkOption {
type = with types; attrsOf (submodule ({ config, name, ... }: {
options = {
enable = lib.mkEnableOption "goaccess web log analyzer service";
# Timer configuration for non-real-time mode
dates = lib.mkOption {
type = with types; nullOr str;
default = null;
example = "daily";
description = ''
Systemd timer specification for periodic report generation.
Only applies when real-time HTML is disabled.
Examples: "daily", "hourly", "*-*-* 06:00:00" (at 6 AM every day)
'';
};
# RFC42-compatible settings module
settings = lib.mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
# Basic configuration
log-file = lib.mkOption {
type = with types; either path (listOf path);
default = "/var/log/nginx/access.log";
description = "Path to the web server access log file to analyze";
};
output = lib.mkOption {
type = types.path;
default = "/var/www/html/goaccess/report.html";
description = "Path for the output HTML report";
};
# Mode selection
real-time-html = lib.mkOption {
type = types.bool;
default = false;
description = "Enable real-time HTML output with WebSocket support";
};
# Log format configuration
log-format = lib.mkOption {
type = types.str;
default = "COMBINED";
description = ''
Log format specification. Can be a predefined format name
(COMBINED, COMMON, VCOMBINED, etc.) or a custom format string.
'';
};
ignore-crawlers = lib.mkOption {
type = types.bool;
default = false;
description = "Ignore crawlers/bots from statistics";
};
anonymize-ip = lib.mkOption {
type = types.bool;
default = false;
description = "Anonymize client IP addresses for privacy";
};
anonymize-level = lib.mkOption {
type = types.enum [
1
2
3
];
default = 1;
description = "IP anonymization level (1=default, 2=strong, 3=pedantic)";
};
# Persistence options
persist = lib.mkOption {
type = types.bool;
default = false;
description = "Persist parsed data to disk for incremental processing";
};
restore = lib.mkOption {
type = types.bool;
default = false;
description = "Load previously stored data from disk";
};
db-path = lib.mkOption {
type = types.path;
default = "/var/lib/goaccess/${name}";
description = "Path for on-disk database files";
};
keep-last = lib.mkOption {
type = with types; nullOr int;
default = null;
description = "Keep only the last N days in storage (enables data recycling)";
};
# Process configuration
jobs = lib.mkOption {
type = types.int;
default = 1;
description = "Number of parallel processing threads (1-6)";
};
chunk-size = lib.mkOption {
type = types.int;
default = 4096;
description = "Number of lines per chunk for parallel processing (256-32768)";
};
# Server configuration for real-time mode
unix-socket = lib.mkOption {
type = with types; nullOr str;
default = null;
example = "/run/goaccess.sock";
};
port = lib.mkOption {
type = types.port;
default = 7890;
description = "Port for the WebSocket server (real-time mode only)";
};
addr = lib.mkOption {
type = with types; nullOr str;
default = "0.0.0.0";
description = "IP address to bind the WebSocket server to";
};
# WebSocket URL for real-time mode
ws-url = lib.mkOption {
type = with types; nullOr str;
default = null;
description = ''
URL for WebSocket connection (useful when running behind proxy).
If not set, defaults to the generated report's hostname.
'';
};
};
};
default = { };
description = ''
RFC42-compatible settings for goaccess configuration file.
These options will be written to a configuration file and passed to GoAccess via --config-file.
See https://goaccess.io/man for all available options.
'';
example = {
log-file = "/var/log/nginx/access.log";
output = "/var/www/html/goaccess/report.html";
log-format = "COMBINED";
html-report-title = "My Server Analytics";
ignore-crawlers = true;
anonymize-ip = true;
};
};
};
}));
default = { };
description = "GoAccess web log analyzer service instances";
example = {
web-logs = {
enable = true;
settings = {
log-file = "/var/log/nginx/access.log";
output = "/var/www/html/goaccess/web-logs.html";
real-time-html = true;
port = 7890;
};
};
api-logs = {
enable = true;
dates = "daily";
settings = {
log-file = "/var/log/nginx/api-access.log";
output = "/var/www/html/goaccess/api-logs.html";
html-report-title = "API Analytics";
};
};
};
};
};
config = lib.mkIf (enabledInstances != { }) (lib.mkMerge [
{
environment.systemPackages = [ cfg.package ];
# Create user and group if needed
users.users.goaccess = lib.mkIf (cfg.user == "goaccess") {
isSystemUser = true;
group = cfg.group;
home = "/var/lib/goaccess";
createHome = true;
};
users.groups.goaccess = lib.mkIf (cfg.group == "goaccess") { };
}
# Generate systemd services for all enabled instances
{
systemd.services = lib.mapAttrs' (
name: instanceCfg:
let
configFile = settingsFormat.generate name instanceCfg.settings;
realTime = instanceCfg.settings.real-time-html or false;
in
lib.nameValuePair (instanceName name) {
description = "GoAccess Web Log Analyzer (${name})";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = if realTime then "simple" else "oneshot";
ExecStart = "${cfg.package}/bin/goaccess --config-file=${configFile}";
ExecStartPre = "+" + pkgs.writeShellScript "goaccess-${name}-prep" ''
install -d -o ${cfg.user} -g ${cfg.group} -m 0750 ${dirOf instanceCfg.settings.output}
'';
User = cfg.user;
Group = cfg.group;
Restart = lib.mkIf realTime "on-failure";
# Ensure directories exist and have correct permissions
StateDirectory = "goaccess/${name}";
RuntimeDirectory = "goaccess";
RuntimeDirectoryMode = "750";
# Security settings
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadOnlyPaths = (lib.flatten instanceCfg.settings.log-file);
ReadWritePaths = [
("-" + dirOf instanceCfg.settings.output)
];
};
}
) enabledInstances;
systemd.timers = lib.mapAttrs' (
name: instanceCfg: (lib.nameValuePair (instanceName name) {
description = "Timer for GoAccess report generation (${name})";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = instanceCfg.dates;
Persistent = true;
};
})
) (lib.filterAttrs (_: icfg: icfg.dates != null) enabledInstances);
systemd.tmpfiles.rules = lib.flatten (lib.mapAttrsToList
(name: icfg: [
"d ${icfg.settings.db-path} 1700 ${cfg.user} ${cfg.group} -"
])
(lib.filterAttrs (_: icfg: icfg.settings.persist || icfg.settings.restore) enabledInstances));
}
# Generate assertions for all enabled instances
{
assertions = lib.mapAttrsToList ( name: instanceCfg: {
assertion = (instanceCfg.settings.real-time-html or false) != (instanceCfg.dates != null);
message = ''
services.goaccess.instances.${name}: Exactly one of real-time-html or dates must be specified.
- Set real-time-html to true for real-time HTML output with WebSocket support.
- Set dates to a timer specification for periodic static report generation.
'';
}) enabledInstances;
}
]);
}
|