Files
OpenFlare/openflare_server/model/node_access_log.go
T

336 lines
10 KiB
Go

package model
import (
"fmt"
"strings"
"time"
"gorm.io/gorm"
)
type NodeAccessLog struct {
ID uint `json:"id" gorm:"primaryKey"`
NodeID string `json:"node_id" gorm:"index:idx_node_access_logs_node_logged_at,priority:1;size:64;not null"`
LoggedAt time.Time `json:"logged_at" gorm:"index:idx_node_access_logs_logged_at;index:idx_node_access_logs_node_logged_at,priority:2"`
RemoteAddr string `json:"remote_addr" gorm:"index:idx_node_access_logs_remote_addr;size:128"`
Region string `json:"region" gorm:"size:128"`
Host string `json:"host" gorm:"index:idx_node_access_logs_host;size:255"`
Path string `json:"path" gorm:"size:2048"`
StatusCode int `json:"status_code" gorm:"index:idx_node_access_logs_status_code"`
RawJSON string `json:"raw_json" gorm:"type:text"`
CreatedAt time.Time `json:"created_at"`
}
type NodeAccessLogRegionCount struct {
Region string `json:"region"`
Count int64 `json:"count"`
}
type NodeAccessLogQuery struct {
NodeID string
RemoteAddr string
Host string
Path string
Since time.Time
Page int
PageSize int
SortBy string
SortOrder string
}
type NodeAccessLogBucketQuery struct {
NodeID string
RemoteAddr string
Host string
Path string
Since time.Time
Page int
PageSize int
SortBy string
SortOrder string
FoldMinutes int
}
type NodeAccessLogBucketRow struct {
BucketEpoch int64 `json:"bucket_epoch"`
RequestCount int64 `json:"request_count"`
UniqueIPCount int64 `json:"unique_ip_count"`
UniqueHostCount int64 `json:"unique_host_count"`
SuccessCount int64 `json:"success_count"`
ClientErrorCount int64 `json:"client_error_count"`
ServerErrorCount int64 `json:"server_error_count"`
}
type NodeAccessLogIPSummaryQuery struct {
NodeID string
RemoteAddr string
Host string
Since time.Time
Page int
PageSize int
SortBy string
SortOrder string
}
type NodeAccessLogIPSummaryRow struct {
RemoteAddr string `json:"remote_addr"`
TotalRequests int64 `json:"total_requests"`
RecentRequests int64 `json:"recent_requests"`
LastSeenEpoch int64 `json:"last_seen_epoch"`
}
type NodeAccessLogIPTrendQuery struct {
NodeID string
RemoteAddr string
Host string
Since time.Time
BucketMinutes int
}
type NodeAccessLogTrendPointRow struct {
BucketEpoch int64 `json:"bucket_epoch"`
RequestCount int64 `json:"request_count"`
}
func ListNodeAccessLogs(query NodeAccessLogQuery) (logs []*NodeAccessLog, err error) {
offset := query.Page * query.PageSize
db := buildNodeAccessLogQuery(DB, query).
Order(buildNodeAccessLogSortClause(query.SortBy, query.SortOrder)).
Limit(query.PageSize).
Offset(offset)
err = db.Find(&logs).Error
return logs, err
}
func CountNodeAccessLogs(query NodeAccessLogQuery) (totalRecords int64, totalIPs int64, err error) {
base := buildNodeAccessLogQuery(DB.Model(&NodeAccessLog{}), query)
if err = base.Count(&totalRecords).Error; err != nil {
return 0, 0, err
}
distinctQuery := buildNodeAccessLogQuery(DB.Model(&NodeAccessLog{}), query).
Where("remote_addr <> ''").
Distinct("remote_addr")
if err = distinctQuery.Count(&totalIPs).Error; err != nil {
return 0, 0, err
}
return totalRecords, totalIPs, nil
}
func ListNodeAccessLogRegionCounts(nodeID string, since time.Time, limit int) (items []*NodeAccessLogRegionCount, err error) {
query := DB.Model(&NodeAccessLog{}).
Select("region as region, count(*) as count").
Where("region <> ''")
if nodeID != "" {
query = query.Where("node_id = ?", nodeID)
}
if !since.IsZero() {
query = query.Where("logged_at >= ?", since)
}
query = query.Group("region").Order("count desc, region asc")
if limit > 0 {
query = query.Limit(limit)
}
err = query.Scan(&items).Error
return items, err
}
func ListNodeAccessLogBuckets(query NodeAccessLogBucketQuery) (items []*NodeAccessLogBucketRow, err error) {
offset := query.Page * query.PageSize
bucketExpr := accessLogBucketEpochExpr(query.FoldMinutes)
base := buildNodeAccessLogQuery(DB.Model(&NodeAccessLog{}), NodeAccessLogQuery{
NodeID: query.NodeID,
RemoteAddr: query.RemoteAddr,
Host: query.Host,
Path: query.Path,
Since: query.Since,
})
err = base.Select(fmt.Sprintf(
"%s as bucket_epoch, count(*) as request_count, count(distinct remote_addr) as unique_ip_count, count(distinct host) as unique_host_count, sum(case when status_code < 400 then 1 else 0 end) as success_count, sum(case when status_code >= 400 and status_code < 500 then 1 else 0 end) as client_error_count, sum(case when status_code >= 500 then 1 else 0 end) as server_error_count",
bucketExpr,
)).
Group(bucketExpr).
Order(buildNodeAccessLogBucketSortClause(query.SortBy, query.SortOrder)).
Limit(query.PageSize).
Offset(offset).
Scan(&items).Error
return items, err
}
func CountNodeAccessLogBuckets(query NodeAccessLogBucketQuery) (total int64, err error) {
bucketExpr := accessLogBucketEpochExpr(query.FoldMinutes)
base := buildNodeAccessLogQuery(DB.Model(&NodeAccessLog{}), NodeAccessLogQuery{
NodeID: query.NodeID,
RemoteAddr: query.RemoteAddr,
Host: query.Host,
Path: query.Path,
Since: query.Since,
})
rows := []struct {
BucketEpoch int64 `gorm:"column:bucket_epoch"`
}{}
err = base.Select(fmt.Sprintf("%s as bucket_epoch", bucketExpr)).
Group(bucketExpr).
Scan(&rows).Error
if err != nil {
return 0, err
}
return int64(len(rows)), nil
}
func ListNodeAccessLogIPSummaries(query NodeAccessLogIPSummaryQuery, recentSince time.Time) (items []*NodeAccessLogIPSummaryRow, err error) {
offset := query.Page * query.PageSize
base := buildNodeAccessLogQuery(DB.Model(&NodeAccessLog{}), NodeAccessLogQuery{
NodeID: query.NodeID,
RemoteAddr: query.RemoteAddr,
Host: query.Host,
Since: query.Since,
}).Where("remote_addr <> ''")
lastSeenExpr := accessLogEpochExpr("max(logged_at)")
err = base.Select(
"remote_addr as remote_addr, count(*) as total_requests, sum(case when logged_at >= ? then 1 else 0 end) as recent_requests, "+lastSeenExpr+" as last_seen_epoch",
recentSince,
).
Group("remote_addr").
Order(buildNodeAccessLogIPSummarySortClause(query.SortBy, query.SortOrder)).
Limit(query.PageSize).
Offset(offset).
Scan(&items).Error
return items, err
}
func CountNodeAccessLogIPSummaries(query NodeAccessLogIPSummaryQuery) (total int64, err error) {
base := buildNodeAccessLogQuery(DB.Model(&NodeAccessLog{}), NodeAccessLogQuery{
NodeID: query.NodeID,
RemoteAddr: query.RemoteAddr,
Host: query.Host,
Since: query.Since,
}).Where("remote_addr <> ''")
rows := []struct {
RemoteAddr string `gorm:"column:remote_addr"`
}{}
err = base.Select("remote_addr").
Group("remote_addr").
Scan(&rows).Error
if err != nil {
return 0, err
}
return int64(len(rows)), nil
}
func ListNodeAccessLogIPTrend(query NodeAccessLogIPTrendQuery) (items []*NodeAccessLogTrendPointRow, err error) {
bucketExpr := accessLogBucketEpochExpr(query.BucketMinutes)
base := buildNodeAccessLogQuery(DB.Model(&NodeAccessLog{}), NodeAccessLogQuery{
NodeID: query.NodeID,
RemoteAddr: query.RemoteAddr,
Host: query.Host,
Since: query.Since,
}).Where("remote_addr = ?", strings.TrimSpace(query.RemoteAddr))
err = base.Select(fmt.Sprintf("%s as bucket_epoch, count(*) as request_count", bucketExpr)).
Group(bucketExpr).
Order("bucket_epoch asc").
Scan(&items).Error
return items, err
}
func DeleteNodeAccessLogsBefore(before time.Time) (deleted int64, err error) {
result := DB.Where("logged_at < ?", before).Delete(&NodeAccessLog{})
return result.RowsAffected, result.Error
}
func buildNodeAccessLogQuery(db *gorm.DB, query NodeAccessLogQuery) *gorm.DB {
if db == nil {
db = DB.Model(&NodeAccessLog{})
}
if db.Statement == nil || db.Statement.Model == nil {
db = db.Model(&NodeAccessLog{})
}
if trimmed := strings.TrimSpace(query.NodeID); trimmed != "" {
db = db.Where("node_id LIKE ?", "%"+trimmed+"%")
}
if trimmed := strings.TrimSpace(query.RemoteAddr); trimmed != "" {
db = db.Where("remote_addr LIKE ?", "%"+trimmed+"%")
}
if trimmed := strings.TrimSpace(query.Host); trimmed != "" {
db = db.Where("host LIKE ?", "%"+trimmed+"%")
}
if trimmed := strings.TrimSpace(query.Path); trimmed != "" {
db = db.Where("path LIKE ?", "%"+trimmed+"%")
}
if !query.Since.IsZero() {
db = db.Where("logged_at >= ?", query.Since)
}
return db
}
func buildNodeAccessLogSortClause(sortBy string, sortOrder string) string {
column := "logged_at"
switch strings.TrimSpace(sortBy) {
case "status_code":
column = "status_code"
case "remote_addr":
column = "remote_addr"
case "host":
column = "host"
case "path":
column = "path"
}
order := normalizeSortOrder(sortOrder)
if column == "logged_at" {
return fmt.Sprintf("%s %s, id %s", column, order, order)
}
return fmt.Sprintf("%s %s, logged_at desc, id desc", column, order)
}
func buildNodeAccessLogBucketSortClause(sortBy string, sortOrder string) string {
order := normalizeSortOrder(sortOrder)
switch strings.TrimSpace(sortBy) {
case "request_count":
return fmt.Sprintf("request_count %s, bucket_epoch desc", order)
default:
return fmt.Sprintf("bucket_epoch %s", order)
}
}
func buildNodeAccessLogIPSummarySortClause(sortBy string, sortOrder string) string {
order := normalizeSortOrder(sortOrder)
switch strings.TrimSpace(sortBy) {
case "recent_requests":
return fmt.Sprintf("recent_requests %s, last_seen_epoch desc, remote_addr asc", order)
case "last_seen_at":
return fmt.Sprintf("last_seen_epoch %s, total_requests desc, remote_addr asc", order)
case "remote_addr":
return fmt.Sprintf("remote_addr %s", order)
default:
return fmt.Sprintf("total_requests %s, last_seen_epoch desc, remote_addr asc", order)
}
}
func accessLogBucketEpochExpr(bucketMinutes int) string {
bucketSeconds := bucketMinutes * 60
if bucketSeconds <= 0 {
bucketSeconds = 180
}
switch DB.Dialector.Name() {
case "postgres":
return fmt.Sprintf("CAST(floor(extract(epoch from logged_at) / %d) * %d AS BIGINT)", bucketSeconds, bucketSeconds)
default:
return fmt.Sprintf("CAST((strftime('%%s', logged_at) / %d) * %d AS INTEGER)", bucketSeconds, bucketSeconds)
}
}
func accessLogEpochExpr(expression string) string {
switch DB.Dialector.Name() {
case "postgres":
return fmt.Sprintf("CAST(extract(epoch from %s) AS BIGINT)", expression)
default:
return fmt.Sprintf("CAST(strftime('%%s', %s) AS INTEGER)", expression)
}
}
func normalizeSortOrder(sortOrder string) string {
if strings.EqualFold(strings.TrimSpace(sortOrder), "asc") {
return "asc"
}
return "desc"
}