prometheus

This commit is contained in:
Paul Zinselmeyer 2023-11-10 14:22:57 +01:00
parent a6a4d68651
commit c7e317985f
6 changed files with 157 additions and 11 deletions

30
Cargo.lock generated
View file

@ -52,6 +52,7 @@ dependencies = [
"env_logger",
"futures-util",
"log",
"prometheus-client",
"qrcode",
"rand",
"sailfish",
@ -466,6 +467,12 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dtoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
[[package]]
name = "dyn-clone"
version = "1.0.16"
@ -1416,6 +1423,29 @@ dependencies = [
"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]]
name = "qrcode"
version = "0.12.0"

View file

@ -24,3 +24,4 @@ rand = "0.8"
qrcode = "0.12"
tower = "0.4.13"
tower-sessions = "0.4.1"
prometheus-client = "0.22.0"

View file

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1699030822,
"narHash": "sha256-a25bCHvTPJfAvK3qLoi5uI2pvwnOYhMQLRpJYNEt55o=",
"lastModified": 1699548976,
"narHash": "sha256-xnpxms0koM8mQpxIup9JnT0F7GrKdvv0QvtxvRuOYR4=",
"owner": "ipetkov",
"repo": "crane",
"rev": "2c89c36bffac32d8267e719f73b0d06e313ede30",
"rev": "6849911446e18e520970cc6b7a691e64ee90d649",
"type": "github"
},
"original": {
@ -40,11 +40,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1698924604,
"narHash": "sha256-GCFbkl2tj8fEZBZCw3Tc0AkGo0v+YrQlohhEGJ/X4s0=",
"lastModified": 1699099776,
"narHash": "sha256-X09iKJ27mGsGambGfkKzqvw5esP1L/Rf8H3u3fCqIiU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fa804edfb7869c9fb230e174182a8a1a7e512c40",
"rev": "85f1ba3e51676fa8cc604a3d863d729026a6b8eb",
"type": "github"
},
"original": {
@ -72,11 +72,11 @@
]
},
"locked": {
"lastModified": 1698977568,
"narHash": "sha256-bnbCqPDFdOUcSANJv9Br3q/b1LyK9vyB1I7os5T4jXI=",
"lastModified": 1699582387,
"narHash": "sha256-sPmUXPDl+cEi+zFtM5lnAs7dWOdRn0ptZ4a/qHwvNDk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "321affd863e3e4e669990a1db5fdabef98387b95",
"rev": "41f7b0618052430d3a050e8f937030d00a2fcced",
"type": "github"
},
"original": {

View file

@ -46,6 +46,9 @@ pub enum Error {
#[error("invalid input")]
InvalidInput,
#[error("Prometheus: {0:?}")]
Prometheus(std::fmt::Error),
}
impl IntoResponse for Error {

View file

@ -1,10 +1,19 @@
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
ops::Deref,
sync::Arc,
time::Duration,
};
use prometheus_client::{
collector::Collector,
encoding::{EncodeLabelSet, EncodeMetric},
metrics::{
family::Family,
gauge::{ConstGauge, Gauge},
},
};
use qrcode::{render::svg, QrCode};
use rand::{distributions, Rng};
use sailfish::TemplateOnce;
@ -256,3 +265,64 @@ impl Game {
.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(())
}
}

View file

@ -10,7 +10,7 @@ use std::{
use axum::{
error_handling::HandleErrorLayer,
extract::{Multipart, Path, Query, State},
http::{StatusCode, Uri},
http::{header::CONTENT_TYPE, HeaderMap, HeaderValue, StatusCode, Uri},
response::{
sse::{Event, KeepAlive},
Html, IntoResponse, Redirect, Sse,
@ -23,8 +23,13 @@ use axum_oidc::{
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
};
use futures_util::Stream;
use game::{Game, GameId, PlayerId};
use game::{Game, GameCollector, GameId, PlayerId};
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 sailfish::TemplateOnce;
use serde::{Deserialize, Serialize};
@ -50,6 +55,13 @@ pub struct AppState {
games: Arc<RwLock<HashMap<GameId, Game>>>,
game_expiry: Arc<RwLock<BinaryHeap<GarbageCollectorItem>>>,
application_base: &'static str,
prometheus_registry: Arc<Registry>,
metrics: Arc<AppMetrics>,
}
#[derive(Clone, Default)]
pub struct AppMetrics {
arc_games_total: Counter,
}
#[tokio::main]
@ -100,12 +112,25 @@ pub async fn main() {
Arc::new(RwLock::new(BinaryHeap::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());
let app_state = AppState {
games,
game_expiry,
application_base: Box::leak(application_base.into()),
prometheus_registry: Arc::new(registry),
metrics: app_metrics,
};
let app = Router::new()
@ -115,6 +140,7 @@ pub async fn main() {
.layer(oidc_login_service)
.route("/:id", get(handle_player).post(handle_player_answer))
.route("/:id/events", get(sse_player))
.route("/metrics", get(metrics))
.nest_service("/static", ServeDir::new("static"))
.with_state(app_state)
.layer(oidc_auth_service)
@ -157,6 +183,8 @@ pub async fn handle_create(
let mut game_expiry = state.game_expiry.write().await;
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"))
}
@ -284,6 +312,20 @@ pub async fn sse_player(
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)]
#[template(path = "index.stpl")]
struct IndexTemplate {}