Make the metrics page configurable

Make the metrics page configurable with an `ip_filter`.

This closes #10.
This commit is contained in:
finga 2021-11-09 14:50:02 +01:00
parent f0f1d3239d
commit f24a786ec6
3 changed files with 222 additions and 43 deletions

View file

@ -59,6 +59,20 @@ Whereas `<config_dir>` depends on the platform:
### Configuration parameters
#### Metrics
A metrics page can optionally enabled to query stats of the currently
running webhookey instance. Note that stats are lost between restarts
of webhookey as those are not stored persistently.
Example:
```yaml
metrics:
enabled: true
ip_filter:
allow:
- 127.0.0.1/31
```
#### Hooks
With `hooks` you can configure a sequence of hooks. A single hook
consists of the following fields:

View file

@ -1,4 +1,9 @@
---
metrics:
enabled: true
ip_filter:
deny:
- 127.0.0.1
hooks:
hook1:
command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf

View file

@ -119,9 +119,17 @@ impl IpFilter {
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct MetricsConfig {
enabled: bool,
ip_filter: Option<IpFilter>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Config {
metrics: Option<MetricsConfig>,
hooks: BTreeMap<String, Hook>,
}
@ -432,6 +440,48 @@ fn get_config() -> Result<File> {
bail!("No configuration file found.");
}
fn get_metrics(metrics: &WebhookeyMetrics) -> String {
format!(
r"# HELP webhookey_requests_received Number of requests received
# TYPE webhookey_requests_received gauge
webhookey_requests_received {}
# HELP webhookey_requests_invalid Number of invalid requests received
# TYPE webhookey_requests_invalid gauge
webhookey_requests_invalid {}
# HELP webhookey_hooks_successful Number of successfully executed hooks
# TYPE webhookey_hooks_successful gauge
webhookey_hooks_sucessful {}
# HELP webhookey_hooks_forbidden Number of forbidden requests
# TYPE webhookey_hooks_forbidden gauge
webhookey_hooks_forbidden {}
# HELP webhookey_hooks_unmatched Number of unmatched requests
# TYPE webhookey_hooks_unmatched gauge
webhookey_hooks_unmatched {}
# HELP webhookey_commands_executed Number of commands executed
# TYPE webhookey_commands_executed gauge
webhookey_commands_executed {}
# HELP webhookey_commands_execution_failed Number of commands failed to execute
# TYPE webhookey_commands_execution_failed gauge
webhookey_commands_execution_failed {}
# HELP webhookey_commands_successful Number of executed commands returning return code 0
# TYPE webhookey_commands_successful gauge
webhookey_commands_successful {}
# HELP webhookey_commands_failed Number of executed commands returning different return code than 0
# TYPE webhookey_commands_failed gauge
webhookey_commands_failed {}
",
metrics.requests_received.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.requests_invalid.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.hooks_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.hooks_forbidden.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.hooks_unmatched.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_executed.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_execution_failed.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_failed.lock().unwrap() // TODO: Check if unwrap need to be fixed
)
}
#[rocket::async_trait]
impl<'r> FromData<'r> for Hooks {
type Error = WebhookeyError;
@ -554,46 +604,24 @@ async fn receive_hook<'a>(
}
#[get("/metrics")]
async fn metrics(metrics: &State<WebhookeyMetrics>) -> String {
format!(
r"# HELP webhookey_requests_received Number of requests received
# TYPE webhookey_requests_received gauge
webhookey_requests_received {:?}
# HELP webhookey_requests_invalid Number of invalid requests received
# TYPE webhookey_requests_invalid gauge
webhookey_requests_invalid {:?}
# HELP webhookey_hooks_successful Number of successfully executed hooks
# TYPE webhookey_hooks_successful gauge
webhookey_hooks_sucessful {:?}
# HELP webhookey_hooks_forbidden Number of forbidden requests
# TYPE webhookey_hooks_forbidden gauge
webhookey_hooks_forbidden {:?}
# HELP webhookey_hooks_unmatched Number of unmatched requests
# TYPE webhookey_hooks_unmatched gauge
webhookey_hooks_unmatched {:?}
# HELP webhookey_commands_executed Number of commands executed
# TYPE webhookey_commands_executed gauge
webhookey_commands_executed {:?}
# HELP webhookey_commands_execution_failed Number of commands failed to execute
# TYPE webhookey_commands_execution_failed gauge
webhookey_commands_execution_failed {:?}
# HELP webhookey_commands_successful Number of executed commands returning return code 0
# TYPE webhookey_commands_successful gauge
webhookey_commands_successful {:?}
# HELP webhookey_commands_failed Number of executed commands returning different return code than 0
# TYPE webhookey_commands_failed gauge
webhookey_commands_failed {:?}
",
metrics.requests_received.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.requests_invalid.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.hooks_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.hooks_forbidden.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.hooks_unmatched.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_executed.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_execution_failed.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed
metrics.commands_failed.lock().unwrap() // TODO: Check if unwrap need to be fixed
)
async fn metrics(
address: SocketAddr,
metrics: &State<WebhookeyMetrics>,
config: &State<Config>,
) -> Option<String> {
if let Some(metrics_config) = &config.metrics {
if let Some(filter) = &metrics_config.ip_filter {
if filter.validate(&address.ip()) {
return Some(get_metrics(&metrics));
}
} else {
return Some(get_metrics(&metrics));
}
}
warn!("Forbidden request for metrics: {:?}", address);
None
}
#[rocket::main]
@ -652,7 +680,10 @@ mod tests {
},
);
let config = Config { hooks: hooks };
let config = Config {
metrics: None,
hooks: hooks,
};
let rocket = rocket::build()
.mount("/", routes![receive_hook])
@ -838,7 +869,7 @@ mod tests {
);
let config = Config {
// default: None,
metrics: None,
hooks: hooks,
};
@ -896,7 +927,7 @@ mod tests {
);
let config = Config {
// default: None,
metrics: None,
hooks: hooks,
};
@ -933,4 +964,133 @@ mod tests {
assert_eq!(response.await.status(), Status::NotFound);
}
#[test]
fn parse_config() {
let config: Config = serde_yaml::from_str(
r#"---
hooks:
hook1:
command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
signature: X-Gitea-Signature
ip_filter:
allow:
- 127.0.0.1/31
secrets:
- secret_key_01
- secret_key_02
filter:
json:
pointer: /ref
regex: refs/heads/master
hook2:
command: /usr/bin/local/script_xy.sh asdfasdf
signature: X-Gitea-Signature
secrets:
- secret_key_01
- secret_key_02
filter:
and:
- json:
pointer: /ref
regex: refs/heads/master
- json:
pointer: /after
regex: f6e5fe4fe37df76629112d55cc210718b6a55e7e"#,
)
.unwrap();
assert_eq!(
serde_yaml::to_string(&config).unwrap(),
serde_yaml::to_string(&Config {
metrics: None,
hooks: BTreeMap::from([
(
"hook1".to_string(),
Hook {
command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf"
.to_string(),
signature: "X-Gitea-Signature".to_string(),
ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet(
"127.0.0.1/31".parse().unwrap()
)])),
secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
filter: FilterType::JsonFilter(JsonFilter {
pointer: "/ref".to_string(),
regex: "refs/heads/master".to_string(),
}),
}
),
(
"hook2".to_string(),
Hook {
command: "/usr/bin/local/script_xy.sh asdfasdf".to_string(),
signature: "X-Gitea-Signature".to_string(),
ip_filter: None,
secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
filter: FilterType::And(vec![
FilterType::JsonFilter(JsonFilter {
pointer: "/ref".to_string(),
regex: "refs/heads/master".to_string(),
}),
FilterType::JsonFilter(JsonFilter {
pointer: "/after".to_string(),
regex: "f6e5fe4fe37df76629112d55cc210718b6a55e7e".to_string(),
}),
]),
}
)
])
})
.unwrap()
);
let config: Config = serde_yaml::from_str(
r#"---
metrics:
enabled: true
hooks:
hook1:
command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
signature: X-Gitea-Signature
ip_filter:
allow:
- 127.0.0.1/31
secrets:
- secret_key_01
- secret_key_02
filter:
json:
pointer: /ref
regex: refs/heads/master"#,
)
.unwrap();
assert_eq!(
serde_yaml::to_string(&config).unwrap(),
serde_yaml::to_string(&Config {
metrics: Some(MetricsConfig {
enabled: true,
ip_filter: None
}),
hooks: BTreeMap::from([(
"hook1".to_string(),
Hook {
command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf"
.to_string(),
signature: "X-Gitea-Signature".to_string(),
ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet(
"127.0.0.1/31".parse().unwrap()
)])),
secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
filter: FilterType::JsonFilter(JsonFilter {
pointer: "/ref".to_string(),
regex: "refs/heads/master".to_string(),
}),
}
),])
})
.unwrap()
);
}
}