mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
7d71f1e4e1
- New internal/repository/logstore abstraction: exported domain interfaces (AccessLogStore/ObservabilityStore/UserAccessLogStore/StatusStore), config-driven provider (Active/Build/Migrating/SetConfigReader), GORM implementation for PostgreSQL/SQLite (incl. hourly rollups computed in real time, migration listers, PG partition maintenance), and a ClickHouse wrapper preserving the native batch path; repository facade delegates to logstore; import-lint test enforces apps never import analyticsrepo. - ClickHouse is now optional: the log DB is either the main DB (postgres when database.enabled, else sqlite) or clickhouse; boot validation + first-run seed; log_database / log_db_migration are protected keys. - New user task 切换日志数据库 (of_log_db_switch): freeze log writes, drain batch writers, copy all 6 raw log tables by id (preserving IDs) with target-partition pre-creation for PG, flip log_database on success, clear the freeze flag on failure. - Per-store retention (log_retention_days_*) with expiry cleanup folded into the daily system_cleanup task; legacy database_auto_cleanup_* and of_database_auto_cleanup decommissioned. - goose migrations: 6 log tables in PG (2 monthly-partitioned) + SQLite, retention config seeds, schedule cleanup; GET /api/v1/admin/status/log-database endpoint; frontend retention settings, switch-task UI and status badge; changelog and docs updated. docs(plan): log database decoupling implementation plan docs(design): log database decoupling design (ClickHouse optional)
125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package analytics
|
|
|
|
import "strings"
|
|
|
|
// User-Agent 浏览器/OS/设备分类(纯函数,无 IO)。
|
|
// 与 internal/repository/analytics/browser.go 的判定逻辑保持一致(Task 4 复制,
|
|
// 因为 model 不得 import analyticsrepo);后续若移除旧 CH 实现,可让 analyticsrepo 改以别名复用本包。
|
|
|
|
const (
|
|
uaLabelUnknown = "Unknown"
|
|
uaLabelBot = "Bot"
|
|
uaLabelOther = "Other"
|
|
uaTokenBot = "bot"
|
|
uaTokenAndroid = "android"
|
|
uaTokenSpider = "spider"
|
|
uaTokenCrawler = "crawler"
|
|
)
|
|
|
|
type uaMatchRule struct {
|
|
label string
|
|
contains []string
|
|
allOf []string
|
|
noneOf []string
|
|
}
|
|
|
|
func matchUARules(uaLower string, rules []uaMatchRule, fallback string) string {
|
|
if uaLower == "" {
|
|
return uaLabelUnknown
|
|
}
|
|
for _, rule := range rules {
|
|
matched := false
|
|
for _, token := range rule.contains {
|
|
if strings.Contains(uaLower, token) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched && len(rule.allOf) > 0 {
|
|
matched = true
|
|
for _, token := range rule.allOf {
|
|
if !strings.Contains(uaLower, token) {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
excluded := false
|
|
for _, token := range rule.noneOf {
|
|
if strings.Contains(uaLower, token) {
|
|
excluded = true
|
|
break
|
|
}
|
|
}
|
|
if excluded {
|
|
continue
|
|
}
|
|
return rule.label
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
var browserRules = []uaMatchRule{
|
|
{label: "WeChat", contains: []string{"micromessenger"}},
|
|
{label: "Postman", contains: []string{"postman"}},
|
|
{label: "CLI", contains: []string{"curl/", "wget/"}},
|
|
{label: "Edge", contains: []string{"edg/", "edgios/", "edga/"}},
|
|
{label: "Opera", contains: []string{"opr/", "opera"}},
|
|
{label: "Firefox", contains: []string{"firefox", "fxios"}},
|
|
{label: "Chrome", contains: []string{"crios", "chrome"}, noneOf: []string{"chromium"}},
|
|
{label: "Chromium", contains: []string{"chromium"}},
|
|
{label: "Safari", contains: []string{"safari"}},
|
|
{label: uaLabelBot, contains: []string{uaTokenBot, uaTokenSpider, uaTokenCrawler, "slurp"}},
|
|
}
|
|
|
|
var osRules = []uaMatchRule{
|
|
{label: "Android", contains: []string{uaTokenAndroid}},
|
|
{label: "iOS", contains: []string{"iphone", "ipad", "ipod", "ios"}},
|
|
{label: "Windows", contains: []string{"windows"}},
|
|
{label: "macOS", contains: []string{"mac os x", "macintosh", "macos"}},
|
|
{label: "Chrome OS", contains: []string{"cros"}},
|
|
{label: "Linux", contains: []string{"linux"}},
|
|
{label: uaLabelBot, contains: []string{uaTokenBot, uaTokenSpider, uaTokenCrawler}},
|
|
}
|
|
|
|
var deviceRules = []uaMatchRule{
|
|
{
|
|
label: uaLabelBot,
|
|
contains: []string{uaTokenBot, uaTokenSpider, uaTokenCrawler, "slurp", "curl/", "wget/", "python-requests", "go-http-client", "postman"},
|
|
},
|
|
{
|
|
label: "Tablet",
|
|
contains: []string{"ipad", "tablet"},
|
|
},
|
|
{
|
|
label: "Tablet",
|
|
allOf: []string{uaTokenAndroid},
|
|
noneOf: []string{"mobile"},
|
|
},
|
|
{
|
|
label: "Mobile",
|
|
contains: []string{"mobi", "iphone", "ipod", uaTokenAndroid},
|
|
},
|
|
}
|
|
|
|
// ParseBrowserName performs lightweight User-Agent browser identification.
|
|
func ParseBrowserName(ua string) string {
|
|
return matchUARules(strings.ToLower(ua), browserRules, uaLabelOther)
|
|
}
|
|
|
|
// ParseOSName performs lightweight User-Agent OS identification.
|
|
func ParseOSName(ua string) string {
|
|
return matchUARules(strings.ToLower(ua), osRules, uaLabelOther)
|
|
}
|
|
|
|
// ParseDeviceType performs lightweight User-Agent device type identification.
|
|
func ParseDeviceType(ua string) string {
|
|
return matchUARules(strings.ToLower(ua), deviceRules, "Desktop")
|
|
}
|