Compare commits

...

4 Commits

Author SHA1 Message Date
Alexey fbee4631d6
Merge pull request #588 from amirotin/feat/active-ips-endpoint
feat(api): add GET /v1/stats/users/active-ips endpoint
2026-03-26 11:12:43 +03:00
Mirotin Artem d0b52ea299
Merge branch 'main' into feat/active-ips-endpoint 2026-03-26 10:00:47 +03:00
Mirotin Artem 677195e587
feat(api): add GET /v1/stats/users/active-ips endpoint
Lightweight endpoint that returns only users with active TCP connections
and their IP addresses. Calls only get_active_ips_for_users() without
collecting recent IPs or building full UserInfo, significantly reducing
CPU and memory overhead compared to /v1/stats/users.
2026-03-26 10:00:29 +03:00
Alexey a383efcb21
Bounded Hybrid Loop + Watch + Family ArcSwap Snapshots + Health in Parallel + ArcSwap Writers + Registry Split + Endpoint on ArcSwap + New Backpressure Model + ME Decomposition: merge pull request #586 from telemt/flow
Bounded Hybrid Loop + Watch + Family ArcSwap Snapshots + Health in Parallel + ArcSwap Writers + Registry Split + Endpoint on ArcSwap + New Backpressure Model + ME Decomposition
2026-03-26 02:31:18 +03:00
2 changed files with 19 additions and 0 deletions

View File

@ -42,6 +42,7 @@ use events::ApiEventStore;
use http_utils::{error_response, read_json, read_optional_json, success_response}; use http_utils::{error_response, read_json, read_optional_json, success_response};
use model::{ use model::{
ApiFailure, CreateUserRequest, HealthData, PatchUserRequest, RotateSecretRequest, SummaryData, ApiFailure, CreateUserRequest, HealthData, PatchUserRequest, RotateSecretRequest, SummaryData,
UserActiveIps,
}; };
use runtime_edge::{ use runtime_edge::{
EdgeConnectionsCacheEntry, build_runtime_connections_summary_data, EdgeConnectionsCacheEntry, build_runtime_connections_summary_data,
@ -362,6 +363,18 @@ async fn handle(
); );
Ok(success_response(StatusCode::OK, data, revision)) Ok(success_response(StatusCode::OK, data, revision))
} }
("GET", "/v1/stats/users/active-ips") => {
let revision = current_revision(&shared.config_path).await?;
let usernames: Vec<_> = cfg.access.users.keys().cloned().collect();
let active_ips_map = shared.ip_tracker.get_active_ips_for_users(&usernames).await;
let mut data: Vec<UserActiveIps> = active_ips_map
.into_iter()
.filter(|(_, ips)| !ips.is_empty())
.map(|(username, active_ips)| UserActiveIps { username, active_ips })
.collect();
data.sort_by(|a, b| a.username.cmp(&b.username));
Ok(success_response(StatusCode::OK, data, revision))
}
("GET", "/v1/stats/users") | ("GET", "/v1/users") => { ("GET", "/v1/stats/users") | ("GET", "/v1/users") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips(); let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();

View File

@ -442,6 +442,12 @@ pub(super) struct UserInfo {
pub(super) links: UserLinks, pub(super) links: UserLinks,
} }
#[derive(Serialize)]
pub(super) struct UserActiveIps {
pub(super) username: String,
pub(super) active_ips: Vec<IpAddr>,
}
#[derive(Serialize)] #[derive(Serialize)]
pub(super) struct CreateUserResponse { pub(super) struct CreateUserResponse {
pub(super) user: UserInfo, pub(super) user: UserInfo,