Skip to content
This repository was archived by the owner on Dec 1, 2018. It is now read-only.

Commit 8a06bbf

Browse files
authored
Merge pull request #1536 from johanneswuerbach/librato-sink
Heapster Librato sink
2 parents 0c5579a + 01f1605 commit 8a06bbf

File tree

10 files changed

+667
-8
lines changed

10 files changed

+667
-8
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ container:
7575
cp deploy/docker/Dockerfile $(TEMP_DIR)
7676
docker build --pull -t $(PREFIX)/heapster-$(ARCH):$(VERSION) $(TEMP_DIR)
7777
ifneq ($(OVERRIDE_IMAGE_NAME),)
78-
docker tag -f $(PREFIX)/heapster-$(ARCH):$(VERSION) $(OVERRIDE_IMAGE_NAME)
78+
docker tag $(PREFIX)/heapster-$(ARCH):$(VERSION) $(OVERRIDE_IMAGE_NAME)
7979
endif
8080

8181
ifndef DOCKER_IN_DOCKER
@@ -110,6 +110,9 @@ push-influxdb:
110110
push-grafana:
111111
PREFIX=$(PREFIX) make -C grafana push
112112

113+
push-override:
114+
docker push $(OVERRIDE_IMAGE_NAME)
115+
113116
gcr-login:
114117
ifeq ($(findstring gcr.io,$(PREFIX)),gcr.io)
115118
@echo "If you are pushing to a gcr.io registry, you have to be logged in via 'docker login'; 'gcloud docker push' can't push manifest lists yet."

common/librato/dummy_librato.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librato
16+
17+
type MeasurementsSavedToLibrato struct {
18+
Measurement Measurement
19+
}
20+
21+
type FakeLibratoClient struct {
22+
Measurements []MeasurementsSavedToLibrato
23+
}
24+
25+
func NewFakeLibratoClient() *FakeLibratoClient {
26+
return &FakeLibratoClient{[]MeasurementsSavedToLibrato{}}
27+
}
28+
29+
func (client *FakeLibratoClient) Write(measurements []Measurement) error {
30+
for _, measurement := range measurements {
31+
client.Measurements = append(client.Measurements, MeasurementsSavedToLibrato{measurement})
32+
}
33+
return nil
34+
}
35+
36+
var FakeClient = NewFakeLibratoClient()
37+
38+
var Config = LibratoConfig{
39+
Username: "root",
40+
Token: "root",
41+
}

common/librato/librato.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librato
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"fmt"
21+
"net"
22+
"net/http"
23+
"net/url"
24+
"strings"
25+
"time"
26+
)
27+
28+
type Measurement struct {
29+
Name string `json:"name,omitempty"`
30+
Value float64 `json:"value,omitempty"`
31+
Tags map[string]string `json:"tags,omitempty"`
32+
Time int64 `json:"time,omitempty"`
33+
}
34+
35+
type request struct {
36+
Tags map[string]string `json:"tags,omitempty"`
37+
Measurements []Measurement `json:"measurements,omitempty"`
38+
}
39+
40+
type Client interface {
41+
Write([]Measurement) error
42+
}
43+
44+
type LibratoClient struct {
45+
httpClient *http.Client
46+
config LibratoConfig
47+
}
48+
49+
func (c *LibratoClient) Write(measurements []Measurement) error {
50+
b, err := json.Marshal(&request{
51+
Measurements: measurements,
52+
Tags: c.config.Tags,
53+
})
54+
if nil != err {
55+
return err
56+
}
57+
req, err := http.NewRequest(
58+
"POST",
59+
c.config.API+"/v1/measurements",
60+
bytes.NewBuffer(b),
61+
)
62+
if nil != err {
63+
return err
64+
}
65+
req.Header.Add("Content-Type", "application/json")
66+
req.Header.Set("User-Agent", "heapster")
67+
req.SetBasicAuth(c.config.Username, c.config.Token)
68+
_, err = c.httpClient.Do(req)
69+
return err
70+
}
71+
72+
type LibratoConfig struct {
73+
Username string
74+
Token string
75+
API string
76+
Prefix string
77+
Tags map[string]string
78+
}
79+
80+
func NewClient(c LibratoConfig) *LibratoClient {
81+
var netTransport = &http.Transport{
82+
Dial: (&net.Dialer{
83+
Timeout: 5 * time.Second,
84+
}).Dial,
85+
TLSHandshakeTimeout: 5 * time.Second,
86+
}
87+
var httpClient = &http.Client{
88+
Timeout: time.Second * 10,
89+
Transport: netTransport,
90+
}
91+
92+
client := &LibratoClient{httpClient: httpClient, config: c}
93+
return client
94+
}
95+
96+
func BuildConfig(uri *url.URL) (*LibratoConfig, error) {
97+
config := LibratoConfig{API: "https://metrics-api.librato.com", Prefix: ""}
98+
99+
opts := uri.Query()
100+
if len(opts["username"]) >= 1 {
101+
config.Username = opts["username"][0]
102+
} else {
103+
return nil, fmt.Errorf("no `username` flag specified")
104+
}
105+
// TODO: use more secure way to pass the password.
106+
if len(opts["token"]) >= 1 {
107+
config.Token = opts["token"][0]
108+
} else {
109+
return nil, fmt.Errorf("no `token` flag specified")
110+
}
111+
if len(opts["api"]) >= 1 {
112+
config.API = opts["api"][0]
113+
}
114+
if len(opts["prefix"]) >= 1 {
115+
config.Prefix = opts["prefix"][0]
116+
117+
if !strings.HasSuffix(config.Prefix, ".") {
118+
config.Prefix = config.Prefix + "."
119+
}
120+
}
121+
if len(opts["tags"]) >= 1 {
122+
config.Tags = make(map[string]string)
123+
124+
tagNames := strings.Split(opts["tags"][0], ",")
125+
126+
for _, tagName := range tagNames {
127+
if val, ok := opts["tag_"+tagName]; ok {
128+
config.Tags[tagName] = val[0]
129+
}
130+
}
131+
}
132+
133+
return &config, nil
134+
}

common/librato/librato_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librato
16+
17+
import (
18+
"net/http/httptest"
19+
"net/url"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
util "k8s.io/client-go/util/testing"
24+
)
25+
26+
func TestLibratoClientWrite(t *testing.T) {
27+
handler := util.FakeHandler{
28+
StatusCode: 200,
29+
ResponseBody: "",
30+
T: t,
31+
}
32+
server := httptest.NewServer(&handler)
33+
defer server.Close()
34+
35+
stubLibratoURL, err := url.Parse("?username=stub&token=stub&api=" + server.URL)
36+
37+
assert.NoError(t, err)
38+
39+
config, err := BuildConfig(stubLibratoURL)
40+
41+
assert.NoError(t, err)
42+
43+
client := NewClient(*config)
44+
45+
err = client.Write([]Measurement{
46+
{
47+
Name: "test",
48+
Value: 1.4,
49+
},
50+
})
51+
52+
assert.NoError(t, err)
53+
54+
handler.ValidateRequestCount(t, 1)
55+
56+
expectedBody := `{"measurements":[{"name":"test","value":1.4}]}`
57+
58+
handler.ValidateRequest(t, "/v1/measurements", "POST", &expectedBody)
59+
}
60+
61+
func TestLibratoClientWriteWithTags(t *testing.T) {
62+
handler := util.FakeHandler{
63+
StatusCode: 200,
64+
ResponseBody: "",
65+
T: t,
66+
}
67+
server := httptest.NewServer(&handler)
68+
defer server.Close()
69+
70+
stubLibratoURL, err := url.Parse("?username=stub&token=stub&tags=a,b&tag_a=test&api=" + server.URL)
71+
72+
assert.NoError(t, err)
73+
74+
config, err := BuildConfig(stubLibratoURL)
75+
76+
assert.NoError(t, err)
77+
78+
client := NewClient(*config)
79+
80+
err = client.Write([]Measurement{
81+
{
82+
Name: "test",
83+
Value: 1.4,
84+
Tags: map[string]string{
85+
"test": "tag",
86+
},
87+
},
88+
})
89+
90+
assert.NoError(t, err)
91+
92+
handler.ValidateRequestCount(t, 1)
93+
94+
expectedBody := `{"tags":{"a":"test"},"measurements":[{"name":"test","value":1.4,"tags":{"test":"tag"}}]}`
95+
96+
handler.ValidateRequest(t, "/v1/measurements", "POST", &expectedBody)
97+
}

docs/sink-configuration.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ For example,
240240
Metrics are sent to Graphite with this hierarchy:
241241
* `PREFIX`
242242
* `cluster`
243-
* `namespaces`
243+
* `namespaces`
244244
* `NAMESPACE`
245245
* `nodes`
246246
* `NODE`
@@ -252,6 +252,28 @@ Metrics are sent to Graphite with this hierarchy:
252252
* `sys-containers`
253253
* `SYS-CONTAINER`
254254

255+
### Librato
256+
257+
This sink supports monitoring metrics only.
258+
259+
To use the librato sink add the following flag:
260+
261+
--sink="librato:<?<OPTIONS>>"
262+
263+
Options can be set in query string, like this:
264+
265+
* `username` - Librato user email address (https://www.librato.com/docs/api/#authentication).
266+
* `token` - Librato API token
267+
* `prefix` - Prefix for all measurement names
268+
* `tags` - By default provided tags (comma separated list)
269+
* `tag_{name}` - Value for the tag `name`
270+
271+
For example,
272+
273+
--sink="librato:?username=xyz&token=secret&prefix=k8s&tags=cluster&tag_cluster=staging"
274+
275+
The librato sink currently only works with accounts, which support [tagged metrics](https://www.librato.com/docs/kb/faq/account_questions/tags_or_sources/).
276+
255277
## Using multiple sinks
256278

257279
Heapster can be configured to send k8s metrics and events to multiple sinks by specifying the`--sink=...` flag multiple times.

docs/sink-owners.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ List of Owners
3636
| Riemann | :heavy_check_mark: | :x: :new: | @jamtur01 @mcorbin | :ok: |
3737
| Graphite | :heavy_check_mark: | :x: | @jsoriano / @theairkit | :new: #1341 |
3838
| Wavefront | :heavy_check_mark: | :x: | @ezeev | :ok: |
39+
| Librato | :heavy_check_mark: | :x: | @johanneswuerbach | :ok: |

metrics/api/v1/historical_handlers_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,34 +196,34 @@ func TestAvailableMetrics(t *testing.T) {
196196
api, src := prepApi()
197197

198198
src.metricNames = map[core.HistoricalKey][]string{
199-
core.HistoricalKey{
199+
{
200200
ObjectType: core.MetricSetTypeCluster,
201201
}: {"cm1", "cm2"},
202202

203-
core.HistoricalKey{
203+
{
204204
ObjectType: core.MetricSetTypeNode,
205205
NodeName: "somenode1",
206206
}: {"nm1", "nm2"},
207207

208-
core.HistoricalKey{
208+
{
209209
ObjectType: core.MetricSetTypeNamespace,
210210
NamespaceName: "somens1",
211211
}: {"nsm1", "nsm2"},
212212

213-
core.HistoricalKey{
213+
{
214214
ObjectType: core.MetricSetTypePod,
215215
NamespaceName: "somens1",
216216
PodName: "somepod1",
217217
}: {"pm1", "pm2"},
218218

219-
core.HistoricalKey{
219+
{
220220
ObjectType: core.MetricSetTypePodContainer,
221221
NamespaceName: "somens1",
222222
PodName: "somepod1",
223223
ContainerName: "somecont1",
224224
}: {"pcm1", "pcm2"},
225225

226-
core.HistoricalKey{
226+
{
227227
ObjectType: core.MetricSetTypeSystemContainer,
228228
NodeName: "somenode1",
229229
ContainerName: "somecont1",

metrics/sinks/factory.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"k8s.io/heapster/metrics/sinks/hawkular"
2828
"k8s.io/heapster/metrics/sinks/influxdb"
2929
"k8s.io/heapster/metrics/sinks/kafka"
30+
"k8s.io/heapster/metrics/sinks/librato"
3031
logsink "k8s.io/heapster/metrics/sinks/log"
3132
metricsink "k8s.io/heapster/metrics/sinks/metric"
3233
"k8s.io/heapster/metrics/sinks/opentsdb"
@@ -54,6 +55,8 @@ func (this *SinkFactory) Build(uri flags.Uri) (core.DataSink, error) {
5455
return influxdb.CreateInfluxdbSink(&uri.Val)
5556
case "kafka":
5657
return kafka.NewKafkaSink(&uri.Val)
58+
case "librato":
59+
return librato.CreateLibratoSink(&uri.Val)
5760
case "log":
5861
return logsink.NewLogSink(), nil
5962
case "metric":

0 commit comments

Comments
 (0)