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

Commit f89890e

Browse files
committed
Add current connections user agent metrics
1 parent 308b30a commit f89890e

File tree

5 files changed

+79
-20
lines changed

5 files changed

+79
-20
lines changed

src/endpoints/global_data.rs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const PROJECT_ID: &str = "p2rxzX0q";
1818
pub struct GlobalDataContainer {
1919
last_full_refresh: DateTime<Utc>,
2020
last_player_refresh: DateTime<Utc>,
21-
data: GlobalData,
21+
pub data: GlobalData,
2222
}
2323

2424
impl Default for GlobalDataContainer {
@@ -33,7 +33,8 @@ impl Default for GlobalDataContainer {
3333
latest_version: String::new(),
3434
},
3535
notes: String::new(),
36-
user_agents: DashMap::new(),
36+
request_user_agents: DashMap::new(),
37+
gateway_user_agents: DashMap::new(),
3738
},
3839
}
3940
}
@@ -48,7 +49,9 @@ pub struct GlobalData {
4849
#[serde(skip_serializing_if = "String::is_empty")]
4950
notes: String,
5051
#[serde(skip)]
51-
pub user_agents: DashMap<String, u32>,
52+
pub request_user_agents: DashMap<String, u32>,
53+
#[serde(skip)]
54+
pub gateway_user_agents: DashMap<String, u32>,
5255
}
5356

5457
#[derive(Serialize)]
@@ -63,7 +66,8 @@ impl GlobalData {
6366
online_players: online,
6467
modrinth_data: self.modrinth_data.clone(),
6568
notes: self.notes.clone(),
66-
user_agents: self.user_agents.clone(),
69+
request_user_agents: self.request_user_agents.clone(),
70+
gateway_user_agents: self.gateway_user_agents.clone(),
6771
}
6872
}
6973
}
@@ -75,7 +79,8 @@ impl Clone for GlobalData {
7579
online_players: self.online_players,
7680
modrinth_data: self.modrinth_data.clone(),
7781
notes: self.notes.clone(),
78-
user_agents: self.user_agents.clone(),
82+
request_user_agents: self.request_user_agents.clone(),
83+
gateway_user_agents: self.gateway_user_agents.clone(),
7984
}
8085
}
8186
}
@@ -112,15 +117,17 @@ pub async fn get(
112117
return Ok(Json(cloned));
113118
}
114119
let data = if full_refresh {
115-
let agents = data_container.data.user_agents.clone();
120+
let request_user_agents = data_container.data.request_user_agents.clone();
121+
let gateway_user_agents = data_container.data.gateway_user_agents.clone();
116122
GlobalData {
117123
total_players: get_total_players(&database).await?,
118124
online_players: online_users.len() as u32,
119125
modrinth_data: fetch_modrinth_data(client).await?,
120126
notes: (cl_args.notes_file.as_ref())
121127
.map(|file| read_to_string(file).unwrap_or_else(|_| String::new()))
122128
.unwrap_or_else(String::new),
123-
user_agents: agents,
129+
request_user_agents,
130+
gateway_user_agents,
124131
}
125132
} else {
126133
data_container
@@ -163,11 +170,15 @@ pub async fn metrics(
163170
writeln!(response, "lifetime_players {lifetime_players}");
164171
writeln!(response, "online_players {online_players}");
165172
let data_container = global_data.read().await;
166-
let agents = data_container.data.user_agents.clone();
167-
for (agent, count) in agents {
173+
let request_agents = data_container.data.request_user_agents.clone();
174+
for (agent, count) in request_agents {
168175
writeln!(response, "request_count{{user_agent=\"{agent}\"}} {count}");
169176
}
170-
data_container.data.user_agents.clear();
177+
data_container.data.request_user_agents.clear();
178+
let gateway_agents = data_container.data.gateway_user_agents.clone();
179+
for (agent, count) in gateway_agents {
180+
writeln!(response, "connections{{user_agent=\"{agent}\"}} {count}");
181+
}
171182
};
172183

173184
Ok(response)
@@ -205,11 +216,14 @@ async fn fetch_modrinth_data(client: Client) -> Result<ModrinthData, ApiError> {
205216
})
206217
}
207218

208-
pub struct UserAgent;
219+
pub struct RequestUserAgentCounter;
209220

210-
impl FromRequestParts<ApiState> for UserAgent {
221+
impl FromRequestParts<ApiState> for RequestUserAgentCounter {
211222
type Rejection = ApiError;
212-
async fn from_request_parts(parts: &mut Parts, state: &ApiState) -> Result<UserAgent, Self::Rejection> {
223+
async fn from_request_parts(
224+
parts: &mut Parts,
225+
state: &ApiState,
226+
) -> Result<RequestUserAgentCounter, Self::Rejection> {
213227
if parts.uri.path().ends_with("metrics") {
214228
return Ok(Self);
215229
}
@@ -222,8 +236,8 @@ impl FromRequestParts<ApiState> for UserAgent {
222236
.replace("\\", "")
223237
.replace("\"", "");
224238

225-
let a = state.global_data.read().await;
226-
let agents = &a.data.user_agents;
239+
let container = state.global_data.read().await;
240+
let agents = &container.data.request_user_agents;
227241
if agents.contains_key(&agent) {
228242
let prev = agents.get(&agent).map(|v| *v.value()).unwrap_or(0);
229243
agents.insert(agent, prev + 1);

src/endpoints/image.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use bytes::Buf;
99
use chrono::{DateTime, Utc};
1010
use reqwest::StatusCode;
1111
use serde::{Deserialize, Serialize};
12-
use sqlx::{query, PgPool};
12+
use sqlx::query;
1313
use uuid::Uuid;
1414

1515
use crate::{

src/extractors.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{errors::ApiError, ApiState};
22
use axum::{
33
extract::{FromRequestParts, OptionalFromRequestParts},
4-
http::{request::Parts, StatusCode},
4+
http::{self, request::Parts, StatusCode},
55
};
66
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine};
77
use sqlx::query;
@@ -63,3 +63,21 @@ impl OptionalFromRequestParts<ApiState> for Authentication {
6363
.ok())
6464
}
6565
}
66+
67+
pub struct UserAgent(pub String);
68+
69+
impl FromRequestParts<ApiState> for UserAgent {
70+
type Rejection = ApiError;
71+
async fn from_request_parts(parts: &mut Parts, _state: &ApiState) -> Result<UserAgent, Self::Rejection> {
72+
let agent = parts
73+
.headers
74+
.get(http::header::USER_AGENT)
75+
.map(|v| v.to_str())
76+
.ok_or(StatusCode::BAD_REQUEST)?
77+
.map_err(|_| StatusCode::BAD_REQUEST)?
78+
.replace("\\", "")
79+
.replace("\"", "");
80+
81+
Ok(Self(agent))
82+
}
83+
}

src/gateway.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::extractors::UserAgent;
12
use crate::{errors::ApiError, extractors::Authentication, ApiState};
23
use axum::extract::{ws::close_code, ws::CloseFrame, ws::Message, ws::WebSocket, State, WebSocketUpgrade};
34
use axum::{body::Body, response::Response};
@@ -12,6 +13,7 @@ use DisconnectReason::*;
1213
pub async fn gateway(
1314
state: State<ApiState>,
1415
Authentication(uuid): Authentication,
16+
UserAgent(agent): UserAgent,
1517
socket: WebSocketUpgrade,
1618
) -> Result<Response<Body>, ApiError> {
1719
if let Some(socket) = state.socket_sender.remove(&uuid) {
@@ -20,22 +22,36 @@ pub async fn gateway(
2022
drop(socket);
2123
}
2224

23-
Ok(socket.on_upgrade(move |socket| gateway_accept_handler(state, uuid, socket)))
25+
Ok(socket.on_upgrade(move |socket| gateway_accept_handler(state, uuid, socket, agent)))
2426
}
2527

2628
async fn gateway_accept_handler(
2729
State(ApiState {
2830
database,
2931
online_users,
3032
socket_sender,
33+
global_data,
3134
..
3235
}): State<ApiState>,
3336
uuid: Uuid,
3437
mut socket: WebSocket,
38+
user_agent: String,
3539
) {
3640
online_users.insert(uuid, None);
3741
let (sender, mut receiver) = unbounded_channel();
3842
socket_sender.insert(uuid, sender);
43+
{
44+
let container = global_data.read().await;
45+
let agents = &container.data.gateway_user_agents;
46+
let user_agent = user_agent.clone();
47+
if agents.contains_key(&user_agent) {
48+
let prev = agents.get(&user_agent).map(|v| *v.value()).unwrap_or(0);
49+
agents.insert(user_agent, prev + 1);
50+
} else {
51+
agents.insert(user_agent, 1);
52+
}
53+
drop(container);
54+
}
3955

4056
let disconnect_reason = gateway_accept(&mut socket, &mut receiver).await.unwrap_err();
4157
let _ = socket
@@ -50,6 +66,17 @@ async fn gateway_accept_handler(
5066
}
5167
receiver.close();
5268
online_users.remove(&uuid);
69+
{
70+
let container = global_data.read().await;
71+
let agents = &container.data.gateway_user_agents;
72+
let prev = agents.get(&user_agent).map(|v| *v.value()).unwrap_or(0);
73+
if prev > 1 {
74+
agents.insert(user_agent, prev - 1);
75+
} else {
76+
agents.remove(&user_agent);
77+
}
78+
drop(container);
79+
};
5380

5481
let _ = query!("UPDATE players SET last_online = 'now' WHERE uuid = $1 AND show_last_online = true", uuid)
5582
.execute(&database)

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::endpoints::global_data::{self, GlobalDataContainer, UserAgent};
1+
use crate::endpoints::global_data::{self, GlobalDataContainer, RequestUserAgentCounter};
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;
@@ -167,7 +167,7 @@ async fn main() -> anyhow::Result<()> {
167167
.route("/hypixel", get(hypixel::get))
168168
//.route("/report/:message", post(channel::report_message))
169169
.route("/brew_coffee", get(brew_coffee).post(brew_coffee))
170-
.layer(axum::middleware::from_extractor_with_state::<UserAgent, ApiState>(state.clone()))
170+
.layer(axum::middleware::from_extractor_with_state::<RequestUserAgentCounter, ApiState>(state.clone()))
171171
.fallback(not_found)
172172
.with_state(state);
173173

0 commit comments

Comments
 (0)