Skip to content

Commit 102eabd

Browse files
feat: added datadog datasource
1 parent 017ccaa commit 102eabd

File tree

7 files changed

+222
-9
lines changed

7 files changed

+222
-9
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ datasources:
1616
url: http://prometheus:9000
1717
type: prometheus
1818
- name: datadog
19-
url: https://api.datadoghq.eu
20-
token: XXXXXXXX
19+
url: datadoghq.eu
2120
type: datadog
21+
extras:
22+
appKey: XXXXX
23+
apiKey: XXXXX
2224

2325
services:
2426
- name: Postgres
@@ -33,12 +35,20 @@ services:
3335
range: 24h
3436
# Optional. The resoltuion of the range query (default: 5m)
3537
step: 5m
36-
# Optional. Whether to generate graphs based on raw values or boolean values (default: false)
38+
# Optional. Whether to generate graphs based on raw values or boolean values (default: false).
39+
# This value is always set to true for the main query (i.e. services[].query.bool), but can be user-configured
40+
# for the extra queries (i.e. services[].extras[].bool)
3741
bool: false
3842
# Optional. The units to display on any graphs (default: null)
3943
units: ""
4044
# Optional. The datasource to use for the query (default: prometheus)
4145
datasource: prometheus
46+
- name: Http Server
47+
group: Web
48+
query:
49+
query: sum:http_server.error_rate{*}.as_rate()
50+
datasource: datadog
51+
expression: (100 - float(result)) > 99.99
4252
# Settings to configure the UI
4353
ui:
4454
# Optional. The title of the page (default: Status Page)

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/henrywhitaker3/prompage
33
go 1.22.2
44

55
require (
6+
github.com/DataDog/datadog-api-client-go/v2 v2.26.0
67
github.com/expr-lang/expr v1.16.7
78
github.com/go-echarts/go-echarts/v2 v2.3.3
89
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
@@ -17,9 +18,12 @@ require (
1718
)
1819

1920
require (
21+
github.com/DataDog/zstd v1.5.2 // indirect
2022
github.com/beorn7/perks v1.0.1 // indirect
2123
github.com/cespare/xxhash/v2 v2.3.0 // indirect
24+
github.com/goccy/go-json v0.10.2 // indirect
2225
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
26+
github.com/golang/protobuf v1.5.3 // indirect
2327
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2428
github.com/json-iterator/go v1.1.12 // indirect
2529
github.com/kr/text v0.2.0 // indirect
@@ -34,8 +38,10 @@ require (
3438
github.com/valyala/fasttemplate v1.2.2 // indirect
3539
golang.org/x/crypto v0.22.0 // indirect
3640
golang.org/x/net v0.24.0 // indirect
41+
golang.org/x/oauth2 v0.18.0 // indirect
3742
golang.org/x/sys v0.19.0 // indirect
3843
golang.org/x/text v0.14.0 // indirect
3944
golang.org/x/time v0.5.0 // indirect
45+
google.golang.org/appengine v1.6.7 // indirect
4046
google.golang.org/protobuf v1.33.0 // indirect
4147
)

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
github.com/DataDog/datadog-api-client-go/v2 v2.26.0 h1:bZr0hu+hx8L91+yU5EGw8wK3FlCVEIashpx+cylWsf0=
2+
github.com/DataDog/datadog-api-client-go/v2 v2.26.0/go.mod h1:QKOu6vscsh87fMY1lHfLEmNSunyXImj8BUaUWJXOehc=
3+
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
4+
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
15
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
26
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
37
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -11,10 +15,15 @@ github.com/expr-lang/expr v1.16.7 h1:gCIiHt5ODA0xIaDbD0DPKyZpM9Drph3b3lolYAYq2Kw
1115
github.com/expr-lang/expr v1.16.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
1216
github.com/go-echarts/go-echarts/v2 v2.3.3 h1:uImZAk6qLkC6F9ju6mZ5SPBqTyK8xjZKwSmwnCg4bxg=
1317
github.com/go-echarts/go-echarts/v2 v2.3.3/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=
18+
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
19+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
1420
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
1521
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
22+
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
23+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
1624
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
1725
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
26+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1827
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1928
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2029
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -75,22 +84,31 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
7584
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
7685
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
7786
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
87+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
7888
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
7989
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
90+
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
8091
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
8192
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
8293
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
8394
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
95+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
8496
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8597
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8698
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
8799
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
100+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
101+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
88102
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
89103
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
90104
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
91105
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
106+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
107+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
92108
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
93109
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
110+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
111+
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
94112
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
95113
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
96114
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/config/config.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ type Graphs struct {
4949
}
5050

5151
type Datasource struct {
52-
Name string `yaml:"name"`
53-
Type string `yaml:"type"`
54-
Url string `yaml:"url"`
55-
Token string `yaml:"token,omitempty"`
52+
Name string `yaml:"name"`
53+
Type string `yaml:"type"`
54+
Url string `yaml:"url"`
55+
Extras map[string]string `yaml:"extras"`
5656
}
5757

5858
type Config struct {
@@ -114,6 +114,7 @@ func setDefaults(conf *Config) {
114114
if svc.Group == "" {
115115
svc.Group = "default"
116116
}
117+
svc.Query.BoolValue = true
117118
setDefaultQueryValues(&svc.Query)
118119

119120
for i, query := range svc.Extras {
@@ -172,6 +173,14 @@ func (c *Config) Validate() error {
172173
if ds.Url == "" {
173174
return errors.New("datasources must have a url configured")
174175
}
176+
if ds.Type == "datadog" {
177+
if _, ok := ds.Extras["apiKey"]; !ok {
178+
return errors.New("datadog extra apiKey must be set")
179+
}
180+
if _, ok := ds.Extras["appKey"]; !ok {
181+
return errors.New("datadog extra appKey must be set")
182+
}
183+
}
175184
}
176185

177186
for _, svc := range c.Services {

internal/querier/datadog.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package querier
2+
3+
import (
4+
"context"
5+
"errors"
6+
"time"
7+
8+
"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
9+
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
10+
"github.com/expr-lang/expr"
11+
"github.com/henrywhitaker3/prompage/internal/config"
12+
)
13+
14+
var (
15+
ErrInvalidNumberOfSeries = errors.New("datadog returned an invalid number of series")
16+
)
17+
18+
type Datadog struct {
19+
// The dd api client
20+
client *datadog.APIClient
21+
// The dd metrics client
22+
metrics *datadogV1.MetricsApi
23+
// The base ctx to make requests from
24+
ctx context.Context
25+
}
26+
27+
func NewDatadog(cfg config.Datasource) (*Datadog, error) {
28+
if cfg.Type != "datadog" {
29+
return nil, ErrInvalidDatasourceMapping
30+
}
31+
32+
client := datadog.NewAPIClient(datadog.NewConfiguration())
33+
metrics := datadogV1.NewMetricsApi(client)
34+
35+
ctx := context.WithValue(
36+
context.Background(),
37+
datadog.ContextAPIKeys,
38+
map[string]datadog.APIKey{
39+
"apiKeyAuth": {
40+
Key: cfg.Extras["apiKey"],
41+
},
42+
"appKeyAuth": {
43+
Key: cfg.Extras["appKey"],
44+
},
45+
},
46+
)
47+
ctx = context.WithValue(
48+
ctx,
49+
datadog.ContextServerVariables,
50+
map[string]string{"site": cfg.Url},
51+
)
52+
53+
return &Datadog{
54+
client: client,
55+
metrics: metrics,
56+
ctx: ctx,
57+
}, nil
58+
}
59+
60+
func (d *Datadog) Uptime(ctx context.Context, query config.Query) (float32, []Item, error) {
61+
resp, _, err := d.metrics.QueryMetrics(
62+
d.ctx,
63+
time.Now().Add(-query.Range).Unix(),
64+
time.Now().Unix(),
65+
query.Query,
66+
)
67+
68+
if err != nil {
69+
return 0, nil, err
70+
}
71+
72+
if len(resp.Series) != 1 {
73+
return 0, nil, ErrInvalidNumberOfSeries
74+
}
75+
76+
series := resp.Series[0]
77+
78+
items := []Item{}
79+
passing := 0
80+
total := 0
81+
82+
for _, point := range series.GetPointlist() {
83+
time := time.Unix(0, int64(*point[0]*float64(time.Millisecond)))
84+
raw := *point[1]
85+
value := float64(0)
86+
87+
total++
88+
env := map[string]any{
89+
"result": raw,
90+
}
91+
if query.BoolValue {
92+
exp, err := expr.Compile(query.Expression, expr.Env(env), expr.AsBool())
93+
if err != nil {
94+
return 0, nil, err
95+
}
96+
out, err := expr.Run(exp, env)
97+
if err != nil {
98+
return 0, nil, err
99+
}
100+
if out.(bool) {
101+
value = 1
102+
passing++
103+
}
104+
} else {
105+
exp, err := expr.Compile(query.Expression, expr.Env(env), expr.AsFloat64())
106+
if err != nil {
107+
return 0, nil, err
108+
}
109+
out, err := expr.Run(exp, env)
110+
if err != nil {
111+
return 0, nil, err
112+
}
113+
value = out.(float64)
114+
}
115+
116+
items = append(items, Item{
117+
Time: time,
118+
Value: value,
119+
})
120+
}
121+
122+
return (float32(passing) / float32(total)) * 100, items, nil
123+
}
124+
125+
func (d *Datadog) Status(ctx context.Context, query config.Query) (bool, error) {
126+
resp, _, err := d.metrics.QueryMetrics(
127+
d.ctx,
128+
time.Now().Add(-query.Range).Unix(),
129+
time.Now().Unix(),
130+
query.Query,
131+
)
132+
if err != nil {
133+
return false, err
134+
}
135+
136+
if len(resp.Series) != 1 {
137+
return false, ErrInvalidNumberOfSeries
138+
}
139+
140+
series := resp.Series[0]
141+
142+
env := map[string]any{"result": 0}
143+
exp, err := expr.Compile(query.Expression, expr.Env(env), expr.AsBool())
144+
if err != nil {
145+
return false, err
146+
}
147+
148+
latest := series.GetPointlist()[series.GetLength()-1]
149+
150+
env["result"] = *latest[1]
151+
out, err := expr.Run(exp, env)
152+
if err != nil {
153+
return false, err
154+
}
155+
156+
return out.(bool), nil
157+
}

internal/querier/prometheus.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,16 @@ func (q *Prometheus) Uptime(ctx context.Context, query config.Query) (float32, [
7474
if err != nil {
7575
return 0, nil, err
7676
}
77-
value = f
77+
env := map[string]any{"result": f}
78+
exp, err := expr.Compile(query.Expression, expr.Env(env), expr.AsFloat64())
79+
if err != nil {
80+
return 0, nil, err
81+
}
82+
out, err := expr.Run(exp, env)
83+
if err != nil {
84+
return 0, nil, err
85+
}
86+
value = out.(float64)
7887
}
7988
total++
8089

internal/querier/querier.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ func BuildQueriers(sources []config.Datasource) (map[string]Querier, error) {
3838
}
3939
out[ds.Name] = q
4040
case "datadog":
41-
return nil, errors.New("datadog not implemented yet")
41+
q, err := NewDatadog(ds)
42+
if err != nil {
43+
return nil, err
44+
}
45+
out[ds.Name] = q
4246
default:
4347
return nil, ErrInvalidDatasourceMapping
4448
}

0 commit comments

Comments
 (0)