diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index d45e2c44..9030f2e5 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -66,7 +66,7 @@ runs: test -e ~/.cargo/bin/rnr || cargo install rnr test -e ~/.cargo/bin/cargo-nextest || cargo install cargo-nextest test -e ~/.cargo/bin/cargo-binstall || cargo install cargo-binstall - test -e ~/.cargo/bin/dx || cargo binstall dioxus-cli -y + test -e ~/.cargo/bin/dx || cargo binstall dioxus-cli@0.7.0 -y test -e ~/.cargo/bin/trunk || cargo install trunk --locked - name: Install Python Build Dependencies diff --git a/.gitignore b/.gitignore index 81daedfb..82b6fd80 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ pkg/ .VSCodeCounter/ *.pyc venv/ +python/probing/probing diff --git a/Cargo.lock b/Cargo.lock index 3ba7fe9f..e5e349c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3203,7 +3203,7 @@ dependencies = [ [[package]] name = "probing" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "arrow", @@ -3222,7 +3222,7 @@ dependencies = [ [[package]] name = "probing-cc" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "async-trait", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "probing-cli" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "clap 4.5.38", @@ -3266,7 +3266,7 @@ dependencies = [ [[package]] name = "probing-core" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "arrow", @@ -3291,7 +3291,7 @@ dependencies = [ [[package]] name = "probing-macros" -version = "0.2.2" +version = "0.2.3" dependencies = [ "probing-core", "quote", @@ -3300,7 +3300,7 @@ dependencies = [ [[package]] name = "probing-proto" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "arrow", @@ -3316,7 +3316,7 @@ dependencies = [ [[package]] name = "probing-python" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "async-trait", @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "probing-server" -version = "0.2.2" +version = "0.2.3" dependencies = [ "anyhow", "axum", @@ -3364,6 +3364,7 @@ dependencies = [ "probing-proto", "probing-python", "procfs", + "serde", "serde_json", "serde_urlencoded", "tempfile", @@ -3373,7 +3374,7 @@ dependencies = [ [[package]] name = "probing-store" -version = "0.2.2" +version = "0.2.3" dependencies = [ "thiserror 2.0.12", "tokio", diff --git a/probing/extensions/cc/src/extensions/taskstats.rs b/probing/extensions/cc/src/extensions/taskstats.rs index 2facb44c..f9b596bd 100644 --- a/probing/extensions/cc/src/extensions/taskstats.rs +++ b/probing/extensions/cc/src/extensions/taskstats.rs @@ -13,7 +13,7 @@ mod datasrc; #[derive(Debug, Default, EngineExtension)] pub struct TaskStatsExtension { /// Task statistics collection interval in milliseconds (0 to disable) - #[option(aliases=["taskstats_interval"])] + #[option(aliases=["taskstats_interval", "task.stats.interval"])] task_stats_interval: Maybe, } diff --git a/probing/extensions/cc/src/extensions/taskstats/datasrc.rs b/probing/extensions/cc/src/extensions/taskstats/datasrc.rs index 3daacd07..65f07947 100644 --- a/probing/extensions/cc/src/extensions/taskstats/datasrc.rs +++ b/probing/extensions/cc/src/extensions/taskstats/datasrc.rs @@ -158,7 +158,7 @@ impl CustomNamespace for TaskStatsSchema { Field::new("cpu_utime", DataType::Int64, false), Field::new("cpu_stime", DataType::Int64, false), ]))), - data: Default::default(), + data: Self::data(expr), }), _ => Arc::new(probing_core::core::LazyTableSource { name: expr.to_string(), diff --git a/probing/proto/src/dto/basic.rs b/probing/proto/src/dto/basic.rs new file mode 100644 index 00000000..155b82f8 --- /dev/null +++ b/probing/proto/src/dto/basic.rs @@ -0,0 +1,120 @@ +use std::fmt::{Display, Formatter}; +use std::time::{Duration, SystemTime}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Element type enumeration for DTO +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +pub enum EleType { + Nil, + BOOL, + I32, + I64, + F32, + F64, + Text, + Url, + DataTime, +} + +/// Element value enumeration for DTO +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +pub enum Ele { + Nil, + BOOL(bool), + I32(i32), + I64(i64), + F32(f32), + F64(f64), + Text(String), + Url(String), + DataTime(u64), +} + +impl Display for Ele { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Ele::Nil => f.write_str("nil"), + Ele::BOOL(x) => f.write_fmt(format_args!("{x}")), + Ele::I32(x) => f.write_fmt(format_args!("{x}")), + Ele::I64(x) => f.write_fmt(format_args!("{x}")), + Ele::F32(x) => f.write_fmt(format_args!("{x}")), + Ele::F64(x) => f.write_fmt(format_args!("{x}")), + Ele::Text(x) => f.write_fmt(format_args!("{x}")), + Ele::Url(x) => f.write_fmt(format_args!("{x}")), + Ele::DataTime(x) => { + let datetime: DateTime = + (SystemTime::UNIX_EPOCH + Duration::from_micros(*x)).into(); + f.write_fmt(format_args!("{}", datetime.to_rfc3339())) + } + } + } +} + +/// Sequence of elements for DTO +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +#[serde(tag = "type", content = "value")] +pub enum Seq { + Nil, + SeqBOOL(Vec), + SeqI32(Vec), + SeqI64(Vec), + SeqF32(Vec), + SeqF64(Vec), + SeqText(Vec), + SeqDateTime(Vec), +} + +impl Seq { + pub fn len(&self) -> usize { + match self { + Seq::SeqBOOL(vec) => vec.len(), + Seq::SeqI32(vec) => vec.len(), + Seq::SeqI64(vec) => vec.len(), + Seq::SeqF32(vec) => vec.len(), + Seq::SeqF64(vec) => vec.len(), + Seq::SeqText(vec) => vec.len(), + Seq::SeqDateTime(vec) => vec.len(), + Seq::Nil => 0, + } + } + + pub fn is_empty(&self) -> bool { + match self { + Seq::Nil => true, + other => other.len() == 0, + } + } + + pub fn get(&self, idx: usize) -> Ele { + match self { + Seq::SeqBOOL(vec) => vec.get(idx).map(|x| Ele::BOOL(*x)), + Seq::SeqI32(vec) => vec.get(idx).map(|x| Ele::I32(*x)), + Seq::SeqI64(vec) => vec.get(idx).map(|x| Ele::I64(*x)), + Seq::SeqF32(vec) => vec.get(idx).map(|x| Ele::F32(*x)), + Seq::SeqF64(vec) => vec.get(idx).map(|x| Ele::F64(*x)), + Seq::SeqText(vec) => vec.get(idx).map(|x| Ele::Text(x.clone())), + Seq::SeqDateTime(vec) => vec.get(idx).map(|x| Ele::DataTime(*x)), + Seq::Nil => None, + } + .unwrap_or(Ele::Nil) + } +} + +/// Value representation for DTO +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, Clone)] +pub struct Value { + pub id: u64, + pub class: String, + pub shape: Option, + pub dtype: Option, + pub device: Option, + pub value: Option, +} + +impl Display for Value { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "value: {:?}", self.value) + } +} diff --git a/probing/proto/src/dto/dataframe.rs b/probing/proto/src/dto/dataframe.rs new file mode 100644 index 00000000..ee8e3ee5 --- /dev/null +++ b/probing/proto/src/dto/dataframe.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +use super::basic::Seq; + +/// Data frame structure for DTO +#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Clone)] +pub struct DataFrame { + pub names: Vec, + pub cols: Vec, + pub size: u64, +} + +impl DataFrame { + pub fn new(names: Vec, columns: Vec) -> Self { + DataFrame { + names, + cols: columns, + size: 0, + } + } + + pub fn len(&self) -> usize { + if self.cols.is_empty() { + return 0; + } + self.cols[0].len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&'_ self) -> DataFrameIterator<'_> { + DataFrameIterator { + df: self, + current: 0, + } + } +} + +pub struct DataFrameIterator<'a> { + df: &'a DataFrame, + current: usize, +} + +impl Iterator for DataFrameIterator<'_> { + type Item = Vec; + + fn next(&mut self) -> Option { + if self.current >= self.df.len() { + None + } else { + let mut row = vec![]; + for i in 0..self.df.cols.len() { + row.push(self.df.cols[i].get(self.current)); + } + self.current += 1; + Some(row) + } + } +} diff --git a/probing/proto/src/dto/mod.rs b/probing/proto/src/dto/mod.rs new file mode 100644 index 00000000..41085e44 --- /dev/null +++ b/probing/proto/src/dto/mod.rs @@ -0,0 +1,5 @@ +//! DTO (Data Transfer Object) modules +pub mod basic; +pub mod dataframe; +pub mod query; +pub mod time_series; diff --git a/probing/proto/src/dto/query.rs b/probing/proto/src/dto/query.rs new file mode 100644 index 00000000..6f990541 --- /dev/null +++ b/probing/proto/src/dto/query.rs @@ -0,0 +1,212 @@ +//! Query DTO (Data Transfer Object) definitions for the query API +//! +//! These structures provide a stable interface for external clients +//! while allowing internal implementations to evolve independently. + +use serde::{Deserialize, Serialize}; + +/// Query request DTO for external API clients +#[derive(Debug, Deserialize, Serialize)] +pub struct QueryRequestDto { + /// SQL expression or query command + pub expr: String, + + /// Optional query options + pub opts: Option, +} + +/// Query options DTO +#[derive(Debug, Deserialize, Serialize)] +pub struct QueryOptionsDto { + /// Maximum number of rows to return + pub limit: Option, +} + +impl QueryRequestDto { + /// Create a new query request DTO + pub fn new(expr: String) -> Self { + Self { expr, opts: None } + } + + /// Create a new query request DTO with options + pub fn with_options(expr: String, limit: Option) -> Self { + Self { + expr, + opts: Some(QueryOptionsDto { limit }), + } + } +} + +/// Query response DTO for external API clients +#[derive(Debug, Serialize)] +pub struct QueryResponseDto { + /// Query result data + pub payload: QueryDataDto, + + /// Response timestamp in microseconds since epoch + pub timestamp: u64, + + /// Indicates if the query was successful + pub success: bool, + + /// Optional message for error cases + pub message: Option, +} + +/// Query data variants for response DTO +#[derive(Debug, Serialize)] +#[serde(tag = "type", content = "value")] +pub enum QueryDataDto { + /// Empty result (e.g., for SET commands) + Nil, + + /// Error response + Error { + /// Error code + code: String, + + /// Error message + message: String, + }, + + /// Data frame result + DataFrame(super::dataframe::DataFrame), + + /// Time series result + TimeSeries(super::time_series::TimeSeries), +} + +impl QueryResponseDto { + /// Create a successful response with payload + pub fn success(payload: QueryDataDto) -> Self { + Self { + payload, + timestamp: Self::now(), + success: true, + message: None, + } + } + + /// Create an error response + pub fn error(code: String, message: String) -> Self { + Self { + payload: QueryDataDto::Error { + code, + message: message.clone(), + }, + timestamp: Self::now(), + success: false, + message: Some(message), + } + } + + /// Create a nil response + pub fn nil() -> Self { + Self { + payload: QueryDataDto::Nil, + timestamp: Self::now(), + success: true, + message: None, + } + } + + #[cfg(not(target_arch = "wasm32"))] + fn now() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_micros() as u64 + } + + #[cfg(target_arch = "wasm32")] + fn now() -> u64 { + 0 + } +} + +/// Convert internal Query to DTO +impl From for QueryRequestDto { + fn from(query: crate::protocol::query::Query) -> Self { + Self { + expr: query.expr, + opts: query.opts.map(|opts| QueryOptionsDto { limit: opts.limit }), + } + } +} + +/// Convert DTO to internal Query +impl From for crate::protocol::query::Query { + fn from(dto: QueryRequestDto) -> Self { + Self { + expr: dto.expr, + opts: dto + .opts + .map(|opts| crate::protocol::query::Options { limit: opts.limit }), + } + } +} + +/// Convert internal Ele to DTO Ele +fn convert_ele(ele: crate::types::basic::Ele) -> super::basic::Ele { + match ele { + crate::types::basic::Ele::Nil => super::basic::Ele::Nil, + crate::types::basic::Ele::BOOL(x) => super::basic::Ele::BOOL(x), + crate::types::basic::Ele::I32(x) => super::basic::Ele::I32(x), + crate::types::basic::Ele::I64(x) => super::basic::Ele::I64(x), + crate::types::basic::Ele::F32(x) => super::basic::Ele::F32(x), + crate::types::basic::Ele::F64(x) => super::basic::Ele::F64(x), + crate::types::basic::Ele::Text(x) => super::basic::Ele::Text(x), + crate::types::basic::Ele::Url(x) => super::basic::Ele::Url(x), + crate::types::basic::Ele::DataTime(x) => super::basic::Ele::DataTime(x), + } +} + +/// Convert internal Data to DTO +impl From for QueryDataDto { + fn from(data: crate::protocol::query::Data) -> Self { + match data { + crate::protocol::query::Data::Nil => QueryDataDto::Nil, + crate::protocol::query::Data::Error(error) => QueryDataDto::Error { + code: format!("{:?}", error.code), + message: error.message, + }, + crate::protocol::query::Data::DataFrame(df) => { + let cols = df + .cols + .into_iter() + .map(|seq| match seq { + crate::types::Seq::Nil => super::basic::Seq::Nil, + crate::types::Seq::SeqBOOL(vec) => super::basic::Seq::SeqBOOL(vec), + crate::types::Seq::SeqI32(vec) => super::basic::Seq::SeqI32(vec), + crate::types::Seq::SeqI64(vec) => super::basic::Seq::SeqI64(vec), + crate::types::Seq::SeqF32(vec) => super::basic::Seq::SeqF32(vec), + crate::types::Seq::SeqF64(vec) => super::basic::Seq::SeqF64(vec), + crate::types::Seq::SeqText(vec) => super::basic::Seq::SeqText(vec), + crate::types::Seq::SeqDateTime(vec) => super::basic::Seq::SeqDateTime(vec), + }) + .collect(); + + QueryDataDto::DataFrame(super::dataframe::DataFrame { + names: df.names, + cols, + size: df.size, + }) + } + crate::protocol::query::Data::TimeSeries(ts) => { + let timestamp = ts.timestamp.iter().map(convert_ele).collect(); + let cols = ts + .cols + .into_iter() + .map(|series| series.iter().map(convert_ele).collect()) + .collect(); + + QueryDataDto::TimeSeries(super::time_series::TimeSeries { + names: ts.names, + timestamp, + cols, + }) + } + } + } +} diff --git a/probing/proto/src/dto/time_series.rs b/probing/proto/src/dto/time_series.rs new file mode 100644 index 00000000..3570c667 --- /dev/null +++ b/probing/proto/src/dto/time_series.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; + +use super::basic::Ele; + +/// Time series structure for DTO +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +pub struct TimeSeries { + pub names: Vec, + pub timestamp: Vec, + pub cols: Vec>, +} + +impl TimeSeries { + pub fn len(&self) -> usize { + self.timestamp.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&'_ self) -> TimeSeriesIter<'_> { + TimeSeriesIter { + timestamp: self.timestamp.iter(), + cols: self.cols.iter().map(|s| s.iter()).collect(), + } + } + + pub fn take(&self, limit: Option) -> Vec<(Ele, Vec)> { + let iter = self.iter(); + if let Some(limit) = limit { + iter.take(limit).collect::>() + } else { + iter.collect::>() + } + } +} + +pub struct TimeSeriesIter<'a> { + timestamp: std::slice::Iter<'a, Ele>, + cols: Vec>, +} + +impl Iterator for TimeSeriesIter<'_> { + type Item = (Ele, Vec); + + fn next(&mut self) -> Option { + let timestamp = self.timestamp.next()?.clone(); + let cols = self + .cols + .iter_mut() + .map(|s| s.next().cloned()) + .collect::>>()?; + Some((timestamp, cols)) + } +} diff --git a/probing/proto/src/lib.rs b/probing/proto/src/lib.rs index 06128883..1574f3cd 100644 --- a/probing/proto/src/lib.rs +++ b/probing/proto/src/lib.rs @@ -1,3 +1,4 @@ +pub mod dto; pub mod protocol; pub mod types; @@ -24,4 +25,7 @@ pub mod prelude { // --- Error Handling --- pub use crate::types::ProtoError; + + // --- DTO Structures --- + pub use crate::dto::query::{QueryDataDto, QueryOptionsDto, QueryRequestDto, QueryResponseDto}; } diff --git a/probing/server/Cargo.toml b/probing/server/Cargo.toml index 6c71a5dd..c9c0b9d6 100644 --- a/probing/server/Cargo.toml +++ b/probing/server/Cargo.toml @@ -20,6 +20,7 @@ anyhow = { workspace = true } log = { workspace = true } nix = { workspace = true } once_cell = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/probing/server/src/server/mod.rs b/probing/server/src/server/mod.rs index 1bf82a41..3defe2df 100644 --- a/probing/server/src/server/mod.rs +++ b/probing/server/src/server/mod.rs @@ -1,4 +1,5 @@ mod apis; +mod query_dto; mod repl; pub mod cluster; @@ -69,6 +70,7 @@ fn build_app(auth: bool) -> axum::Router { .route("/chrome-tracing", axum::routing::get(index)) .route("/index.html", axum::routing::get(index)) .route("/query", axum::routing::post(query)) + .route("/query/dto", axum::routing::post(query_dto::query_dto)) .route( "/config/{config_key}", axum::routing::get(get_config_value_handler), diff --git a/probing/server/src/server/query_dto.rs b/probing/server/src/server/query_dto.rs new file mode 100644 index 00000000..7b25422f --- /dev/null +++ b/probing/server/src/server/query_dto.rs @@ -0,0 +1,95 @@ +//! Query DTO handler functions +//! +//! This module contains all the functions related to handling query DTOs, +//! separated from the main server module for better organization. + +use axum::http::StatusCode; +use axum::response::IntoResponse; +use probing_proto::protocol::message::Message; +use probing_proto::protocol::query::{Data as ProtoData, Query as ProtoQuery}; +use serde_json; + +/// HTTP handler wrapper for query endpoint with DTO interface +/// This provides a stable external API while keeping the internal implementation unchanged +#[axum::debug_handler] +pub async fn query_dto( + axum::extract::Json(request_dto): axum::extract::Json< + probing_proto::dto::query::QueryRequestDto, + >, +) -> impl IntoResponse { + handle_query_dto(request_dto).await +} + +/// Handle query DTO processing and convert to internal format +async fn handle_query_dto( + request_dto: probing_proto::dto::query::QueryRequestDto, +) -> impl IntoResponse { + // Convert DTO to internal Query structure + let query: ProtoQuery = request_dto.into(); + + // Wrap in Message for internal processing + let message = Message::new(query); + + // Serialize to JSON string for existing engine interface + match serde_json::to_string(&message) { + Ok(json_request) => process_engine_query(json_request).await, + Err(e) => ( + StatusCode::BAD_REQUEST, + format!("Failed to serialize request: {}", e), + ) + .into_response(), + } +} + +/// Process the engine query and convert response to DTO format +async fn process_engine_query(json_request: String) -> axum::response::Response { + match crate::engine::query(json_request).await { + Ok(response_json) => convert_engine_response_to_dto(response_json).await, + Err(api_error) => convert_engine_error_to_dto(api_error).await, + } +} + +/// Convert engine response to DTO format +async fn convert_engine_response_to_dto(response_json: String) -> axum::response::Response { + // Parse the response to convert to DTO format + match serde_json::from_str::>(&response_json) { + Ok(message_response) => { + let response_dto = probing_proto::dto::query::QueryResponseDto::success( + message_response.payload.into(), + ); + + match serde_json::to_string(&response_dto) { + Ok(dto_response_json) => (StatusCode::OK, dto_response_json).into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to serialize DTO response: {}", e), + ) + .into_response(), + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to parse engine response: {}", e), + ) + .into_response(), + } +} + +/// Convert engine error to DTO error response +async fn convert_engine_error_to_dto( + api_error: crate::server::error::ApiError, +) -> axum::response::Response { + // Convert ApiError to DTO error response + let error_response = probing_proto::dto::query::QueryResponseDto::error( + "INTERNAL_ERROR".to_string(), + format!("Engine error: {}", api_error.0), + ); + match serde_json::to_string(&error_response) { + Ok(error_json) => (StatusCode::INTERNAL_SERVER_ERROR, error_json).into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to serialize error response: {}", e), + ) + .into_response(), + } +} diff --git a/src/lib.rs b/src/lib.rs index 7deecd53..edb8ede8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ extern crate ctor; use anyhow::Result; use pyo3::prelude::*; +use std::net::ToSocketAddrs; use probing_python::extensions::python::ExternalTable; use probing_python::features::config; @@ -23,19 +24,19 @@ const ENV_PROBING_PORT: &str = "PROBING_PORT"; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; pub fn get_hostname() -> Result { - let ips = nix::ifaddrs::getifaddrs()? - .filter_map(|addr| addr.address) - .filter_map(|addr| addr.as_sockaddr_in().cloned()) - .filter_map(|addr| { - let ip_addr = addr.ip(); - match ip_addr.is_unspecified() { - true => None, - false => Some(ip_addr.to_string()), + // Pod environment - prioritize IP environment variables + let ip_env_vars = ["POD_IP"]; + for env_var in &ip_env_vars { + if let Ok(ip) = std::env::var(env_var) { + if !ip.is_empty() && ip != "None" { + log::debug!("Using IP from environment variable {env_var}: {ip}"); + return Ok(ip); } - }) - .collect::>(); + } + } + + let ips = get_network_interfaces()?; - // Check for address pattern match from environment variable if let Ok(pattern) = std::env::var("PROBING_SERVER_ADDRPATTERN") { for ip in ips.iter() { if ip.starts_with(pattern.as_str()) { @@ -46,12 +47,28 @@ pub fn get_hostname() -> Result { } } - // Return first IP if no pattern match found ips.first() .cloned() .ok_or_else(|| anyhow::anyhow!("No suitable IP address found")) } +fn get_network_interfaces() -> Result> { + let ips = nix::ifaddrs::getifaddrs()? + .filter_map(|addr| addr.address) + .filter_map(|addr| addr.as_sockaddr_in().cloned()) + .filter_map(|addr| { + let ip_addr = addr.ip(); + match ip_addr.is_unspecified() { + true => None, + false => Some(ip_addr.to_string()), + } + }) + .collect::>(); + + log::debug!("Found network interface IPs: {:?}", ips); + Ok(ips) +} + /// Setup environment variables for server configuration fn setup_env_settings() { let mut report_port_basis: Option = None;