more views things
Signed-off-by: AKP <tom@tdpain.net>
This commit is contained in:
parent
b54400e1b3
commit
b817d6a23f
11 changed files with 171 additions and 24 deletions
|
@ -3,8 +3,10 @@ package db
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/codemicro/analytics/analytics/config"
|
||||
"github.com/codemicro/analytics/analytics/db/migrations"
|
||||
"github.com/codemicro/analytics/analytics/db/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
|
@ -45,3 +47,21 @@ func New(conf *config.Config) (*DB, error) {
|
|||
DB: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetSessionsWithActivityAfter(minutes int, sort string) ([]*models.Session, error) {
|
||||
var sessions []*models.Session
|
||||
q := db.DB.NewSelect().
|
||||
Model((*models.Session)(nil)).
|
||||
ColumnExpr("*").
|
||||
ColumnExpr(`(select max("time") as "time" from requests where session_id = "session"."id") as "last_seen"`)
|
||||
if sort != "" {
|
||||
q = q.Order(sort)
|
||||
}
|
||||
if minutes > 0 {
|
||||
q = q.Where(fmt.Sprintf(`datetime() < datetime("last_seen", '+%d minutes')`, minutes))
|
||||
}
|
||||
if err := q.Scan(context.Background(), &sessions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ func (i *Ingest) assignToSession(tx bun.Tx, request *models.Request) (*models.Se
|
|||
Model(sess).
|
||||
Where("ip_addr = ?", request.IPAddr).
|
||||
Where("user_agent = ?", request.UserAgent).
|
||||
Where(`? < datetime((select max("time") as "time" from requests where session_id = "session"."id"), '+30 minutes')`, request.Time).
|
||||
Where(`? < datetime((select max("time") as "time" from requests where session_id = "session"."id"), '+2 hours')`, request.Time).
|
||||
Scan(context.Background(), sess)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
|
|
|
@ -3,7 +3,6 @@ package webui
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/codemicro/analytics/analytics/db/models"
|
||||
"github.com/flosch/pongo2/v6"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"strconv"
|
||||
|
@ -23,16 +22,12 @@ func (wui *WebUI) partial_activeSessionsTable(ctx *fiber.Ctx) error {
|
|||
{"Last seen", "last_seen", true, false},
|
||||
},
|
||||
Data: func(sortKey, sortDirection string) ([][]any, error) {
|
||||
var sessions []*models.Session
|
||||
q := wui.db.DB.NewSelect().
|
||||
Model((*models.Session)(nil)).
|
||||
ColumnExpr("*").
|
||||
ColumnExpr(`(select max("time") as "time" from requests where session_id = "session"."id") as "last_seen"`).
|
||||
Where(`datetime() < datetime("last_seen", '+30 minutes')`)
|
||||
if sortKey != "" {
|
||||
q = q.Order(sortKey + " " + sortDirection)
|
||||
var sort string
|
||||
if !(sortKey == "" || sortDirection == "") {
|
||||
sort = sortKey + " " + sortDirection
|
||||
}
|
||||
if err := q.Scan(context.Background(), &sessions); err != nil {
|
||||
sessions, err := wui.db.GetSessionsWithActivityAfter(30, sort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -64,6 +59,12 @@ func (wui *WebUI) partial_topURLs(ctx *fiber.Ctx) error {
|
|||
n, _ = strconv.Atoi(nStr)
|
||||
}
|
||||
|
||||
hoursStr := ctx.Query("hours")
|
||||
var hours int
|
||||
if hoursStr != "" {
|
||||
hours, _ = strconv.Atoi(hoursStr)
|
||||
}
|
||||
|
||||
ht := &HTMLTable{
|
||||
Headers: []*HTMLTableHeader{
|
||||
{"Count", "", false, false},
|
||||
|
@ -77,10 +78,16 @@ func (wui *WebUI) partial_topURLs(ctx *fiber.Ctx) error {
|
|||
Count int
|
||||
}
|
||||
q := wui.db.DB.NewSelect().
|
||||
ColumnExpr(`"host", "uri", COUNT(*) as "count"`).Table("requests").GroupExpr(`"host", "uri"`).OrderExpr(`"count" DESC`)
|
||||
ColumnExpr(`"host", "uri", COUNT(*) as "count"`).
|
||||
Table("requests").
|
||||
GroupExpr(`"host", "uri"`).
|
||||
OrderExpr(`"count" DESC`)
|
||||
if n > 0 {
|
||||
q = q.Limit(n)
|
||||
}
|
||||
if hours > 0 {
|
||||
q = q.Where(fmt.Sprintf(`datetime() < datetime(time, '+%d hours')`, hours))
|
||||
}
|
||||
if err := q.Scan(context.Background(), &counts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
{% extends "extendable/base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<h2>Top 10 URLs</h2>
|
||||
<h2>Top 10 URLs (past 24 hours)</h2>
|
||||
|
||||
<div hx-get="/partial/topURLs?n=10"
|
||||
<div hx-get="/partial/topURLs?n=10&hours=24"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
<a href="/top">[See all]</a>
|
||||
|
||||
<h2>Active sessions</h2>
|
||||
|
||||
<div hx-get="/partial/activeSessions"
|
||||
<div hx-get="/partial/activeSessions?minutes=30"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
<a href="/sessions">[See all]</a>
|
||||
{% endblock %}
|
14
analytics/webui/internal/templates/list-sessions.html
Normal file
14
analytics/webui/internal/templates/list-sessions.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "extendable/base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<a href="/">[Home]</a>
|
||||
|
||||
<h2>All sessions</h2>
|
||||
|
||||
<div hx-get="/partial/listSessions"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "extendable/base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<a href="/">[< Back]</a>
|
||||
<a href="/">[Home]</a>
|
||||
|
||||
<h2>Logs from session</h2>
|
||||
|
||||
|
@ -17,6 +17,4 @@
|
|||
>
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
20
analytics/webui/internal/templates/request-detail.html
Normal file
20
analytics/webui/internal/templates/request-detail.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "extendable/base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<a href="/">[Home]</a>
|
||||
|
||||
<h2>Request detail</h2>
|
||||
|
||||
<p>
|
||||
ID: {{ request.ID }}<br>
|
||||
Time: {{ request.Time | date:"2006-01-02 15:04:05" }}<br>
|
||||
IP address: {{ request.IPAddr }}<br>
|
||||
Host: {{ request.Host }}<br>
|
||||
Raw path: {{ request.RawURI }}<br>
|
||||
Normalised path: {{ request.URI }}<br>
|
||||
Referer: {% if request.Referer %}{{ request.Referer }}{% else %}<span class="italic">unset</span>{% endif %}<br>
|
||||
User agent: {% if request.UserAgent %}{{ request.UserAgent }}{% else %}<span class="italic">unset</span>{% endif %}<br>
|
||||
Status code: {{ request.StatusCode }}<br>
|
||||
Session: <a href="/session?id={{ request.SessionID }}">{{ request.SessionID }}</a>
|
||||
</p>
|
||||
{% endblock %}
|
51
analytics/webui/listSessions.go
Normal file
51
analytics/webui/listSessions.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/flosch/pongo2/v6"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (wui *WebUI) page_listSessions(ctx *fiber.Ctx) error {
|
||||
return wui.sendTemplate(ctx, "list-sessions.html", nil)
|
||||
}
|
||||
|
||||
func (wui *WebUI) partial_listSessions(ctx *fiber.Ctx) error {
|
||||
ht := &HTMLTable{
|
||||
Path: "/partial/listSessions",
|
||||
Headers: []*HTMLTableHeader{
|
||||
{"", "", false, true},
|
||||
{"User agent", "", false, false},
|
||||
{"IP", "", false, false},
|
||||
{"Last seen", "last_seen", true, false},
|
||||
},
|
||||
Data: func(sortKey, sortDirection string) ([][]any, error) {
|
||||
var sort string
|
||||
if !(sortKey == "" || sortDirection == "") {
|
||||
sort = sortKey + " " + sortDirection
|
||||
}
|
||||
sessions, err := wui.db.GetSessionsWithActivityAfter(60*24, sort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res [][]any
|
||||
for _, sess := range sessions {
|
||||
ua, _ := pongo2.ApplyFilter("truncatechars", pongo2.AsValue(sess.UserAgent), pongo2.AsValue(40))
|
||||
ua, _ = pongo2.ApplyFilter("default", ua, unsetValue)
|
||||
res = append(res, []any{
|
||||
fmt.Sprintf(`<a href="/session?id=%s">[Link]</a>`, sess.ID),
|
||||
ua,
|
||||
getValue(pongo2.ApplyFilter("truncatechars", pongo2.AsValue(sess.IPAddr), pongo2.AsValue(30))),
|
||||
getValue(pongo2.ApplyFilter("shortTimeSince", pongo2.AsValue(sess.LastSeen), nil)),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
},
|
||||
DefaultSortKey: "last_seen",
|
||||
DefaultSortDirection: "desc",
|
||||
ShowNumberOfEntries: true,
|
||||
}
|
||||
return wui.renderHTMLTable(ctx, ht)
|
||||
}
|
30
analytics/webui/requestDetail.go
Normal file
30
analytics/webui/requestDetail.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/codemicro/analytics/analytics/db/models"
|
||||
"github.com/flosch/pongo2/v6"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func (wui *WebUI) page_requestDetail(ctx *fiber.Ctx) error {
|
||||
id := ctx.Query("id")
|
||||
if id == "" {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
request := new(models.Request)
|
||||
if err := wui.db.DB.NewSelect().Model(request).Where("id = ?", id).Scan(context.Background(), request); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
pctx := pongo2.Context{
|
||||
"request": request,
|
||||
}
|
||||
|
||||
return wui.sendTemplate(ctx, "request-detail.html", pctx)
|
||||
}
|
|
@ -14,10 +14,8 @@ import (
|
|||
func (wui *WebUI) page_logsFromSession(ctx *fiber.Ctx) error {
|
||||
id := ctx.Query("id")
|
||||
|
||||
pctx := make(pongo2.Context)
|
||||
|
||||
if id == "" {
|
||||
return ctx.RedirectBack("/")
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
session := new(models.Session)
|
||||
|
@ -27,7 +25,9 @@ func (wui *WebUI) page_logsFromSession(ctx *fiber.Ctx) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
pctx["session"] = session
|
||||
pctx := pongo2.Context{
|
||||
"session": session,
|
||||
}
|
||||
|
||||
return wui.sendTemplate(ctx, "logs-from-session.html", pctx)
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ func (wui *WebUI) partial_logsFromSession(ctx *fiber.Ctx) error {
|
|||
{"Raw path", "raw_uri", true, false},
|
||||
{"Status", "status_code", true, false},
|
||||
{"Referer", "", false, false},
|
||||
{"", "", false, true},
|
||||
},
|
||||
Data: func(sortKey, sortDirection string) ([][]any, error) {
|
||||
var reqs []*models.Request
|
||||
|
@ -63,6 +64,7 @@ func (wui *WebUI) partial_logsFromSession(ctx *fiber.Ctx) error {
|
|||
request.RawURI,
|
||||
request.StatusCode,
|
||||
getValue(pongo2.ApplyFilter("default", pongo2.AsValue(request.Referer), unsetValue)),
|
||||
fmt.Sprintf(`<a href="/request?id=%s">[Link]</a>`, request.ID),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,11 @@ func (wui *WebUI) registerHandlers() {
|
|||
|
||||
wui.app.Get("/partial/topURLs", wui.partial_topURLs)
|
||||
|
||||
wui.app.Get("/request", wui.page_requestDetail)
|
||||
|
||||
wui.app.Get("/sessions", wui.page_listSessions)
|
||||
wui.app.Get("/partial/listSessions", wui.partial_listSessions)
|
||||
|
||||
wui.app.Use("/", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(static),
|
||||
PathPrefix: "static",
|
||||
|
|
Reference in a new issue