Files
forgetting/internal/cli/monitor.go
T
maeneko 105f184a93 Monitor: 7 and 30 day views, traffic used, merged tiles
- ranges 7d (30 min step) and 30d (2 h step); Prometheus keeps 31d
- rate() window is at least the step, so long views show averages
  instead of a 2-minute sample; "now" values still use a 2m window
- overview returns used_down/used_up: increase() of the interface
  counters over the whole range
- tiles merged: traffic (with volume for the period), devices, load
2026-10-17 21:36:51 +03:00

266 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cli
// `awg-ctrl monitor install|status|remove` — встроенный Prometheus для вкладки «Монитор» (порт
// cli/src/monitor.ts). Работает от root (ставит пакет, пишет в /etc): панель сама пакетов не
// ставит, она только просит (через awg-ctrl) или показывает эту команду. Всё слушает
// 127.0.0.1: наружу Prometheus не торчит, а панель читает его по HTTP на той же машине.
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)
const (
awgDir = "/etc/amnezia/amneziawg"
promDir = "/etc/prometheus"
promConf = promDir + "/prometheus.yml"
promToken = promDir + "/forgetting.token"
monitorHashFile = awgDir + "/monitor_token.sha256" // хэш локального токена: его читает awg-ui
promDefaults = "/etc/default/prometheus"
promMarker = "# managed by forgetting"
promListen = "127.0.0.1:9090"
promRetention = "31d" // с запасом на вид «30 дней» во вкладке «Монитор»
)
func mlog(s string) { fmt.Println(" " + s) }
func mfail(s string) {
fmt.Fprintf(os.Stderr, "\n ✗ %s\n\n", s)
os.Exit(1)
}
type shResult struct {
ok bool
output string
}
// sh — команда без stdin; quiet — вывод забираем себе, иначе он идёт в терминал.
func sh(quiet bool, name string, args ...string) shResult {
cmd := exec.Command(name, args...)
cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
if quiet {
out, err := cmd.CombinedOutput()
return shResult{err == nil, string(out)}
}
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
return shResult{cmd.Run() == nil, ""}
}
var (
reOSID = regexp.MustCompile(`(?m)^ID=(.*)$`)
reOSIDLike = regexp.MustCompile(`(?m)^ID_LIKE=(.*)$`)
reDebian = regexp.MustCompile(`(?i)debian|ubuntu`)
rePromVer = regexp.MustCompile(`version (\d+)\.(\d+)`)
reArgsLine = regexp.MustCompile(`(?m)^ARGS=.*$`)
)
func osFamily() string {
raw, err := os.ReadFile("/etc/os-release")
if err != nil {
return ""
}
val := func(re *regexp.Regexp) string {
if m := re.FindStringSubmatch(string(raw)); m != nil {
return strings.ReplaceAll(m[1], `"`, "")
}
return ""
}
return val(reOSID) + " " + val(reOSIDLike)
}
func promReady() bool {
cl := &http.Client{Timeout: 1500 * time.Millisecond, Transport: &http.Transport{Proxy: nil}}
resp, err := cl.Get("http://" + promListen + "/-/ready")
if err != nil {
return false
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
return resp.StatusCode == 200
}
func promInstalled() bool { return sh(true, "dpkg", "-s", "prometheus").ok }
func promActive() bool { return sh(true, "systemctl", "is-active", "--quiet", "prometheus").ok }
func promManaged() bool {
raw, err := os.ReadFile(promConf)
return err == nil && strings.Contains(string(raw), promMarker)
}
// authBlock — новые схемы (authorization.credentials_file) есть с 2.26; в 3.x bearer_token_file убран.
func authBlock() string {
r := sh(true, "prometheus", "--version")
maj, min := 2, 0
if m := rePromVer.FindStringSubmatch(r.output); m != nil {
maj, _ = strconv.Atoi(m[1])
min, _ = strconv.Atoi(m[2])
}
if maj > 2 || maj == 2 && min >= 26 {
return " authorization:\n credentials_file: " + promToken + "\n"
}
return " bearer_token_file: " + promToken + "\n"
}
// backupStamp — new Date().toISOString() с «:» и «.» → «-».
func backupStamp() string {
return strings.NewReplacer(":", "-", ".", "-").Replace(time.Now().UTC().Format("2006-01-02T15:04:05.000Z"))
}
func copyFile(src, dst string) {
if raw, err := os.ReadFile(src); err == nil {
os.WriteFile(dst, raw, 0o644)
}
}
func writePromConfig(uiPort string) {
if exists(promConf) {
copyFile(promConf, promConf+".bak-"+backupStamp())
}
os.WriteFile(promConf, []byte(promMarker+`
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: forgetting
scheme: http
metrics_path: /metrics
`+authBlock()+` static_configs:
- targets: ['127.0.0.1:`+uiPort+`']
`), 0o644)
}
// writePromToken — токен для самого Prometheus. Файл читает пользователь prometheus, в панели лежит только хэш.
func writePromToken() {
b := make([]byte, 24)
rand.Read(b)
token := "awgm_" + b64url.EncodeToString(b)
os.WriteFile(promToken, []byte(token), 0o640)
sh(true, "chown", "root:prometheus", promToken)
sum := sha256.Sum256([]byte(token))
os.WriteFile(monitorHashFile, []byte(hex.EncodeToString(sum[:])+"\n"), 0o644)
}
func setPromArgs() {
line := `ARGS="--web.listen-address=` + promListen + ` --storage.tsdb.retention.time=` + promRetention + `"`
raw, _ := os.ReadFile(promDefaults)
text := string(raw)
if text != "" {
copyFile(promDefaults, promDefaults+".bak-"+backupStamp())
}
if loc := reArgsLine.FindStringIndex(text); loc != nil {
text = text[:loc[0]] + line + text[loc[1]:]
} else {
text = strings.TrimSuffix(text, "\n") + "\n" + line + "\n"
}
os.WriteFile(promDefaults, []byte(text), 0o644)
}
func (c *CLI) monitorInstall() {
if os.Getuid() != 0 {
mfail("Нужен root: sudo awg-ctrl monitor install")
}
if !reDebian.MatchString(osFamily()) {
mfail("Автоустановка поддерживает только Debian и Ubuntu. На другом дистрибутиве поставьте Prometheus сами и подключите его кнопкой в панели (токен метрик).")
}
uiPort := os.Getenv("UI_PORT")
if uiPort == "" {
mfail("На этой машине нет панели (UI_PORT не задан): Prometheus скрейпит метрики панели.")
}
// Чужой Prometheus не трогаем: перезаписать его prometheus.yml значит сломать чужой мониторинг.
if promInstalled() && exists(promConf) && !promManaged() {
mfail("Prometheus уже установлен и настроен не нами (" + promConf + "). Подключите его кнопкой «Подключить Prometheus» во вкладке «Монитор»: она выдаст токен и готовый конфиг.")
}
mlog("1/4 установка пакета prometheus")
if !promInstalled() {
if !sh(false, "apt-get", "update", "-qq").ok {
mfail("apt-get update завершился ошибкой")
}
if !sh(false, "apt-get", "install", "-y", "--no-install-recommends", "prometheus").ok {
mfail("apt-get install prometheus завершился ошибкой")
}
} else {
mlog("уже установлен")
}
mlog("2/4 токен и конфиг")
writePromToken()
writePromConfig(uiPort)
mlog("3/4 слушаем только " + promListen + ", хранение " + promRetention)
setPromArgs()
mlog("4/4 запуск")
sh(true, "systemctl", "enable", "prometheus")
if !sh(false, "systemctl", "restart", "prometheus").ok {
mfail("prometheus не запустился: journalctl -u prometheus")
}
for range 30 {
if promReady() {
fmt.Printf("\n ✓ Prometheus работает на %s, метрики панели скрейпятся каждые 15 с.\n Во вкладке «Монитор» графики появятся через минуту-две.\n\n", promListen)
return
}
time.Sleep(time.Second)
}
mfail("Prometheus не ответил за 30 с: journalctl -u prometheus")
}
func yesNo(b bool, yes, no string) string {
if b {
return yes
}
return no
}
func monitorStatus() {
ok := promReady()
fmt.Println(" пакет: " + yesNo(promInstalled(), "установлен", "нет"))
fmt.Println(" сервис: " + yesNo(promActive(), "работает", "остановлен"))
fmt.Println(" наш конфиг: " + yesNo(promManaged(), "да", "нет"))
fmt.Println(" готов: " + yesNo(ok, "да, "+promListen, "нет"))
}
func monitorRemove() {
if os.Getuid() != 0 {
mfail("Нужен root: sudo awg-ctrl monitor remove")
}
// Чужой Prometheus не удаляем: у него могут быть свои данные и задачи, которых мы не ставили.
if promInstalled() && exists(promConf) && !promManaged() {
mfail("Prometheus настроен не нами (" + promConf + "), поэтому панель его не удаляет. Если он не нужен: sudo apt purge prometheus")
}
mlog("1/3 остановка сервиса")
sh(true, "systemctl", "disable", "--now", "prometheus")
mlog("2/3 токен")
os.Remove(promToken)
os.Remove(monitorHashFile)
mlog("3/3 удаление пакета и накопленных данных")
if promInstalled() && !sh(false, "apt-get", "purge", "-y", "prometheus").ok {
mfail("apt-get purge prometheus завершился ошибкой")
}
fmt.Print("\n ✓ Prometheus удалён вместе с накопленными графиками. Панель продолжает отдавать /metrics.\n\n")
}
func (c *CLI) monitorCmd(sub string) {
switch sub {
case "install":
c.monitorInstall()
case "status":
monitorStatus()
case "remove":
monitorRemove()
default:
fmt.Println("Usage: awg-ctrl monitor <install|status|remove>")
os.Exit(1)
}
}