Skip to content

Commit 9c35c1a

Browse files
authored
Merge pull request #5 from vshn/feat/http-sd
Add http service discovery
2 parents 0c9d842 + d8a43e5 commit 9c35c1a

File tree

13 files changed

+439
-10
lines changed

13 files changed

+439
-10
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,15 @@ To test the `exporter-filterproxy` in Kubernetes using the Kubernetes service di
3636
make kind
3737
```
3838

39-
This will start a local kind cluster and deploy a `exporter-filterproxy` that proxies the metrics of the CoreDNS service.
39+
This will start a local kind cluster and deploy a `exporter-filterproxy` that proxies the metrics of the CoreDNS service, as well as a Prometheus instance.
40+
41+
Once the cluster started you can expose the test Prometheus using port-forward
42+
43+
```
44+
kubectl port-forward svc/prometheus 9090
45+
```
46+
47+
You should see two discovered targets through the exporter-filterproxy when you open `http://localhost:9090/targets`
4048

4149

4250
## Configuration

discovery.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/vshn/exporter-filterproxy/target"
8+
)
9+
10+
type targetConfigFetcher interface {
11+
FetchTargetConfigs(ctx context.Context, baseTarget string, basePath string) ([]target.StaticConfig, error)
12+
}
13+
14+
type multiTargetConfigFetcher map[string]targetConfigFetcher
15+
16+
func (mf multiTargetConfigFetcher) FetchTargetConfigs(ctx context.Context, baseTarget string, basePath string) ([]target.StaticConfig, error) {
17+
18+
configs := []target.StaticConfig{}
19+
for path, f := range mf {
20+
c, err := f.FetchTargetConfigs(ctx, baseTarget, basePath+path)
21+
if err != nil {
22+
log.Printf("Failed to fetch targets: %s", err.Error())
23+
continue
24+
}
25+
configs = append(configs, c...)
26+
}
27+
28+
return configs, nil
29+
}

discovery_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/prometheus/common/model"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"github.com/vshn/exporter-filterproxy/target"
11+
)
12+
13+
func TestMultiTargetConfigFetcher(t *testing.T) {
14+
15+
fa := fakeTargetConfigFetcher{
16+
t: t,
17+
path: "/a",
18+
target: "proxy.example.com",
19+
configs: []target.StaticConfig{
20+
{
21+
Targets: []string{"proxy.example.com"},
22+
Labels: map[model.LabelName]model.LabelValue{
23+
"foo": "a1",
24+
},
25+
},
26+
{
27+
Targets: []string{"proxy.example.com"},
28+
Labels: map[model.LabelName]model.LabelValue{
29+
"foo": "a2",
30+
},
31+
},
32+
},
33+
}
34+
fb := fakeTargetConfigFetcher{
35+
t: t,
36+
path: "/b",
37+
target: "proxy.example.com",
38+
configs: []target.StaticConfig{
39+
{
40+
Targets: []string{"proxy.example.com"},
41+
Labels: map[model.LabelName]model.LabelValue{
42+
"foo": "b1",
43+
},
44+
},
45+
{
46+
Targets: []string{"proxy.example.com"},
47+
Labels: map[model.LabelName]model.LabelValue{
48+
"foo": "b2",
49+
},
50+
},
51+
},
52+
}
53+
54+
mf := multiTargetConfigFetcher{
55+
"/a": fa,
56+
"/b": fb,
57+
}
58+
59+
conf, err := mf.FetchTargetConfigs(context.TODO(), "proxy.example.com", "")
60+
require.NoError(t, err)
61+
require.Len(t, conf, 4)
62+
63+
seenFoolabels := []string{}
64+
for _, c := range conf {
65+
require.Len(t, c.Targets, 1)
66+
assert.Equal(t, "proxy.example.com", c.Targets[0])
67+
require.Len(t, c.Labels, 1)
68+
69+
seenFoolabels = append(seenFoolabels, string(c.Labels["foo"]))
70+
}
71+
assert.ElementsMatch(t, seenFoolabels, []string{"b1", "b2", "a1", "a2"})
72+
73+
}
74+
75+
type fakeTargetConfigFetcher struct {
76+
configs []target.StaticConfig
77+
t *testing.T
78+
path string
79+
target string
80+
}
81+
82+
func (f fakeTargetConfigFetcher) FetchTargetConfigs(ctx context.Context, baseTarget string, basePath string) ([]target.StaticConfig, error) {
83+
assert.Equal(f.t, f.path, basePath)
84+
assert.Equal(f.t, f.target, baseTarget)
85+
return f.configs, nil
86+
}

handle.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"log"
@@ -67,6 +68,33 @@ func multiHandler(prefix string, fetcher multiMetricsFetcher) http.HandlerFunc {
6768
})
6869
}
6970

71+
func serviceDiscoveryHandler(prefix string, fetcher targetConfigFetcher) http.HandlerFunc {
72+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73+
74+
configs, err := fetcher.FetchTargetConfigs(r.Context(), r.Host, strings.TrimSuffix(r.URL.Path, "/"))
75+
if err != nil {
76+
log.Printf("Failed to discover Endpoints: %s", err.Error())
77+
w.WriteHeader(http.StatusInternalServerError)
78+
return
79+
}
80+
81+
res, err := json.Marshal(configs)
82+
if err != nil {
83+
log.Printf("Failed to marshal Endpoints: %s", err.Error())
84+
w.WriteHeader(http.StatusInternalServerError)
85+
return
86+
}
87+
88+
w.Header().Set("Content-Type", "application/json")
89+
_, err = w.Write(res)
90+
91+
if err != nil {
92+
log.Printf("Failed to write: %s", err.Error())
93+
return
94+
}
95+
})
96+
}
97+
7098
func parseURLParams(values url.Values) (map[string]string, error) {
7199
res := map[string]string{}
72100
for k, v := range values {

handle_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
dto "github.com/prometheus/client_model/go"
11+
"github.com/prometheus/common/model"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
1314
"github.com/vshn/exporter-filterproxy/target"
@@ -152,3 +153,52 @@ func (f fakeMultiMetricsFetcher) FetchMetricsFor(ctx context.Context, endpoint s
152153
func deref[T any](x T) *T {
153154
return &x
154155
}
156+
157+
func TestDiscoveryHandler(t *testing.T) {
158+
159+
f := fakeTargetConfigFetcher{
160+
configs: []target.StaticConfig{
161+
{
162+
Targets: []string{"proxy.example.com"},
163+
Labels: map[model.LabelName]model.LabelValue{
164+
"__metrics_path__": "/foo/a.b.c.d",
165+
"metrics_path": "/foo",
166+
"instance": "a.b.c.d",
167+
},
168+
},
169+
{
170+
Targets: []string{"proxy.example.com"},
171+
Labels: map[model.LabelName]model.LabelValue{
172+
"__metrics_path__": "/foo/d.e.f.g",
173+
"metrics_path": "/foo",
174+
"instance": "d.e.f.g",
175+
},
176+
},
177+
{
178+
Targets: []string{"proxy.example.com"},
179+
Labels: map[model.LabelName]model.LabelValue{
180+
"__metrics_path__": "/bar",
181+
"metrics_path": "/bar",
182+
},
183+
},
184+
},
185+
t: t,
186+
path: "",
187+
target: "proxy.example.com",
188+
}
189+
h := serviceDiscoveryHandler("", f)
190+
191+
req, err := http.NewRequest("GET", "/", nil)
192+
req.Host = "proxy.example.com"
193+
require.NoError(t, err)
194+
rr := httptest.NewRecorder()
195+
196+
h.ServeHTTP(rr, req)
197+
assert.Equal(t, http.StatusOK, rr.Code)
198+
199+
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
200+
201+
assert.Equal(t, expectedDiscoveryRes, rr.Body.String())
202+
}
203+
204+
var expectedDiscoveryRes = `[{"targets":["proxy.example.com"],"labels":{"__metrics_path__":"/foo/a.b.c.d","instance":"a.b.c.d","metrics_path":"/foo"}},{"targets":["proxy.example.com"],"labels":{"__metrics_path__":"/foo/d.e.f.g","instance":"d.e.f.g","metrics_path":"/foo"}},{"targets":["proxy.example.com"],"labels":{"__metrics_path__":"/bar","metrics_path":"/bar"}}]`

main.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ func main() {
2525
return
2626
}
2727

28+
targetDiscovery := multiTargetConfigFetcher{}
29+
2830
for name, endpoint := range conf.Endpoints {
2931

3032
authToken, err := getAuthToken(endpoint.Auth)
@@ -36,11 +38,11 @@ func main() {
3638
switch {
3739
case endpoint.Target != "":
3840
log.Printf("Registering static endpoint %q at %s", name, endpoint.Path)
41+
sf := target.NewStaticFetcher(endpoint.Target, authToken, endpoint.RefreshInterval, endpoint.InsecureSkipVerify)
3942
mux.HandleFunc(endpoint.Path,
40-
handler(
41-
target.NewStaticFetcher(endpoint.Target, authToken, endpoint.RefreshInterval, endpoint.InsecureSkipVerify),
42-
),
43+
handler(sf),
4344
)
45+
targetDiscovery[endpoint.Path] = sf
4446
case endpoint.KubernetesTarget != nil:
4547
log.Printf("Registering kube endpoint %q at %s", name, endpoint.Path)
4648
kf, err := target.NewKubernetesEndpointFetcher(
@@ -62,13 +64,21 @@ func main() {
6264
mux.HandleFunc(endpoint.Path+"/",
6365
multiHandler(endpoint.Path, kf),
6466
)
67+
mux.HandleFunc(endpoint.Path,
68+
serviceDiscoveryHandler(endpoint.Path, kf),
69+
)
70+
targetDiscovery[endpoint.Path] = kf
6571
default:
6672
log.Fatalf("No target set for endpoint %s", name)
6773
return
6874
}
6975

7076
}
7177

78+
mux.HandleFunc("/",
79+
serviceDiscoveryHandler("", targetDiscovery),
80+
)
81+
7282
srv := &http.Server{
7383
Addr: conf.Addr,
7484
ReadTimeout: 5 * time.Second,

sample-config/prometheus.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ global:
55

66

77
scrape_configs:
8-
- job_name: 'node-cpu-2'
9-
metrics_path: /node
10-
params:
11-
cpu: ['2']
12-
static_configs:
13-
- targets: ['filterproxy:8082']
8+
- job_name: 'exporter-filterproxy'
9+
http_sd_configs:
10+
- url: 'http://filterproxy:8082'
1411

target/kubernetes.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"golang.org/x/sync/errgroup"
1212

1313
dto "github.com/prometheus/client_model/go"
14+
"github.com/prometheus/common/model"
1415
corev1 "k8s.io/api/core/v1"
1516
"k8s.io/apimachinery/pkg/types"
1617

@@ -123,6 +124,28 @@ func (f *KubernetesEndpointFetcher) FetchMetricsFor(ctx context.Context, endpoin
123124
return f.cache[endpoint], nil
124125
}
125126

127+
func (f *KubernetesEndpointFetcher) FetchTargetConfigs(ctx context.Context, baseTarget string, basePath string) ([]StaticConfig, error) {
128+
staticConfig := []StaticConfig{}
129+
130+
endpoints, err := f.discover(ctx)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
for _, ip := range endpoints {
136+
conf := StaticConfig{
137+
Targets: []string{baseTarget},
138+
Labels: map[model.LabelName]model.LabelValue{
139+
"__metrics_path__": model.LabelValue(fmt.Sprintf("%s/%s", basePath, ip)),
140+
"metrics_path": model.LabelValue(basePath),
141+
"instance": model.LabelValue(ip),
142+
},
143+
}
144+
staticConfig = append(staticConfig, conf)
145+
}
146+
return staticConfig, nil
147+
}
148+
126149
func (f *KubernetesEndpointFetcher) now() time.Time {
127150
if f.clock != nil {
128151
return f.clock()

target/kubernetes_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package target
22

33
import (
44
"context"
5+
"fmt"
56
"net"
67
"net/http"
78
"net/http/httptest"
@@ -386,3 +387,38 @@ func newTestEndpoint(port int, ips ...string) *corev1.Endpoints {
386387
},
387388
}
388389
}
390+
391+
func TestKube_FetchTargetConfigs(t *testing.T) {
392+
podIps := []string{"127.0.18.1", "127.0.18.2", "127.0.18.4", "127.0.18.8"}
393+
394+
f := KubernetesEndpointFetcher{
395+
endpointname: "test-ep",
396+
namespace: "fetch-test",
397+
port: 8119,
398+
kube: newTestKubeEnv(
399+
newTestEndpoint(8119, podIps...),
400+
),
401+
}
402+
403+
tconfs, err := f.FetchTargetConfigs(context.TODO(), "proxy.example.com", "/buzz")
404+
require.NoError(t, err)
405+
require.Len(t, tconfs, 4)
406+
407+
confMap := map[string]StaticConfig{}
408+
409+
for i, tconf := range tconfs {
410+
require.Len(t, tconf.Targets, 1)
411+
assert.Equal(t, "proxy.example.com", tconf.Targets[0])
412+
assert.Len(t, tconf.Labels, 3)
413+
414+
inst := string(tconf.Labels["instance"])
415+
assert.Contains(t, podIps, inst)
416+
confMap[inst] = tconfs[i]
417+
}
418+
419+
for _, pip := range podIps {
420+
require.Contains(t, confMap, pip)
421+
assert.EqualValues(t, fmt.Sprintf("/buzz/%s", pip), confMap[pip].Labels["__metrics_path__"])
422+
assert.EqualValues(t, "/buzz", confMap[pip].Labels["metrics_path"])
423+
}
424+
}

0 commit comments

Comments
 (0)