172 lines
5.0 KiB
Go
172 lines
5.0 KiB
Go
package rrd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Manager struct {
|
|
Tool string
|
|
Path string
|
|
GraphDir string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// Graph colors mirror the dashboard's theme tokens (internal/web/assets/app.css)
|
|
// so rendered PNGs sit visually in place inside the panel cards. Each range
|
|
// is rendered twice (dark + light) so the client can pick the variant that
|
|
// matches the active site theme without round-tripping through rrdtool.
|
|
const (
|
|
graphColorPanel = "#181c19" // --panel (dark)
|
|
graphColorField = "#121713" // --field (dark plot canvas)
|
|
graphColorLine = "#313832" // --line (dark minor grid)
|
|
graphColorLine2 = "#485149" // --line2 (dark axis / major grid)
|
|
graphColorText = "#edf0ecCC"
|
|
graphColorAccent = "#8fb9a4" // --accent (dark)
|
|
graphColorAccentFill = "#8fb9a43d"
|
|
|
|
graphLightColorPanel = "#ffffff" // --panel (light)
|
|
graphLightColorField = "#f7f8f6" // --panel2 (light plot canvas)
|
|
graphLightColorLine = "#cbd2cb" // --line (light minor grid)
|
|
graphLightColorLine2 = "#aeb8af" // --line2 (light axis / major grid)
|
|
graphLightColorText = "#202622CC"
|
|
graphLightColorAccent = "#507b67" // --accent (light)
|
|
graphLightColorAccentFill = "#507b673d"
|
|
)
|
|
|
|
type Range struct{ Key, Start, Title string }
|
|
|
|
var Ranges = []Range{
|
|
{"hour", "-12h", "Ostatnie 12 godzin"},
|
|
{"day", "-24h", "Ostatnie 24 godziny"},
|
|
{"72h", "-72h", "Ostatnie 72 godziny"},
|
|
{"week", "-1w", "Ostatni tydzień"},
|
|
{"month", "-1m", "Ostatni miesiąc"},
|
|
{"year", "-1y", "Ostatni rok"},
|
|
}
|
|
|
|
func (m *Manager) Validate() error {
|
|
if _, err := exec.LookPath(m.Tool); err != nil {
|
|
return fmt.Errorf("rrdtool not found: %w", err)
|
|
}
|
|
if st, err := os.Stat(m.Path); err != nil || st.IsDir() {
|
|
return fmt.Errorf("RRD file unavailable: %s", m.Path)
|
|
}
|
|
return os.MkdirAll(m.GraphDir, 0750)
|
|
}
|
|
|
|
func (m *Manager) Update(ctx context.Context, online int) error {
|
|
if online < 0 {
|
|
return errors.New("online count cannot be negative")
|
|
}
|
|
cmd := exec.CommandContext(ctx, m.Tool, "update", m.Path, "N:"+strconv.Itoa(online))
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("rrd update: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) Last(ctx context.Context) (int, error) {
|
|
cmd := exec.CommandContext(ctx, m.Tool, "lastupdate", m.Path)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
|
if len(lines) < 2 {
|
|
return 0, errors.New("unexpected rrd lastupdate output")
|
|
}
|
|
fields := strings.Fields(lines[len(lines)-1])
|
|
if len(fields) < 2 {
|
|
return 0, errors.New("unexpected rrd lastupdate data")
|
|
}
|
|
v := fields[len(fields)-1]
|
|
if v == "U" {
|
|
return 0, nil
|
|
}
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
return int(f + 0.5), err
|
|
}
|
|
|
|
func (m *Manager) Graph(ctx context.Context, key, theme string) (string, error) {
|
|
var rg *Range
|
|
for i := range Ranges {
|
|
if Ranges[i].Key == key {
|
|
rg = &Ranges[i]
|
|
break
|
|
}
|
|
}
|
|
if rg == nil {
|
|
return "", errors.New("unknown graph range")
|
|
}
|
|
if err := os.MkdirAll(m.GraphDir, 0750); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
panel, field, line, line2, text, accent, accentFill := graphColorPanel, graphColorField, graphColorLine, graphColorLine2, graphColorText, graphColorAccent, graphColorAccentFill
|
|
suffix := ""
|
|
if theme == "light" {
|
|
panel, field, line, line2, text, accent, accentFill = graphLightColorPanel, graphLightColorField, graphLightColorLine, graphLightColorLine2, graphLightColorText, graphLightColorAccent, graphLightColorAccentFill
|
|
suffix = "-light"
|
|
}
|
|
out := filepath.Join(m.GraphDir, key+suffix+".png")
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if st, err := os.Stat(out); err == nil && time.Since(st.ModTime()) < 55*time.Second {
|
|
return out, nil
|
|
}
|
|
|
|
def := "DEF:online=" + m.Path + ":online:LAST"
|
|
watermark := "linuxiarz.pl · " + time.Now().Format("2006-01-02 15:04")
|
|
args := []string{
|
|
"graph", out,
|
|
"--imgformat", "PNG",
|
|
"--start", rg.Start, "--end", "now",
|
|
"--width", "1200", "--height", "320",
|
|
"--lower-limit", "0", "--alt-autoscale-max", "--rigid",
|
|
"--slope-mode",
|
|
"--title", rg.Title,
|
|
"--vertical-label", "użytkownicy",
|
|
"--border", "0",
|
|
"--color", "BACK" + panel,
|
|
"--color", "CANVAS" + field,
|
|
"--color", "FONT" + text,
|
|
"--color", "AXIS" + line2,
|
|
"--color", "ARROW" + line2,
|
|
"--color", "GRID" + line,
|
|
"--color", "MGRID" + line2,
|
|
"--color", "FRAME" + panel,
|
|
"--font", "DEFAULT:0:",
|
|
"--font", "TITLE:13:",
|
|
"--font", "AXIS:9:",
|
|
"--font", "LEGEND:10:",
|
|
"--font", "UNIT:9:",
|
|
"--watermark", watermark,
|
|
def,
|
|
"AREA:online" + accentFill,
|
|
"LINE2:online" + accent + ":Online",
|
|
"GPRINT:online:LAST:Aktualnie\\: %4.0lf",
|
|
"GPRINT:online:AVERAGE:Średnio\\: %4.0lf",
|
|
"GPRINT:online:MAX:Maks.\\: %4.0lf\\n",
|
|
}
|
|
cmd := exec.CommandContext(ctx, m.Tool, args...)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("rrd graph: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
return out, nil
|
|
}
|