Feature: PF States, HAProxy + Caddy collectors and dashboard
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CaddySite struct {
|
||||
Domain string `json:"domain"`
|
||||
Upstream string `json:"upstream"`
|
||||
TLS bool `json:"tls"`
|
||||
}
|
||||
|
||||
type CaddyData struct {
|
||||
Version string `json:"version"`
|
||||
Sites []CaddySite `json:"sites"`
|
||||
}
|
||||
|
||||
const caddyFile = "/usr/local/etc/caddy/Caddyfile"
|
||||
|
||||
func CollectCaddy() *CaddyData {
|
||||
if _, err := os.Stat(caddyFile); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := &CaddyData{}
|
||||
|
||||
// Version
|
||||
if out, err := exec.Command("/usr/local/bin/caddy", "version").Output(); err == nil {
|
||||
fields := strings.Fields(strings.TrimSpace(string(out)))
|
||||
if len(fields) > 0 {
|
||||
data.Version = fields[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Caddyfile
|
||||
content, err := os.ReadFile(caddyFile)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var currentDomain string
|
||||
var currentTLS bool
|
||||
depth := 0
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for domain block start: "domain.com {" or "http://domain.com {"
|
||||
if depth == 0 && strings.Contains(trimmed, "{") {
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[len(parts)-1] == "{" {
|
||||
currentDomain = parts[0]
|
||||
currentTLS = !strings.HasPrefix(currentDomain, "http://")
|
||||
depth = 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if depth > 0 {
|
||||
// Track nested braces
|
||||
depth += strings.Count(trimmed, "{") - strings.Count(trimmed, "}")
|
||||
|
||||
if strings.HasPrefix(trimmed, "reverse_proxy") {
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 {
|
||||
upstream := parts[1]
|
||||
data.Sites = append(data.Sites, CaddySite{
|
||||
Domain: currentDomain,
|
||||
Upstream: upstream,
|
||||
TLS: currentTLS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if depth <= 0 {
|
||||
currentDomain = ""
|
||||
depth = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HAProxyInfo struct {
|
||||
Uptime string `json:"uptime"`
|
||||
CurrentConns int `json:"current_conns"`
|
||||
TotalConns int `json:"total_conns"`
|
||||
TotalReqs int `json:"total_requests"`
|
||||
CurrentSSL int `json:"current_ssl"`
|
||||
TotalSSL int `json:"total_ssl"`
|
||||
MaxConns int `json:"max_conns"`
|
||||
}
|
||||
|
||||
type HAProxyEntry struct {
|
||||
ProxyName string `json:"proxy_name"`
|
||||
ServerName string `json:"server_name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
CurrentSessions int `json:"current_sessions"`
|
||||
TotalSessions int `json:"total_sessions"`
|
||||
BytesIn int64 `json:"bytes_in"`
|
||||
BytesOut int64 `json:"bytes_out"`
|
||||
RequestsTotal int `json:"requests_total"`
|
||||
ErrorsReq int `json:"errors_req"`
|
||||
ErrorsConn int `json:"errors_conn"`
|
||||
ErrorsResp int `json:"errors_resp"`
|
||||
}
|
||||
|
||||
type HAProxyData struct {
|
||||
Info HAProxyInfo `json:"info"`
|
||||
Entries []HAProxyEntry `json:"entries"`
|
||||
}
|
||||
|
||||
const haproxySocket = "/var/run/haproxy.socket"
|
||||
|
||||
func CollectHAProxy() *HAProxyData {
|
||||
if _, err := os.Stat(haproxySocket); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := &HAProxyData{}
|
||||
|
||||
// show info
|
||||
if out, err := exec.Command("/bin/sh", "-c", `echo "show info" | /usr/bin/nc -U `+haproxySocket).Output(); err == nil {
|
||||
info := parseHAProxyInfo(string(out))
|
||||
data.Info = info
|
||||
}
|
||||
|
||||
// show stat
|
||||
if out, err := exec.Command("/bin/sh", "-c", `echo "show stat" | /usr/bin/nc -U `+haproxySocket).Output(); err == nil {
|
||||
data.Entries = parseHAProxyStats(string(out))
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func parseHAProxyInfo(output string) HAProxyInfo {
|
||||
info := HAProxyInfo{}
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
switch key {
|
||||
case "Uptime":
|
||||
info.Uptime = val
|
||||
case "CurrConns":
|
||||
info.CurrentConns, _ = strconv.Atoi(val)
|
||||
case "CumConns":
|
||||
info.TotalConns, _ = strconv.Atoi(val)
|
||||
case "CumReq":
|
||||
info.TotalReqs, _ = strconv.Atoi(val)
|
||||
case "CurrSslConns":
|
||||
info.CurrentSSL, _ = strconv.Atoi(val)
|
||||
case "CumSslConns":
|
||||
info.TotalSSL, _ = strconv.Atoi(val)
|
||||
case "Maxconn":
|
||||
info.MaxConns, _ = strconv.Atoi(val)
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func parseHAProxyStats(output string) []HAProxyEntry {
|
||||
var entries []HAProxyEntry
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(line, ",")
|
||||
if len(fields) < 18 {
|
||||
continue
|
||||
}
|
||||
|
||||
svname := fields[1]
|
||||
entryType := "server"
|
||||
if svname == "FRONTEND" {
|
||||
entryType = "frontend"
|
||||
} else if svname == "BACKEND" {
|
||||
entryType = "backend"
|
||||
}
|
||||
|
||||
e := HAProxyEntry{
|
||||
ProxyName: fields[0],
|
||||
ServerName: svname,
|
||||
Type: entryType,
|
||||
Status: fields[17],
|
||||
}
|
||||
e.CurrentSessions, _ = strconv.Atoi(fields[4])
|
||||
e.TotalSessions, _ = strconv.Atoi(fields[7])
|
||||
e.BytesIn, _ = strconv.ParseInt(fields[8], 10, 64)
|
||||
e.BytesOut, _ = strconv.ParseInt(fields[9], 10, 64)
|
||||
e.ErrorsReq, _ = strconv.Atoi(fields[12])
|
||||
e.ErrorsConn, _ = strconv.Atoi(fields[13])
|
||||
e.ErrorsResp, _ = strconv.Atoi(fields[14])
|
||||
if len(fields) > 48 {
|
||||
e.RequestsTotal, _ = strconv.Atoi(fields[48])
|
||||
}
|
||||
|
||||
entries = append(entries, e)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PFStates struct {
|
||||
CurrentEntries int `json:"current_entries"`
|
||||
HardLimit int `json:"hard_limit"`
|
||||
SearchRate float64 `json:"search_rate"`
|
||||
InsertRate float64 `json:"insert_rate"`
|
||||
RemovalRate float64 `json:"removal_rate"`
|
||||
MatchRate float64 `json:"match_rate"`
|
||||
}
|
||||
|
||||
func CollectPFStates() *PFStates {
|
||||
result := &PFStates{}
|
||||
|
||||
// pfctl -si for state table info
|
||||
if out, err := exec.Command("/sbin/pfctl", "-si").Output(); err == nil {
|
||||
lines := strings.Split(string(out), "\n")
|
||||
rateRe := regexp.MustCompile(`([\d.]+)/s`)
|
||||
for _, line := range lines {
|
||||
lower := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(lower, "current entries") {
|
||||
result.CurrentEntries = parseLastInt(lower)
|
||||
} else if strings.HasPrefix(lower, "searches") {
|
||||
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||
result.SearchRate, _ = strconv.ParseFloat(m[1], 64)
|
||||
}
|
||||
} else if strings.HasPrefix(lower, "inserts") {
|
||||
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||
result.InsertRate, _ = strconv.ParseFloat(m[1], 64)
|
||||
}
|
||||
} else if strings.HasPrefix(lower, "removals") {
|
||||
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||
result.RemovalRate, _ = strconv.ParseFloat(m[1], 64)
|
||||
}
|
||||
} else if strings.HasPrefix(lower, "match") {
|
||||
if m := rateRe.FindStringSubmatch(line); len(m) > 1 {
|
||||
result.MatchRate, _ = strconv.ParseFloat(m[1], 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pfctl -sm for hard limit
|
||||
if out, err := exec.Command("/sbin/pfctl", "-sm").Output(); err == nil {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if strings.Contains(line, "states") && strings.Contains(line, "hard limit") {
|
||||
result.HardLimit = parseLastInt(line)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func parseLastInt(line string) int {
|
||||
fields := strings.Fields(line)
|
||||
for i := len(fields) - 1; i >= 0; i-- {
|
||||
if v, err := strconv.Atoi(fields[i]); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user