Make the metrics page configurable
Make the metrics page configurable with an `ip_filter`. This closes #10.
This commit is contained in:
parent
f0f1d3239d
commit
f24a786ec6
3 changed files with 222 additions and 43 deletions
14
README.md
14
README.md
|
@ -59,6 +59,20 @@ Whereas `<config_dir>` depends on the platform:
|
||||||
|
|
||||||
### Configuration parameters
|
### 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
|
#### Hooks
|
||||||
With `hooks` you can configure a sequence of hooks. A single hook
|
With `hooks` you can configure a sequence of hooks. A single hook
|
||||||
consists of the following fields:
|
consists of the following fields:
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
---
|
---
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
ip_filter:
|
||||||
|
deny:
|
||||||
|
- 127.0.0.1
|
||||||
hooks:
|
hooks:
|
||||||
hook1:
|
hook1:
|
||||||
command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
|
command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
|
||||||
|
|
246
src/main.rs
246
src/main.rs
|
@ -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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct Config {
|
struct Config {
|
||||||
|
metrics: Option<MetricsConfig>,
|
||||||
hooks: BTreeMap<String, Hook>,
|
hooks: BTreeMap<String, Hook>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,6 +440,48 @@ fn get_config() -> Result<File> {
|
||||||
bail!("No configuration file found.");
|
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]
|
#[rocket::async_trait]
|
||||||
impl<'r> FromData<'r> for Hooks {
|
impl<'r> FromData<'r> for Hooks {
|
||||||
type Error = WebhookeyError;
|
type Error = WebhookeyError;
|
||||||
|
@ -554,46 +604,24 @@ async fn receive_hook<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/metrics")]
|
#[get("/metrics")]
|
||||||
async fn metrics(metrics: &State<WebhookeyMetrics>) -> String {
|
async fn metrics(
|
||||||
format!(
|
address: SocketAddr,
|
||||||
r"# HELP webhookey_requests_received Number of requests received
|
metrics: &State<WebhookeyMetrics>,
|
||||||
# TYPE webhookey_requests_received gauge
|
config: &State<Config>,
|
||||||
webhookey_requests_received {:?}
|
) -> Option<String> {
|
||||||
# HELP webhookey_requests_invalid Number of invalid requests received
|
if let Some(metrics_config) = &config.metrics {
|
||||||
# TYPE webhookey_requests_invalid gauge
|
if let Some(filter) = &metrics_config.ip_filter {
|
||||||
webhookey_requests_invalid {:?}
|
if filter.validate(&address.ip()) {
|
||||||
# HELP webhookey_hooks_successful Number of successfully executed hooks
|
return Some(get_metrics(&metrics));
|
||||||
# TYPE webhookey_hooks_successful gauge
|
}
|
||||||
webhookey_hooks_sucessful {:?}
|
} else {
|
||||||
# HELP webhookey_hooks_forbidden Number of forbidden requests
|
return Some(get_metrics(&metrics));
|
||||||
# TYPE webhookey_hooks_forbidden gauge
|
}
|
||||||
webhookey_hooks_forbidden {:?}
|
}
|
||||||
# HELP webhookey_hooks_unmatched Number of unmatched requests
|
|
||||||
# TYPE webhookey_hooks_unmatched gauge
|
warn!("Forbidden request for metrics: {:?}", address);
|
||||||
webhookey_hooks_unmatched {:?}
|
|
||||||
# HELP webhookey_commands_executed Number of commands executed
|
None
|
||||||
# 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::main]
|
#[rocket::main]
|
||||||
|
@ -652,7 +680,10 @@ mod tests {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let config = Config { hooks: hooks };
|
let config = Config {
|
||||||
|
metrics: None,
|
||||||
|
hooks: hooks,
|
||||||
|
};
|
||||||
|
|
||||||
let rocket = rocket::build()
|
let rocket = rocket::build()
|
||||||
.mount("/", routes![receive_hook])
|
.mount("/", routes![receive_hook])
|
||||||
|
@ -838,7 +869,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
// default: None,
|
metrics: None,
|
||||||
hooks: hooks,
|
hooks: hooks,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -896,7 +927,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
// default: None,
|
metrics: None,
|
||||||
hooks: hooks,
|
hooks: hooks,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -933,4 +964,133 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(response.await.status(), Status::NotFound);
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue