Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add scraping for Prometheus endpoint in Kubernetes #3901

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Godeps
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
collectd.org 2ce144541b8903101fb8f1483cc0497a68798122
github.com/PuerkitoBio/purell fd18e053af8a4ff11039269006e8037ff374ce0e
github.com/PuerkitoBio/urlesc de5bf2ad457846296e2031421a34e2568e304e35
github.com/Shopify/sarama 3b1b38866a79f06deddf0487d5c27ba0697ccd65
github.com/Sirupsen/logrus 61e43dc76f7ee59a82bdf3d71033dc12bea4c77d
github.com/aerospike/aerospike-client-go 95e1ad7791bdbca44707fedbb29be42024900d9c
github.com/amir/raidman c74861fe6a7bb8ede0a010ce4485bdbb4fc4c985
github.com/apache/thrift 4aaa92ece8503a6da9bc6701604f69acf2b99d07
Expand All @@ -17,15 +21,22 @@ github.com/eapache/go-resiliency b86b1ec0dd4209a588dc1285cdd471e73525c0b3
github.com/eapache/go-xerial-snappy bb955e01b9346ac19dc29eb16586c90ded99a98c
github.com/eapache/queue 44cc805cf13205b55f69e14bcb69867d1ae92f98
github.com/eclipse/paho.mqtt.golang aff15770515e3c57fc6109da73d42b0d46f7f483
github.com/emicklei/go-restful 2dd44038f0b95ae693b266c5f87593b5d2fdd78d
github.com/go-logfmt/logfmt 390ab7935ee28ec6b286364bba9b4dd6410cb3d5
github.com/go-openapi/jsonpointer 779f45308c19820f1a69e9a4cd965f496e0da10f
github.com/go-openapi/jsonreference 36d33bfe519efae5632669801b180bf1a245da3b
github.com/go-openapi/spec a4fa9574c7aa73b2fc54e251eb9524d0482bb592
github.com/go-openapi/swag cf0bdb963811675a4d7e74901cefc7411a1df939
github.com/go-sql-driver/mysql 2e00b5cd70399450106cec6431c2e2ce3cae5034
github.com/gobwas/glob bea32b9cd2d6f55753d94a28e959b13f0244797a
github.com/go-ini/ini 9144852efba7c4daf409943ee90767da62d55438
github.com/gogo/protobuf 7b6c6391c4ff245962047fc1e2c6e08b1cdfa0e8
github.com/golang/glog 23def4e6c14b4da8ac2ed8007337bc5eb5007998
github.com/golang/protobuf 8ee79997227bf9b34611aee7946ae64735e6fd93
github.com/golang/snappy 7db9049039a047d955fe8c19b83c8ff5abd765c7
github.com/go-ole/go-ole be49f7c07711fcb603cff39e1de7c67926dc0ba7
github.com/google/go-cmp f94e52cad91c65a63acc1e75d4be223ea22e99bc
github.com/google/gofuzz 24818f796faf91cd76ec7bddd72458fbced7a6c1
github.com/gorilla/mux 392c28fe23e1c45ddba891b0320b3b5df220beea
github.com/go-redis/redis 73b70592cdaa9e6abdfcfbf97b4a90d80728c836
github.com/go-sql-driver/mysql 2e00b5cd70399450106cec6431c2e2ce3cae5034
Expand All @@ -39,6 +50,7 @@ github.com/jmespath/go-jmespath bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d
github.com/kardianos/osext c2c54e542fb797ad986b31721e1baedf214ca413
github.com/kardianos/service 6d3a0ee7d3425d9d835debc51a0ca1ffa28f4893
github.com/kballard/go-shellquote d8ec1a69a250a17bb0e419c386eac1f3711dc142
github.com/mailru/easyjson 5f62e4f3afa2f576dc86531b7df4d966b19ef8f8
github.com/matttproud/golang_protobuf_extensions c12348ce28de40eed0136aa2b644d0ee0650e56c
github.com/Microsoft/go-winio ce2922f643c8fd76b46cadc7f404a06282678b34
github.com/miekg/dns 99f84ae56e75126dd77e5de4fae2ea034a468ca1
Expand Down Expand Up @@ -93,3 +105,7 @@ gopkg.in/mgo.v2 3f83fa5005286a7fe593b055f0d7771a7dce4655
gopkg.in/olivere/elastic.v5 3113f9b9ad37509fe5f8a0e5e91c96fdc4435e26
gopkg.in/tomb.v1 dd632973f1e7218eb1089048e0798ec9ae7dceb8
gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6
k8s.io/api 5584376ceeffeb13a2e98b5e9f0e9dab37de4bab
k8s.io/apimachinery 18a564baac720819100827c16fdebcadb05b2d0d
k8s.io/client-go a3c9a30b35fc56f709f4428827b807c90af99fc3
k8s.io/kube-openapi 39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1
16 changes: 16 additions & 0 deletions plugins/inputs/prometheus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ in Prometheus format.
## An array of Kubernetes services to scrape metrics from.
# kubernetes_services = ["http://my-service-dns.my-namespace:9100/metrics"]

# Scrape Kubernetes service for prometheus annotations.
# prometheus.io/scrape: Enable scraping for this service
# prometheus.io/path: If the metrics path is not /metrics, define it with this annotation.
# prometheus.io/port: If port is not 9102 use this annotation
# kubernetes_scraping = true

## Use bearer token for authorization
# bearer_token = /path/to/bearer/token

Expand All @@ -37,6 +43,16 @@ by looking up all A records assigned to the hostname as described in
This method can be used to locate all
[Kubernetes headless services](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services).

#### Kubernetes scraping

Enabling this option will allow the plugin to scrape for prometheus annotation on Kubernetes
pods.
Currently the following annotation are supported:

* `prometheus.io/scrape` Enable scraping for this pod
* `prometheus.io/path` Override the path for the metrics endpoint on the service. (default metrics).
* `prometheus.io/port` Used to override the port, the default value is 9102

#### Bearer Token

If set, the file specified by the `bearer_token` parameter will be read on
Expand Down
116 changes: 116 additions & 0 deletions plugins/inputs/prometheus/kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package prometheus

import (
"fmt"
"log"
"strings"
"time"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"

"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
)

func start(p *Prometheus) error {
config, err := rest.InClusterConfig()
if err != nil {
return err
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return err
}
watchlist := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "pods", v1.NamespaceAll, fields.Everything())
_, controller := cache.NewInformer(
watchlist,
&v1.Pod{},
time.Second*0,
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pod := obj.(*v1.Pod)
registerPod(pod, p)
},
DeleteFunc: func(obj interface{}) {
pod := obj.(*v1.Pod)
unregisterPod(pod, p)
},
UpdateFunc: func(oldObj, newObj interface{}) {
podPod := oldObj.(*v1.Pod)
newPod := newObj.(*v1.Pod)
unregisterPod(podPod, p)
registerPod(newPod, p)
},
},
)

go controller.Run(wait.NeverStop)
return nil
}

func registerPod(pod *v1.Pod, p *Prometheus) {
url := scrapeURL(pod)
if url != nil {
log.Printf("Will scrape metrics from %v\n", *url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these log at debug level: "D! [inputs.prometheus] adding %q to scrape targets"

p.lock.Lock()
// add annotation as metrics tags
tags := pod.GetAnnotations()
tags["pod_name"] = pod.Name
tags["namespace"] = pod.Namespace
// add labels as metrics tags
for k, v := range pod.GetLabels() {
tags[k] = v
}
p.KubernetesPods = append(p.KubernetesPods, Target{url: *url, tags: tags})
p.lock.Unlock()
}
}

func scrapeURL(pod *v1.Pod) *string {
scrape := pod.ObjectMeta.Annotations["prometheus.io/scrape"]
if pod.Status.PodIP == "" {
// return as if scrape was disabled, we will be notified again once the pod
// has an IP
return nil
}
if scrape == "true" {
path := pod.ObjectMeta.Annotations["prometheus.io/path"]
port := pod.ObjectMeta.Annotations["prometheus.io/port"]
if port == "" {
port = "9102" // default
}
if path == "" {
path = "/metrics"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}

ip := pod.Status.PodIP
x := fmt.Sprintf("http://%v:%v%v", ip, port, path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using url.URL to create the url, which would avoid the need to fixup the path above and potentially handle other cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always using http seems like a potential issue to me. What if the service is using https?

return &x
}
return nil
}

func unregisterPod(pod *v1.Pod, p *Prometheus) {
url := scrapeURL(pod)
if url != nil {
p.lock.Lock()
defer p.lock.Unlock()
log.Printf("Registred a delete request for %v in namespace '%v'\n", pod.Name, pod.Namespace)
var result []Target
for _, v := range p.KubernetesPods {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use a map[string]bool for the targets

if v.url != *url {
result = append(result, v)
} else {
log.Printf("Will stop scraping for %v\n", *url)
}

}
p.KubernetesPods = result
}
}
83 changes: 83 additions & 0 deletions plugins/inputs/prometheus/kubernetes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package prometheus

import (
"sync"
"testing"

"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestScrapeURLNoAnnotations(t *testing.T) {
p := &v1.Pod{}
p.Annotations = map[string]string{}
url := scrapeURL(p)
assert.Nil(t, url)
}
func TestScrapeURLAnnotationsNoScrape(t *testing.T) {
p := &v1.Pod{}
p.Name = "myPod"
p.Annotations = map[string]string{"prometheus.io/scrape": "false"}
url := scrapeURL(p)
assert.Nil(t, url)
}
func TestScrapeURLAnnotations(t *testing.T) {
p := pod()
p.Annotations = map[string]string{"prometheus.io/scrape": "true"}
url := scrapeURL(p)
assert.Equal(t, "http://127.0.0.1:9102/metrics", *url)
}
func TestScrapeURLAnnotationsCustomPort(t *testing.T) {
p := pod()
p.Annotations = map[string]string{"prometheus.io/scrape": "true", "prometheus.io/port": "9000"}
url := scrapeURL(p)
assert.Equal(t, "http://127.0.0.1:9000/metrics", *url)
}
func TestScrapeURLAnnotationsCustomPath(t *testing.T) {
p := pod()
p.Annotations = map[string]string{"prometheus.io/scrape": "true", "prometheus.io/path": "mymetrics"}
url := scrapeURL(p)
assert.Equal(t, "http://127.0.0.1:9102/mymetrics", *url)
}

func TestScrapeURLAnnotationsCustomPathWithSep(t *testing.T) {
p := pod()
p.Annotations = map[string]string{"prometheus.io/scrape": "true", "prometheus.io/path": "/mymetrics"}
url := scrapeURL(p)
assert.Equal(t, "http://127.0.0.1:9102/mymetrics", *url)
}

func TestAddPod(t *testing.T) {
prom := &Prometheus{lock: &sync.Mutex{}}
p := pod()
p.Annotations = map[string]string{"prometheus.io/scrape": "true"}
registerPod(p, prom)
assert.Equal(t, 1, len(prom.KubernetesPods))
}
func TestAddMultiplePods(t *testing.T) {
prom := &Prometheus{lock: &sync.Mutex{}}

p := pod()
p.Annotations = map[string]string{"prometheus.io/scrape": "true"}
registerPod(p, prom)
p.Name = "Pod2"
registerPod(p, prom)
assert.Equal(t, 2, len(prom.KubernetesPods))
}
func TestDeletePods(t *testing.T) {
prom := &Prometheus{lock: &sync.Mutex{}}

p := pod()
p.Annotations = map[string]string{"prometheus.io/scrape": "true"}
registerPod(p, prom)
unregisterPod(p, prom)
assert.Equal(t, 0, len(prom.KubernetesPods))
}

func pod() *v1.Pod {
p := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}}
p.Status.PodIP = "127.0.0.1"
p.Name = "myPod"
return p
}
54 changes: 44 additions & 10 deletions plugins/inputs/prometheus/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,22 @@ import (

const acceptHeader = `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.7,text/plain;version=0.0.4;q=0.3`

type Target struct {
url string
tags map[string]string
}
type Prometheus struct {
// An array of urls to scrape metrics from.
URLs []string `toml:"urls"`

// An array of Kubernetes services to scrape metrics from.
KubernetesServices []string

// Should we scrape Kubernetes services for prometheus annotations
KubernetesScraping bool `toml:"kubernetes_scraping"`
lock *sync.Mutex
KubernetesPods []Target
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be set in the config file? If not make it unexported, move to bottom of struct too please.


// Bearer Token authorization file path
BearerToken string `toml:"bearer_token"`

Expand All @@ -48,6 +57,12 @@ var sampleConfig = `

## An array of Kubernetes services to scrape metrics from.
# kubernetes_services = ["http://my-service-dns.my-namespace:9100/metrics"]

# Scrape Kubernetes pods for prometheus annotations.
# prometheus.io/scrape: Enable scraping for this pod
# prometheus.io/path: If the metrics path is not /metrics, define it with this annotation.
# prometheus.io/port: If port is not 9102 use this annotation
# kubernetes_scraping = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kubernetes_scraping seems too ambiguous, maybe: monitor_kubernetes_pods?


## Use bearer token for authorization
# bearer_token = /path/to/bearer/token
Expand Down Expand Up @@ -96,6 +111,7 @@ type URLAndAddress struct {
OriginalURL *url.URL
URL *url.URL
Address string
Tags map[string]string
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a good idea to rename this struct... perhaps we merge this with the Target struct?


func (p *Prometheus) GetAllURLs() ([]URLAndAddress, error) {
Expand All @@ -109,11 +125,25 @@ func (p *Prometheus) GetAllURLs() ([]URLAndAddress, error) {

allURLs = append(allURLs, URLAndAddress{URL: URL, OriginalURL: URL})
}
p.lock.Lock()
defer p.lock.Unlock()
// loop through all pods scraped via the prometheus annotation on the pods
for _, pod := range p.KubernetesPods {
URL, err := url.Parse(pod.url)
if err != nil {
log.Printf("prometheus: Could not parse url %s, skipping it. Error: %s", pod.url, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this is based on some existing log messages, but could you do me a favor and update them to look more like:

"E! [inputs.prometheus] could not parse URL %q: %v"`

continue
}
podURL := p.AddressToURL(URL, URL.Hostname())
allURLs = append(allURLs, URLAndAddress{URL: podURL, Address: URL.Hostname(), OriginalURL: URL, Tags: pod.tags})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we merge URLAndAddress with Target then we can do this logic in kubernetes.go and all we need to do here is add the items to the lists.

}

for _, service := range p.KubernetesServices {
URL, err := url.Parse(service)
if err != nil {
return nil, err
}

resolvedAddresses, err := net.LookupHost(URL.Hostname())
if err != nil {
log.Printf("prometheus: Could not resolve %s, skipping it. Error: %s", URL.Host, err)
Expand Down Expand Up @@ -157,15 +187,6 @@ func (p *Prometheus) Gather(acc telegraf.Accumulator) error {
return nil
}

var tr = &http.Transport{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

ResponseHeaderTimeout: time.Duration(3 * time.Second),
}

var client = &http.Client{
Transport: tr,
Timeout: time.Duration(4 * time.Second),
}

func (p *Prometheus) createHttpClient() (*http.Client, error) {
tlsCfg, err := internal.GetTLSConfig(
p.SSLCert, p.SSLKey, p.SSLCA, p.InsecureSkipVerify)
Expand Down Expand Up @@ -226,6 +247,9 @@ func (p *Prometheus) gatherURL(u URLAndAddress, acc telegraf.Accumulator) error
if u.Address != "" {
tags["address"] = u.Address
}
for k, v := range u.Tags {
tags[k] = v
}

switch metric.Type() {
case telegraf.Counter:
Expand All @@ -244,8 +268,18 @@ func (p *Prometheus) gatherURL(u URLAndAddress, acc telegraf.Accumulator) error
return nil
}

// Start will start the Kubernetes scraping if enabled in the configuration
func (p *Prometheus) Start(a telegraf.Accumulator) error {
if p.KubernetesScraping {
return start(p)
}
return nil
}

func (p *Prometheus) Stop() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to stop the kubernetes pod monitor. Easiest way to test this is by sending a SIGHUP, the config could be completely different after reload.


func init() {
inputs.Add("prometheus", func() telegraf.Input {
return &Prometheus{ResponseTimeout: internal.Duration{Duration: time.Second * 3}}
return &Prometheus{ResponseTimeout: internal.Duration{Duration: time.Second * 3}, lock: &sync.Mutex{}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: can you wrap this line?

})
}
Loading