Metric history + Live metrics
This commit is contained in:
@@ -57,7 +57,7 @@ lz4_flex = { version = "0.11", default-features = false }
|
||||
rev_lines = "0.3.0"
|
||||
x509-parser = "0.16.0"
|
||||
quick-xml = "0.36"
|
||||
|
||||
memory-stats = "1.2.0"
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
|
||||
@@ -297,16 +297,26 @@ impl JMAP {
|
||||
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed))
|
||||
&& self.core.is_enterprise_edition()
|
||||
{
|
||||
if let Some(token) =
|
||||
req.uri().path().strip_prefix("/api/tracing/live/")
|
||||
if let Some((live_path, token)) = req
|
||||
.uri()
|
||||
.path()
|
||||
.strip_prefix("/api/telemetry/")
|
||||
.and_then(|p| {
|
||||
p.strip_suffix("traces/live/")
|
||||
.map(|t| ("traces", t))
|
||||
.or_else(|| {
|
||||
p.strip_suffix("metrics/live/")
|
||||
.map(|t| ("metrics", t))
|
||||
})
|
||||
})
|
||||
{
|
||||
let (account_id, _, _) =
|
||||
self.validate_access_token("live_tracing", token).await?;
|
||||
self.validate_access_token("live_telemetry", token).await?;
|
||||
|
||||
return self
|
||||
.handle_tracing_api_request(
|
||||
.handle_telemetry_api_request(
|
||||
&req,
|
||||
vec!["", "live"],
|
||||
vec!["", live_path, "live"],
|
||||
account_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
*
|
||||
*/
|
||||
|
||||
pub mod tracing;
|
||||
pub mod telemetry;
|
||||
pub mod undelete;
|
||||
|
||||
@@ -8,9 +8,15 @@
|
||||
*
|
||||
*/
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::telemetry::tracers::store::{TracingQuery, TracingStore};
|
||||
use common::telemetry::{
|
||||
metrics::store::{Metric, MetricsStore},
|
||||
tracers::store::{TracingQuery, TracingStore},
|
||||
};
|
||||
use directory::backend::internal::manage;
|
||||
use http_body_util::{combinators::BoxBody, StreamBody};
|
||||
use hyper::{
|
||||
@@ -23,7 +29,7 @@ use store::ahash::{AHashMap, AHashSet};
|
||||
use trc::{
|
||||
ipc::{bitset::Bitset, subscriber::SubscriberBuilder},
|
||||
serializers::json::JsonEventSerializer,
|
||||
DeliveryEvent, EventType, Key, QueueEvent, Value,
|
||||
Collector, DeliveryEvent, EventType, Key, MetricType, QueueEvent, Value,
|
||||
};
|
||||
use utils::{snowflake::SnowflakeIdGenerator, url_params::UrlParams};
|
||||
|
||||
@@ -36,7 +42,7 @@ use crate::{
|
||||
};
|
||||
|
||||
impl JMAP {
|
||||
pub async fn handle_tracing_api_request(
|
||||
pub async fn handle_telemetry_api_request(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
path: Vec<&str>,
|
||||
@@ -49,7 +55,7 @@ impl JMAP {
|
||||
path.get(2).copied(),
|
||||
req.method(),
|
||||
) {
|
||||
("spans", None, &Method::GET) => {
|
||||
("traces", None, &Method::GET) => {
|
||||
let page: usize = params.parse("page").unwrap_or(0);
|
||||
let limit: usize = params.parse("limit").unwrap_or(0);
|
||||
let mut tracing_query = Vec::new();
|
||||
@@ -100,12 +106,13 @@ impl JMAP {
|
||||
.and_then(SnowflakeIdGenerator::from_timestamp)
|
||||
.unwrap_or(0);
|
||||
let values = params.get("values").is_some();
|
||||
let store = self
|
||||
let store = &self
|
||||
.core
|
||||
.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.trace_store.as_ref())
|
||||
.ok_or_else(|| manage::unsupported("No tracing store has been configured"))?;
|
||||
.ok_or_else(|| manage::unsupported("No tracing store has been configured"))?
|
||||
.store;
|
||||
let span_ids = store.query_spans(&tracing_query, after, before).await?;
|
||||
|
||||
let (total, span_ids) = if limit > 0 {
|
||||
@@ -154,52 +161,7 @@ impl JMAP {
|
||||
.into_http_response())
|
||||
}
|
||||
}
|
||||
("span", id, &Method::GET) => {
|
||||
let store = self
|
||||
.core
|
||||
.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.trace_store.as_ref())
|
||||
.ok_or_else(|| manage::unsupported("No tracing store has been configured"))?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
for span_id in id
|
||||
.or_else(|| params.get("id"))
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
{
|
||||
if let Ok(span_id) = span_id.parse::<u64>() {
|
||||
events.push(
|
||||
JsonEventSerializer::new(store.get_span(span_id).await?)
|
||||
.with_description()
|
||||
.with_explanation(),
|
||||
);
|
||||
} else {
|
||||
events.push(JsonEventSerializer::new(Vec::new()));
|
||||
}
|
||||
}
|
||||
|
||||
if events.len() == 1 && id.is_some() {
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": events.into_iter().next().unwrap(),
|
||||
}))
|
||||
.into_http_response())
|
||||
} else {
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": events,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
}
|
||||
("live", Some("token"), &Method::GET) => {
|
||||
// Issue a live tracing token valid for 60 seconds
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.issue_custom_token(account_id, "live_tracing", "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("live", _, &Method::GET) => {
|
||||
("traces", Some("live"), &Method::GET) => {
|
||||
let mut key_filters = AHashMap::new();
|
||||
let mut filter = None;
|
||||
|
||||
@@ -327,6 +289,190 @@ impl JMAP {
|
||||
))),
|
||||
})
|
||||
}
|
||||
("trace", id, &Method::GET) => {
|
||||
let store = &self
|
||||
.core
|
||||
.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.trace_store.as_ref())
|
||||
.ok_or_else(|| manage::unsupported("No tracing store has been configured"))?
|
||||
.store;
|
||||
|
||||
let mut events = Vec::new();
|
||||
for span_id in id
|
||||
.or_else(|| params.get("id"))
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
{
|
||||
if let Ok(span_id) = span_id.parse::<u64>() {
|
||||
events.push(
|
||||
JsonEventSerializer::new(store.get_span(span_id).await?)
|
||||
.with_description()
|
||||
.with_explanation(),
|
||||
);
|
||||
} else {
|
||||
events.push(JsonEventSerializer::new(Vec::new()));
|
||||
}
|
||||
}
|
||||
|
||||
if events.len() == 1 && id.is_some() {
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": events.into_iter().next().unwrap(),
|
||||
}))
|
||||
.into_http_response())
|
||||
} else {
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": events,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
}
|
||||
("live", Some("token"), &Method::GET) => {
|
||||
// Issue a live telemetry token valid for 60 seconds
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self.issue_custom_token(account_id, "live_telemetry", "web", 60).await?,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("metrics", None, &Method::GET) => {
|
||||
let before = params
|
||||
.parse::<Timestamp>("before")
|
||||
.map(|t| t.into_inner())
|
||||
.unwrap_or(0);
|
||||
let after = params
|
||||
.parse::<Timestamp>("after")
|
||||
.map(|t| t.into_inner())
|
||||
.unwrap_or(0);
|
||||
let results = self
|
||||
.core
|
||||
.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.metrics_store.as_ref())
|
||||
.ok_or_else(|| manage::unsupported("No metrics store has been configured"))?
|
||||
.store
|
||||
.query_metrics(after, before)
|
||||
.await?;
|
||||
let mut metrics = Vec::with_capacity(results.len());
|
||||
|
||||
for metric in results {
|
||||
metrics.push(match metric {
|
||||
Metric::Counter {
|
||||
id,
|
||||
timestamp,
|
||||
value,
|
||||
} => Metric::Counter {
|
||||
id: id.name(),
|
||||
timestamp: DateTime::from_timestamp(timestamp as i64).to_rfc3339(),
|
||||
value,
|
||||
},
|
||||
Metric::Histogram {
|
||||
id,
|
||||
timestamp,
|
||||
count,
|
||||
sum,
|
||||
} => Metric::Histogram {
|
||||
id: id.name(),
|
||||
timestamp: DateTime::from_timestamp(timestamp as i64).to_rfc3339(),
|
||||
count,
|
||||
sum,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": metrics,
|
||||
}))
|
||||
.into_http_response())
|
||||
}
|
||||
("metrics", Some("live"), &Method::GET) => {
|
||||
let interval = Duration::from_secs(
|
||||
params
|
||||
.parse::<u64>("interval")
|
||||
.filter(|interval| *interval >= 1)
|
||||
.unwrap_or(30),
|
||||
);
|
||||
let mut event_types = AHashSet::new();
|
||||
let mut metric_types = AHashSet::new();
|
||||
for metric_name in params.get("metrics").unwrap_or_default().split(',') {
|
||||
let metric_name = metric_name.trim();
|
||||
if !metric_name.is_empty() {
|
||||
if let Some(event_type) = EventType::try_parse(metric_name) {
|
||||
event_types.insert(event_type);
|
||||
} else if let Some(metric_type) = MetricType::try_parse(metric_name) {
|
||||
metric_types.insert(metric_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse {
|
||||
status: StatusCode::OK,
|
||||
content_type: "text/event-stream".into(),
|
||||
content_disposition: "".into(),
|
||||
cache_control: "no-store".into(),
|
||||
body: HttpResponseBody::Stream(BoxBody::new(StreamBody::new(
|
||||
async_stream::stream! {
|
||||
|
||||
loop {
|
||||
let mut metrics = String::with_capacity(512);
|
||||
metrics.push_str("event: metrics\ndata: [");
|
||||
let mut is_first = true;
|
||||
|
||||
for counter in Collector::collect_counters(true) {
|
||||
if event_types.is_empty() || event_types.contains(&counter.id()) {
|
||||
if !is_first {
|
||||
metrics.push(',');
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
let _ = write!(
|
||||
&mut metrics,
|
||||
"{{\"id\":\"{}\",\"type\":\"counter\",\"value\":{}}}",
|
||||
counter.id().name(),
|
||||
counter.value()
|
||||
);
|
||||
}
|
||||
}
|
||||
for gauge in Collector::collect_gauges(true) {
|
||||
if metric_types.is_empty() || metric_types.contains(&gauge.id()) {
|
||||
if !is_first {
|
||||
metrics.push(',');
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
let _ = write!(
|
||||
&mut metrics,
|
||||
"{{\"id\":\"{}\",\"type\":\"gauge\",\"value\":{}}}",
|
||||
gauge.id().name(),
|
||||
gauge.get()
|
||||
);
|
||||
}
|
||||
}
|
||||
for histogram in Collector::collect_histograms(true) {
|
||||
if metric_types.is_empty() || metric_types.contains(&histogram.id()) {
|
||||
if !is_first {
|
||||
metrics.push(',');
|
||||
} else {
|
||||
is_first = false;
|
||||
}
|
||||
let _ = write!(
|
||||
&mut metrics,
|
||||
"{{\"id\":\"{}\",\"type\":\"histogram\",\"count\":{},\"sum\":{}}}",
|
||||
histogram.id().name(),
|
||||
histogram.count(),
|
||||
histogram.sum()
|
||||
);
|
||||
}
|
||||
}
|
||||
metrics.push_str("]\n\n");
|
||||
|
||||
yield Ok(Frame::data(Bytes::from(metrics)));
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
},
|
||||
))),
|
||||
})
|
||||
}
|
||||
_ => Err(trc::ResourceEvent::NotFound.into_err()),
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ impl JMAP {
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
"tracing" if is_superuser => {
|
||||
"telemetry" if is_superuser => {
|
||||
// WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED
|
||||
// Any attempt to modify, bypass, or disable this license validation mechanism
|
||||
// constitutes a severe violation of the Stalwart Enterprise License Agreement.
|
||||
@@ -94,7 +94,7 @@ impl JMAP {
|
||||
// for copyright infringement, breach of contract, and fraud.
|
||||
|
||||
if self.core.is_enterprise_edition() {
|
||||
self.handle_tracing_api_request(req, path, access_token.primary_id())
|
||||
self.handle_telemetry_api_request(req, path, access_token.primary_id())
|
||||
.await
|
||||
} else {
|
||||
Err(manage::enterprise())
|
||||
|
||||
@@ -12,14 +12,17 @@ use std::{
|
||||
use common::{config::telemetry::OtelMetrics, IPC_CHANNEL_BUFFER};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use common::telemetry::tracers::store::TracingStore;
|
||||
use common::telemetry::{
|
||||
metrics::store::{MetricsStore, SharedMetricHistory},
|
||||
tracers::store::TracingStore,
|
||||
};
|
||||
|
||||
use store::{
|
||||
write::{now, purge::PurgeStore},
|
||||
BlobStore, LookupStore, Store,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use trc::HousekeeperEvent;
|
||||
use trc::{Collector, HousekeeperEvent, MetricType};
|
||||
use utils::map::ttl_dashmap::TtlMap;
|
||||
|
||||
use crate::{Inner, JmapInstance, JMAP, LONG_SLUMBER};
|
||||
@@ -55,6 +58,9 @@ enum ActionClass {
|
||||
Acme(String),
|
||||
OtelMetrics,
|
||||
#[cfg(feature = "enterprise")]
|
||||
InternalMetrics,
|
||||
CalculateMetrics,
|
||||
#[cfg(feature = "enterprise")]
|
||||
ReloadSettings,
|
||||
}
|
||||
|
||||
@@ -99,6 +105,9 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
|
||||
queue.schedule(Instant::now() + otel.interval, ActionClass::OtelMetrics);
|
||||
}
|
||||
|
||||
// Calculate expensive metrics
|
||||
queue.schedule(Instant::now(), ActionClass::CalculateMetrics);
|
||||
|
||||
// Add all ACME renewals to heap
|
||||
for provider in core_.tls.acme_providers.values() {
|
||||
match core_.init_acme(provider).await {
|
||||
@@ -125,10 +134,21 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
|
||||
Instant::now() + enterprise.license.expires_in(),
|
||||
ActionClass::ReloadSettings,
|
||||
);
|
||||
|
||||
if let Some(metrics_store) = enterprise.metrics_store.as_ref() {
|
||||
queue.schedule(
|
||||
Instant::now() + metrics_store.interval.time_to_next(),
|
||||
ActionClass::InternalMetrics,
|
||||
);
|
||||
}
|
||||
}
|
||||
// SPDX-SnippetEnd
|
||||
}
|
||||
|
||||
// Metrics history
|
||||
#[cfg(feature = "enterprise")]
|
||||
let metrics_history = SharedMetricHistory::default();
|
||||
|
||||
loop {
|
||||
match tokio::time::timeout(queue.wake_up_time(), rx.recv()).await {
|
||||
Ok(Some(event)) => match event {
|
||||
@@ -185,12 +205,21 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
let trace_hold_period = core
|
||||
let trace_retention = core
|
||||
.core
|
||||
.load()
|
||||
.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.trace_hold_period);
|
||||
.and_then(|e| e.trace_store.as_ref())
|
||||
.and_then(|t| t.retention);
|
||||
#[cfg(feature = "enterprise")]
|
||||
let metrics_retention = core
|
||||
.core
|
||||
.load()
|
||||
.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.metrics_store.as_ref())
|
||||
.and_then(|m| m.retention);
|
||||
// SPDX-SnippetEnd
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -206,11 +235,18 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
if let Some(trace_hold_period) = trace_hold_period {
|
||||
if let Err(err) = store.purge_spans(trace_hold_period).await {
|
||||
if let Some(trace_retention) = trace_retention {
|
||||
if let Err(err) = store.purge_spans(trace_retention).await {
|
||||
trc::error!(err.details("Failed to purge tracing spans"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
if let Some(metrics_retention) = metrics_retention {
|
||||
if let Err(err) = store.purge_metrics(metrics_retention).await {
|
||||
trc::error!(err.details("Failed to purge metrics"));
|
||||
}
|
||||
}
|
||||
// SPDX-SnippetEnd
|
||||
});
|
||||
}
|
||||
@@ -382,10 +418,83 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) {
|
||||
});
|
||||
}
|
||||
}
|
||||
ActionClass::CalculateMetrics => {
|
||||
// Calculate expensive metrics every 5 minutes
|
||||
queue.schedule(
|
||||
Instant::now() + Duration::from_secs(5 * 60),
|
||||
ActionClass::OtelMetrics,
|
||||
);
|
||||
|
||||
let core = core_.clone();
|
||||
tokio::spawn(async move {
|
||||
#[cfg(feature = "enterprise")]
|
||||
if core.is_enterprise_edition() {
|
||||
// Obtain queue size
|
||||
match core.message_queue_size().await {
|
||||
Ok(total) => {
|
||||
Collector::update_gauge(
|
||||
MetricType::QueueCount,
|
||||
total,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.details("Failed to obtain queue size")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match tokio::task::spawn_blocking(memory_stats::memory_stats)
|
||||
.await
|
||||
{
|
||||
Ok(Some(stats)) => {
|
||||
Collector::update_gauge(
|
||||
MetricType::ServerMemory,
|
||||
stats.physical_mem as u64,
|
||||
);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
trc::error!(trc::EventType::Server(
|
||||
trc::ServerEvent::ThreadError,
|
||||
)
|
||||
.reason(err)
|
||||
.caused_by(trc::location!())
|
||||
.details("Join Error"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// SPDX-SnippetBegin
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
ActionClass::InternalMetrics => {
|
||||
if let Some(metrics_store) = &core_
|
||||
.enterprise
|
||||
.as_ref()
|
||||
.and_then(|e| e.metrics_store.as_ref())
|
||||
{
|
||||
queue.schedule(
|
||||
Instant::now() + metrics_store.interval.time_to_next(),
|
||||
ActionClass::InternalMetrics,
|
||||
);
|
||||
|
||||
let metrics_store = metrics_store.store.clone();
|
||||
let metrics_history = metrics_history.clone();
|
||||
let core = core_.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) =
|
||||
metrics_store.write_metrics(core, metrics_history).await
|
||||
{
|
||||
trc::error!(err.details("Failed to write metrics"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
ActionClass::ReloadSettings => {
|
||||
match core_.reload().await {
|
||||
|
||||
Reference in New Issue
Block a user