Compare commits

..

No commits in common. "6b3b77384e54c7a251a513f2c18abb324a869a29" and "beee62832ff4c3ca0e504991a6fda3b9ed4d20af" have entirely different histories.

5 changed files with 436 additions and 0 deletions

View File

@ -9,6 +9,8 @@ use super::generated::admin::{
KickUserFromWorkspaceRequest, KickUserRequest,
GetUserStatusRequest, GetUserInfoRequest,
GetWorkspaceOnlineUsersRequest, IsUserOnlineRequest,
GetMetricsRequest, ExportMetricsCsvRequest,
InstanceMetrics,
};
use super::generated::admin_session_admin::session_admin_client::SessionAdminClient;
use super::types::from_proto_status;
@ -133,6 +135,31 @@ impl AdminGrpcClient {
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().online)
}
/// Query metrics across all app instances.
pub async fn get_metrics(
&mut self,
instance_filter: &str,
limit: u32,
) -> anyhow::Result<Vec<InstanceMetrics>> {
let req = tonic::Request::new(GetMetricsRequest {
instance_filter: instance_filter.to_string(),
limit,
});
let res = self.inner.get_metrics(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().instances)
}
/// Export all metrics as CSV string.
pub async fn export_metrics_csv(&mut self, instance_filter: &str) -> anyhow::Result<String> {
let req = tonic::Request::new(ExportMetricsCsvRequest {
instance_filter: instance_filter.to_string(),
});
let res = self.inner.export_metrics_csv(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().csv)
}
}
// ---------------------------------------------------------------------------

View File

@ -305,6 +305,58 @@ pub mod session_admin_client {
.insert(GrpcMethod::new("admin.SessionAdmin", "IsUserOnline"));
self.inner.unary(req, path, codec).await
}
pub async fn get_metrics(
&mut self,
request: impl tonic::IntoRequest<
crate::admin::generated::admin::GetMetricsRequest,
>,
) -> std::result::Result<
tonic::Response<crate::admin::generated::admin::GetMetricsResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/admin.SessionAdmin/GetMetrics",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("admin.SessionAdmin", "GetMetrics"));
self.inner.unary(req, path, codec).await
}
pub async fn export_metrics_csv(
&mut self,
request: impl tonic::IntoRequest<
crate::admin::generated::admin::ExportMetricsCsvRequest,
>,
) -> std::result::Result<
tonic::Response<crate::admin::generated::admin::ExportMetricsCsvResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/admin.SessionAdmin/ExportMetricsCsv",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("admin.SessionAdmin", "ExportMetricsCsv"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
@ -390,6 +442,22 @@ pub mod session_admin_server {
tonic::Response<crate::admin::generated::admin::IsUserOnlineResponse>,
tonic::Status,
>;
async fn get_metrics(
&self,
request: tonic::Request<crate::admin::generated::admin::GetMetricsRequest>,
) -> std::result::Result<
tonic::Response<crate::admin::generated::admin::GetMetricsResponse>,
tonic::Status,
>;
async fn export_metrics_csv(
&self,
request: tonic::Request<
crate::admin::generated::admin::ExportMetricsCsvRequest,
>,
) -> std::result::Result<
tonic::Response<crate::admin::generated::admin::ExportMetricsCsvResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct SessionAdminServer<T> {
@ -864,6 +932,103 @@ pub mod session_admin_server {
};
Box::pin(fut)
}
"/admin.SessionAdmin/GetMetrics" => {
#[allow(non_camel_case_types)]
struct GetMetricsSvc<T: SessionAdmin>(pub Arc<T>);
impl<
T: SessionAdmin,
> tonic::server::UnaryService<
crate::admin::generated::admin::GetMetricsRequest,
> for GetMetricsSvc<T> {
type Response = crate::admin::generated::admin::GetMetricsResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<
crate::admin::generated::admin::GetMetricsRequest,
>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as SessionAdmin>::get_metrics(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetMetricsSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/admin.SessionAdmin/ExportMetricsCsv" => {
#[allow(non_camel_case_types)]
struct ExportMetricsCsvSvc<T: SessionAdmin>(pub Arc<T>);
impl<
T: SessionAdmin,
> tonic::server::UnaryService<
crate::admin::generated::admin::ExportMetricsCsvRequest,
> for ExportMetricsCsvSvc<T> {
type Response = crate::admin::generated::admin::ExportMetricsCsvResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<
crate::admin::generated::admin::ExportMetricsCsvRequest,
>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as SessionAdmin>::export_metrics_csv(&inner, request)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = ExportMetricsCsvSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(

View File

@ -109,6 +109,49 @@ pub struct IsUserOnlineResponse {
#[prost(bool, tag = "1")]
pub online: bool,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetMetricsRequest {
/// filter by instance_id substring
#[prost(string, tag = "1")]
pub instance_filter: ::prost::alloc::string::String,
/// max snapshots per instance (default 100)
#[prost(uint32, tag = "2")]
pub limit: u32,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetMetricsResponse {
#[prost(message, repeated, tag = "1")]
pub instances: ::prost::alloc::vec::Vec<InstanceMetrics>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct InstanceMetrics {
#[prost(string, tag = "1")]
pub instance_id: ::prost::alloc::string::String,
#[prost(int64, tag = "2")]
pub timestamp_secs: i64,
/// HTTP metrics, key = metric name, value = JSON value
#[prost(map = "string, string", tag = "3")]
pub http: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost::alloc::string::String,
>,
/// Room/room metrics
#[prost(map = "string, string", tag = "4")]
pub room: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost::alloc::string::String,
>,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ExportMetricsCsvRequest {
#[prost(string, tag = "1")]
pub instance_filter: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ExportMetricsCsvResponse {
#[prost(string, tag = "1")]
pub csv: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SyncModelsRequest {}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
@ -533,6 +576,54 @@ pub mod session_admin_client {
.insert(GrpcMethod::new("admin.SessionAdmin", "IsUserOnline"));
self.inner.unary(req, path, codec).await
}
pub async fn get_metrics(
&mut self,
request: impl tonic::IntoRequest<super::GetMetricsRequest>,
) -> std::result::Result<
tonic::Response<super::GetMetricsResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/admin.SessionAdmin/GetMetrics",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("admin.SessionAdmin", "GetMetrics"));
self.inner.unary(req, path, codec).await
}
pub async fn export_metrics_csv(
&mut self,
request: impl tonic::IntoRequest<super::ExportMetricsCsvRequest>,
) -> std::result::Result<
tonic::Response<super::ExportMetricsCsvResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/admin.SessionAdmin/ExportMetricsCsv",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("admin.SessionAdmin", "ExportMetricsCsv"));
self.inner.unary(req, path, codec).await
}
/// AI
pub async fn sync_models(
&mut self,
@ -882,6 +973,20 @@ pub mod session_admin_server {
tonic::Response<super::IsUserOnlineResponse>,
tonic::Status,
>;
async fn get_metrics(
&self,
request: tonic::Request<super::GetMetricsRequest>,
) -> std::result::Result<
tonic::Response<super::GetMetricsResponse>,
tonic::Status,
>;
async fn export_metrics_csv(
&self,
request: tonic::Request<super::ExportMetricsCsvRequest>,
) -> std::result::Result<
tonic::Response<super::ExportMetricsCsvResponse>,
tonic::Status,
>;
/// AI
async fn sync_models(
&self,
@ -1399,6 +1504,97 @@ pub mod session_admin_server {
};
Box::pin(fut)
}
"/admin.SessionAdmin/GetMetrics" => {
#[allow(non_camel_case_types)]
struct GetMetricsSvc<T: SessionAdmin>(pub Arc<T>);
impl<
T: SessionAdmin,
> tonic::server::UnaryService<super::GetMetricsRequest>
for GetMetricsSvc<T> {
type Response = super::GetMetricsResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetMetricsRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as SessionAdmin>::get_metrics(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetMetricsSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/admin.SessionAdmin/ExportMetricsCsv" => {
#[allow(non_camel_case_types)]
struct ExportMetricsCsvSvc<T: SessionAdmin>(pub Arc<T>);
impl<
T: SessionAdmin,
> tonic::server::UnaryService<super::ExportMetricsCsvRequest>
for ExportMetricsCsvSvc<T> {
type Response = super::ExportMetricsCsvResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::ExportMetricsCsvRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as SessionAdmin>::export_metrics_csv(&inner, request)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = ExportMetricsCsvSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/admin.SessionAdmin/SyncModels" => {
#[allow(non_camel_case_types)]
struct SyncModelsSvc<T: SessionAdmin>(pub Arc<T>);

View File

@ -82,6 +82,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.codec_path("tonic_prost::ProstCodec")
.build(),
)
.method(
tonic_prost_build::manual::Method::builder()
.name("get_metrics")
.route_name("GetMetrics")
.input_type("crate::admin::generated::admin::GetMetricsRequest")
.output_type("crate::admin::generated::admin::GetMetricsResponse")
.codec_path("tonic_prost::ProstCodec")
.build(),
)
.method(
tonic_prost_build::manual::Method::builder()
.name("export_metrics_csv")
.route_name("ExportMetricsCsv")
.input_type("crate::admin::generated::admin::ExportMetricsCsvRequest")
.output_type("crate::admin::generated::admin::ExportMetricsCsvResponse")
.codec_path("tonic_prost::ProstCodec")
.build(),
)
.build();
tonic_prost_build::manual::Builder::new()

View File

@ -93,6 +93,34 @@ message IsUserOnlineResponse {
bool online = 1;
}
// ---------------------------------------------------------------------------
// Metrics
// ---------------------------------------------------------------------------
message GetMetricsRequest {
string instance_filter = 1; // filter by instance_id substring
uint32 limit = 2; // max snapshots per instance (default 100)
}
message GetMetricsResponse {
repeated InstanceMetrics instances = 1;
}
message InstanceMetrics {
string instance_id = 1;
int64 timestamp_secs = 2;
// HTTP metrics, key = metric name, value = JSON value
map<string, string> http = 3;
// Room/room metrics
map<string, string> room = 4;
}
message ExportMetricsCsvRequest {
string instance_filter = 1;
}
message ExportMetricsCsvResponse {
string csv = 1;
}
// ---------------------------------------------------------------------------
// AI Model Sync
// ---------------------------------------------------------------------------
@ -198,6 +226,8 @@ service SessionAdmin {
rpc GetUserInfo(GetUserInfoRequest) returns (GetUserInfoResponse);
rpc GetWorkspaceOnlineUsers(GetWorkspaceOnlineUsersRequest) returns (GetWorkspaceOnlineUsersResponse);
rpc IsUserOnline(IsUserOnlineRequest) returns (IsUserOnlineResponse);
rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse);
rpc ExportMetricsCsv(ExportMetricsCsvRequest) returns (ExportMetricsCsvResponse);
// AI
rpc SyncModels(SyncModelsRequest) returns (SyncModelsResponse);
rpc CheckAlerts(CheckAlertsRequest) returns (CheckAlertsResponse);