224 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			224 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
| package prometheus
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"log"
 | |
| 	"net"
 | |
| 	"net/url"
 | |
| 	"os/user"
 | |
| 	"path/filepath"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/ericchiang/k8s"
 | |
| 	corev1 "github.com/ericchiang/k8s/apis/core/v1"
 | |
| 	"github.com/ghodss/yaml"
 | |
| )
 | |
| 
 | |
| type payload struct {
 | |
| 	eventype string
 | |
| 	pod      *corev1.Pod
 | |
| }
 | |
| 
 | |
| // loadClient parses a kubeconfig from a file and returns a Kubernetes
 | |
| // client. It does not support extensions or client auth providers.
 | |
| func loadClient(kubeconfigPath string) (*k8s.Client, error) {
 | |
| 	data, err := ioutil.ReadFile(kubeconfigPath)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed reading '%s': %v", kubeconfigPath, err)
 | |
| 	}
 | |
| 
 | |
| 	// Unmarshal YAML into a Kubernetes config object.
 | |
| 	var config k8s.Config
 | |
| 	if err := yaml.Unmarshal(data, &config); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return k8s.NewClient(&config)
 | |
| }
 | |
| 
 | |
| func (p *Prometheus) start(ctx context.Context) error {
 | |
| 	client, err := k8s.NewInClusterClient()
 | |
| 	if err != nil {
 | |
| 		u, err := user.Current()
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("Failed to get current user - %v", err)
 | |
| 		}
 | |
| 
 | |
| 		configLocation := filepath.Join(u.HomeDir, ".kube/config")
 | |
| 		if p.KubeConfig != "" {
 | |
| 			configLocation = p.KubeConfig
 | |
| 		}
 | |
| 		client, err = loadClient(configLocation)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	p.wg = sync.WaitGroup{}
 | |
| 
 | |
| 	p.wg.Add(1)
 | |
| 	go func() {
 | |
| 		defer p.wg.Done()
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-ctx.Done():
 | |
| 				return
 | |
| 			case <-time.After(time.Second):
 | |
| 				err := p.watch(ctx, client)
 | |
| 				if err != nil {
 | |
| 					log.Printf("E! [inputs.prometheus] unable to watch resources: %v", err)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // An edge case exists if a pod goes offline at the same time a new pod is created
 | |
| // (without the scrape annotations). K8s may re-assign the old pod ip to the non-scrape
 | |
| // pod, causing errors in the logs. This is only true if the pod going offline is not
 | |
| // directed to do so by K8s.
 | |
| func (p *Prometheus) watch(ctx context.Context, client *k8s.Client) error {
 | |
| 	pod := &corev1.Pod{}
 | |
| 	watcher, err := client.Watch(ctx, p.PodNamespace, &corev1.Pod{})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer watcher.Close()
 | |
| 
 | |
| 	for {
 | |
| 		select {
 | |
| 		case <-ctx.Done():
 | |
| 			return nil
 | |
| 		default:
 | |
| 			pod = &corev1.Pod{}
 | |
| 			// An error here means we need to reconnect the watcher.
 | |
| 			eventType, err := watcher.Next(pod)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			// If the pod is not "ready", there will be no ip associated with it.
 | |
| 			if pod.GetMetadata().GetAnnotations()["prometheus.io/scrape"] != "true" ||
 | |
| 				!podReady(pod.Status.GetContainerStatuses()) {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			switch eventType {
 | |
| 			case k8s.EventAdded:
 | |
| 				registerPod(pod, p)
 | |
| 			case k8s.EventModified:
 | |
| 				// To avoid multiple actions for each event, unregister on the first event
 | |
| 				// in the delete sequence, when the containers are still "ready".
 | |
| 				if pod.Metadata.GetDeletionTimestamp() != nil {
 | |
| 					unregisterPod(pod, p)
 | |
| 				} else {
 | |
| 					registerPod(pod, p)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func podReady(statuss []*corev1.ContainerStatus) bool {
 | |
| 	if len(statuss) == 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 	for _, cs := range statuss {
 | |
| 		if !cs.GetReady() {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| func registerPod(pod *corev1.Pod, p *Prometheus) {
 | |
| 	if p.kubernetesPods == nil {
 | |
| 		p.kubernetesPods = map[string]URLAndAddress{}
 | |
| 	}
 | |
| 	targetURL := getScrapeURL(pod)
 | |
| 	if targetURL == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("D! [inputs.prometheus] will scrape metrics from %s", *targetURL)
 | |
| 	// add annotation as metrics tags
 | |
| 	tags := pod.GetMetadata().GetAnnotations()
 | |
| 	if tags == nil {
 | |
| 		tags = map[string]string{}
 | |
| 	}
 | |
| 	tags["pod_name"] = pod.GetMetadata().GetName()
 | |
| 	tags["namespace"] = pod.GetMetadata().GetNamespace()
 | |
| 	// add labels as metrics tags
 | |
| 	for k, v := range pod.GetMetadata().GetLabels() {
 | |
| 		tags[k] = v
 | |
| 	}
 | |
| 	URL, err := url.Parse(*targetURL)
 | |
| 	if err != nil {
 | |
| 		log.Printf("E! [inputs.prometheus] could not parse URL %s: %v", *targetURL, err)
 | |
| 		return
 | |
| 	}
 | |
| 	podURL := p.AddressToURL(URL, URL.Hostname())
 | |
| 	p.lock.Lock()
 | |
| 	p.kubernetesPods[podURL.String()] = URLAndAddress{
 | |
| 		URL:         podURL,
 | |
| 		Address:     URL.Hostname(),
 | |
| 		OriginalURL: URL,
 | |
| 		Tags:        tags,
 | |
| 	}
 | |
| 	p.lock.Unlock()
 | |
| }
 | |
| 
 | |
| func getScrapeURL(pod *corev1.Pod) *string {
 | |
| 	ip := pod.Status.GetPodIP()
 | |
| 	if ip == "" {
 | |
| 		// return as if scrape was disabled, we will be notified again once the pod
 | |
| 		// has an IP
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	scheme := pod.GetMetadata().GetAnnotations()["prometheus.io/scheme"]
 | |
| 	path := pod.GetMetadata().GetAnnotations()["prometheus.io/path"]
 | |
| 	port := pod.GetMetadata().GetAnnotations()["prometheus.io/port"]
 | |
| 
 | |
| 	if scheme == "" {
 | |
| 		scheme = "http"
 | |
| 	}
 | |
| 	if port == "" {
 | |
| 		port = "9102"
 | |
| 	}
 | |
| 	if path == "" {
 | |
| 		path = "/metrics"
 | |
| 	}
 | |
| 
 | |
| 	u := &url.URL{
 | |
| 		Scheme: scheme,
 | |
| 		Host:   net.JoinHostPort(ip, port),
 | |
| 		Path:   path,
 | |
| 	}
 | |
| 
 | |
| 	x := u.String()
 | |
| 
 | |
| 	return &x
 | |
| }
 | |
| 
 | |
| func unregisterPod(pod *corev1.Pod, p *Prometheus) {
 | |
| 	url := getScrapeURL(pod)
 | |
| 	if url == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("D! [inputs.prometheus] registered a delete request for %s in namespace %s",
 | |
| 		pod.GetMetadata().GetName(), pod.GetMetadata().GetNamespace())
 | |
| 
 | |
| 	p.lock.Lock()
 | |
| 	defer p.lock.Unlock()
 | |
| 	if _, ok := p.kubernetesPods[*url]; ok {
 | |
| 		delete(p.kubernetesPods, *url)
 | |
| 		log.Printf("D! [inputs.prometheus] will stop scraping for %s", *url)
 | |
| 	}
 | |
| }
 |