319 lines
7.6 KiB
Go
319 lines
7.6 KiB
Go
|
package kubernetes
|
||
|
|
||
|
// Plugin inspired from
|
||
|
// https://github.com/prometheus/prom2json/blob/master/main.go
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/influxdata/telegraf"
|
||
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||
|
"github.com/influxdata/telegraf/plugins/parsers"
|
||
|
)
|
||
|
|
||
|
type Kubernetes struct {
|
||
|
Apiserver []Apiserver
|
||
|
Scheduler []Scheduler
|
||
|
Controllermanager []Controllermanager
|
||
|
Kubelet []Kubelet
|
||
|
client HTTPClient
|
||
|
}
|
||
|
|
||
|
type KubeService struct {
|
||
|
Url string
|
||
|
Endpoint string
|
||
|
Timeout float64
|
||
|
Excludes []string
|
||
|
Includes []string
|
||
|
}
|
||
|
|
||
|
type Apiserver struct {
|
||
|
KubeService
|
||
|
}
|
||
|
|
||
|
type Scheduler struct {
|
||
|
KubeService
|
||
|
}
|
||
|
|
||
|
type Controllermanager struct {
|
||
|
KubeService
|
||
|
}
|
||
|
|
||
|
type Kubelet struct {
|
||
|
KubeService
|
||
|
}
|
||
|
|
||
|
type HTTPClient interface {
|
||
|
// Returns the result of an http request
|
||
|
//
|
||
|
// Parameters:
|
||
|
// req: HTTP request object
|
||
|
//
|
||
|
// Returns:
|
||
|
// http.Response: HTTP respons object
|
||
|
// error : Any error that may have occurred
|
||
|
MakeRequest(req *http.Request, timeout float64) (*http.Response, error)
|
||
|
}
|
||
|
|
||
|
type RealHTTPClient struct {
|
||
|
client *http.Client
|
||
|
}
|
||
|
|
||
|
func (c RealHTTPClient) MakeRequest(req *http.Request, timeout float64) (*http.Response, error) {
|
||
|
c.client.Timeout = time.Duration(timeout) * time.Second
|
||
|
return c.client.Do(req)
|
||
|
}
|
||
|
|
||
|
var sampleConfig = `
|
||
|
# Get metrics from Kubernetes services
|
||
|
[[inputs.kubernetes.apiserver]]
|
||
|
url = "http://mtl-nvcbladea-15.nuance.com:8080"
|
||
|
endpoint = "/metrics"
|
||
|
timeout = 5.0
|
||
|
# includes only metrics which match one of the
|
||
|
# following regexp
|
||
|
includes = ["apiserver_.*"]
|
||
|
|
||
|
[[inputs.kubernetes.scheduler]]
|
||
|
url = "http://mtl-nvcbladea-15.nuance.com:10251"
|
||
|
endpoint = "/metrics"
|
||
|
timeout = 1.0
|
||
|
# DO NOT include metrics which match one of the
|
||
|
# following regexp
|
||
|
excludes = ["scheduler_.*"]
|
||
|
|
||
|
[[inputs.kubernetes.controllermanager]]
|
||
|
url = "http://mtl-nvcbladea-15.nuance.com:10252"
|
||
|
|
||
|
[[inputs.kubernetes.kubelet]]
|
||
|
url = "http://mtl-nvcbladea-15.nuance.com:4194"
|
||
|
# You should increase metric_buffer_limit
|
||
|
# Because of number of kubelet metrics
|
||
|
# otherwise you can limit metrics with
|
||
|
# the following 'excludes' argument
|
||
|
excludes = ["container_.*"]
|
||
|
|
||
|
`
|
||
|
|
||
|
func (k *Kubernetes) SampleConfig() string {
|
||
|
return sampleConfig
|
||
|
}
|
||
|
|
||
|
func (k *Kubernetes) Description() string {
|
||
|
return "Read metrics from Kubernetes services"
|
||
|
}
|
||
|
|
||
|
// Gathers data for all servers.
|
||
|
func (k *Kubernetes) Gather(acc telegraf.Accumulator) error {
|
||
|
var wg sync.WaitGroup
|
||
|
|
||
|
errorChannel := make(chan error,
|
||
|
len(k.Apiserver)+len(k.Scheduler)+
|
||
|
len(k.Controllermanager)+len(k.Kubelet))
|
||
|
|
||
|
// Routine to gather metrics and add it to acc
|
||
|
GatherMetric := func(url string, endpoint string, serviceType string,
|
||
|
excludes []string, includes []string, timeout float64) {
|
||
|
defer wg.Done()
|
||
|
if timeout == 0. {
|
||
|
timeout = 1.0
|
||
|
}
|
||
|
|
||
|
if endpoint == "" {
|
||
|
endpoint = "/metrics"
|
||
|
} else if string(endpoint[0]) != "/" {
|
||
|
endpoint = "/" + endpoint
|
||
|
}
|
||
|
url = url + endpoint
|
||
|
if err := k.gatherServer(acc, url, serviceType, excludes, includes, timeout); err != nil {
|
||
|
errorChannel <- err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Apiservers
|
||
|
for _, service := range k.Apiserver {
|
||
|
wg.Add(1)
|
||
|
serviceType := "apiserver"
|
||
|
go GatherMetric(service.Url, service.Endpoint, serviceType, service.Excludes, service.Includes, service.Timeout)
|
||
|
}
|
||
|
// Schedulers
|
||
|
for _, service := range k.Scheduler {
|
||
|
wg.Add(1)
|
||
|
serviceType := "scheduler"
|
||
|
go GatherMetric(service.Url, service.Endpoint, serviceType, service.Excludes, service.Includes, service.Timeout)
|
||
|
}
|
||
|
// Controllermanager
|
||
|
for _, service := range k.Controllermanager {
|
||
|
wg.Add(1)
|
||
|
serviceType := "controllermanager"
|
||
|
go GatherMetric(service.Url, service.Endpoint, serviceType, service.Excludes, service.Includes, service.Timeout)
|
||
|
}
|
||
|
// Kubelet
|
||
|
for _, service := range k.Kubelet {
|
||
|
wg.Add(1)
|
||
|
serviceType := "kubelet"
|
||
|
go GatherMetric(service.Url, service.Endpoint, serviceType, service.Excludes, service.Includes, service.Timeout)
|
||
|
}
|
||
|
|
||
|
wg.Wait()
|
||
|
close(errorChannel)
|
||
|
|
||
|
// Get all errors and return them as one giant error
|
||
|
errorStrings := []string{}
|
||
|
for err := range errorChannel {
|
||
|
errorStrings = append(errorStrings, err.Error())
|
||
|
}
|
||
|
|
||
|
if len(errorStrings) == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
return errors.New(strings.Join(errorStrings, "\n"))
|
||
|
}
|
||
|
|
||
|
// Gathers data from a particular server
|
||
|
// Parameters:
|
||
|
// acc : The telegraf Accumulator to use
|
||
|
// serverURL : endpoint to send request to
|
||
|
// serviceType : service type (apiserver, kubelet, ...)
|
||
|
// timeout : http timeout
|
||
|
//
|
||
|
// Returns:
|
||
|
// error: Any error that may have occurred
|
||
|
func (k *Kubernetes) gatherServer(
|
||
|
acc telegraf.Accumulator,
|
||
|
serviceURL string,
|
||
|
serviceType string,
|
||
|
excludes []string,
|
||
|
includes []string,
|
||
|
timeout float64,
|
||
|
) error {
|
||
|
// Get raw data from Kube service
|
||
|
collectDate := time.Now()
|
||
|
raw_data, err := k.sendRequest(serviceURL, timeout)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// Prepare Prometheus parser config
|
||
|
config := parsers.Config{
|
||
|
DataFormat: "prometheus",
|
||
|
}
|
||
|
// Create Prometheus parser
|
||
|
promparser, err := parsers.NewParser(&config)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// Set default tags
|
||
|
tags := map[string]string{
|
||
|
"kubeservice": serviceType,
|
||
|
"serverURL": serviceURL,
|
||
|
}
|
||
|
promparser.SetDefaultTags(tags)
|
||
|
|
||
|
// Parseing
|
||
|
metrics, err := promparser.Parse(raw_data)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// Add (or not) collected metrics
|
||
|
for _, metric := range metrics {
|
||
|
if len(includes) > 0 {
|
||
|
// includes regexp
|
||
|
IncludeMetric:
|
||
|
for _, include := range includes {
|
||
|
r, err := regexp.Compile(include)
|
||
|
if err == nil {
|
||
|
if r.MatchString(metric.Name()) {
|
||
|
acc.AddFields(metric.Name(), metric.Fields(), metric.Tags(), collectDate)
|
||
|
break IncludeMetric
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else if len(excludes) > 0 {
|
||
|
// excludes regexp
|
||
|
includeMetric := true
|
||
|
ExcludeMetric:
|
||
|
for _, exclude := range excludes {
|
||
|
r, err := regexp.Compile(exclude)
|
||
|
if err == nil {
|
||
|
if r.MatchString(metric.Name()) {
|
||
|
includeMetric = false
|
||
|
break ExcludeMetric
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if includeMetric {
|
||
|
acc.AddFields(metric.Name(), metric.Fields(), metric.Tags(), collectDate)
|
||
|
}
|
||
|
} else {
|
||
|
// no includes/excludes regexp
|
||
|
acc.AddFields(metric.Name(), metric.Fields(), metric.Tags(), metric.Time())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Sends an HTTP request to the server using the Kubernetes object's HTTPClient
|
||
|
// Parameters:
|
||
|
// serverURL: endpoint to send request to
|
||
|
// timeout: request timeout
|
||
|
//
|
||
|
// Returns:
|
||
|
// []byte: body of the response
|
||
|
// error : Any error that may have occurred
|
||
|
func (k *Kubernetes) sendRequest(serverURL string, timeout float64) ([]byte, error) {
|
||
|
// Prepare URL
|
||
|
requestURL, err := url.Parse(serverURL)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Invalid server URL \"%s\"", serverURL)
|
||
|
}
|
||
|
params := url.Values{}
|
||
|
requestURL.RawQuery = params.Encode()
|
||
|
|
||
|
// Create request
|
||
|
req, err := http.NewRequest("GET", requestURL.String(), nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Make request
|
||
|
resp, err := k.client.MakeRequest(req, timeout)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
body, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return body, err
|
||
|
}
|
||
|
|
||
|
// Process response
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
err = fmt.Errorf("Response from url \"%s\" has status code %d (%s), expected %d (%s)",
|
||
|
requestURL.String(),
|
||
|
resp.StatusCode,
|
||
|
http.StatusText(resp.StatusCode),
|
||
|
http.StatusOK,
|
||
|
http.StatusText(http.StatusOK))
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return body, err
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
inputs.Add("kubernetes", func() telegraf.Input {
|
||
|
return &Kubernetes{client: RealHTTPClient{client: &http.Client{}}}
|
||
|
})
|
||
|
}
|