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

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 {}