mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
group by route
This commit is contained in:
parent
a00565cf65
commit
e982a12df5
18 changed files with 2153 additions and 446 deletions
|
|
@ -1,5 +1,6 @@
|
|||
mod app;
|
||||
mod remote;
|
||||
mod route;
|
||||
mod unique;
|
||||
mod url;
|
||||
mod user;
|
||||
|
|
@ -8,6 +9,7 @@ mod useragent;
|
|||
use crate::app::{Filter, LineSet};
|
||||
use crate::grouping::app::{AppGrouping, APP_GROUPING_UI};
|
||||
use crate::grouping::remote::{RemoteGrouping, REMOTE_GROUPING_UI};
|
||||
use crate::grouping::route::{match_url, RouteGrouping, ROUTE_GROUPING_UI};
|
||||
use crate::grouping::url::{UrlGrouping, URL_GROUPING_UI};
|
||||
use crate::grouping::user::{UserGrouping, USER_GROUPING_UI};
|
||||
use crate::grouping::useragent::{UserAgentGrouping, USER_AGENT_GROUPING_UI};
|
||||
|
|
@ -105,7 +107,7 @@ impl<'logs> LogGrouping<'logs> {
|
|||
|
||||
pub fn by_identifier(&self) -> &[LineSet<'logs>] {
|
||||
self.by_identifier
|
||||
.get_or_init(|| group_lines_by(self.lines.iter(), LogLine::identity))
|
||||
.get_or_init(|| group_lines_by(self.lines.iter(), |line| Some(line.identity())))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,17 +115,24 @@ pub fn group_lines_by<'logs, I, F, K>(indices: I, f: F) -> Vec<LineSet<'logs>>
|
|||
where
|
||||
I: Iterator<Item = &'logs LogLine<'logs>>,
|
||||
K: Ord,
|
||||
F: Fn(&'logs LogLine<'logs>) -> K,
|
||||
F: Fn(&'logs LogLine<'logs>) -> Option<K>,
|
||||
{
|
||||
let mut map: BTreeMap<K, Vec<&'logs LogLine<'logs>>> = BTreeMap::new();
|
||||
let mut unmatched: Vec<&'logs LogLine<'logs>> = Vec::new();
|
||||
|
||||
for line in indices {
|
||||
map.entry(f(line)).or_default().push(line);
|
||||
match f(line) {
|
||||
Some(key) => map.entry(key).or_default().push(line),
|
||||
None => unmatched.push(line),
|
||||
}
|
||||
}
|
||||
|
||||
let mut list: Vec<_> = map.into_values().map(LineSet::new).collect();
|
||||
list.sort_by_key(|list| list.len());
|
||||
list.reverse();
|
||||
if !unmatched.is_empty() {
|
||||
list.push(LineSet::new(unmatched));
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +144,7 @@ pub enum Groupings<'logs> {
|
|||
User(UserGrouping<'logs>),
|
||||
Remote(RemoteGrouping<'logs>),
|
||||
UserAgent(UserAgentGrouping<'logs>),
|
||||
Route(RouteGrouping<'logs>),
|
||||
}
|
||||
|
||||
impl<'logs> GroupingResult<'logs> for Groupings<'logs> {
|
||||
|
|
@ -146,6 +156,7 @@ impl<'logs> GroupingResult<'logs> for Groupings<'logs> {
|
|||
Groupings::User(r) => r.matches(filter),
|
||||
Groupings::Remote(r) => r.matches(filter),
|
||||
Groupings::UserAgent(r) => r.matches(filter),
|
||||
Groupings::Route(r) => r.matches(filter),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +170,7 @@ impl<'logs> GroupingResult<'logs> for Groupings<'logs> {
|
|||
Groupings::User(r) => Box::new(r.render()),
|
||||
Groupings::Remote(r) => Box::new(r.render()),
|
||||
Groupings::UserAgent(r) => Box::new(r.render()),
|
||||
Groupings::Route(r) => Box::new(r.render()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -170,6 +182,7 @@ pub enum GroupingOptions {
|
|||
User,
|
||||
Remote,
|
||||
UserAgent,
|
||||
Route,
|
||||
}
|
||||
|
||||
impl GroupingOptions {
|
||||
|
|
@ -180,11 +193,12 @@ impl GroupingOptions {
|
|||
GroupingOptions::User,
|
||||
GroupingOptions::Remote,
|
||||
GroupingOptions::UserAgent,
|
||||
GroupingOptions::Route,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn group_key(&self, line: &LogLine) -> u64 {
|
||||
pub fn group_key(&self, line: &LogLine) -> Option<u64> {
|
||||
let mut hasher = AHasher::default();
|
||||
match self {
|
||||
GroupingOptions::Url => line.url.hash(&mut hasher),
|
||||
|
|
@ -192,27 +206,31 @@ impl GroupingOptions {
|
|||
GroupingOptions::User => line.user.hash(&mut hasher),
|
||||
GroupingOptions::Remote => line.remote.hash(&mut hasher),
|
||||
GroupingOptions::UserAgent => line.user_agent.hash(&mut hasher),
|
||||
GroupingOptions::Route => match_url(&line.method, &line.url)?.hash(&mut hasher),
|
||||
}
|
||||
hasher.finish()
|
||||
Some(hasher.finish())
|
||||
}
|
||||
|
||||
pub fn group<'logs>(&self, line: &'logs LogLine<'logs>) -> Groupings<'logs> {
|
||||
pub fn group<'logs>(&self, line: &'logs LogLine<'logs>) -> Option<Groupings<'logs>> {
|
||||
match self {
|
||||
GroupingOptions::Url => Groupings::Url(UrlGrouping {
|
||||
GroupingOptions::Url => Some(Groupings::Url(UrlGrouping {
|
||||
url: line.url.as_ref(),
|
||||
}),
|
||||
GroupingOptions::App => Groupings::App(AppGrouping {
|
||||
})),
|
||||
GroupingOptions::App => Some(Groupings::App(AppGrouping {
|
||||
app: line.app.as_ref(),
|
||||
}),
|
||||
GroupingOptions::User => Groupings::User(UserGrouping {
|
||||
})),
|
||||
GroupingOptions::User => Some(Groupings::User(UserGrouping {
|
||||
user: line.user.as_str(),
|
||||
}),
|
||||
GroupingOptions::Remote => Groupings::Remote(RemoteGrouping {
|
||||
})),
|
||||
GroupingOptions::Remote => Some(Groupings::Remote(RemoteGrouping {
|
||||
remote: line.remote.as_str(),
|
||||
}),
|
||||
GroupingOptions::UserAgent => Groupings::UserAgent(UserAgentGrouping {
|
||||
})),
|
||||
GroupingOptions::UserAgent => Some(Groupings::UserAgent(UserAgentGrouping {
|
||||
user_agent: line.user_agent.as_ref(),
|
||||
}),
|
||||
})),
|
||||
GroupingOptions::Route => Some(Groupings::Route(RouteGrouping {
|
||||
route: match_url(&line.method, &line.url)?,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,6 +241,7 @@ impl GroupingOptions {
|
|||
GroupingOptions::User => "User",
|
||||
GroupingOptions::Remote => "Remote",
|
||||
GroupingOptions::UserAgent => "User Agent",
|
||||
GroupingOptions::Route => "Route",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +252,7 @@ impl GroupingOptions {
|
|||
GroupingOptions::User => USER_GROUPING_UI,
|
||||
GroupingOptions::Remote => REMOTE_GROUPING_UI,
|
||||
GroupingOptions::UserAgent => USER_AGENT_GROUPING_UI,
|
||||
GroupingOptions::Route => ROUTE_GROUPING_UI,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,9 +261,9 @@ impl GroupingOptions {
|
|||
results.extend(
|
||||
group_lines_by(lines.into_iter(), |line| self.group_key(line))
|
||||
.into_iter()
|
||||
.map(|lines| {
|
||||
let group = self.group(lines.lines[0]);
|
||||
LogGrouping::new(group, lines)
|
||||
.map(|lines| match self.group(lines.lines[0]) {
|
||||
Some(group) => LogGrouping::new(group, lines),
|
||||
None => LogGrouping::named("Unmatched", lines.lines),
|
||||
}),
|
||||
);
|
||||
results
|
||||
|
|
|
|||
146
src/grouping/route.rs
Normal file
146
src/grouping/route.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
use crate::app::Filter;
|
||||
use crate::grouping::{GroupingResult, GroupingUi};
|
||||
use ahash::HashMap;
|
||||
use logsmash_data::all_routes;
|
||||
use ratatui::layout::{Alignment, Constraint};
|
||||
use regex::Regex;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
type Router = matchit::Router<(&'static str, &'static str)>;
|
||||
|
||||
static ROUTER: OnceLock<HashMap<&'static str, Router>> = OnceLock::new();
|
||||
static OCS_ROUTER: OnceLock<HashMap<&'static str, Router>> = OnceLock::new();
|
||||
|
||||
fn get_router(verb: &str) -> Option<&'static Router> {
|
||||
ROUTER
|
||||
.get_or_init(|| {
|
||||
let mut routers: HashMap<&'static str, Router> = HashMap::default();
|
||||
for route in all_routes().filter(|route| !route.ocs) {
|
||||
let router = routers.entry(route.verb).or_insert_with(|| Router::new());
|
||||
let _ = router.insert(route.url, (route.id, route.url));
|
||||
}
|
||||
routers
|
||||
})
|
||||
.get(verb)
|
||||
}
|
||||
|
||||
fn get_ocs_router(verb: &str) -> Option<&'static Router> {
|
||||
OCS_ROUTER
|
||||
.get_or_init(|| {
|
||||
let mut routers: HashMap<&'static str, Router> = HashMap::default();
|
||||
for route in all_routes().filter(|route| route.ocs) {
|
||||
let router = routers.entry(route.verb).or_insert_with(|| Router::new());
|
||||
let _ = router.insert(route.url, (route.id, route.url));
|
||||
}
|
||||
routers
|
||||
})
|
||||
.get(verb)
|
||||
}
|
||||
|
||||
pub fn match_url<'a>(verb: &str, url: &'a str) -> Option<RouteMatch<'a>> {
|
||||
if url == "--" {
|
||||
return Some(RouteMatch::None);
|
||||
}
|
||||
if url.starts_with("/remote.php/") {
|
||||
let mut parts = url.split('/').skip(2);
|
||||
let first = parts.next().unwrap_or_default();
|
||||
if first == "webdav" {
|
||||
return Some(RouteMatch::Remote(first, ""));
|
||||
}
|
||||
return Some(RouteMatch::Remote(first, parts.next().unwrap_or_default()));
|
||||
}
|
||||
if url.starts_with("/public.php/") {
|
||||
let mut parts = url.split('/').skip(2);
|
||||
let first = parts.next().unwrap_or_default();
|
||||
if first == "webdav" {
|
||||
return Some(RouteMatch::Public(first, ""));
|
||||
}
|
||||
return Some(RouteMatch::Public(first, parts.next().unwrap_or_default()));
|
||||
}
|
||||
|
||||
let url = url.split_once('?').map(|(url, _)| url).unwrap_or(url);
|
||||
let (router, url) = if let Some(ocs_url) = url.strip_prefix("/ocs/v2.php") {
|
||||
(get_ocs_router(verb), ocs_url)
|
||||
} else if let Some(ocs_url) = url.strip_prefix("/ocs/v1.php") {
|
||||
(get_ocs_router(verb), ocs_url)
|
||||
} else {
|
||||
(
|
||||
get_router(verb),
|
||||
url.strip_prefix("/index.php").unwrap_or(url),
|
||||
)
|
||||
};
|
||||
let Some(router) = router else {
|
||||
return None;
|
||||
};
|
||||
router
|
||||
.at(url)
|
||||
.map(|m| RouteMatch::Route(m.value.0, m.value.1))
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Clone)]
|
||||
pub enum RouteMatch<'a> {
|
||||
Unmatched,
|
||||
Route(&'static str, &'static str),
|
||||
Remote(&'a str, &'a str),
|
||||
Public(&'a str, &'a str),
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'a> RouteMatch<'a> {
|
||||
pub fn matches(&self, filter_part: &Regex) -> bool {
|
||||
let s: Cow<'a, str> = self.into();
|
||||
filter_part.is_match(s.as_ref())
|
||||
}
|
||||
|
||||
pub fn url(&self) -> Cow<'a, str> {
|
||||
match self {
|
||||
RouteMatch::Route(_, url) => (*url).into(),
|
||||
RouteMatch::Remote(a, "") => format!("/remote.php/{a}").into(),
|
||||
RouteMatch::Public(a, "") => format!("/public.php/{a}").into(),
|
||||
RouteMatch::Remote(a, b) => format!("/remote.php/{a}/{b}/{{path}}").into(),
|
||||
RouteMatch::Public(a, b) => format!("/public.php/{a}/{b}/{{path}}").into(),
|
||||
_ => "".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&RouteMatch<'a>> for Cow<'a, str> {
|
||||
fn from(value: &RouteMatch<'a>) -> Self {
|
||||
match value {
|
||||
RouteMatch::Unmatched => "Umatched".into(),
|
||||
RouteMatch::Route(route, _) => (*route).into(),
|
||||
RouteMatch::Remote(a, "") => format!("remote.{a}").into(),
|
||||
RouteMatch::Public(a, "") => format!("public.{a}").into(),
|
||||
RouteMatch::Remote(a, b) => format!("remote.{a}.{b}").into(),
|
||||
RouteMatch::Public(a, b) => format!("public.{a}.{b}").into(),
|
||||
RouteMatch::None => "--".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct RouteGrouping<'logs> {
|
||||
pub route: RouteMatch<'logs>,
|
||||
}
|
||||
|
||||
pub const ROUTE_GROUPING_UI: GroupingUi = GroupingUi {
|
||||
header: &[("Route", Alignment::Left), ("Url", Alignment::Left)],
|
||||
widths: &[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
};
|
||||
|
||||
impl<'a> GroupingResult<'a> for RouteGrouping<'a> {
|
||||
fn matches(&self, filter: &Filter) -> bool {
|
||||
if filter.is_empty() {
|
||||
return true;
|
||||
}
|
||||
filter
|
||||
.parts()
|
||||
.all(|filter_part| self.route.matches(filter_part))
|
||||
}
|
||||
|
||||
fn render(&self) -> impl Iterator<Item = Cow<'a, str>> {
|
||||
[(&self.route).into(), self.route.url()].into_iter()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue