/
node_exporter_data.go
129 lines (113 loc) · 3.46 KB
/
node_exporter_data.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package promtop
import (
"cmp"
"io"
"log"
"net/http"
"slices"
"strconv"
"strings"
"time"
"net/url"
"github.com/prometheus/common/expfmt"
// "github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/spf13/viper"
)
type NodeExporterData struct {
cpus [][]*dto.Metric
timestamps []time.Time
instances map[string]*url.URL
}
func (n *NodeExporterData) instanceToUrl() map[string]*url.URL {
if n.instances != nil {
return n.instances
}
n.instances = make(map[string]*url.URL)
for _, raw := range viper.GetStringSlice("node_exporter_url") {
u, err := url.Parse(raw)
if err != nil {
log.Fatalln("Error parsing url:", raw, err)
}
n.instances[u.Hostname()] = u
}
return n.instances
}
func (n *NodeExporterData) GetInstances() []string {
keys := make([]string, 0, len(n.instanceToUrl()))
for k := range n.instanceToUrl() {
keys = append(keys, k)
}
slices.Sort(keys)
return keys
}
func (n *NodeExporterData) GetCpu(instance string) []float64 {
client := http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get(n.instanceToUrl()[instance].String())
if err != nil {
log.Fatalln("Error querying node exporter:", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalln("Failed to read response body:", err)
}
parser := expfmt.TextParser{}
data, err := parser.TextToMetricFamilies(strings.NewReader(string(body)))
if err != nil {
log.Fatalln("Failed to parse metrics:", err)
}
// extract cpu idle time metrics
currentCpu := slices.DeleteFunc(data["node_cpu_seconds_total"].GetMetric(), func(metric *dto.Metric) bool {
var cpu, mode string
for _, label := range metric.GetLabel() {
if label.GetName() == "cpu" {
cpu = label.GetValue()
}
if label.GetName() == "mode" {
mode = label.GetValue()
}
}
_, err := strconv.Atoi(cpu)
return mode != "idle" || err != nil
})
// clear the slice to free memory
clear(currentCpu[len(currentCpu):cap(currentCpu)])
// sort by cpu number
slices.SortFunc(currentCpu, func(a, b *dto.Metric) int {
return cmp.Compare(a.GetCounter().GetValue(), b.GetCounter().GetValue())
})
// append the new reading to the readings slice
n.cpus = append(n.cpus, currentCpu)
n.timestamps = append(n.timestamps, time.Now())
// limit the readings slice to 60 entries
if len(n.cpus) > 60 {
n.cpus = n.cpus[1:]
n.timestamps = n.timestamps[1:]
}
// calculate the cpu usage rates
rates := []float64{}
if len(n.cpus) < 2 {
return rates
}
// calculate the interval between the first and last reading
interval := n.timestamps[len(n.timestamps)-1].Sub(n.timestamps[0]).Seconds()
for cpuIndex := 0; cpuIndex < len(n.cpus[0]); cpuIndex++ {
// each cpu counter might have been reset so we need to calculate an offset
offset := 0.0
for readingIndex, reading := range n.cpus {
if readingIndex > 0 && reading[cpuIndex].GetCounter().GetValue() < n.cpus[readingIndex-1][cpuIndex].GetCounter().GetValue() {
offset += n.cpus[readingIndex-1][cpuIndex].GetCounter().GetValue()
}
}
// use the first nad last reading to calculate the cpu usage rate
first := n.cpus[0][cpuIndex].GetCounter().GetValue() + offset
last := n.cpus[len(n.cpus)-1][cpuIndex].GetCounter().GetValue() + offset
// because the times should add up to the interval
// we can calculate the cpu usage rate by subtracting the idle time from 100%
rates = append(rates, 100-100*(last-first)/interval)
}
return rates
}