talas-group/talas-wiki/internal/wiki/metrics.go
senke 66471934af Initial commit: Talas Group project management & documentation
Knowledge base of ~80+ markdown files across 14 domains (00-13),
Logseq graph, hardware design files (KiCAD), infrastructure configs,
and talas-wiki static site.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:10:41 +02:00

132 lines
3.3 KiB
Go

package wiki
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
)
type Metrics struct {
mu sync.RWMutex
pageViews map[string]int64
searchCount int64
editCount int64
startTime time.Time
renderTimeSum int64 // nanoseconds
renderCount int64
}
func NewMetrics() *Metrics {
return &Metrics{
pageViews: make(map[string]int64),
startTime: time.Now(),
}
}
func (m *Metrics) RecordPageView(path string) {
m.mu.Lock()
m.pageViews[path]++
m.mu.Unlock()
}
func (m *Metrics) RecordSearch() {
atomic.AddInt64(&m.searchCount, 1)
}
func (m *Metrics) RecordEdit() {
atomic.AddInt64(&m.editCount, 1)
}
func (m *Metrics) StartTime() time.Time { return m.startTime }
func (m *Metrics) RecordRenderTime(d time.Duration) {
atomic.AddInt64(&m.renderTimeSum, int64(d))
atomic.AddInt64(&m.renderCount, 1)
}
// PrometheusMetrics returns metrics in Prometheus text exposition format
func (m *Metrics) PrometheusMetrics(idx *Index) string {
m.mu.RLock()
defer m.mu.RUnlock()
var b strings.Builder
// Uptime
uptime := time.Since(m.startTime).Seconds()
fmt.Fprintf(&b, "# HELP wiki_uptime_seconds Time since server start\n")
fmt.Fprintf(&b, "# TYPE wiki_uptime_seconds gauge\nwiki_uptime_seconds %.0f\n\n", uptime)
// Total pages
fmt.Fprintf(&b, "# HELP wiki_pages_total Total indexed pages\n")
fmt.Fprintf(&b, "# TYPE wiki_pages_total gauge\nwiki_pages_total %d\n\n", len(idx.GetAllPages()))
// Page views
totalViews := int64(0)
for _, v := range m.pageViews {
totalViews += v
}
fmt.Fprintf(&b, "# HELP wiki_page_views_total Total page views\n")
fmt.Fprintf(&b, "# TYPE wiki_page_views_total counter\nwiki_page_views_total %d\n\n", totalViews)
// Searches
fmt.Fprintf(&b, "# HELP wiki_searches_total Total searches performed\n")
fmt.Fprintf(&b, "# TYPE wiki_searches_total counter\nwiki_searches_total %d\n\n", atomic.LoadInt64(&m.searchCount))
// Edits
fmt.Fprintf(&b, "# HELP wiki_edits_total Total edits performed\n")
fmt.Fprintf(&b, "# TYPE wiki_edits_total counter\nwiki_edits_total %d\n\n", atomic.LoadInt64(&m.editCount))
// Render time
rc := atomic.LoadInt64(&m.renderCount)
if rc > 0 {
avg := float64(atomic.LoadInt64(&m.renderTimeSum)) / float64(rc) / 1e6 // ms
fmt.Fprintf(&b, "# HELP wiki_render_avg_ms Average render time in ms\n")
fmt.Fprintf(&b, "# TYPE wiki_render_avg_ms gauge\nwiki_render_avg_ms %.2f\n\n", avg)
}
// Domains
for _, dom := range idx.GetDomains() {
fmt.Fprintf(&b, "wiki_domain_pages{domain=\"%s\"} %d\n", dom.FullDir, dom.Count)
}
// Top viewed pages (top 10)
type pv struct {
path string
count int64
}
var pvs []pv
for path, count := range m.pageViews {
pvs = append(pvs, pv{path, count})
}
// Simple sort
for i := 0; i < len(pvs); i++ {
for j := i + 1; j < len(pvs); j++ {
if pvs[j].count > pvs[i].count {
pvs[i], pvs[j] = pvs[j], pvs[i]
}
}
}
if len(pvs) > 10 {
pvs = pvs[:10]
}
fmt.Fprintf(&b, "\n# HELP wiki_page_views Page view counts\n# TYPE wiki_page_views counter\n")
for _, p := range pvs {
fmt.Fprintf(&b, "wiki_page_views{page=\"%s\"} %d\n", p.path, p.count)
}
return b.String()
}
// GetTopViewed returns the most viewed pages
func (m *Metrics) GetTopViewed(n int) map[string]int64 {
m.mu.RLock()
defer m.mu.RUnlock()
result := make(map[string]int64)
for k, v := range m.pageViews {
result[k] = v
}
return result
}