Skip to main content

rws_proxy/traffic/
config.rs

1//! `[traffic]` settings (`RWS_CONFIG_TRAFFIC_*`).
2
3use std::path::PathBuf;
4
5pub const RWS_CONFIG_TRAFFIC_ENABLED: &str = "RWS_CONFIG_TRAFFIC_ENABLED";
6pub const RWS_CONFIG_TRAFFIC_RETENTION_DAYS: &str = "RWS_CONFIG_TRAFFIC_RETENTION_DAYS";
7pub const RWS_CONFIG_TRAFFIC_PERSIST: &str = "RWS_CONFIG_TRAFFIC_PERSIST";
8pub const RWS_CONFIG_TRAFFIC_EXCLUDE: &str = "RWS_CONFIG_TRAFFIC_EXCLUDE";
9pub const RWS_CONFIG_TRAFFIC_GEO_DB: &str = "RWS_CONFIG_TRAFFIC_GEO_DB";
10pub const RWS_CONFIG_TRAFFIC_SCRIPT: &str = "RWS_CONFIG_TRAFFIC_SCRIPT";
11pub const RWS_CONFIG_TRAFFIC_GOALS: &str = "RWS_CONFIG_TRAFFIC_GOALS";
12pub const RWS_CONFIG_TRAFFIC_DATABASE_URL_ENV: &str = "RWS_CONFIG_TRAFFIC_DATABASE_URL_ENV";
13
14fn read(key: &str) -> String {
15    std::env::var(key).unwrap_or_default().trim().to_string()
16}
17
18/// `enabled`: `Some(false)` turns counting off even with the admin API on;
19/// `None` when unset.
20pub fn enabled_setting() -> Option<bool> {
21    match read(RWS_CONFIG_TRAFFIC_ENABLED).to_ascii_lowercase().as_str() {
22        "" => None,
23        "false" | "0" | "no" | "off" => Some(false),
24        _ => Some(true),
25    }
26}
27
28/// `retention_days`: 1–90, default 30.
29pub fn retention_days() -> u64 {
30    read(RWS_CONFIG_TRAFFIC_RETENTION_DAYS).parse::<u64>().ok().filter(|d| *d >= 1).map_or(30, |d| d.min(90))
31}
32
33/// `exclude`: exact paths not counted (comma-separated); default the
34/// health, readiness and metrics endpoints.
35pub fn exclude() -> Vec<String> {
36    let value = std::env::var(RWS_CONFIG_TRAFFIC_EXCLUDE);
37    match value {
38        Ok(v) => v.split(',').map(str::trim).filter(|p| !p.is_empty()).map(str::to_string).collect(),
39        Err(_) => ["/healthz", "/readyz", "/metrics"].iter().map(|p| p.to_string()).collect(),
40    }
41}
42
43/// `persist`: the file the counts are saved to. Unset: `traffic.json` next
44/// to the certificate (the ACME certificate's directory, else the TLS
45/// certificate's) — the state directory; without either, nothing is
46/// saved. `"off"`: not saved.
47pub fn persist_path() -> Option<PathBuf> {
48    let value = read(RWS_CONFIG_TRAFFIC_PERSIST);
49    if value.eq_ignore_ascii_case("off") {
50        return None;
51    }
52    if !value.is_empty() {
53        return Some(PathBuf::from(value));
54    }
55    [crate::entry_point::Config::RWS_CONFIG_ACME_CERT_PATH, crate::entry_point::Config::RWS_CONFIG_TLS_CERT_FILE]
56        .iter()
57        .map(|key| read(key))
58        .find(|path| !path.is_empty())
59        .and_then(|cert| PathBuf::from(cert).parent().map(|dir| dir.join("traffic.json")))
60}
61
62/// `geo_db`: a country table (`ci/gen_geo.py`'s format) to use instead of
63/// the built-in one; unset: the built-in one.
64pub fn geo_db() -> Option<PathBuf> {
65    let value = read(RWS_CONFIG_TRAFFIC_GEO_DB);
66    (!value.is_empty()).then(|| PathBuf::from(value))
67}
68
69/// `script`: serve the first-party script at `/_rws/t.js` and take its
70/// reports at `/_rws/t`. Default off.
71pub fn script() -> bool {
72    matches!(read(RWS_CONFIG_TRAFFIC_SCRIPT).to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on")
73}
74
75/// The most goals counted.
76pub const MAX_GOALS: usize = 32;
77
78/// `goals`: what counts as a conversion — a page path (`/thanks/`, or a
79/// prefix ending in `*`: `/docs/*`) or a script event (`event:signup`),
80/// comma-separated; at most [`MAX_GOALS`].
81pub fn goals() -> Vec<String> {
82    read(RWS_CONFIG_TRAFFIC_GOALS).split(',').map(str::trim).filter(|g| !g.is_empty()).take(MAX_GOALS).map(str::to_string).collect()
83}
84
85/// `database_url_env`: the environment variable holding a `postgres://` URL
86/// to write page views and script reports to (the URL itself stays out of
87/// the config file and the admin API). `None` when unset or the variable is
88/// empty.
89pub fn database_url() -> Option<String> {
90    let name = read(RWS_CONFIG_TRAFFIC_DATABASE_URL_ENV);
91    if name.is_empty() {
92        return None;
93    }
94    std::env::var(&name).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty())
95}