Skip to content

Commit 017ccaa

Browse files
refactor: added new datasources config
1 parent 6775e6c commit 017ccaa

File tree

7 files changed

+251
-138
lines changed

7 files changed

+251
-138
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ A stateless status page using data from prometheus. [Demo page](https://status.h
1111
The status page is configured using a yaml config file:
1212

1313
```yaml
14-
# The url of the prometheus instance you want to query
15-
prometheus: http://prometheus:9090
14+
datasources:
15+
- name: prometheus
16+
url: http://prometheus:9000
17+
type: prometheus
18+
- name: datadog
19+
url: https://api.datadoghq.eu
20+
token: XXXXXXXX
21+
type: datadog
22+
1623
services:
1724
- name: Postgres
1825
# Optional. Group services together
@@ -30,6 +37,8 @@ services:
3037
bool: false
3138
# Optional. The units to display on any graphs (default: null)
3239
units: ""
40+
# Optional. The datasource to use for the query (default: prometheus)
41+
datasource: prometheus
3342
# Settings to configure the UI
3443
ui:
3544
# Optional. The title of the page (default: Status Page)

internal/app/app.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import (
99
type App struct {
1010
Version string
1111
Config *config.Config
12-
Querier *querier.Querier
12+
Queriers map[string]querier.Querier
1313
Collector *collector.Collector
1414
}
1515

16-
func NewApp(conf *config.Config, q *querier.Querier) *App {
16+
func NewApp(conf *config.Config, q map[string]querier.Querier) *App {
1717
app := &App{
18-
Config: conf,
19-
Querier: q,
18+
Config: conf,
19+
Queriers: q,
2020
}
2121

22-
app.Collector = collector.NewCollector(app.Querier, conf.Services)
22+
app.Collector = collector.NewCollector(conf.Services, q)
2323

2424
return app
2525
}

internal/collector/collector.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ type Result struct {
3636
}
3737

3838
type Collector struct {
39-
q *querier.Querier
40-
svcs []config.Service
39+
queriers map[string]querier.Querier
40+
svcs []config.Service
4141
}
4242

43-
func NewCollector(q *querier.Querier, svcs []config.Service) *Collector {
43+
func NewCollector(svcs []config.Service, qs map[string]querier.Querier) *Collector {
4444
return &Collector{
45-
q: q,
46-
svcs: svcs,
45+
queriers: qs,
46+
svcs: svcs,
4747
}
4848
}
4949

@@ -87,19 +87,19 @@ func (c *Collector) collectService(ctx context.Context, svc config.Service, ch c
8787
}
8888
log.Printf("collecting metrics for %s\n", svc.Name)
8989

90-
status, err := c.q.Status(ctx, svc.Query)
90+
status, err := c.queriers[svc.Query.Datasource].Status(ctx, svc.Query)
9191
if err != nil {
9292
log.Printf("ERROR - Failed to scrape status metric for %s query %s: %s", svc.Name, svc.Query.Name, err)
9393
res.Success = false
9494
}
95-
uptime, series, err := c.q.Uptime(ctx, svc.Query)
95+
uptime, series, err := c.queriers[svc.Query.Datasource].Uptime(ctx, svc.Query)
9696
if err != nil {
9797
log.Printf("ERROR - Failed to scrape uptime metric for %s query %s: %s", svc.Name, svc.Query.Name, err)
9898
res.Success = false
9999
}
100100

101101
for _, extra := range svc.Extras {
102-
_, series, err := c.q.Uptime(ctx, extra)
102+
_, series, err := c.queriers[extra.Datasource].Uptime(ctx, extra)
103103
if err != nil {
104104
log.Printf("ERROR - Failed to scrape uptime metric for %s query %s: %s", svc.Name, extra.Name, err)
105105
}

internal/config/config.go

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ package config
33
import (
44
"errors"
55
"fmt"
6+
"log"
67
"os"
8+
"slices"
79
"time"
810

911
"gopkg.in/yaml.v3"
1012
)
1113

14+
var (
15+
ErrUnknownDatasource = errors.New("unknown datasource for query")
16+
)
17+
1218
type Query struct {
1319
Name string `yaml:"name"`
1420
Query string `yaml:"query"`
@@ -17,6 +23,7 @@ type Query struct {
1723
Step time.Duration `yaml:"step"`
1824
BoolValue bool `yaml:"bool"`
1925
Units string `yaml:"units"`
26+
Datasource string `yaml:"datasource"`
2027
}
2128

2229
type Service struct {
@@ -41,12 +48,23 @@ type Graphs struct {
4148
Points int `yaml:"points"`
4249
}
4350

51+
type Datasource struct {
52+
Name string `yaml:"name"`
53+
Type string `yaml:"type"`
54+
Url string `yaml:"url"`
55+
Token string `yaml:"token,omitempty"`
56+
}
57+
4458
type Config struct {
45-
Port int `yaml:"port"`
46-
Metrics Metrics `yaml:"metrics"`
47-
Services []Service `yaml:"services"`
48-
Prometheus string `yaml:"prometheus"`
49-
Refresh time.Duration `yaml:"refresh"`
59+
Port int `yaml:"port"`
60+
Metrics Metrics `yaml:"metrics"`
61+
Services []Service `yaml:"services"`
62+
63+
Datasources []Datasource `yaml:"datasources"`
64+
65+
Prometheus string `yaml:"prometheus"`
66+
67+
Refresh time.Duration `yaml:"refresh"`
5068

5169
UI UI `yaml:"ui"`
5270
}
@@ -83,6 +101,14 @@ func setDefaults(conf *Config) {
83101
conf.Metrics.Port = 9743
84102
}
85103

104+
if conf.Prometheus != "" {
105+
conf.Datasources = append(conf.Datasources, Datasource{
106+
Name: "prometheus",
107+
Type: "prometheus",
108+
Url: conf.Prometheus,
109+
})
110+
}
111+
86112
for i, svc := range conf.Services {
87113
svc.Query.Name = "main"
88114
if svc.Group == "" {
@@ -122,20 +148,54 @@ func setDefaultQueryValues(q *Query) {
122148
if q.Step == 0 {
123149
q.Step = time.Minute * 5
124150
}
151+
if q.Datasource == "" {
152+
q.Datasource = "prometheus"
153+
}
125154
}
126155

127156
func (c *Config) Validate() error {
128-
if c.Prometheus == "" {
129-
return errors.New("prometheus cannot be empty")
157+
if c.Prometheus == "" && len(c.Datasources) == 0 {
158+
return errors.New("you must configure a datasource")
159+
}
160+
161+
if c.Prometheus != "" {
162+
log.Println("DEPRECATED - the prometheus config option is deprecated, replace with an entry in datasources with name prometheus")
163+
}
164+
165+
for _, ds := range c.Datasources {
166+
if ds.Name == "" {
167+
return errors.New("all datasources must have a name")
168+
}
169+
if !slices.Contains([]string{"prometheus", "datadog"}, ds.Type) {
170+
return errors.New("datasources must be one of: prometheus, datadog")
171+
}
172+
if ds.Url == "" {
173+
return errors.New("datasources must have a url configured")
174+
}
130175
}
131176

132177
for _, svc := range c.Services {
178+
if !c.containsDatasource(svc.Query.Datasource) {
179+
return ErrUnknownDatasource
180+
}
133181
for _, extra := range svc.Extras {
134182
if extra.Name == "" {
135183
return errors.New("extra query name cannot be empty")
136184
}
185+
if !c.containsDatasource(extra.Datasource) {
186+
return ErrUnknownDatasource
187+
}
137188
}
138189
}
139190

140191
return nil
141192
}
193+
194+
func (c *Config) containsDatasource(name string) bool {
195+
for _, ds := range c.Datasources {
196+
if name == ds.Name {
197+
return true
198+
}
199+
}
200+
return false
201+
}

internal/querier/prometheus.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package querier
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"strconv"
9+
"time"
10+
11+
"github.com/expr-lang/expr"
12+
"github.com/henrywhitaker3/prompage/internal/config"
13+
prometheus "github.com/prometheus/client_golang/api"
14+
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
15+
"github.com/prometheus/common/model"
16+
)
17+
18+
var (
19+
ErrTypeNotImplemented = errors.New("query result type not implemented yet")
20+
)
21+
22+
type Prometheus struct {
23+
client v1.API
24+
}
25+
26+
func NewPrometheus(conf config.Datasource) (*Prometheus, error) {
27+
client, err := prometheus.NewClient(prometheus.Config{
28+
Address: conf.Url,
29+
Client: http.DefaultClient,
30+
})
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
api := v1.NewAPI(client)
36+
37+
return &Prometheus{
38+
client: api,
39+
}, nil
40+
}
41+
42+
func (q *Prometheus) Uptime(ctx context.Context, query config.Query) (float32, []Item, error) {
43+
val, _, err := q.client.QueryRange(ctx, query.Query, v1.Range{
44+
Start: time.Now().Add(-query.Range),
45+
End: time.Now(),
46+
Step: query.Step,
47+
})
48+
if err != nil {
49+
return 0, nil, err
50+
}
51+
52+
switch r := val.(type) {
53+
case model.Matrix:
54+
if r.Len() < 1 {
55+
return 0, nil, errors.New("no results for query")
56+
}
57+
58+
passing := 0
59+
total := 0
60+
series := []Item{}
61+
for _, val := range r[0].Values {
62+
value := float64(0)
63+
if query.BoolValue {
64+
res, err := q.vector(val.Value, query)
65+
if err != nil {
66+
return 0, nil, err
67+
}
68+
if res {
69+
passing++
70+
value = 1
71+
}
72+
} else {
73+
f, err := q.asFloat(val.Value)
74+
if err != nil {
75+
return 0, nil, err
76+
}
77+
value = f
78+
}
79+
total++
80+
81+
series = append(series, Item{Time: val.Timestamp.Time(), Value: value})
82+
}
83+
84+
return (float32(passing) / float32(total)) * 100, series, nil
85+
}
86+
87+
return 100, nil, ErrTypeNotImplemented
88+
}
89+
90+
func (q *Prometheus) Status(ctx context.Context, query config.Query) (bool, error) {
91+
val, _, err := q.client.Query(ctx, query.Query, time.Now())
92+
if err != nil {
93+
return false, err
94+
}
95+
96+
switch r := val.(type) {
97+
case model.Vector:
98+
if r.Len() < 1 {
99+
return false, errors.New("no results for query")
100+
}
101+
return q.vector(r[0].Value, query)
102+
}
103+
104+
return false, ErrTypeNotImplemented
105+
}
106+
107+
func (q *Prometheus) vector(v model.SampleValue, query config.Query) (bool, error) {
108+
env := map[string]any{
109+
"result": 0,
110+
}
111+
112+
exp, err := expr.Compile(query.Expression, expr.Env(env), expr.AsBool())
113+
if err != nil {
114+
return false, fmt.Errorf("failed to compile expr: %v", err)
115+
}
116+
val, err := q.asFloat(v)
117+
if err != nil {
118+
return false, err
119+
}
120+
121+
env["result"] = val
122+
out, err := expr.Run(exp, env)
123+
if err != nil {
124+
return false, err
125+
}
126+
127+
return out.(bool), nil
128+
}
129+
130+
func (q *Prometheus) asFloat(v model.SampleValue) (float64, error) {
131+
val, err := strconv.ParseFloat(v.String(), 64)
132+
if err != nil {
133+
return 0, fmt.Errorf("failed to parse float: %v", err)
134+
}
135+
return val, nil
136+
}

0 commit comments

Comments
 (0)