Skip to content

Commit 9e35e3d

Browse files
committed
Base Version
0 parents  commit 9e35e3d

File tree

9 files changed

+603
-0
lines changed

9 files changed

+603
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/

.goreleaser.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
project_name: prom-exporter
2+
3+
builds:
4+
- env:
5+
- CGO_ENABLED=0
6+
main: ./main.go
7+
goos:
8+
- linux
9+
- windows
10+
- darwin
11+
goarch:
12+
- amd64
13+
ldflags:
14+
- -s -w
15+
archives:
16+
- format: binary
17+
snapshot:
18+
name_template: "{{ .Tag }}-next"

exporter/exporter.go

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package exporter
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strconv"
7+
8+
"github.com/prometheus/client_golang/prometheus"
9+
log "github.com/sirupsen/logrus"
10+
)
11+
12+
type (
13+
Exporter struct {
14+
uri string
15+
name string
16+
version string
17+
metrics Metrics
18+
19+
data []string
20+
}
21+
22+
Metrics []struct {
23+
help *prometheus.Desc
24+
value float64
25+
vtype prometheus.ValueType
26+
}
27+
)
28+
29+
func NewCollector(name, uri, version string) *Exporter {
30+
return &Exporter{
31+
name: name,
32+
uri: uri,
33+
version: version,
34+
metrics: Metrics{},
35+
}
36+
}
37+
38+
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
39+
for _, metric := range e.metrics {
40+
ch <- metric.help
41+
}
42+
}
43+
44+
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
45+
e.process(ch)
46+
47+
for _, metric := range e.metrics {
48+
ch <- prometheus.MustNewConstMetric(metric.help, metric.vtype, metric.value)
49+
}
50+
}
51+
52+
var (
53+
// s = []string{"processor_rate_limit_1::dropped::0", "registrar_states::cleanup::0", "registrar_states::update::0", "registrar_writes::success::0", "system_cpu::cores::4", "system_load::1::0.71", "system_load::15::0.47", "system_load::5::0.53", "system_load_norm::1::0.1775", "system_load_norm::15::0.1175", "system_load_norm::5::0.1325"}
54+
vl, future []string
55+
56+
fqname string
57+
labels prometheus.Labels
58+
mType prometheus.ValueType
59+
)
60+
61+
func (e *Exporter) process(ch chan<- prometheus.Metric) {
62+
body, err := FetchMetrics(e.uri)
63+
if err != nil {
64+
// set target down on error
65+
ch <- prometheus.MustNewConstMetric(
66+
prometheus.NewDesc(fmt.Sprintf("%s_up", e.name), "Target up", nil, nil), prometheus.GaugeValue, float64(0))
67+
log.Debugf("Failed getting metrics endpoint of target: %s ", err.Error())
68+
return
69+
}
70+
71+
log.Debugln("Service scrapped up")
72+
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(fmt.Sprintf("%s_up", e.name), "Target Up", nil, nil), prometheus.GaugeValue, float64(1))
73+
74+
// inject service version is exists
75+
if e.version != "" {
76+
ch <- prometheus.MustNewConstMetric(
77+
prometheus.NewDesc(fmt.Sprintf("%s_version_info", e.name), fmt.Sprintf("%s service info", e.name), nil,
78+
prometheus.Labels{"version": e.version}), prometheus.GaugeValue, float64(1))
79+
}
80+
81+
// parse response body
82+
p := Parser{data: make([]string, 0)}
83+
p.parse(body)
84+
e.data = p.data
85+
86+
sort.Slice(e.data, func(i, j int) bool { return e.data[i] < e.data[j] })
87+
88+
stats := make(Metrics, len(e.data))
89+
for k, v := range e.data {
90+
vl = split(v, "::")
91+
92+
i, err := strconv.ParseFloat(vl[2], 64)
93+
if err != nil {
94+
// a string was found
95+
log.Debugln(err)
96+
i = 0
97+
}
98+
99+
// check last filed for the specific ValueType
100+
if contains(vl[1], "total") {
101+
mType = prometheus.CounterValue
102+
} else {
103+
if len(vl) > 3 {
104+
mType = prometheus.UntypedValue
105+
} else {
106+
mType = prometheus.GaugeValue
107+
}
108+
}
109+
110+
// build Full-Qualified Name
111+
e.build(k)
112+
113+
// setup metrics
114+
stats[k].help = prometheus.NewDesc(
115+
fqname, "", nil, labels)
116+
stats[k].value = float64(i)
117+
stats[k].vtype = mType
118+
}
119+
120+
e.metrics = stats
121+
}
122+
123+
func (e *Exporter) build(k int) {
124+
labels = nil
125+
fp := split(vl[0], "_")
126+
127+
if len(fp) == 2 {
128+
129+
if k < (len(e.data) - 1) {
130+
future = split(e.data[k+1], "::")
131+
if vl[0] == future[0] {
132+
133+
fqname = prometheus.BuildFQName(
134+
e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1])
135+
labels = prometheus.Labels{"metric": vl[1]}
136+
} else {
137+
future = split(e.data[k-1], "::")
138+
if vl[0] == future[0] {
139+
fqname = prometheus.BuildFQName(
140+
e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1])
141+
labels = prometheus.Labels{"metric": vl[1]}
142+
} else {
143+
fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1])
144+
}
145+
}
146+
} else if k == (len(e.data) - 1) {
147+
future = split(e.data[k-1], "::")
148+
if vl[0] == future[0] {
149+
fqname = prometheus.BuildFQName(
150+
e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1])
151+
labels = prometheus.Labels{"metric": vl[1]}
152+
} else {
153+
fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1])
154+
}
155+
} else {
156+
fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1])
157+
}
158+
159+
} else if len(fp) >= 3 {
160+
161+
if k < (len(e.data) - 1) {
162+
future = split(e.data[k+1], "::")
163+
if vl[0] == future[0] {
164+
165+
fqname = prometheus.BuildFQName(
166+
e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1])
167+
labels = prometheus.Labels{"metric": vl[1]}
168+
} else {
169+
170+
if k == 0 {
171+
fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1])
172+
} else {
173+
future = split(e.data[k-1], "::")
174+
if vl[0] == future[0] {
175+
fqname = prometheus.BuildFQName(
176+
e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1])
177+
labels = prometheus.Labels{"metric": vl[1]}
178+
} else {
179+
fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1])
180+
}
181+
}
182+
}
183+
} else if k == (len(e.data) - 1) {
184+
future = split(e.data[k-1], "::")
185+
if vl[0] == future[0] {
186+
labels = prometheus.Labels{"metric": vl[1]}
187+
}
188+
}
189+
fqname = prometheus.BuildFQName(
190+
e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1])
191+
}
192+
}

exporter/exporter_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package exporter
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/prometheus/client_golang/prometheus"
8+
)
9+
10+
func TestNewCollector(t *testing.T) {
11+
type args struct {
12+
name string
13+
uri string
14+
}
15+
tests := []struct {
16+
name string
17+
args args
18+
want *Exporter
19+
}{
20+
// TODO: Add test cases.
21+
}
22+
for _, tt := range tests {
23+
t.Run(tt.name, func(t *testing.T) {
24+
if got := NewCollector(tt.args.name, tt.args.uri); !reflect.DeepEqual(got, tt.want) {
25+
t.Errorf("NewCollector() = %v, want %v", got, tt.want)
26+
}
27+
})
28+
}
29+
}
30+
31+
func TestExporter_Collect(t *testing.T) {
32+
type fields struct {
33+
uri string
34+
name string
35+
metrics Metrics
36+
}
37+
type args struct {
38+
ch chan<- prometheus.Metric
39+
}
40+
tests := []struct {
41+
name string
42+
fields fields
43+
args args
44+
}{
45+
// TODO: Add test cases.
46+
}
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
e := &Exporter{
50+
uri: tt.fields.uri,
51+
name: tt.fields.name,
52+
metrics: tt.fields.metrics,
53+
}
54+
e.Collect(tt.args.ch)
55+
})
56+
}
57+
}
58+
59+
func TestExporter_process(t *testing.T) {
60+
type fields struct {
61+
uri string
62+
name string
63+
metrics Metrics
64+
}
65+
type args struct {
66+
ch chan<- prometheus.Metric
67+
}
68+
tests := []struct {
69+
name string
70+
fields fields
71+
args args
72+
}{
73+
{
74+
// TODO: Add test cases.
75+
name: "test",
76+
fields: fields{
77+
uri: "http://localhost:5066/stats",
78+
name: "test",
79+
metrics: Metrics{},
80+
},
81+
},
82+
}
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
e := &Exporter{
86+
uri: tt.fields.uri,
87+
name: tt.fields.name,
88+
metrics: tt.fields.metrics,
89+
}
90+
e.process(tt.args.ch)
91+
})
92+
}
93+
}

exporter/http.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package exporter
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
8+
log "github.com/sirupsen/logrus"
9+
)
10+
11+
// TODO: customize this
12+
type ServiceStats struct {
13+
Hostname string `json:"hostname"`
14+
Name string `json:"name"`
15+
Version string `json:"version"`
16+
}
17+
18+
// FetchStats Fetch base endpoint for internal stats data from the specified service
19+
func FetchStats(uri string) *ServiceStats {
20+
body, err := get(uri)
21+
if err != nil {
22+
return nil
23+
}
24+
25+
serviceStats := ServiceStats{}
26+
if err = json.Unmarshal(body, &serviceStats); err != nil {
27+
log.Error("Could not parse JSON response from target stats", uri)
28+
return nil
29+
}
30+
return &serviceStats
31+
}
32+
33+
// FetchMetrics Fetch internal metrics from the specified service
34+
func FetchMetrics(uri string) (any, error) {
35+
var raw any
36+
body, err := get(uri)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
if err = json.Unmarshal(body, &raw); err != nil {
42+
log.Error("Could not parse JSON response for target")
43+
return nil, err
44+
}
45+
return raw, nil
46+
}
47+
48+
func get(uri string) ([]byte, error) {
49+
res, err := http.Get(uri)
50+
if err != nil {
51+
log.Errorf("Could not fetch metrics for endpoint of target: %s", uri)
52+
return nil, err
53+
}
54+
defer res.Body.Close()
55+
56+
body, err := io.ReadAll(res.Body)
57+
if err != nil {
58+
log.Error("Can't read body of response")
59+
return nil, err
60+
}
61+
return body, nil
62+
}

0 commit comments

Comments
 (0)