changeset 30:24f2baa54870

Show top pages
author Lewin Bormann <lbo@spheniscida.de>
date Fri, 15 Jul 2022 08:19:13 -0700
parents 8ad244c1b061
children c25ab9d90bb2
files assets/index.html.hbs src/logsdb.rs src/main.rs
diffstat 3 files changed, 94 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/assets/index.html.hbs	Thu Jul 14 22:37:16 2022 -0700
+++ b/assets/index.html.hbs	Fri Jul 15 08:19:13 2022 -0700
@@ -54,14 +54,17 @@
         </div>
     </div>
     <div class="plotrow row2">
-        <div class="plotframe halfwidth"> </div>
+        <div class="plotframe halfwidth"><canvas id="topPaths" height="100"></canvas> </div>
         <div class="plotframe halfwidth"> </div>
     </div>
 
     <script>
-        let plots = {"visitsAndSessions": {{{ chartconfig.visitsAndSessions }}} };
+        let plots = {"visitsAndSessions": {{{ chartconfig.visitsAndSessions }}}, "topPaths": {{{ chartconfig.topPaths }}} };
 
         Object.keys(plots).forEach((cv) => {
+                    if (plots[cv] == undefined) {
+                                return;
+                            }
                     const ctx = document.getElementById(cv).getContext('2d');
                     const myChart = new Chart(ctx, plots[cv]);
                 });
--- a/src/logsdb.rs	Thu Jul 14 22:37:16 2022 -0700
+++ b/src/logsdb.rs	Fri Jul 15 08:19:13 2022 -0700
@@ -4,7 +4,7 @@
 use log::{debug, error, info, warn, Level};
 use time::{Duration, OffsetDateTime};
 
-use rocket::futures::StreamExt;
+use rocket::futures::{future::ready, StreamExt};
 use rocket_db_pools::sqlx::{Executor, Row, Sqlite, SqlitePool};
 use rocket_db_pools::{Connection, Database, Pool};
 use sqlx::prelude::FromRow;
@@ -140,4 +140,38 @@
 
         Ok((dates, visits, sessions))
     }
+
+    pub async fn query_top_paths(
+        &mut self,
+        from: OffsetDateTime,
+        to: OffsetDateTime,
+        tz_offset: Option<i64>,
+        domainpattern: Option<&str>,
+        n: i64,
+    ) -> Result<Vec<(String, i64)>, Error> {
+        let tz_offset = tz_offset.unwrap_or(0);
+        let result = sqlx::query(
+            r#"
+SELECT path, COUNT(*) AS pathcount
+FROM RequestLog
+WHERE atime+? > ? AND atime+? < ? AND domain LIKE ?
+GROUP BY path
+ORDER BY pathcount DESC
+LIMIT ?;"#,
+        )
+        .bind(tz_offset)
+        .bind(from.unix_timestamp())
+        .bind(tz_offset)
+        .bind(to.unix_timestamp())
+        .bind(domainpattern.unwrap_or("%"))
+        .bind(n)
+        .fetch(&mut *self.0);
+
+        Ok(result
+            .map(|r| r.map(|e| (e.get(0), e.get(1))))
+            .filter(|e| ready(e.is_ok()))
+            .map(|e| e.unwrap())
+            .collect()
+            .await)
+    }
 }
--- a/src/main.rs	Thu Jul 14 22:37:16 2022 -0700
+++ b/src/main.rs	Fri Jul 15 08:19:13 2022 -0700
@@ -119,10 +119,17 @@
     }
 }
 
+#[derive(Default)]
+struct ChartOptions {
+    typ: String,               // bar/line/etc.
+    indexAxis: Option<String>, // x/y
+    stack: Option<String>,
+}
+
 fn create_chart<S1: Serialize, S2: Serialize>(
     labels: Vec<S1>,
     values: Vec<(String, Vec<S2>)>,
-    typ: String,
+    opt: &ChartOptions,
 ) -> Value {
     let colors = vec![
         "red", "blue", "orange", "green", "gray", "purple", "black", "brown",
@@ -133,16 +140,25 @@
     let datasets: Vec<Value> = values
         .iter()
         .zip(colors)
-        .map(|((name, val), col)| json!({"label": name, "data": val, "borderColor": col}))
+        .map(|((name, val), col)| {
+            json!({
+                "label": name,
+                "data": val,
+                "borderColor": col,
+                "backgroundColor": col,
+            })
+        })
         .collect();
     let inner = json!({
-        "type": typ,
+        "type": &opt.typ,
         "data": {
             "labels": labels,
             "datasets": datasets,
         },
         "options": {
             "scales": { "y": { "beginAtZero": true }},
+            "indexAxis": opt.indexAxis,
+            "stack": opt.stack.as_ref().map(String::as_str),
         },
     });
     inner
@@ -213,7 +229,7 @@
             0
         }
     };
-    match LogsDBSession(&mut conn)
+    let vissess = match LogsDBSession(&mut conn)
         .query_visits_sessions_counts(begin, end, Some(tz_offset), domain)
         .await
     {
@@ -222,27 +238,43 @@
                 "Successfully queried visits/sessions: {:?}, {:?}",
                 visits, sessions
             );
-            let vissess = create_chart(
+            create_chart(
                 dates,
                 vec![("Visits".into(), visits), ("Sessions".into(), sessions)],
-                "line".into(),
+                &ChartOptions {
+                    typ: "line".into(),
+                    ..ChartOptions::default()
+                },
             )
-            .to_string();
-            Template::render(
-                "index",
-                context![
-                loggedin: true,
-                domain: domain,
-                username: lig.0,
-                flash: f,
-                chartconfig: context![ visitsAndSessions: vissess ]],
-            )
+            .to_string()
         }
-        Err(e) => Template::render(
-            "index",
-            context![loggedin: true, domain: domain, username: lig.0, flash: f, chartconfig: context![], error: format!("{:?}", e)],
-        ),
-    }
+        Err(e) => "undefined".to_string(), //return Template::render("index", context![loggedin: true, domain: domain, username: lig.0, flash: f, chartconfig: context![], error: format!("{:?}", e)],),
+    };
+    let toppaths = match LogsDBSession(&mut conn)
+        .query_top_paths(begin, end, Some(tz_offset), domain, 10)
+        .await
+    {
+        Ok(tp) => create_chart(
+            tp.iter().map(|(p, c)| p).collect(),
+            vec![("Top Pages".into(), tp.iter().map(|(p, c)| c).collect())],
+            &ChartOptions {
+                typ: "bar".into(),
+                indexAxis: Some("y".into()),
+                ..ChartOptions::default()
+            },
+        )
+        .to_string(),
+        Err(e) => "undefined".to_string(),
+    };
+    Template::render(
+        "index",
+        context![
+        loggedin: true,
+        domain: domain,
+        username: lig.0,
+        flash: f,
+        chartconfig: context![ visitsAndSessions: vissess, topPaths: toppaths ]],
+    )
 }
 
 #[rocket::get("/", rank = 2)]
@@ -310,7 +342,7 @@
                 .0
                 .get("host")
                 .take(1)
-                .map(|s| s.to_string())
+                .map(str::to_string)
                 .collect::<Vec<String>>()
                 .pop()
                 .unwrap_or(String::new()),
@@ -343,14 +375,6 @@
     }
 }
 
-#[rocket::get("/testchart")]
-async fn route_testchart() -> Template {
-    Template::render(
-        "testchart",
-        context![ chartconfig: create_chart(vec!["1", "2", "3", "4"], vec![("Series A".into(), vec![1,4,3,8]), ("Series B".into(), vec![5,2,3,9])], "line".into()).to_string() ],
-    )
-}
-
 #[derive(rocket::serde::Deserialize)]
 #[serde(crate = "rocket::serde")]
 struct CustomConfig {
@@ -389,7 +413,6 @@
                 route_login_form,
                 route_login_post,
                 route_log,
-                route_testchart,
             ],
         );
     r