Split src/ into an api/cli/engine/config module hierarchy
- src/api/: HTTP router + DTOs + the MCP server (the HTTP/MCP-facing surface): mod.rs (formerly api.rs), dto.rs, mcp_server.rs. - src/cli/: argument parsing and process entry (formerly inline in main.rs) plus session registry: mod.rs, sessions.rs. main.rs is now a thin entry point that just declares modules and calls cli::run(). - src/engine/: the actor + everything it depends on to parse/index/ query log lines: mod.rs (formerly engine.rs), alerts.rs, promql.rs, rate.rs, ingest.rs, logfmt.rs, filter_parser.rs/.pest, filter_datetime.rs, index_key.rs, data_value.rs, metric_primitive.rs, timestamp.rs. - src/config/: persisted configuration: mod.rs (formerly settings.rs), profile.rs. - src/consts.rs stays at the crate root, used from every module above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
311713986a
commit
a810ecf396
@@ -1,8 +1,8 @@
|
|||||||
use crate::alerts::AlertCondition;
|
use crate::engine::alerts::AlertCondition;
|
||||||
use crate::data_value::MetricPrimitiveKind;
|
use crate::engine::data_value::MetricPrimitiveKind;
|
||||||
use crate::metric_primitive::MetricPrimitive;
|
use crate::engine::metric_primitive::MetricPrimitive;
|
||||||
use crate::profile::{ChartType, Dashboard, Panel, Profile};
|
use crate::config::profile::{ChartType, Dashboard, Panel, Profile};
|
||||||
use crate::settings::{ColorScheme, CssEntry, Settings};
|
use crate::config::{ColorScheme, CssEntry, Settings};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ impl From<&CssEntry> for CssEntryDto {
|
|||||||
path_or_url: None,
|
path_or_url: None,
|
||||||
},
|
},
|
||||||
CssEntry::Linked { path_or_url } => CssEntryDto {
|
CssEntry::Linked { path_or_url } => CssEntryDto {
|
||||||
id: crate::settings::css_entry_id(entry),
|
id: crate::config::css_entry_id(entry),
|
||||||
kind: CssEntryKind::Linked,
|
kind: CssEntryKind::Linked,
|
||||||
filename: None,
|
filename: None,
|
||||||
path_or_url: Some(path_or_url.clone()),
|
path_or_url: Some(path_or_url.clone()),
|
||||||
@@ -1,21 +1,24 @@
|
|||||||
|
pub mod dto;
|
||||||
|
pub mod mcp_server;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::dto::{
|
use crate::api::dto::{
|
||||||
BucketValuesParams, CreateAlertRuleRequest, CreateDashboardRequest, CreateProfileRequest,
|
BucketValuesParams, CreateAlertRuleRequest, CreateDashboardRequest, CreateProfileRequest,
|
||||||
CssEntryDto, DashboardDto, ErrorDto, ImportCssRequest, LinkCssRequest, MetricDeclarationDto,
|
CssEntryDto, DashboardDto, ErrorDto, ImportCssRequest, LinkCssRequest, MetricDeclarationDto,
|
||||||
ParamDeclarationDto, LogsParams, ProfileSummaryDto, PromqlParams, SaveDashboardRequest,
|
ParamDeclarationDto, LogsParams, ProfileSummaryDto, PromqlParams, SaveDashboardRequest,
|
||||||
SettingsDto, TestAlertNotificationRequest, TimelineParams, UpdateAlertRuleRequest, UpdateSettingsRequest,
|
SettingsDto, TestAlertNotificationRequest, TimelineParams, UpdateAlertRuleRequest, UpdateSettingsRequest,
|
||||||
};
|
};
|
||||||
use crate::data_value::MetricPrimitiveKind;
|
use crate::engine::data_value::MetricPrimitiveKind;
|
||||||
use crate::engine::{
|
use crate::engine::{
|
||||||
DeclareMetricRequest, DeclareParamRequest, EngineHandle, EngineMessage, EngineQuery,
|
DeclareMetricRequest, DeclareParamRequest, EngineHandle, EngineMessage, EngineQuery,
|
||||||
EngineResponse,
|
EngineResponse,
|
||||||
};
|
};
|
||||||
use crate::alerts::{deliver_ntfy, AlertError};
|
use crate::engine::alerts::{deliver_ntfy, AlertError};
|
||||||
use crate::profile::{Dashboard, ParamDeclarationRecord, Profile, ProfileError};
|
use crate::config::profile::{Dashboard, ParamDeclarationRecord, Profile, ProfileError};
|
||||||
use crate::settings::{CssError, SettingsHandle};
|
use crate::config::{CssError, SettingsHandle};
|
||||||
use axum::extract::{Path as AxumPath, Query, State};
|
use axum::extract::{Path as AxumPath, Query, State};
|
||||||
use axum::http::{header, StatusCode};
|
use axum::http::{header, StatusCode};
|
||||||
use axum::response::{IntoResponse, Json, Response};
|
use axum::response::{IntoResponse, Json, Response};
|
||||||
@@ -26,7 +29,7 @@ use tokio::sync::oneshot;
|
|||||||
|
|
||||||
static WEBAPP_DIST: EmbedDir = fs_embed::fs_embed!("webapp/dist");
|
static WEBAPP_DIST: EmbedDir = fs_embed::fs_embed!("webapp/dist");
|
||||||
|
|
||||||
const SKILL_MD: &str = include_str!("../SKILL.md");
|
const SKILL_MD: &str = include_str!("../../SKILL.md");
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ApiState {
|
struct ApiState {
|
||||||
@@ -577,7 +580,7 @@ async fn import_css(
|
|||||||
let entry = settings
|
let entry = settings
|
||||||
.css_entries
|
.css_entries
|
||||||
.iter()
|
.iter()
|
||||||
.find(|entry| crate::settings::css_entry_id(entry) == id)
|
.find(|entry| crate::config::css_entry_id(entry) == id)
|
||||||
.expect("entry was just inserted by import_css");
|
.expect("entry was just inserted by import_css");
|
||||||
(StatusCode::CREATED, Json(CssEntryDto::from(entry))).into_response()
|
(StatusCode::CREATED, Json(CssEntryDto::from(entry))).into_response()
|
||||||
}
|
}
|
||||||
@@ -900,8 +903,8 @@ async fn delete_dashboard(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::engine::{self, IngestedLine};
|
use crate::engine::{self, IngestedLine};
|
||||||
use crate::settings::Settings;
|
use crate::config::Settings;
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::http::Request;
|
use axum::http::Request;
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
pub mod sessions;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, LazyLock, Mutex};
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
use crate::config::Settings;
|
||||||
|
|
||||||
|
static SETTINGS_PATH_HELP: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
format!(
|
||||||
|
"Path to the settings file. Defaults to {}.",
|
||||||
|
crate::config::default_settings_path()
|
||||||
|
.map(|path| path.display().to_string())
|
||||||
|
.unwrap_or_else(|| "a per-OS config location".to_string())
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<Command>,
|
||||||
|
|
||||||
|
#[arg(long, default_value_t = 0)]
|
||||||
|
port: u16,
|
||||||
|
|
||||||
|
#[arg(long, help = SETTINGS_PATH_HELP.as_str())]
|
||||||
|
settings_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
metrics_url: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum Command {
|
||||||
|
Mcp {
|
||||||
|
#[arg(long)]
|
||||||
|
session: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
port: Option<u16>,
|
||||||
|
},
|
||||||
|
Sessions,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let settings_path = args.settings_path.clone().unwrap_or_else(|| {
|
||||||
|
crate::config::default_settings_path()
|
||||||
|
.expect("could not determine a default settings path; pass --settings-path")
|
||||||
|
});
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Some(Command::Mcp { session, port }) => {
|
||||||
|
run_mcp(&settings_path, session, port).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(Command::Sessions) => {
|
||||||
|
print_sessions(&settings_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = Arc::new(Mutex::new(Settings::load(&settings_path)));
|
||||||
|
|
||||||
|
let engine_tx = crate::engine::spawn(settings.clone(), crate::consts::ENGINE_CHANNEL_CAPACITY);
|
||||||
|
{
|
||||||
|
let max_logs = settings.lock().unwrap().max_logs;
|
||||||
|
let _ = engine_tx.tx.send(crate::engine::EngineMessage::SetMaxLogs(max_logs)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let (active_profile_name, active_profile) = {
|
||||||
|
let mut settings_guard = settings.lock().unwrap();
|
||||||
|
settings_guard
|
||||||
|
.ensure_default_profile(&settings_path)
|
||||||
|
.expect("failed to ensure the Default profile exists");
|
||||||
|
let active_profile_name = settings_guard.active_profile.clone();
|
||||||
|
let active_profile = active_profile_name
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|name| settings_guard.profile(name).cloned());
|
||||||
|
(active_profile_name, active_profile)
|
||||||
|
};
|
||||||
|
match (active_profile_name, active_profile) {
|
||||||
|
(Some(_), Some(active_profile)) => {
|
||||||
|
active_profile.load_into_engine(&engine_tx).await;
|
||||||
|
}
|
||||||
|
(Some(profile_name), None) => {
|
||||||
|
eprintln!(
|
||||||
|
"warning: active profile '{profile_name}' not found in {}; skipping",
|
||||||
|
settings_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(None, _) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match args.file.clone() {
|
||||||
|
Some(path) => {
|
||||||
|
tokio::spawn(crate::engine::ingest::run_file(engine_tx.clone(), path));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tokio::spawn(crate::engine::ingest::run(engine_tx.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(metrics_url) = args.metrics_url.clone() {
|
||||||
|
tokio::spawn(crate::engine::ingest::run_metrics_poll(engine_tx.clone(), metrics_url));
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = crate::api::router(engine_tx, settings, settings_path.clone());
|
||||||
|
let listener = TcpListener::bind((crate::consts::DEFAULT_BIND_HOST, args.port))
|
||||||
|
.await
|
||||||
|
.expect("failed to bind listener");
|
||||||
|
let bound_port = listener
|
||||||
|
.local_addr()
|
||||||
|
.expect("failed to read back bound address")
|
||||||
|
.port();
|
||||||
|
|
||||||
|
let source = match &args.file {
|
||||||
|
Some(path) => format!("tailing {}", path.display()),
|
||||||
|
None => "reading logs from stdin".to_string(),
|
||||||
|
};
|
||||||
|
let session_name = args.name.clone().unwrap_or_else(sessions::generate_name);
|
||||||
|
match sessions::register_session(&settings_path, &session_name, bound_port) {
|
||||||
|
Ok(_) => eprintln!(
|
||||||
|
"ottyl serving on http://localhost:{} ({}), session name: {}",
|
||||||
|
bound_port, source, session_name
|
||||||
|
),
|
||||||
|
Err(err) => eprintln!(
|
||||||
|
"ottyl serving on http://localhost:{} ({}); warning: failed to register session '{}': {}",
|
||||||
|
bound_port, source, session_name, err
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("server error");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_mcp(settings_path: &std::path::Path, session: Option<String>, port: Option<u16>) {
|
||||||
|
use rmcp::ServiceExt;
|
||||||
|
|
||||||
|
let port = match (port, session) {
|
||||||
|
(Some(port), _) => port,
|
||||||
|
(None, Some(name)) => match sessions::resolve_session(settings_path, &name) {
|
||||||
|
Some(info) => info.port,
|
||||||
|
None => {
|
||||||
|
eprintln!("error: no running ottyl session named '{name}'");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(None, None) => {
|
||||||
|
eprintln!("error: `ottyl mcp` requires --session <name> or --port <port>");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_url = format!("http://localhost:{port}");
|
||||||
|
let service = crate::api::mcp_server::OttylMcp::new(base_url)
|
||||||
|
.serve(rmcp::transport::stdio())
|
||||||
|
.await
|
||||||
|
.expect("failed to start MCP server");
|
||||||
|
service.waiting().await.expect("MCP server error");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_sessions(settings_path: &std::path::Path) {
|
||||||
|
let dir = sessions::sessions_dir(settings_path);
|
||||||
|
let sessions = sessions::list_sessions(&dir);
|
||||||
|
if sessions.is_empty() {
|
||||||
|
println!("no known ottyl sessions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for session in sessions {
|
||||||
|
println!("{}\tport={}\tpid={}", session.name, session.port, session.pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_path_defaults_to_none() {
|
||||||
|
let args = Args::parse_from(["ottyl"]);
|
||||||
|
assert_eq!(args.settings_path, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_path_flag_overrides_default() {
|
||||||
|
let args = Args::parse_from(["ottyl", "--settings-path", "/tmp/custom-settings.json"]);
|
||||||
|
assert_eq!(
|
||||||
|
args.settings_path,
|
||||||
|
Some(PathBuf::from("/tmp/custom-settings.json"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod profile;
|
||||||
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -10,8 +12,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::alerts::AlertRule;
|
use crate::engine::alerts::AlertRule;
|
||||||
use crate::profile::Profile;
|
use crate::config::profile::Profile;
|
||||||
|
|
||||||
pub type SettingsHandle = Arc<Mutex<Settings>>;
|
pub type SettingsHandle = Arc<Mutex<Settings>>;
|
||||||
|
|
||||||
@@ -197,7 +199,7 @@ impl Settings {
|
|||||||
.map(|parent| parent.join("profiles"))
|
.map(|parent| parent.join("profiles"))
|
||||||
.unwrap_or_else(|| PathBuf::from("profiles"))
|
.unwrap_or_else(|| PathBuf::from("profiles"))
|
||||||
.join("profiles.json");
|
.join("profiles.json");
|
||||||
let Some(profiles) = crate::profile::Profile::load_legacy_store(&legacy_path) else {
|
let Some(profiles) = crate::config::profile::Profile::load_legacy_store(&legacy_path) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.profiles = profiles;
|
self.profiles = profiles;
|
||||||
@@ -218,7 +220,7 @@ impl Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn migrate_legacy_profile_shapes(&mut self, settings_path: &Path) {
|
fn migrate_legacy_profile_shapes(&mut self, settings_path: &Path) {
|
||||||
if crate::profile::migrate_legacy_profile_shapes(&mut self.profiles)
|
if crate::config::profile::migrate_legacy_profile_shapes(&mut self.profiles)
|
||||||
&& let Err(err) = self.save(settings_path)
|
&& let Err(err) = self.save(settings_path)
|
||||||
{
|
{
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -418,13 +420,13 @@ mod tests {
|
|||||||
settings.css_entries.push(CssEntry::Linked {
|
settings.css_entries.push(CssEntry::Linked {
|
||||||
path_or_url: "https://example.com/style.css".to_string(),
|
path_or_url: "https://example.com/style.css".to_string(),
|
||||||
});
|
});
|
||||||
settings.profiles.push(crate::profile::Profile::new("default".to_string()));
|
settings.profiles.push(crate::config::profile::Profile::new("default".to_string()));
|
||||||
settings.active_profile = Some("default".to_string());
|
settings.active_profile = Some("default".to_string());
|
||||||
settings.alert_rules.push(crate::alerts::AlertRule {
|
settings.alert_rules.push(crate::engine::alerts::AlertRule {
|
||||||
id: "rule1".to_string(),
|
id: "rule1".to_string(),
|
||||||
name: "error spike".to_string(),
|
name: "error spike".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
condition: crate::alerts::AlertCondition::Immediate {
|
condition: crate::engine::alerts::AlertCondition::Immediate {
|
||||||
query: r#"regex("error")"#.to_string(),
|
query: r#"regex("error")"#.to_string(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -12,9 +12,9 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::consts::{DEFAULT_PROFILE_NAME, SEEDED_METRIC_LABEL, SEEDED_METRIC_NAME};
|
use crate::consts::{DEFAULT_PROFILE_NAME, SEEDED_METRIC_LABEL, SEEDED_METRIC_NAME};
|
||||||
use crate::data_value::MetricPrimitiveKind;
|
use crate::engine::data_value::MetricPrimitiveKind;
|
||||||
use crate::engine::{DeclareMetricRequest, DeclareParamRequest, EngineHandle, EngineMessage};
|
use crate::engine::{DeclareMetricRequest, DeclareParamRequest, EngineHandle, EngineMessage};
|
||||||
use crate::settings::Settings;
|
use crate::config::Settings;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -678,10 +678,10 @@ impl Settings {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::data_value::MetricPrimitiveKind;
|
use crate::engine::data_value::MetricPrimitiveKind;
|
||||||
use crate::dto::LogsParams;
|
use crate::api::dto::LogsParams;
|
||||||
use crate::engine::{EngineMessage, EngineQuery, EngineResponse, IngestedLine};
|
use crate::engine::{EngineMessage, EngineQuery, EngineResponse, IngestedLine};
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
|
|
||||||
fn param_request(name: &str, pattern: &str, capture_group: &str) -> DeclareParamRequest {
|
fn param_request(name: &str, pattern: &str, capture_group: &str) -> DeclareParamRequest {
|
||||||
DeclareParamRequest {
|
DeclareParamRequest {
|
||||||
@@ -798,7 +798,7 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn seeded_metric_renders_real_error_vs_other_data() {
|
async fn seeded_metric_renders_real_error_vs_other_data() {
|
||||||
use crate::engine::{self, EngineMessage, EngineQuery, EngineResponse, IngestedLine};
|
use crate::engine::{self, EngineMessage, EngineQuery, EngineResponse, IngestedLine};
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let settings_path = dir.path().join("settings.json");
|
let settings_path = dir.path().join("settings.json");
|
||||||
@@ -10,11 +10,11 @@ use regex::Regex;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::engine::{self, eval_json_comparison};
|
use crate::engine::{self, eval_json_comparison};
|
||||||
use crate::filter_datetime;
|
use crate::engine::filter_datetime;
|
||||||
use crate::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
|
use crate::engine::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
|
||||||
use crate::rate::RateWindow;
|
use crate::engine::rate::RateWindow;
|
||||||
use crate::settings::Settings;
|
use crate::config::Settings;
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct AlertRule {
|
pub struct AlertRule {
|
||||||
@@ -190,7 +190,7 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
|
|||||||
FilterValue::Number(n) => n.to_string(),
|
FilterValue::Number(n) => n.to_string(),
|
||||||
FilterValue::Raw(raw) => raw.clone(),
|
FilterValue::Raw(raw) => raw.clone(),
|
||||||
};
|
};
|
||||||
Ok(op.matches(crate::logfmt::detect_format(text).cmp(target.as_str())))
|
Ok(op.matches(crate::engine::logfmt::detect_format(text).cmp(target.as_str())))
|
||||||
}
|
}
|
||||||
FilterExpr::Comparison { metric, .. } => Err(format!(
|
FilterExpr::Comparison { metric, .. } => Err(format!(
|
||||||
"alert queries only support regex()/ottyl_seq/ottyl_ts/ottyl_fmt (not metric comparisons like \
|
"alert queries only support regex()/ottyl_seq/ottyl_ts/ottyl_fmt (not metric comparisons like \
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::metric_primitive::MetricPrimitive;
|
use crate::engine::metric_primitive::MetricPrimitive;
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
|
use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
|
||||||
|
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum DateTimeParseError {
|
pub enum DateTimeParseError {
|
||||||
@@ -6,7 +6,7 @@ use pest_derive::Parser as PestParser;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(PestParser)]
|
#[derive(PestParser)]
|
||||||
#[grammar = "filter_parser.pest"]
|
#[grammar = "engine/filter_parser.pest"]
|
||||||
struct FilterGrammar;
|
struct FilterGrammar;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::metric_primitive::MetricPrimitive;
|
use crate::engine::metric_primitive::MetricPrimitive;
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use crate::consts::{METRICS_POLL_INTERVAL, STDIN_POLL_INTERVAL as POLL_INTERVAL};
|
use crate::consts::{METRICS_POLL_INTERVAL, STDIN_POLL_INTERVAL as POLL_INTERVAL};
|
||||||
use crate::engine::{EngineHandle, EngineMessage, IngestedLine, MetricSample};
|
use crate::engine::{EngineHandle, EngineMessage, IngestedLine, MetricSample};
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOG_STREAM_FIXTURE: &str = include_str!("fixtures/log_stream_sample.txt");
|
const LOG_STREAM_FIXTURE: &str = include_str!("../fixtures/log_stream_sample.txt");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detects_log_stream_shaped_lines() {
|
fn detects_log_stream_shaped_lines() {
|
||||||
@@ -467,8 +467,8 @@ mod tests {
|
|||||||
assert!(!extract_pairs(line).is_empty());
|
assert!(!extract_pairs(line).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
const GO_LOG_FIXTURE: &str = include_str!("fixtures/go_log_sample.txt");
|
const GO_LOG_FIXTURE: &str = include_str!("../fixtures/go_log_sample.txt");
|
||||||
const PY_LOG_FIXTURE: &str = include_str!("fixtures/py_log_sample.txt");
|
const PY_LOG_FIXTURE: &str = include_str!("../fixtures/py_log_sample.txt");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detects_go_log_shaped_lines() {
|
fn detects_go_log_shaped_lines() {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -1,22 +1,32 @@
|
|||||||
use crate::alerts::{self, AlertRuntimeState};
|
pub mod alerts;
|
||||||
|
pub mod data_value;
|
||||||
|
pub mod filter_datetime;
|
||||||
|
pub mod filter_parser;
|
||||||
|
pub mod index_key;
|
||||||
|
pub mod ingest;
|
||||||
|
pub mod logfmt;
|
||||||
|
pub mod metric_primitive;
|
||||||
|
pub mod promql;
|
||||||
|
pub mod rate;
|
||||||
|
pub mod timestamp;
|
||||||
|
|
||||||
|
use crate::engine::alerts::AlertRuntimeState;
|
||||||
use crate::consts::{
|
use crate::consts::{
|
||||||
DEFAULT_TIMELINE_BUCKET_MS, INGEST_LAG_WATERMARK_MAX_ENTRIES, INGEST_LAG_WATERMARK_WINDOW_MS,
|
DEFAULT_TIMELINE_BUCKET_MS, INGEST_LAG_WATERMARK_MAX_ENTRIES, INGEST_LAG_WATERMARK_WINDOW_MS,
|
||||||
MAX_AUTO_DECLARED_PARAMS, MAX_NEW_AUTO_PARAMS_PER_LINE, MERGE_WINDOW_MS,
|
MAX_AUTO_DECLARED_PARAMS, MAX_NEW_AUTO_PARAMS_PER_LINE, MERGE_WINDOW_MS,
|
||||||
OTTYL_RESERVED_PARAM_NAMES, SORT_HELPER_INDEX_PURGE_AFTER,
|
OTTYL_RESERVED_PARAM_NAMES, SORT_HELPER_INDEX_PURGE_AFTER,
|
||||||
};
|
};
|
||||||
use crate::data_value::{DataValue, MetricPrimitiveKind, ParamDeclaration};
|
use crate::engine::data_value::{DataValue, MetricPrimitiveKind, ParamDeclaration};
|
||||||
use crate::dto::{
|
use crate::api::dto::{
|
||||||
BucketValueDto, LogEntryDto, LogsParams, MetricDeclarationDto, ParamDeclarationDto,
|
BucketValueDto, LogEntryDto, LogsParams, MetricDeclarationDto, ParamDeclarationDto,
|
||||||
PromqlSeriesDto, StatsDto, TimelineBucketDto, TimelineParams,
|
PromqlSeriesDto, StatsDto, TimelineBucketDto, TimelineParams,
|
||||||
};
|
};
|
||||||
use crate::filter_datetime;
|
use crate::engine::filter_parser::{CompareOp, FilterExpr, FilterValue};
|
||||||
use crate::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
|
use crate::engine::index_key::IndexKey;
|
||||||
use crate::index_key::IndexKey;
|
use crate::engine::metric_primitive::{MetricPrimitive, Number};
|
||||||
use crate::metric_primitive::{MetricPrimitive, Number};
|
use crate::engine::promql::{AggOp, LabelMatcher, PromqlQuery};
|
||||||
use crate::promql::{AggOp, LabelMatcher, PromqlQuery};
|
use crate::config::SettingsHandle;
|
||||||
use crate::rate;
|
use crate::engine::timestamp::Timestamp;
|
||||||
use crate::settings::SettingsHandle;
|
|
||||||
use crate::timestamp::Timestamp;
|
|
||||||
use left_right::Absorb;
|
use left_right::Absorb;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -227,7 +237,7 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
|
|||||||
let mut max_logs: Option<usize> = None;
|
let mut max_logs: Option<usize> = None;
|
||||||
let mut metric_sample_sequence: u64 = 0;
|
let mut metric_sample_sequence: u64 = 0;
|
||||||
let mut alert_states: HashMap<String, AlertRuntimeState> = HashMap::new();
|
let mut alert_states: HashMap<String, AlertRuntimeState> = HashMap::new();
|
||||||
let mut log_stream_header: Option<crate::logfmt::LogStreamHeader> = None;
|
let mut log_stream_header: Option<crate::engine::logfmt::LogStreamHeader> = None;
|
||||||
let mut ingest_lag_samples: Vec<(Timestamp, i64)> = Vec::new();
|
let mut ingest_lag_samples: Vec<(Timestamp, i64)> = Vec::new();
|
||||||
|
|
||||||
while let Some(message) = rx.recv().await {
|
while let Some(message) = rx.recv().await {
|
||||||
@@ -243,7 +253,7 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
|
|||||||
let ingest_lag_high_watermark_ms =
|
let ingest_lag_high_watermark_ms =
|
||||||
ingest_lag_high_watermark_ms(&ingest_lag_samples, processed_at);
|
ingest_lag_high_watermark_ms(&ingest_lag_samples, processed_at);
|
||||||
|
|
||||||
let is_new_header = crate::logfmt::LogStreamHeader::detect(&text).is_some();
|
let is_new_header = crate::engine::logfmt::LogStreamHeader::detect(&text).is_some();
|
||||||
log_stream_header = log_stream_header_transition(&log_stream_header, &text);
|
log_stream_header = log_stream_header_transition(&log_stream_header, &text);
|
||||||
let row_header = if is_new_header { None } else { log_stream_header.clone() };
|
let row_header = if is_new_header { None } else { log_stream_header.clone() };
|
||||||
|
|
||||||
@@ -493,8 +503,8 @@ impl EngineSnapshot {
|
|||||||
if total >= MAX_AUTO_DECLARED_PARAMS {
|
if total >= MAX_AUTO_DECLARED_PARAMS {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let candidates: Vec<AutoParamCandidate> = if crate::logfmt::detect_format(text) == "json" {
|
let candidates: Vec<AutoParamCandidate> = if crate::engine::logfmt::detect_format(text) == "json" {
|
||||||
crate::data_value::flatten_json_object(text)
|
crate::engine::data_value::flatten_json_object(text)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(path, value)| AutoParamCandidate {
|
.map(|(path, value)| AutoParamCandidate {
|
||||||
name: path.clone(),
|
name: path.clone(),
|
||||||
@@ -505,7 +515,7 @@ impl EngineSnapshot {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
crate::logfmt::extract_pairs(text)
|
crate::engine::logfmt::extract_pairs(text)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(key, value, quoted)| {
|
.map(|(key, value, quoted)| {
|
||||||
let group = sanitize_group_name(key);
|
let group = sanitize_group_name(key);
|
||||||
@@ -549,18 +559,18 @@ impl EngineSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn log_stream_header_transition(
|
fn log_stream_header_transition(
|
||||||
current: &Option<crate::logfmt::LogStreamHeader>,
|
current: &Option<crate::engine::logfmt::LogStreamHeader>,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Option<crate::logfmt::LogStreamHeader> {
|
) -> Option<crate::engine::logfmt::LogStreamHeader> {
|
||||||
crate::logfmt::LogStreamHeader::detect(text).or_else(|| current.clone())
|
crate::engine::logfmt::LogStreamHeader::detect(text).or_else(|| current.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_log_stream_params_from_row(
|
fn derive_log_stream_params_from_row(
|
||||||
index: &mut BTreeMap<IndexKey, DataValue>,
|
index: &mut BTreeMap<IndexKey, DataValue>,
|
||||||
header: &crate::logfmt::LogStreamHeader,
|
header: &crate::engine::logfmt::LogStreamHeader,
|
||||||
text: &str,
|
text: &str,
|
||||||
) {
|
) {
|
||||||
if !crate::logfmt::is_log_stream_line(text) {
|
if !crate::engine::logfmt::is_log_stream_line(text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let row = header.split_row(text);
|
let row = header.split_row(text);
|
||||||
@@ -605,10 +615,10 @@ fn derive_log_stream_params_from_row(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn auto_declare_log_stream_params(
|
fn auto_declare_log_stream_params(
|
||||||
index: &mut BTreeMap<IndexKey, DataValue>,
|
index: &mut BTreeMap<IndexKey, DataValue>,
|
||||||
header: &mut Option<crate::logfmt::LogStreamHeader>,
|
header: &mut Option<crate::engine::logfmt::LogStreamHeader>,
|
||||||
text: &str,
|
text: &str,
|
||||||
) {
|
) {
|
||||||
let is_new_header = crate::logfmt::LogStreamHeader::detect(text).is_some();
|
let is_new_header = crate::engine::logfmt::LogStreamHeader::detect(text).is_some();
|
||||||
*header = log_stream_header_transition(header, text);
|
*header = log_stream_header_transition(header, text);
|
||||||
if is_new_header {
|
if is_new_header {
|
||||||
return;
|
return;
|
||||||
@@ -764,7 +774,7 @@ fn parse_metric_primitive(raw: &str, kind: MetricPrimitiveKind) -> Option<Metric
|
|||||||
.ok()
|
.ok()
|
||||||
.map(MetricPrimitive::Number),
|
.map(MetricPrimitive::Number),
|
||||||
MetricPrimitiveKind::Timestamp => {
|
MetricPrimitiveKind::Timestamp => {
|
||||||
crate::filter_datetime::parse_datetime(raw, Timestamp::now())
|
crate::engine::filter_datetime::parse_datetime(raw, Timestamp::now())
|
||||||
.ok()
|
.ok()
|
||||||
.map(MetricPrimitive::Timestamp)
|
.map(MetricPrimitive::Timestamp)
|
||||||
}
|
}
|
||||||
@@ -811,11 +821,11 @@ fn extract_param_raw(
|
|||||||
json_field_path: Option<&str>,
|
json_field_path: Option<&str>,
|
||||||
log_stream_gated: bool,
|
log_stream_gated: bool,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
if log_stream_gated && !crate::logfmt::is_log_stream_line(text) {
|
if log_stream_gated && !crate::engine::logfmt::is_log_stream_line(text) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match json_field_path.filter(|path| !path.is_empty()) {
|
match json_field_path.filter(|path| !path.is_empty()) {
|
||||||
Some(field_path) => crate::data_value::extract_json_field(text, field_path),
|
Some(field_path) => crate::engine::data_value::extract_json_field(text, field_path),
|
||||||
None => regex.captures(text)?.name(capture_group).map(|m| m.as_str().to_string()),
|
None => regex.captures(text)?.name(capture_group).map(|m| m.as_str().to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1250,7 +1260,7 @@ impl CompiledFilter {
|
|||||||
op.matches(timestamp.as_millis().cmp(target_ms))
|
op.matches(timestamp.as_millis().cmp(target_ms))
|
||||||
}
|
}
|
||||||
CompiledFilter::Fmt { op, target } => {
|
CompiledFilter::Fmt { op, target } => {
|
||||||
op.matches(crate::logfmt::detect_format(text).cmp(target.as_str()))
|
op.matches(crate::engine::logfmt::detect_format(text).cmp(target.as_str()))
|
||||||
}
|
}
|
||||||
CompiledFilter::MetricMatch(sequences) => sequences.contains(&sequence),
|
CompiledFilter::MetricMatch(sequences) => sequences.contains(&sequence),
|
||||||
CompiledFilter::Json { field, op, value } => eval_json_comparison(text, field, *op, value),
|
CompiledFilter::Json { field, op, value } => eval_json_comparison(text, field, *op, value),
|
||||||
@@ -1356,7 +1366,7 @@ fn derive_single_param(
|
|||||||
) -> Option<MetricPrimitive> {
|
) -> Option<MetricPrimitive> {
|
||||||
let (_, regex, capture_group, json_field_path, kind, log_stream_gated) =
|
let (_, regex, capture_group, json_field_path, kind, log_stream_gated) =
|
||||||
declarations.iter().find(|(name, ..)| name == param_name)?;
|
declarations.iter().find(|(name, ..)| name == param_name)?;
|
||||||
if *log_stream_gated && !crate::logfmt::is_log_stream_line(text) {
|
if *log_stream_gated && !crate::engine::logfmt::is_log_stream_line(text) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if *kind == MetricPrimitiveKind::Boolean {
|
if *kind == MetricPrimitiveKind::Boolean {
|
||||||
@@ -1364,7 +1374,7 @@ fn derive_single_param(
|
|||||||
}
|
}
|
||||||
let captures = regex.captures(text)?;
|
let captures = regex.captures(text)?;
|
||||||
let raw: String = match json_field_path.as_deref().filter(|path| !path.is_empty()) {
|
let raw: String = match json_field_path.as_deref().filter(|path| !path.is_empty()) {
|
||||||
Some(field_path) => crate::data_value::extract_json_field(text, field_path)?,
|
Some(field_path) => crate::engine::data_value::extract_json_field(text, field_path)?,
|
||||||
None => captures.name(capture_group)?.as_str().to_string(),
|
None => captures.name(capture_group)?.as_str().to_string(),
|
||||||
};
|
};
|
||||||
parse_metric_primitive(&raw, *kind)
|
parse_metric_primitive(&raw, *kind)
|
||||||
@@ -1437,7 +1447,7 @@ impl EngineSnapshot {
|
|||||||
fn derive_log_params(text: &str, declarations: &[CompiledParamDeclaration]) -> BTreeMap<String, MetricPrimitive> {
|
fn derive_log_params(text: &str, declarations: &[CompiledParamDeclaration]) -> BTreeMap<String, MetricPrimitive> {
|
||||||
let mut params = BTreeMap::new();
|
let mut params = BTreeMap::new();
|
||||||
for (name, regex, capture_group, json_field_path, kind, log_stream_gated) in declarations {
|
for (name, regex, capture_group, json_field_path, kind, log_stream_gated) in declarations {
|
||||||
if *log_stream_gated && !crate::logfmt::is_log_stream_line(text) {
|
if *log_stream_gated && !crate::engine::logfmt::is_log_stream_line(text) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if *kind == MetricPrimitiveKind::Boolean {
|
if *kind == MetricPrimitiveKind::Boolean {
|
||||||
@@ -1448,7 +1458,7 @@ fn derive_log_params(text: &str, declarations: &[CompiledParamDeclaration]) -> B
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let raw: String = match json_field_path.as_deref().filter(|path| !path.is_empty()) {
|
let raw: String = match json_field_path.as_deref().filter(|path| !path.is_empty()) {
|
||||||
Some(field_path) => match crate::data_value::extract_json_field(text, field_path) {
|
Some(field_path) => match crate::engine::data_value::extract_json_field(text, field_path) {
|
||||||
Some(value) => value,
|
Some(value) => value,
|
||||||
None => continue,
|
None => continue,
|
||||||
},
|
},
|
||||||
@@ -1467,7 +1477,7 @@ fn derive_log_params(text: &str, declarations: &[CompiledParamDeclaration]) -> B
|
|||||||
|
|
||||||
fn log_entry_params(text: &str, declarations: &[CompiledParamDeclaration]) -> BTreeMap<String, MetricPrimitive> {
|
fn log_entry_params(text: &str, declarations: &[CompiledParamDeclaration]) -> BTreeMap<String, MetricPrimitive> {
|
||||||
let mut params = derive_log_params(text, declarations);
|
let mut params = derive_log_params(text, declarations);
|
||||||
params.insert("ottyl_fmt".to_string(), MetricPrimitive::String(crate::logfmt::detect_format(text).to_string()));
|
params.insert("ottyl_fmt".to_string(), MetricPrimitive::String(crate::engine::logfmt::detect_format(text).to_string()));
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2244,7 +2254,7 @@ mod tests {
|
|||||||
assert!(declarations[0].6, "the original auto-derived declaration must be untouched");
|
assert!(declarations[0].6, "the original auto-derived declaration must be untouched");
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOG_STREAM_FIXTURE: &str = include_str!("fixtures/log_stream_sample.txt");
|
const LOG_STREAM_FIXTURE: &str = include_str!("../fixtures/log_stream_sample.txt");
|
||||||
|
|
||||||
fn ingest_log_stream_fixture(index: &mut EngineSnapshot) {
|
fn ingest_log_stream_fixture(index: &mut EngineSnapshot) {
|
||||||
let mut header = None;
|
let mut header = None;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use crate::timestamp::Timestamp;
|
use crate::engine::timestamp::Timestamp;
|
||||||
|
|
||||||
fn count_in_window(points: &[(Timestamp, f64)], now: Timestamp, window_ms: i64) -> usize {
|
fn count_in_window(points: &[(Timestamp, f64)], now: Timestamp, window_ms: i64) -> usize {
|
||||||
let now_ms = now.as_millis();
|
let now_ms = now.as_millis();
|
||||||
+3
-217
@@ -1,224 +1,10 @@
|
|||||||
mod alerts;
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod cli;
|
||||||
|
mod config;
|
||||||
mod consts;
|
mod consts;
|
||||||
mod data_value;
|
|
||||||
mod dto;
|
|
||||||
mod engine;
|
mod engine;
|
||||||
mod filter_datetime;
|
|
||||||
mod filter_parser;
|
|
||||||
mod index_key;
|
|
||||||
mod ingest;
|
|
||||||
mod logfmt;
|
|
||||||
mod mcp_server;
|
|
||||||
mod metric_primitive;
|
|
||||||
mod profile;
|
|
||||||
mod promql;
|
|
||||||
mod rate;
|
|
||||||
mod sessions;
|
|
||||||
mod settings;
|
|
||||||
mod timestamp;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::{Arc, LazyLock, Mutex};
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
use settings::Settings;
|
|
||||||
|
|
||||||
static SETTINGS_PATH_HELP: LazyLock<String> = LazyLock::new(|| {
|
|
||||||
format!(
|
|
||||||
"Path to the settings file. Defaults to {}.",
|
|
||||||
settings::default_settings_path()
|
|
||||||
.map(|path| path.display().to_string())
|
|
||||||
.unwrap_or_else(|| "a per-OS config location".to_string())
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
struct Args {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Option<Command>,
|
|
||||||
|
|
||||||
#[arg(long, default_value_t = 0)]
|
|
||||||
port: u16,
|
|
||||||
|
|
||||||
#[arg(long, help = SETTINGS_PATH_HELP.as_str())]
|
|
||||||
settings_path: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
file: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
metrics_url: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
enum Command {
|
|
||||||
Mcp {
|
|
||||||
#[arg(long)]
|
|
||||||
session: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
port: Option<u16>,
|
|
||||||
},
|
|
||||||
Sessions,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let args = Args::parse();
|
cli::run().await;
|
||||||
|
|
||||||
let settings_path = args.settings_path.clone().unwrap_or_else(|| {
|
|
||||||
settings::default_settings_path()
|
|
||||||
.expect("could not determine a default settings path; pass --settings-path")
|
|
||||||
});
|
|
||||||
|
|
||||||
match args.command {
|
|
||||||
Some(Command::Mcp { session, port }) => {
|
|
||||||
run_mcp(&settings_path, session, port).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Some(Command::Sessions) => {
|
|
||||||
print_sessions(&settings_path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let settings = Arc::new(Mutex::new(Settings::load(&settings_path)));
|
|
||||||
|
|
||||||
let engine_tx = engine::spawn(settings.clone(), consts::ENGINE_CHANNEL_CAPACITY);
|
|
||||||
{
|
|
||||||
let max_logs = settings.lock().unwrap().max_logs;
|
|
||||||
let _ = engine_tx.tx.send(engine::EngineMessage::SetMaxLogs(max_logs)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let (active_profile_name, active_profile) = {
|
|
||||||
let mut settings_guard = settings.lock().unwrap();
|
|
||||||
settings_guard
|
|
||||||
.ensure_default_profile(&settings_path)
|
|
||||||
.expect("failed to ensure the Default profile exists");
|
|
||||||
let active_profile_name = settings_guard.active_profile.clone();
|
|
||||||
let active_profile = active_profile_name
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|name| settings_guard.profile(name).cloned());
|
|
||||||
(active_profile_name, active_profile)
|
|
||||||
};
|
|
||||||
match (active_profile_name, active_profile) {
|
|
||||||
(Some(_), Some(active_profile)) => {
|
|
||||||
active_profile.load_into_engine(&engine_tx).await;
|
|
||||||
}
|
|
||||||
(Some(profile_name), None) => {
|
|
||||||
eprintln!(
|
|
||||||
"warning: active profile '{profile_name}' not found in {}; skipping",
|
|
||||||
settings_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(None, _) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match args.file.clone() {
|
|
||||||
Some(path) => {
|
|
||||||
tokio::spawn(ingest::run_file(engine_tx.clone(), path));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
tokio::spawn(ingest::run(engine_tx.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(metrics_url) = args.metrics_url.clone() {
|
|
||||||
tokio::spawn(ingest::run_metrics_poll(engine_tx.clone(), metrics_url));
|
|
||||||
}
|
|
||||||
|
|
||||||
let app = api::router(engine_tx, settings, settings_path.clone());
|
|
||||||
let listener = TcpListener::bind((consts::DEFAULT_BIND_HOST, args.port))
|
|
||||||
.await
|
|
||||||
.expect("failed to bind listener");
|
|
||||||
let bound_port = listener
|
|
||||||
.local_addr()
|
|
||||||
.expect("failed to read back bound address")
|
|
||||||
.port();
|
|
||||||
|
|
||||||
let source = match &args.file {
|
|
||||||
Some(path) => format!("tailing {}", path.display()),
|
|
||||||
None => "reading logs from stdin".to_string(),
|
|
||||||
};
|
|
||||||
let session_name = args.name.clone().unwrap_or_else(sessions::generate_name);
|
|
||||||
match sessions::register_session(&settings_path, &session_name, bound_port) {
|
|
||||||
Ok(_) => eprintln!(
|
|
||||||
"ottyl serving on http://localhost:{} ({}), session name: {}",
|
|
||||||
bound_port, source, session_name
|
|
||||||
),
|
|
||||||
Err(err) => eprintln!(
|
|
||||||
"ottyl serving on http://localhost:{} ({}); warning: failed to register session '{}': {}",
|
|
||||||
bound_port, source, session_name, err
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
axum::serve(listener, app)
|
|
||||||
.await
|
|
||||||
.expect("server error");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_mcp(settings_path: &std::path::Path, session: Option<String>, port: Option<u16>) {
|
|
||||||
use rmcp::ServiceExt;
|
|
||||||
|
|
||||||
let port = match (port, session) {
|
|
||||||
(Some(port), _) => port,
|
|
||||||
(None, Some(name)) => match sessions::resolve_session(settings_path, &name) {
|
|
||||||
Some(info) => info.port,
|
|
||||||
None => {
|
|
||||||
eprintln!("error: no running ottyl session named '{name}'");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(None, None) => {
|
|
||||||
eprintln!("error: `ottyl mcp` requires --session <name> or --port <port>");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let base_url = format!("http://localhost:{port}");
|
|
||||||
let service = mcp_server::OttylMcp::new(base_url)
|
|
||||||
.serve(rmcp::transport::stdio())
|
|
||||||
.await
|
|
||||||
.expect("failed to start MCP server");
|
|
||||||
service.waiting().await.expect("MCP server error");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_sessions(settings_path: &std::path::Path) {
|
|
||||||
let dir = sessions::sessions_dir(settings_path);
|
|
||||||
let sessions = sessions::list_sessions(&dir);
|
|
||||||
if sessions.is_empty() {
|
|
||||||
println!("no known ottyl sessions");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for session in sessions {
|
|
||||||
println!("{}\tport={}\tpid={}", session.name, session.port, session.pid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn settings_path_defaults_to_none() {
|
|
||||||
let args = Args::parse_from(["ottyl"]);
|
|
||||||
assert_eq!(args.settings_path, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn settings_path_flag_overrides_default() {
|
|
||||||
let args = Args::parse_from(["ottyl", "--settings-path", "/tmp/custom-settings.json"]);
|
|
||||||
assert_eq!(
|
|
||||||
args.settings_path,
|
|
||||||
Some(PathBuf::from("/tmp/custom-settings.json"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user