105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"ts3monolith/internal/config"
|
|
"ts3monolith/internal/rrd"
|
|
"ts3monolith/internal/ts3"
|
|
webapp "ts3monolith/internal/web"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(log.LstdFlags | log.LUTC)
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
tq := &ts3.Query{Addr: cfg.TSQueryHost + ":" + strconv.Itoa(cfg.TSQueryPort), User: cfg.TSQueryUser, Password: cfg.TSQueryPassword, VoicePort: cfg.TSVoicePort, Timeout: cfg.TSQueryTimeout}
|
|
rm := &rrd.Manager{Tool: cfg.RRDTool, Path: cfg.RRDPath, GraphDir: cfg.GraphDir}
|
|
if err := rm.Validate(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
app, err := webapp.New(cfg, tq, rm)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
srv := &http.Server{Addr: cfg.ListenAddr, Handler: app.Handler(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 16 << 10}
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
go collect(ctx, cfg, tq, rm)
|
|
go func() {
|
|
<-ctx.Done()
|
|
c, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(c)
|
|
}()
|
|
log.Printf("listening on %s", cfg.ListenAddr)
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func collect(ctx context.Context, cfg config.Config, tq *ts3.Query, rm *rrd.Manager) {
|
|
run := func() {
|
|
c, cancel := context.WithTimeout(ctx, cfg.TSQueryTimeout)
|
|
n, err := tq.CountOnline(c)
|
|
cancel()
|
|
if err != nil {
|
|
log.Printf("stats query failed: %v", err)
|
|
}
|
|
// Treat "query failed" and "query succeeded but nobody is online"
|
|
// the same way: repeat the last known sample instead of writing 0.
|
|
// This matches the original PHP app's collector exactly (it never
|
|
// distinguished the two cases either). At a 1-minute sampling
|
|
// interval a real but brief empty-server moment plots as a sharp
|
|
// spike down to 0 and back up, which reads as broken/jagged far
|
|
// more than it reads as useful information -- so, deliberately,
|
|
// a true "server went empty for a while" no longer shows as 0 in
|
|
// the graph, same trade-off the original app made.
|
|
raw := n
|
|
if n == 0 {
|
|
c2, cancel2 := context.WithTimeout(ctx, 2*time.Second)
|
|
last, lastErr := rm.Last(c2)
|
|
cancel2()
|
|
if lastErr != nil {
|
|
log.Printf("collect: raw=0, rrd lastupdate failed (writing raw 0): %v", lastErr)
|
|
} else {
|
|
n = last
|
|
}
|
|
}
|
|
// DEBUG=1 turns on a log line for every single collection cycle
|
|
// (raw TS3 reading vs. what actually got written to the RRD) --
|
|
// useful while diagnosing the stats collector, far too noisy to
|
|
// leave on permanently (one line per STATS_INTERVAL, forever).
|
|
if cfg.Debug {
|
|
log.Printf("collect: raw=%d written=%d", raw, n)
|
|
}
|
|
c3, cancel3 := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel3()
|
|
if err := rm.Update(c3, n); err != nil {
|
|
log.Printf("rrd update failed: %v", err)
|
|
}
|
|
}
|
|
run()
|
|
t := time.NewTicker(cfg.StatsInterval)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
run()
|
|
}
|
|
}
|
|
}
|