first commit

This commit is contained in:
2025-12-22 18:49:05 +01:00
commit 658ab8e961
19 changed files with 1356 additions and 0 deletions

146
apis/docker/docker.go Normal file
View File

@@ -0,0 +1,146 @@
package docker
import (
"ArinDash/util"
"context"
"strings"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
type ContainerMetrics struct {
ID string
Name string
Image string
Status string
CPUPercent float64
MemUsage uint64
MemLimit uint64
MemPercent float64
NetRx uint64
NetTx uint64
BlockRead uint64
BlockWrite uint64
PidsCurrent uint64
}
func newDockerClient() (*client.Client, error) {
return client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
}
func FetchDockerMetrics(ctx context.Context) ([]ContainerMetrics, error) {
cli, err := newDockerClient()
if err != nil {
return nil, err
}
defer cli.Close()
_, err = cli.Ping(ctx)
if err != nil {
return nil, err
}
containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: true})
if err != nil {
return nil, err
}
out := make([]ContainerMetrics, 0, len(containers))
for _, c := range containers {
m := ContainerMetrics{
ID: c.ID,
Image: c.Image,
Name: containerName(c.Names),
Status: c.Status,
}
stats, err := cli.ContainerStats(ctx, c.ID, false)
if err != nil {
continue
}
func() {
defer stats.Body.Close()
var sj containertypes.StatsResponse
if err := util.DecodeJSON(stats.Body, &sj); err != nil {
return
}
m.CPUPercent = cpuPercentFromStats(sj)
m.MemUsage, m.MemLimit, m.MemPercent = memoryFromStats(sj)
m.NetRx, m.NetTx = networkTotals(sj)
m.BlockRead, m.BlockWrite = blockIOTotals(sj)
m.PidsCurrent = sj.PidsStats.Current
}()
out = append(out, m)
}
return out, nil
}
func containerName(names []string) string {
for _, n := range names {
n = strings.TrimSpace(n)
n = strings.TrimPrefix(n, "/")
if n != "" {
return n
}
}
return ""
}
func cpuPercentFromStats(s containertypes.StatsResponse) float64 {
// Protect against zero or missing data.
if s.PreCPUStats.SystemUsage == 0 || s.CPUStats.SystemUsage == 0 {
return 0
}
cpuDelta := float64(s.CPUStats.CPUUsage.TotalUsage - s.PreCPUStats.CPUUsage.TotalUsage)
systemDelta := float64(s.CPUStats.SystemUsage - s.PreCPUStats.SystemUsage)
if systemDelta <= 0 || cpuDelta < 0 {
return 0
}
// Number of CPUs available to the container.
numCPU := float64(len(s.CPUStats.CPUUsage.PercpuUsage))
if numCPU == 0 {
numCPU = 1
}
return (cpuDelta / systemDelta) * numCPU * 100.0
}
func memoryFromStats(s containertypes.StatsResponse) (usage, limit uint64, percent float64) {
usage = s.MemoryStats.Usage
limit = s.MemoryStats.Limit
// Optionally discount cached memory if stats present.
if stats := s.MemoryStats.Stats; stats != nil {
// The common Linux approach: usage - cache
if cache, ok := stats["cache"]; ok && cache <= usage {
usage -= cache
}
}
if limit > 0 {
percent = (float64(usage) / float64(limit)) * 100
}
return
}
func networkTotals(s containertypes.StatsResponse) (rx, tx uint64) {
if s.Networks == nil {
return 0, 0
}
for _, v := range s.Networks {
rx += v.RxBytes
tx += v.TxBytes
}
return rx, tx
}
func blockIOTotals(s containertypes.StatsResponse) (read, write uint64) {
for _, e := range s.BlkioStats.IoServiceBytesRecursive {
switch strings.ToLower(e.Op) {
case "read":
read += e.Value
case "write":
write += e.Value
}
}
return read, write
}

44
apis/pihole/info.go Normal file
View File

@@ -0,0 +1,44 @@
package pihole
import (
"encoding/json"
"log"
)
type Version struct {
Version Versions `json:"version"`
}
type Versions struct {
Core ModuleVersion `json:"core"`
Web ModuleVersion `json:"web"`
FTL ModuleVersion `json:"FTL"`
Docker DockerVersion `json:"docker"`
}
type ModuleVersion struct {
Local LocalVersion `json:"local"`
Remote LocalVersion `json:"remote"`
}
type LocalVersion struct {
Branch string `json:"branch"`
Version string `json:"version"`
Hash string `json:"hash"`
Date string `json:"date"`
}
type DockerVersion struct {
Local string `json:"local"`
Remote string `json:"remote"`
}
func (ph *PiHConnector) Version() Version {
version := &Version{}
err := json.Unmarshal(ph.get("info/version"), version)
if err != nil {
log.Fatal(err)
}
return *version
}

100
apis/pihole/pihole.go Normal file
View File

@@ -0,0 +1,100 @@
package pihole
import (
"ArinDash/config"
"encoding/json"
"io"
"log"
"net/http"
"strings"
)
type configFile struct {
Pihole piholeConfig
}
type piholeConfig struct {
Host string
Password string
}
type PiHConnector struct {
Host string
Session PiHSession
}
type PiHAuth struct {
Session PiHSession `json:"session"`
}
type PiHSession struct {
SID string `json:"sid"`
CSRF string `json:"csrf"`
}
func (ph *PiHConnector) get(endpoint string) []byte {
return ph.do("GET", endpoint, nil)
}
func (ph *PiHConnector) post(endpoint string, body io.Reader) []byte {
return ph.do("POST", endpoint, body)
}
func (ph *PiHConnector) do(method string, endpoint string, body io.Reader) []byte {
var requestString = "http://" + ph.Host + "/api/" + endpoint
client := &http.Client{}
req, err := http.NewRequest(method, requestString, body)
if err != nil {
log.Fatal(err)
}
req.Header.Add("X-FTL-SID", ph.Session.SID)
req.Header.Add("X-FTL-CSRF", ph.Session.CSRF)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
return respBody
}
func Connect() PiHConnector {
cfg := &configFile{}
config.LoadConfig(cfg)
client := &http.Client{}
req, err := http.NewRequest("POST", "http://"+cfg.Pihole.Host+"/api/auth", strings.NewReader("{\"password\": \""+cfg.Pihole.Password+"\"}"))
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
s := &PiHAuth{}
err = json.Unmarshal(respBody, s)
if err != nil {
log.Fatal(err)
}
return PiHConnector{
Host: cfg.Pihole.Host,
Session: s.Session,
}
}

41
apis/pihole/stats.go Normal file
View File

@@ -0,0 +1,41 @@
package pihole
import (
"encoding/json"
"log"
)
type Summary struct {
Queries Queries `json:"queries"`
Clients Clients `json:"clients"`
Gravity Gravity `json:"gravity"`
}
type Queries struct {
Total int64 `json:"total"`
Blocked int64 `json:"blocked"`
PercentBlocked float64 `json:"percent_blocked"`
UniqueDomains int64 `json:"unique_domains"`
Forwarded int64 `json:"forwarded"`
Cached int64 `json:"cached"`
}
type Clients struct {
Active int64 `json:"active"`
Total int64 `json:"total"`
}
type Gravity struct {
DomainsBeingBlocked int64 `json:"domains_being_blocked"`
LastUpdated int64 `json:"last_updated"`
}
func (ph *PiHConnector) Summary() Summary {
summary := &Summary{}
err := json.Unmarshal(ph.get("stats/summary"), summary)
if err != nil {
log.Fatal(err)
}
return *summary
}