Ctrl K
ComunidadGuías…

api/metric/apiserver.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package metric

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"math"
	"net"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strconv"
	"sync"
	"time"

	promclient "github.com/goodrain/rainbond/api/client/prometheus"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/prometheus/common/expfmt"
	"github.com/sirupsen/logrus"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

const nativeAPIServerMetricsQuery = `count(apiserver_request_total{rainbond_source!="rbd-api"})`

var apiServerRequestDimensions = []string{"code", "component", "dry_run", "group", "resource", "scope", "subresource", "verb", "version"}

var apiServerRequestsDesc = prometheus.NewDesc("apiserver_request_total", "Kubernetes API server requests.",
	append(append([]string{"apiserver"}, apiServerRequestDimensions...), "rainbond_source", "rainbond_api_instance"), nil)
var apiServerCollectionDesc = prometheus.NewDesc("rbd_api_exporter_apiserver_collection_success",
	"Whether API server request counters are available from an existing source or compatibility collection.", nil, nil)

type apiServerContextMonitor interface {
	GetMetricWithContext(context.Context, string, time.Time) promclient.Metric
}

// APIServerCollector fills missing request counters through the existing rbdapi scrape job.
// It does not change monitoring configuration or require an Operator upgrade.
type APIServerCollector struct {
	mu      sync.Mutex
	fetch   func(context.Context) ([]prometheus.Metric, error)
	now     func() time.Time
	timeout time.Duration
	expires time.Time
	metrics []prometheus.Metric
	success float64
}

// NewAPIServerCollector reuses the Kubernetes identity already used by rbd-api.
func NewAPIServerCollector(kube kubernetes.Interface, config *rest.Config, monitor promclient.Interface) (*APIServerCollector, error) {
	if kube == nil || config == nil || monitor == nil {
		return nil, errors.New("API server metric dependencies unavailable")
	}
	reader, ok := monitor.(apiServerContextMonitor)
	if !ok {
		return nil, errors.New("context-aware monitoring client required")
	}
	host, err := url.Parse(config.Host)
	if err != nil || host.Scheme != "https" || host.Hostname() == "" || host.User != nil || config.Insecure {
		return nil, errors.New("verified Kubernetes HTTPS configuration required")
	}
	copyConfig := rest.CopyConfig(config)
	if copyConfig.ServerName == "" {
		copyConfig.ServerName = host.Hostname()
	}
	transport, err := rest.TransportFor(copyConfig)
	if err != nil {
		return nil, errors.New("Kubernetes metrics transport unavailable")
	}
	instance, err := os.Hostname()
	if err != nil || instance == "" {
		return nil, errors.New("rbd-api replica identity unavailable")
	}
	source := &apiServerMetricSource{
		kube: kube, monitor: reader, maxBytes: 32 << 20, instance: instance,
		client: &http.Client{Transport: transport, Timeout: 3 * time.Second,
			CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }},
	}
	return &APIServerCollector{fetch: source.collect, now: time.Now, timeout: 3 * time.Second}, nil
}

// Describe only declares metrics; registering the collector never calls Kubernetes.
func (c *APIServerCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- apiServerRequestsDesc
	ch <- apiServerCollectionDesc
}

// Collect publishes real upstream counters, or only an unavailable status on failure.
func (c *APIServerCollector) Collect(ch chan<- prometheus.Metric) {
	c.mu.Lock()
	if !c.now().Before(c.expires) {
		ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
		metrics, err := c.fetch(ctx)
		cancel()
		c.metrics, c.success = metrics, 1
		if err != nil {
			c.metrics, c.success = nil, 0
			// Upstream errors and response bodies may contain sensitive connection details.
			logrus.Debug("API server request-counter collection unavailable")
		}
		c.expires = c.now().Add(15 * time.Second)
	}
	metrics, success := c.metrics, c.success
	c.mu.Unlock()
	ch <- prometheus.MustNewConstMetric(apiServerCollectionDesc, prometheus.GaugeValue, success)
	for _, metric := range metrics {
		ch <- metric
	}
}

type apiServerMetricSource struct {
	kube     kubernetes.Interface
	monitor  apiServerContextMonitor
	client   *http.Client
	maxBytes int64
	instance string
}

func (s *apiServerMetricSource) collect(ctx context.Context) ([]prometheus.Metric, error) {
	native := s.monitor.GetMetricWithContext(ctx, nativeAPIServerMetricsQuery, time.Now())
	if native.Error != "" || native.MetricType != promclient.MetricTypeVector || len(native.MetricValues) > 1 {
		return nil, errors.New("cannot determine existing API server metric availability")
	}
	if len(native.MetricValues) == 1 {
		point := native.MetricValues[0].Sample
		if point == nil || math.IsNaN(point.Value()) || math.IsInf(point.Value(), 0) || point.Value() < 0 {
			return nil, errors.New("invalid existing API server metric availability")
		}
		if point.Value() > 0 {
			return nil, nil
		}
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	endpoints, err := s.kube.CoreV1().Endpoints("default").Get(ctx, "kubernetes", metav1.GetOptions{})
	if err != nil {
		return nil, errors.New("API server endpoint discovery unavailable")
	}
	unique := make(map[string]struct{})
	for _, subset := range endpoints.Subsets {
		for _, port := range subset.Ports {
			if port.Name != "https" || (port.Protocol != "" && port.Protocol != "TCP") {
				continue
			}
			if port.Port <= 0 || port.Port > 65535 {
				return nil, errors.New("invalid API server HTTPS port")
			}
			for _, address := range subset.Addresses {
				if net.ParseIP(address.IP) == nil {
					return nil, errors.New("invalid API server endpoint address")
				}
				unique[net.JoinHostPort(address.IP, strconv.Itoa(int(port.Port)))] = struct{}{}
			}
		}
	}
	if len(unique) == 0 || len(unique) > 16 {
		return nil, errors.New("API server endpoint set unavailable")
	}
	targets := make([]string, 0, len(unique))
	for target := range unique {
		targets = append(targets, target)
	}
	sort.Strings(targets)
	type result struct {
		metrics []prometheus.Metric
		err     error
	}
	results := make(chan result, len(targets))
	workers := make(chan struct{}, 4)
	for _, target := range targets {
		go func(target string) {
			select {
			case workers <- struct{}{}:
				defer func() { <-workers }()
			case <-ctx.Done():
				results <- result{err: ctx.Err()}
				return
			}
			metrics, err := s.read(ctx, target)
			results <- result{metrics, err}
		}(target)
	}
	var metrics []prometheus.Metric
	for range targets {
		result := <-results
		if result.err != nil {
			return nil, result.err
		}
		metrics = append(metrics, result.metrics...)
	}
	return metrics, nil
}

func (s *apiServerMetricSource) read(ctx context.Context, target string) ([]prometheus.Metric, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+target+"/metrics", nil)
	if err != nil {
		return nil, errors.New("invalid API server metrics request")
	}
	req.Header.Set("Accept", "text/plain; version=0.0.4")
	response, err := s.client.Do(req)
	if err != nil {
		return nil, errors.New("API server metrics request failed")
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API server metrics returned HTTP %d", response.StatusCode)
	}
	reader := &io.LimitedReader{R: response.Body, N: s.maxBytes + 1}
	scanner := bufio.NewScanner(reader)
	scanner.Buffer(make([]byte, 64<<10), 1<<20)
	var counters bytes.Buffer
	for scanner.Scan() {
		line := scanner.Bytes()
		if bytes.HasPrefix(line, []byte("# TYPE apiserver_request_total ")) ||
			bytes.HasPrefix(line, []byte("apiserver_request_total{")) ||
			bytes.HasPrefix(line, []byte("apiserver_request_total ")) {
			if counters.Len()+len(line) > 4<<20 {
				return nil, errors.New("API server request-counter response too large")
			}
			counters.Write(line)
			counters.WriteByte('\n')
		}
	}
	if scanner.Err() != nil || reader.N == 0 {
		return nil, errors.New("API server metrics response incomplete")
	}
	var parser expfmt.TextParser
	families, err := parser.TextToMetricFamilies(&counters)
	if err != nil {
		return nil, errors.New("invalid API server request counters")
	}
	family := families["apiserver_request_total"]
	if family == nil || family.GetType() != dto.MetricType_COUNTER || len(family.Metric) == 0 {
		return nil, errors.New("API server request counters unavailable")
	}
	totals := make(map[[9]string]float64)
	for _, metric := range family.Metric {
		if metric.Counter == nil {
			return nil, errors.New("invalid API server counter type")
		}
		value := metric.Counter.GetValue()
		if value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
			return nil, errors.New("invalid API server counter value")
		}
		labels := make(map[string]string)
		for _, label := range metric.Label {
			labels[label.GetName()] = label.GetValue()
		}
		if labels["code"] == "" || labels["verb"] == "" {
			return nil, errors.New("API server request dimensions unavailable")
		}
		var key [9]string
		for i, name := range apiServerRequestDimensions {
			key[i] = labels[name]
		}
		totals[key] += value
		if math.IsInf(totals[key], 0) {
			return nil, errors.New("API server request counter overflow")
		}
	}
	metrics := make([]prometheus.Metric, 0, len(totals))
	for key, value := range totals {
		labels := append(append([]string{target}, key[:]...), "rbd-api", s.instance)
		metrics = append(metrics, prometheus.MustNewConstMetric(apiServerRequestsDesc, prometheus.CounterValue, value, labels...))
	}
	return metrics, nil
}