Add geo ip logging support
This commit is contained in:
parent
1c00c993bf
commit
c4fa53fa40
5 changed files with 170 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@ perf.data*
|
||||||
dhat.out.*
|
dhat.out.*
|
||||||
settings.yaml
|
settings.yaml
|
||||||
tarpaulin-report.html
|
tarpaulin-report.html
|
||||||
|
GeoLite2-Country.mmdb
|
|
@ -75,3 +75,8 @@ following:
|
||||||
#### Beta testers
|
#### Beta testers
|
||||||
|
|
||||||
- NigelVH#7162
|
- NigelVH#7162
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If using the geo IP logging feature, then this product includes GeoLite2 data
|
||||||
|
created by MaxMind, available from https://www.maxmind.com.
|
|
@ -200,6 +200,12 @@ impl Config {
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct ClientSecret(String);
|
pub struct ClientSecret(String);
|
||||||
|
|
||||||
|
impl ClientSecret {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for ClientSecret {
|
impl std::fmt::Debug for ClientSecret {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "[client secret]")
|
write!(f, "[client secret]")
|
||||||
|
|
28
src/main.rs
28
src/main.rs
|
@ -5,17 +5,21 @@
|
||||||
use std::env::VarError;
|
use std::env::VarError;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::net::SocketAddr;
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use actix_web::dev::Service;
|
||||||
use actix_web::rt::{spawn, time, System};
|
use actix_web::rt::{spawn, time, System};
|
||||||
use actix_web::web::{self, Data};
|
use actix_web::web::{self, Data};
|
||||||
use actix_web::{App, HttpResponse, HttpServer};
|
use actix_web::{App, HttpResponse, HttpServer};
|
||||||
use cache::{Cache, DiskCache};
|
use cache::{Cache, DiskCache};
|
||||||
use chacha20::Key;
|
use chacha20::Key;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
use maxminddb::geoip2;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use rustls::{NoClientAuth, ServerConfig};
|
use rustls::{NoClientAuth, ServerConfig};
|
||||||
use sodiumoxide::crypto::stream::xchacha20::gen_key;
|
use sodiumoxide::crypto::stream::xchacha20::gen_key;
|
||||||
|
@ -27,6 +31,7 @@ use tracing::{debug, error, info, warn};
|
||||||
use crate::cache::mem::{Lfu, Lru};
|
use crate::cache::mem::{Lfu, Lru};
|
||||||
use crate::cache::{MemoryCache, ENCRYPTION_KEY};
|
use crate::cache::{MemoryCache, ENCRYPTION_KEY};
|
||||||
use crate::config::{CacheType, UnstableOptions, OFFLINE_MODE};
|
use crate::config::{CacheType, UnstableOptions, OFFLINE_MODE};
|
||||||
|
use crate::metrics::{record_country_visit, GEOIP_DATABASE};
|
||||||
use crate::state::DynamicServerCert;
|
use crate::state::DynamicServerCert;
|
||||||
|
|
||||||
mod cache;
|
mod cache;
|
||||||
|
@ -107,6 +112,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
metrics::init();
|
metrics::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(key) = config.geoip_license_key.clone() {
|
||||||
|
if let Err(e) = metrics::load_geo_ip_data(key).await {
|
||||||
|
error!("Failed to initialize geo ip db: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP Server init
|
// HTTP Server init
|
||||||
|
|
||||||
let server = if OFFLINE_MODE.load(Ordering::Acquire) {
|
let server = if OFFLINE_MODE.load(Ordering::Acquire) {
|
||||||
|
@ -171,6 +182,23 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let server = HttpServer::new(move || {
|
let server = HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(actix_web::middleware::Compress::default())
|
.wrap(actix_web::middleware::Compress::default())
|
||||||
|
.wrap_fn(|req, srv| {
|
||||||
|
if let Some(reader) = GEOIP_DATABASE.get() {
|
||||||
|
let maybe_country = req
|
||||||
|
.connection_info()
|
||||||
|
.realip_remote_addr()
|
||||||
|
.map(SocketAddr::from_str)
|
||||||
|
.and_then(Result::ok)
|
||||||
|
.as_ref()
|
||||||
|
.map(SocketAddr::ip)
|
||||||
|
.map(|ip| reader.lookup::<geoip2::Country>(ip))
|
||||||
|
.and_then(Result::ok);
|
||||||
|
|
||||||
|
record_country_visit(maybe_country);
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.call(req)
|
||||||
|
})
|
||||||
.service(routes::index)
|
.service(routes::index)
|
||||||
.service(routes::token_data)
|
.service(routes::token_data)
|
||||||
.service(routes::token_data_saver)
|
.service(routes::token_data_saver)
|
||||||
|
|
132
src/metrics.rs
132
src/metrics.rs
|
@ -1,5 +1,28 @@
|
||||||
use once_cell::sync::Lazy;
|
use std::fs::metadata;
|
||||||
use prometheus::{register_int_counter, IntCounter};
|
use std::hint::unreachable_unchecked;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use chrono::Duration;
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use maxminddb::geoip2::Country;
|
||||||
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
|
use prometheus::{register_int_counter, register_int_counter_vec, IntCounter, IntCounterVec};
|
||||||
|
use tar::Archive;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::{debug, field::debug, info, warn};
|
||||||
|
|
||||||
|
use crate::config::ClientSecret;
|
||||||
|
|
||||||
|
pub static GEOIP_DATABASE: OnceCell<maxminddb::Reader<Vec<u8>>> = OnceCell::new();
|
||||||
|
|
||||||
|
static COUNTRY_VISIT_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||||
|
register_int_counter_vec!(
|
||||||
|
"country_visits_total",
|
||||||
|
"The number of visits from a country",
|
||||||
|
&["country"]
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
macro_rules! init_counters {
|
macro_rules! init_counters {
|
||||||
($(($counter:ident, $ty:ty, $name:literal, $desc:literal),)*) => {
|
($(($counter:ident, $ty:ty, $name:literal, $desc:literal),)*) => {
|
||||||
|
@ -11,7 +34,11 @@ macro_rules! init_counters {
|
||||||
|
|
||||||
#[allow(clippy::shadow_unrelated)]
|
#[allow(clippy::shadow_unrelated)]
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
|
// These need to be called at least once, otherwise the macro never
|
||||||
|
// called and thus the metrics don't get logged
|
||||||
$(let _a = $counter.get();)*
|
$(let _a = $counter.get();)*
|
||||||
|
|
||||||
|
init_other();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -54,3 +81,104 @@ init_counters!(
|
||||||
"The total number of request not served by primary endpoints."
|
"The total number of request not served by primary endpoints."
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// initialization for any other counters that aren't simple int counters
|
||||||
|
fn init_other() {
|
||||||
|
let _a = COUNTRY_VISIT_COUNTER.local();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum DbLoadError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
MaxMindDb(#[from] maxminddb::MaxMindDBError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_geo_ip_data(license_key: ClientSecret) -> Result<(), DbLoadError> {
|
||||||
|
const DB_PATH: &str = "./GeoLite2-Country.mmdb";
|
||||||
|
|
||||||
|
// Check date of db
|
||||||
|
let db_date_created = metadata(DB_PATH)
|
||||||
|
.ok()
|
||||||
|
.and_then(|metadata| match metadata.created() {
|
||||||
|
Ok(time) => Some(time),
|
||||||
|
Err(_) => {
|
||||||
|
debug("fs didn't report birth time, fall back to last modified instead");
|
||||||
|
metadata.modified().ok()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
let duration = match SystemTime::now().duration_since(dbg!(db_date_created)) {
|
||||||
|
Ok(time) => Duration::from_std(time).expect("duration to fit"),
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Clock may have gone backwards?");
|
||||||
|
Duration::max_value()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DB expired, fetch a new one
|
||||||
|
if duration > Duration::weeks(1) {
|
||||||
|
fetch_db(license_key).await?;
|
||||||
|
} else {
|
||||||
|
info!("Geo IP database isn't old enough, not updating.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result literally cannot panic here, buuuuuut if it does we'll panic
|
||||||
|
GEOIP_DATABASE
|
||||||
|
.set(maxminddb::Reader::open_readfile(DB_PATH)?)
|
||||||
|
.map_err(|_| ())
|
||||||
|
.expect("to set the geo ip db singleton");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_db(license_key: ClientSecret) -> Result<(), DbLoadError> {
|
||||||
|
let resp = reqwest::get(format!("https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key={}&suffix=tar.gz", license_key.as_str()))
|
||||||
|
.await?
|
||||||
|
.bytes()
|
||||||
|
.await?;
|
||||||
|
let mut decoder = Archive::new(GzDecoder::new(resp.as_ref()));
|
||||||
|
let mut decoded_paths: Vec<_> = decoder
|
||||||
|
.entries()?
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|mut entry| {
|
||||||
|
let path = entry.path().ok()?.to_path_buf();
|
||||||
|
let file_name = path.file_name()?;
|
||||||
|
if file_name != "GeoLite2-Country.mmdb" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
entry.unpack(file_name).ok()?;
|
||||||
|
Some(path)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(decoded_paths.len(), 1);
|
||||||
|
|
||||||
|
let path = match decoded_paths.pop() {
|
||||||
|
Some(path) => path,
|
||||||
|
None => unsafe { unreachable_unchecked() },
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Extracted {}", path.as_path().to_string_lossy());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_country_visit(country: Option<Country>) {
|
||||||
|
let iso_code = if let Some(country) = country {
|
||||||
|
country
|
||||||
|
.country
|
||||||
|
.and_then(|c| c.iso_code)
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
COUNTRY_VISIT_COUNTER
|
||||||
|
.get_metric_with_label_values(&[iso_code])
|
||||||
|
.unwrap()
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue