prometheus
This commit is contained in:
parent
a6a4d68651
commit
c7e317985f
6 changed files with 157 additions and 11 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -52,6 +52,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
|
"prometheus-client",
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"rand",
|
"rand",
|
||||||
"sailfish",
|
"sailfish",
|
||||||
|
@ -466,6 +467,12 @@ version = "0.15.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dtoa"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clone"
|
name = "dyn-clone"
|
||||||
version = "1.0.16"
|
version = "1.0.16"
|
||||||
|
@ -1416,6 +1423,29 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prometheus-client"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "510c4f1c9d81d556458f94c98f857748130ea9737bbd6053da497503b26ea63c"
|
||||||
|
dependencies = [
|
||||||
|
"dtoa",
|
||||||
|
"itoa",
|
||||||
|
"parking_lot",
|
||||||
|
"prometheus-client-derive-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prometheus-client-derive-encode"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qrcode"
|
name = "qrcode"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
|
@ -24,3 +24,4 @@ rand = "0.8"
|
||||||
qrcode = "0.12"
|
qrcode = "0.12"
|
||||||
tower = "0.4.13"
|
tower = "0.4.13"
|
||||||
tower-sessions = "0.4.1"
|
tower-sessions = "0.4.1"
|
||||||
|
prometheus-client = "0.22.0"
|
||||||
|
|
18
flake.lock
18
flake.lock
|
@ -7,11 +7,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1699030822,
|
"lastModified": 1699548976,
|
||||||
"narHash": "sha256-a25bCHvTPJfAvK3qLoi5uI2pvwnOYhMQLRpJYNEt55o=",
|
"narHash": "sha256-xnpxms0koM8mQpxIup9JnT0F7GrKdvv0QvtxvRuOYR4=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "2c89c36bffac32d8267e719f73b0d06e313ede30",
|
"rev": "6849911446e18e520970cc6b7a691e64ee90d649",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -40,11 +40,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1698924604,
|
"lastModified": 1699099776,
|
||||||
"narHash": "sha256-GCFbkl2tj8fEZBZCw3Tc0AkGo0v+YrQlohhEGJ/X4s0=",
|
"narHash": "sha256-X09iKJ27mGsGambGfkKzqvw5esP1L/Rf8H3u3fCqIiU=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fa804edfb7869c9fb230e174182a8a1a7e512c40",
|
"rev": "85f1ba3e51676fa8cc604a3d863d729026a6b8eb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -72,11 +72,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1698977568,
|
"lastModified": 1699582387,
|
||||||
"narHash": "sha256-bnbCqPDFdOUcSANJv9Br3q/b1LyK9vyB1I7os5T4jXI=",
|
"narHash": "sha256-sPmUXPDl+cEi+zFtM5lnAs7dWOdRn0ptZ4a/qHwvNDk=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "321affd863e3e4e669990a1db5fdabef98387b95",
|
"rev": "41f7b0618052430d3a050e8f937030d00a2fcced",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -46,6 +46,9 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("invalid input")]
|
#[error("invalid input")]
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
|
|
||||||
|
#[error("Prometheus: {0:?}")]
|
||||||
|
Prometheus(std::fmt::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for Error {
|
||||||
|
|
70
src/game.rs
70
src/game.rs
|
@ -1,10 +1,19 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Debug,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use prometheus_client::{
|
||||||
|
collector::Collector,
|
||||||
|
encoding::{EncodeLabelSet, EncodeMetric},
|
||||||
|
metrics::{
|
||||||
|
family::Family,
|
||||||
|
gauge::{ConstGauge, Gauge},
|
||||||
|
},
|
||||||
|
};
|
||||||
use qrcode::{render::svg, QrCode};
|
use qrcode::{render::svg, QrCode};
|
||||||
use rand::{distributions, Rng};
|
use rand::{distributions, Rng};
|
||||||
use sailfish::TemplateOnce;
|
use sailfish::TemplateOnce;
|
||||||
|
@ -256,3 +265,64 @@ impl Game {
|
||||||
.render_once()?)
|
.render_once()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(EncodeLabelSet, PartialEq, Eq, Hash, Clone, Debug)]
|
||||||
|
pub struct GameLabels {
|
||||||
|
game: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GameCollector {
|
||||||
|
games: Arc<RwLock<HashMap<GameId, Game>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameCollector {
|
||||||
|
pub fn new(games: Arc<RwLock<HashMap<GameId, Game>>>) -> Self {
|
||||||
|
Self { games }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for GameCollector {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("GameCollector").finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collector for GameCollector {
|
||||||
|
fn encode(
|
||||||
|
&self,
|
||||||
|
mut encoder: prometheus_client::encoding::DescriptorEncoder,
|
||||||
|
) -> Result<(), std::fmt::Error> {
|
||||||
|
let games = self.games.blocking_read();
|
||||||
|
let running_games = ConstGauge::new(games.len() as i64);
|
||||||
|
|
||||||
|
let participants = Family::<GameLabels, Gauge>::default();
|
||||||
|
|
||||||
|
games.iter().for_each(|(id, game)| {
|
||||||
|
participants
|
||||||
|
.get_or_create(&GameLabels {
|
||||||
|
game: id.0.to_string(),
|
||||||
|
})
|
||||||
|
.set(game.players.len() as i64);
|
||||||
|
});
|
||||||
|
|
||||||
|
drop(games);
|
||||||
|
|
||||||
|
let running_games_encoder = encoder.encode_descriptor(
|
||||||
|
"ars_running_games",
|
||||||
|
"number of running games",
|
||||||
|
None,
|
||||||
|
running_games.metric_type(),
|
||||||
|
)?;
|
||||||
|
running_games.encode(running_games_encoder)?;
|
||||||
|
|
||||||
|
let participants_encoder = encoder.encode_descriptor(
|
||||||
|
"ars_game_participants",
|
||||||
|
"number of participants for a game",
|
||||||
|
None,
|
||||||
|
participants.metric_type(),
|
||||||
|
)?;
|
||||||
|
participants.encode(participants_encoder)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
46
src/main.rs
46
src/main.rs
|
@ -10,7 +10,7 @@ use std::{
|
||||||
use axum::{
|
use axum::{
|
||||||
error_handling::HandleErrorLayer,
|
error_handling::HandleErrorLayer,
|
||||||
extract::{Multipart, Path, Query, State},
|
extract::{Multipart, Path, Query, State},
|
||||||
http::{StatusCode, Uri},
|
http::{header::CONTENT_TYPE, HeaderMap, HeaderValue, StatusCode, Uri},
|
||||||
response::{
|
response::{
|
||||||
sse::{Event, KeepAlive},
|
sse::{Event, KeepAlive},
|
||||||
Html, IntoResponse, Redirect, Sse,
|
Html, IntoResponse, Redirect, Sse,
|
||||||
|
@ -23,8 +23,13 @@ use axum_oidc::{
|
||||||
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
|
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
|
||||||
};
|
};
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use game::{Game, GameId, PlayerId};
|
use game::{Game, GameCollector, GameId, PlayerId};
|
||||||
use garbage_collector::{start_gc, GarbageCollectorItem};
|
use garbage_collector::{start_gc, GarbageCollectorItem};
|
||||||
|
use prometheus_client::{
|
||||||
|
encoding::EncodeLabelSet,
|
||||||
|
metrics::{counter::Counter, family::Family, gauge::Gauge},
|
||||||
|
registry::Registry,
|
||||||
|
};
|
||||||
use question::{single_choice::SingleChoiceQuestion, Question};
|
use question::{single_choice::SingleChoiceQuestion, Question};
|
||||||
use sailfish::TemplateOnce;
|
use sailfish::TemplateOnce;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -50,6 +55,13 @@ pub struct AppState {
|
||||||
games: Arc<RwLock<HashMap<GameId, Game>>>,
|
games: Arc<RwLock<HashMap<GameId, Game>>>,
|
||||||
game_expiry: Arc<RwLock<BinaryHeap<GarbageCollectorItem>>>,
|
game_expiry: Arc<RwLock<BinaryHeap<GarbageCollectorItem>>>,
|
||||||
application_base: &'static str,
|
application_base: &'static str,
|
||||||
|
prometheus_registry: Arc<Registry>,
|
||||||
|
metrics: Arc<AppMetrics>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct AppMetrics {
|
||||||
|
arc_games_total: Counter,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -100,12 +112,25 @@ pub async fn main() {
|
||||||
Arc::new(RwLock::new(BinaryHeap::new()));
|
Arc::new(RwLock::new(BinaryHeap::new()));
|
||||||
let games = Arc::new(RwLock::new(HashMap::new()));
|
let games = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
let app_metrics = Arc::new(AppMetrics::default());
|
||||||
|
let mut registry = Registry::default();
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
"arc_games_total",
|
||||||
|
"number of games created",
|
||||||
|
app_metrics.arc_games_total.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
registry.register_collector(Box::new(GameCollector::new(games.clone())));
|
||||||
|
|
||||||
start_gc(game_expiry.clone(), games.clone());
|
start_gc(game_expiry.clone(), games.clone());
|
||||||
|
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
games,
|
games,
|
||||||
game_expiry,
|
game_expiry,
|
||||||
application_base: Box::leak(application_base.into()),
|
application_base: Box::leak(application_base.into()),
|
||||||
|
prometheus_registry: Arc::new(registry),
|
||||||
|
metrics: app_metrics,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
@ -115,6 +140,7 @@ pub async fn main() {
|
||||||
.layer(oidc_login_service)
|
.layer(oidc_login_service)
|
||||||
.route("/:id", get(handle_player).post(handle_player_answer))
|
.route("/:id", get(handle_player).post(handle_player_answer))
|
||||||
.route("/:id/events", get(sse_player))
|
.route("/:id/events", get(sse_player))
|
||||||
|
.route("/metrics", get(metrics))
|
||||||
.nest_service("/static", ServeDir::new("static"))
|
.nest_service("/static", ServeDir::new("static"))
|
||||||
.with_state(app_state)
|
.with_state(app_state)
|
||||||
.layer(oidc_auth_service)
|
.layer(oidc_auth_service)
|
||||||
|
@ -157,6 +183,8 @@ pub async fn handle_create(
|
||||||
let mut game_expiry = state.game_expiry.write().await;
|
let mut game_expiry = state.game_expiry.write().await;
|
||||||
game_expiry.push(GarbageCollectorItem::new_in(game_id, 24 * 3600));
|
game_expiry.push(GarbageCollectorItem::new_in(game_id, 24 * 3600));
|
||||||
|
|
||||||
|
state.metrics.arc_games_total.inc();
|
||||||
|
|
||||||
Ok((HxRedirect(Uri::from_maybe_shared(url.clone())?), "Ok"))
|
Ok((HxRedirect(Uri::from_maybe_shared(url.clone())?), "Ok"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,6 +312,20 @@ pub async fn sse_player(
|
||||||
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn metrics(State(app_state): State<AppState>) -> HandlerResult<impl IntoResponse> {
|
||||||
|
let mut buffer = String::new();
|
||||||
|
prometheus_client::encoding::text::encode(&mut buffer, &app_state.prometheus_registry)
|
||||||
|
.map_err(Error::Prometheus)?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("text/plain; version=0.0.4"),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((headers, buffer))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(TemplateOnce)]
|
#[derive(TemplateOnce)]
|
||||||
#[template(path = "index.stpl")]
|
#[template(path = "index.stpl")]
|
||||||
struct IndexTemplate {}
|
struct IndexTemplate {}
|
||||||
|
|
Loading…
Reference in a new issue