use crate::alerts::AlertCondition; use crate::data_value::{MetricPrimitiveKind, RegexToMetricPrimitiveMatcher}; use crate::metric_primitive::MetricPrimitive; use crate::profile::{ChartType, Dashboard, Panel, Profile}; use crate::settings::{ColorScheme, CssEntry, Settings}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; #[derive(Debug, Clone, Serialize)] pub struct LogEntryDto { pub sequence: u64, pub timestamp_ms: i64, pub text: String, /// Only params whose regex matched (and parsed as the declared kind) are present — omitted rather than sent as null, so the frontend can render a blank for anything missing. pub params: BTreeMap, } #[derive(Debug, Clone, Serialize)] pub struct ParamDeclarationDto { pub name: String, pub message_match_regex: String, pub capture_group: String, pub kind: MetricPrimitiveKind, } #[derive(Debug, Clone, Serialize)] pub struct MetricDeclarationDto { pub name: String, pub message_match_regex: String, pub params: Vec, /// See `crate::engine::DeclareMetricRequest::matched_labels`. pub matched_labels: Option>, /// See `crate::engine::DeclareMetricRequest::unmatched_labels`. pub unmatched_labels: Option>, } #[derive(Debug, Clone, Serialize)] pub struct TimelineBucketDto { pub start_ms: i64, pub count: u64, } #[derive(Debug, Clone, Serialize)] pub struct StatsDto { pub log_count: u64, pub first_sequence: Option, pub last_sequence: Option, pub first_timestamp_ms: Option, pub last_timestamp_ms: Option, /// Ticket 011: a running, *approximate* estimate of the in-memory heap /// held by currently-ingested logs, in bytes. "Approximate" because it /// only counts each `IngestedLine::text`'s UTF-8 byte length — it does /// not account for `String`'s allocator overhead/capacity slack, the /// `BTreeMap`/`IndexKey` node overhead the text is stored under, or any /// derived data (bucket values, params) the line produces elsewhere in /// the index. That's judged the best cheap proxy for "how much memory is /// this log's text costing us": it's O(1) to compute per log (`text.len()`, /// already an interned byte count for UTF-8 `String`), moves in lockstep /// with the dominant cost (raw log text, which is usually much larger /// than the fixed per-entry bookkeeping), and needs no per-entry /// bookkeeping beyond what eviction already tracks. pub approx_bytes_total: u64, } #[derive(Debug, Deserialize)] pub struct LogsParams { pub since_sequence: Option, pub start_ms: Option, pub end_ms: Option, pub limit: Option, /// Raw FilterBar query string, e.g. `regex("error") and sequence<5`. pub filter: Option, } #[derive(Debug, Deserialize)] pub struct TimelineParams { pub start_ms: Option, pub end_ms: Option, pub bucket_ms: Option, /// Same FilterBar query string as `LogsParams::filter`; only matching logs are counted into timeline buckets. pub filter: Option, } /// One (sequence, timestamp, value) occurrence within a metric's bucket for a given label combination. #[derive(Debug, Clone, Serialize)] pub struct BucketValueDto { pub labels: BTreeMap, pub sequence: u64, pub timestamp_ms: i64, pub value: MetricPrimitive, } /// `labels` is a comma-separated `key=value` list (not repeated params) since axum's `Query` extractor needs a fixed set of field names. #[derive(Debug, Deserialize)] pub struct BucketValuesParams { pub labels: Option, pub since_sequence: Option, pub start_ms: Option, pub end_ms: Option, pub limit: Option, } /// `labels` uses plain `String` (already Display-formatted) since charts just need something to show; `value` is always `f64` for plottability. #[derive(Debug, Clone, Serialize, PartialEq)] pub struct PromqlSeriesDto { pub labels: BTreeMap, pub points: Vec<(i64, f64)>, } #[derive(Debug, Deserialize)] pub struct PromqlParams { pub promql: String, pub start_ms: Option, pub end_ms: Option, } /// JSON error body returned by API handlers on a 4xx/5xx response. #[derive(Debug, Serialize)] pub struct ErrorDto { pub error: String, } /// `is_theme` marks this as a selectable colour-scheme theme rather than arbitrary CSS; defaults to false. #[derive(Debug, Deserialize)] pub struct ImportCssRequest { pub filename: String, pub content: String, #[serde(default)] pub is_theme: bool, } /// A reference to an external CSS path/URL that ottyl doesn't copy or own. #[derive(Debug, Deserialize)] pub struct LinkCssRequest { pub path_or_url: String, #[serde(default)] pub is_theme: bool, } /// `id` is the real id for `Imported` entries or a stable id derived from the path/URL for `Linked` ones, so the frontend can key both kinds uniformly (only `Imported` entries are deletable/content-fetchable). #[derive(Debug, Clone, Serialize)] pub struct CssEntryDto { pub id: String, pub kind: CssEntryKind, pub filename: Option, pub path_or_url: Option, pub is_theme: bool, } #[derive(Debug, Clone, Copy, Serialize)] #[serde(rename_all = "lowercase")] pub enum CssEntryKind { Imported, Linked, } impl From<&CssEntry> for CssEntryDto { fn from(entry: &CssEntry) -> Self { match entry { CssEntry::Imported { id, filename, is_theme } => CssEntryDto { id: id.clone(), kind: CssEntryKind::Imported, filename: Some(filename.clone()), path_or_url: None, is_theme: *is_theme, }, CssEntry::Linked { path_or_url, is_theme } => CssEntryDto { id: crate::settings::css_entry_id(entry), kind: CssEntryKind::Linked, filename: None, path_or_url: Some(path_or_url.clone()), is_theme: *is_theme, }, } } } /// `css_entries` is omitted here since it has its own `GET /api/settings/css` endpoint. #[derive(Debug, Clone, Serialize)] pub struct SettingsDto { pub color_scheme: ColorScheme, pub active_profile: Option, /// `None` means unlimited. pub max_logs: Option, /// Id of the selected custom theme entry, if any. `None` means `color_scheme` alone determines the active palette. pub custom_theme_id: Option, /// Ntfy topic every alert rule delivers to; `None` until configured. pub ntfy_topic: Option, /// Ntfy server override; `None` means `https://ntfy.sh`. pub ntfy_server: Option, } impl From<&Settings> for SettingsDto { fn from(settings: &Settings) -> Self { SettingsDto { color_scheme: settings.color_scheme, active_profile: settings.profiles.active_profile.clone(), max_logs: settings.max_logs, custom_theme_id: settings.custom_theme_id.clone(), ntfy_topic: settings.ntfy_topic.clone(), ntfy_server: settings.ntfy_server.clone(), } } } /// `max_logs`/`custom_theme_id`/`ntfy_topic`/`ntfy_server` distinguish "omitted" (leave unchanged) from "present" (null clears, value sets), via `deserialize_double_option`. #[derive(Debug, Deserialize)] pub struct UpdateSettingsRequest { pub color_scheme: Option, #[serde(default, deserialize_with = "deserialize_double_option")] pub max_logs: Option>, #[serde(default, deserialize_with = "deserialize_double_option")] pub custom_theme_id: Option>, #[serde(default, deserialize_with = "deserialize_double_option")] pub ntfy_topic: Option>, #[serde(default, deserialize_with = "deserialize_double_option")] pub ntfy_server: Option>, } /// Distinguishes a missing field (outer `None`) from present `null` (`Some(None)`) or present value (`Some(Some(value))`), which plain `Option>` can't do alone. fn deserialize_double_option<'de, D, T>(deserializer: D) -> Result>, D::Error> where D: serde::Deserializer<'de>, T: Deserialize<'de>, { Option::::deserialize(deserializer).map(Some) } #[derive(Debug, Deserialize)] pub struct CreateAlertRuleRequest { pub name: String, pub enabled: bool, pub condition: AlertCondition, } #[derive(Debug, Deserialize)] pub struct UpdateAlertRuleRequest { pub name: String, pub enabled: bool, pub condition: AlertCondition, } /// `server` defaults to `https://ntfy.sh` when absent. #[derive(Debug, Deserialize)] pub struct TestAlertNotificationRequest { pub topic: String, pub server: Option, } #[derive(Debug, Clone, Serialize)] pub struct ProfileSummaryDto { pub name: String, pub param_count: usize, pub metric_count: usize, pub dashboard_count: usize, } impl From<&Profile> for ProfileSummaryDto { fn from(profile: &Profile) -> Self { ProfileSummaryDto { name: profile.name.clone(), param_count: profile.params.len(), metric_count: profile.metrics.len(), dashboard_count: profile.dashboards.len(), } } } #[derive(Debug, Deserialize)] pub struct CreateProfileRequest { pub name: String, } /// Mirrors `profile::Panel`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PanelDto { pub id: String, pub title: String, pub promql: String, pub chart_type: ChartType, } impl From<&Panel> for PanelDto { fn from(panel: &Panel) -> Self { PanelDto { id: panel.id.clone(), title: panel.title.clone(), promql: panel.promql.clone(), chart_type: panel.chart_type, } } } impl From<&PanelDto> for Panel { fn from(dto: &PanelDto) -> Self { Panel { id: dto.id.clone(), title: dto.title.clone(), promql: dto.promql.clone(), chart_type: dto.chart_type, } } } /// Mirrors `profile::Dashboard`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DashboardDto { pub id: String, pub name: String, pub panels: Vec, } impl From<&Dashboard> for DashboardDto { fn from(dashboard: &Dashboard) -> Self { DashboardDto { id: dashboard.id.clone(), name: dashboard.name.clone(), panels: dashboard.panels.iter().map(PanelDto::from).collect(), } } } impl From<&DashboardDto> for Dashboard { fn from(dto: &DashboardDto) -> Self { Dashboard { id: dto.id.clone(), name: dto.name.clone(), panels: dto.panels.iter().map(Panel::from).collect(), } } } #[derive(Debug, Deserialize)] pub struct CreateDashboardRequest { pub name: String, } /// The dashboard `id` in the URL is authoritative; any `id` in the body is ignored. #[derive(Debug, Deserialize)] pub struct SaveDashboardRequest { pub name: String, #[serde(default)] pub panels: Vec, }