Skip to content
This repository was archived by the owner on Dec 24, 2025. It is now read-only.

Commit 308b30a

Browse files
committed
Add user agent stats to metrics endpoint
1 parent 3fdfb3d commit 308b30a

File tree

3 files changed

+70
-17
lines changed

3 files changed

+70
-17
lines changed

src/endpoints/global_data.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use crate::{errors::ApiError, ApiState};
2-
use axum::{extract::State, Json};
2+
use axum::{
3+
extract::{FromRequestParts, State},
4+
http::{self, request::Parts},
5+
Json,
6+
};
37
use chrono::{DateTime, Utc};
8+
use dashmap::DashMap;
49
use reqwest::{Client, StatusCode};
510
use serde::Serialize;
611
use serde_json::Value;
@@ -28,6 +33,7 @@ impl Default for GlobalDataContainer {
2833
latest_version: String::new(),
2934
},
3035
notes: String::new(),
36+
user_agents: DashMap::new(),
3137
},
3238
}
3339
}
@@ -41,6 +47,8 @@ pub struct GlobalData {
4147
modrinth_data: ModrinthData,
4248
#[serde(skip_serializing_if = "String::is_empty")]
4349
notes: String,
50+
#[serde(skip)]
51+
pub user_agents: DashMap<String, u32>,
4452
}
4553

4654
#[derive(Serialize)]
@@ -55,6 +63,7 @@ impl GlobalData {
5563
online_players: online,
5664
modrinth_data: self.modrinth_data.clone(),
5765
notes: self.notes.clone(),
66+
user_agents: self.user_agents.clone(),
5867
}
5968
}
6069
}
@@ -66,6 +75,7 @@ impl Clone for GlobalData {
6675
online_players: self.online_players,
6776
modrinth_data: self.modrinth_data.clone(),
6877
notes: self.notes.clone(),
78+
user_agents: self.user_agents.clone(),
6979
}
7080
}
7181
}
@@ -102,13 +112,15 @@ pub async fn get(
102112
return Ok(Json(cloned));
103113
}
104114
let data = if full_refresh {
115+
let agents = data_container.data.user_agents.clone();
105116
GlobalData {
106117
total_players: get_total_players(&database).await?,
107118
online_players: online_users.len() as u32,
108119
modrinth_data: fetch_modrinth_data(client).await?,
109120
notes: (cl_args.notes_file.as_ref())
110121
.map(|file| read_to_string(file).unwrap_or_else(|_| String::new()))
111122
.unwrap_or_else(String::new),
123+
user_agents: agents,
112124
}
113125
} else {
114126
data_container
@@ -130,7 +142,10 @@ pub async fn get(
130142

131143
pub async fn metrics(
132144
State(ApiState {
133-
database, online_users, ..
145+
database,
146+
online_users,
147+
global_data,
148+
..
134149
}): State<ApiState>,
135150
) -> Result<String, ApiError> {
136151
let lifetime_players = get_total_players(&database).await?;
@@ -147,6 +162,12 @@ pub async fn metrics(
147162
writeln!(response, "");
148163
writeln!(response, "lifetime_players {lifetime_players}");
149164
writeln!(response, "online_players {online_players}");
165+
let data_container = global_data.read().await;
166+
let agents = data_container.data.user_agents.clone();
167+
for (agent, count) in agents {
168+
writeln!(response, "request_count{{user_agent=\"{agent}\"}} {count}");
169+
}
170+
data_container.data.user_agents.clear();
150171
};
151172

152173
Ok(response)
@@ -183,3 +204,32 @@ async fn fetch_modrinth_data(client: Client) -> Result<ModrinthData, ApiError> {
183204
.to_string(),
184205
})
185206
}
207+
208+
pub struct UserAgent;
209+
210+
impl FromRequestParts<ApiState> for UserAgent {
211+
type Rejection = ApiError;
212+
async fn from_request_parts(parts: &mut Parts, state: &ApiState) -> Result<UserAgent, Self::Rejection> {
213+
if parts.uri.path().ends_with("metrics") {
214+
return Ok(Self);
215+
}
216+
let agent = parts
217+
.headers
218+
.get(http::header::USER_AGENT)
219+
.map(|v| v.to_str())
220+
.ok_or(StatusCode::BAD_REQUEST)?
221+
.map_err(|_| StatusCode::BAD_REQUEST)?
222+
.replace("\\", "")
223+
.replace("\"", "");
224+
225+
let a = state.global_data.read().await;
226+
let agents = &a.data.user_agents;
227+
if agents.contains_key(&agent) {
228+
let prev = agents.get(&agent).map(|v| *v.value()).unwrap_or(0);
229+
agents.insert(agent, prev + 1);
230+
} else {
231+
agents.insert(agent, 1);
232+
}
233+
Ok(Self)
234+
}
235+
}

src/endpoints/image.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub async fn post(
8080
Ok(id.to_string())
8181
}
8282

83-
pub async fn evict_expired(database: &PgPool) -> Result<(), TaskError> {
83+
pub async fn evict_expired(ApiState { database, .. }: &ApiState) -> Result<(), TaskError> {
8484
query!("DELETE FROM images WHERE (LOCALTIMESTAMP - timestamp) > '1 week'")
8585
.execute(database)
8686
.await?;

src/main.rs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::endpoints::global_data::{self, GlobalDataContainer};
1+
use crate::endpoints::global_data::{self, GlobalDataContainer, UserAgent};
22
use crate::endpoints::user::{self, Activity};
33
use crate::endpoints::{account, brew_coffee, channel, get_authenticate, image, not_found};
44
use crate::gateway::gateway;
@@ -103,15 +103,27 @@ async fn main() -> anyhow::Result<()> {
103103

104104
migrate!().run(&database).await?;
105105

106-
let db = database.clone();
106+
let state = ApiState {
107+
database,
108+
hypixel_api_state: Arc::new(HypixelApiProxyState::new(cl_args.cache_limit_bytes)),
109+
cl_args,
110+
client: Client::builder()
111+
.user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
112+
.build()?,
113+
online_users: Default::default(),
114+
socket_sender: Default::default(),
115+
global_data: Default::default(),
116+
};
117+
118+
let task_state = state.clone();
107119
tokio::spawn(async move {
108120
let mut interval = interval(Duration::from_secs(1 * 24 * 60 * 60));
109121
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
110122
let tasks = [image::evict_expired];
111123
loop {
112124
let _ = interval.tick().await;
113125
for task in tasks {
114-
let _ = task(&db).await;
126+
let _ = task(&task_state).await;
115127
}
116128
}
117129
});
@@ -155,18 +167,9 @@ async fn main() -> anyhow::Result<()> {
155167
.route("/hypixel", get(hypixel::get))
156168
//.route("/report/:message", post(channel::report_message))
157169
.route("/brew_coffee", get(brew_coffee).post(brew_coffee))
170+
.layer(axum::middleware::from_extractor_with_state::<UserAgent, ApiState>(state.clone()))
158171
.fallback(not_found)
159-
.with_state(ApiState {
160-
database,
161-
hypixel_api_state: Arc::new(HypixelApiProxyState::new(cl_args.cache_limit_bytes)),
162-
cl_args,
163-
client: Client::builder()
164-
.user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
165-
.build()?,
166-
online_users: Default::default(),
167-
socket_sender: Default::default(),
168-
global_data: Default::default(),
169-
});
172+
.with_state(state);
170173

171174
let listener = tokio::net::TcpListener::bind("[::]:8000").await?;
172175

0 commit comments

Comments
 (0)