github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/exporter/collector.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"sort"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/prometheus/client_golang/prometheus"
    27  	"github.com/sirupsen/logrus"
    28  
    29  	"k8s.io/apimachinery/pkg/labels"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  
    32  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    33  	"sigs.k8s.io/prow/pkg/kube"
    34  )
    35  
    36  type lister interface {
    37  	List(selector labels.Selector) ([]*prowapi.ProwJob, error)
    38  }
    39  
    40  // https://godoc.org/github.com/prometheus/client_golang/prometheus#Collector
    41  type prowJobCollector struct {
    42  	lister lister
    43  }
    44  
    45  func (pjc prowJobCollector) Describe(ch chan<- *prometheus.Desc) {
    46  	//prometheus.DescribeByCollect(pjc, ch)
    47  	// Normally, we'd send descriptors into the channel. However, we cannot do so for these
    48  	// metrics as their label sets are dynamic. This is a take-our-own-risk action and also a
    49  	// compromise for implementing a metric with both dynamic keys and dynamic values in
    50  	// the label set.
    51  	// https://godoc.org/github.com/prometheus/client_golang/prometheus#hdr-Custom_Collectors_and_constant_Metrics
    52  }
    53  
    54  func (pjc prowJobCollector) Collect(ch chan<- prometheus.Metric) {
    55  	logrus.Debug("ProwJobCollector collecting ...")
    56  	prowJobs, err := pjc.lister.List(labels.Everything())
    57  	if err != nil {
    58  		logrus.WithError(err).Error("Failed to list prow jobs")
    59  		return
    60  	}
    61  	//We need to filter out the latest jobs
    62  	//because sending the same sample twice would lead to prometheus runtime error
    63  	for _, pj := range getLatest(prowJobs) {
    64  		agent := string(pj.Spec.Agent)
    65  		pjLabelKeys, pjLabelValues := kubeLabelsToPrometheusLabels(filterWithDenylist(pj.Labels), "label_")
    66  		pjLabelKeys = append([]string{"job_name", "job_namespace", "job_agent"}, pjLabelKeys...)
    67  		pjLabelValues = append([]string{pj.Spec.Job, pj.Namespace, agent}, pjLabelValues...)
    68  		labelDesc := prometheus.NewDesc(
    69  			"prow_job_labels",
    70  			"Kubernetes labels converted to Prometheus labels.",
    71  			pjLabelKeys, nil,
    72  		)
    73  		ch <- prometheus.MustNewConstMetric(
    74  			labelDesc,
    75  			prometheus.GaugeValue,
    76  			// See README.md for details
    77  			float64(1),
    78  			pjLabelValues...,
    79  		)
    80  		pjAnnotationKeys, pjAnnotationValues := kubeLabelsToPrometheusLabels(pj.Annotations, "annotation_")
    81  		pjAnnotationKeys = append([]string{"job_name", "job_namespace", "job_agent"}, pjAnnotationKeys...)
    82  		pjAnnotationValues = append([]string{pj.Spec.Job, pj.Namespace, agent}, pjAnnotationValues...)
    83  		annotationDesc := prometheus.NewDesc(
    84  			"prow_job_annotations",
    85  			"Kubernetes annotations converted to Prometheus labels.",
    86  			pjAnnotationKeys, nil,
    87  		)
    88  		ch <- prometheus.MustNewConstMetric(
    89  			annotationDesc,
    90  			prometheus.GaugeValue,
    91  			float64(1),
    92  			pjAnnotationValues...,
    93  		)
    94  	}
    95  }
    96  
    97  func getLatest(jobs []*prowapi.ProwJob) map[string]*prowapi.ProwJob {
    98  	latest := map[string]time.Time{}
    99  	latestJobs := map[string]*prowapi.ProwJob{}
   100  	for _, job := range jobs {
   101  		if _, ok := latest[job.Spec.Job]; !ok {
   102  			latest[job.Spec.Job] = job.Status.StartTime.Time
   103  			latestJobs[job.Spec.Job] = job
   104  			continue
   105  		}
   106  		if job.Status.StartTime.Time.After(latest[job.Spec.Job]) {
   107  			latest[job.Spec.Job] = job.Status.StartTime.Time
   108  			latestJobs[job.Spec.Job] = job
   109  		}
   110  	}
   111  	return latestJobs
   112  }
   113  
   114  var (
   115  	labelKeyDenylist = sets.New[string](
   116  		kube.CreatedByProw,
   117  		kube.ProwJobTypeLabel,
   118  		kube.ProwJobIDLabel,
   119  		kube.ProwBuildIDLabel,
   120  		kube.ProwJobAnnotation,
   121  		kube.OrgLabel,
   122  		kube.RepoLabel,
   123  		kube.PullLabel,
   124  	)
   125  )
   126  
   127  func filterWithDenylist(labels map[string]string) map[string]string {
   128  	if labels == nil {
   129  		return nil
   130  	}
   131  	result := map[string]string{}
   132  	for k, v := range labels {
   133  		if !labelKeyDenylist.Has(k) {
   134  			result[k] = v
   135  		}
   136  	}
   137  	return result
   138  }
   139  
   140  var (
   141  	invalidLabelCharRE    = regexp.MustCompile(`[^a-zA-Z0-9_]`)
   142  	escapeWithDoubleQuote = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`)
   143  )
   144  
   145  // aligned with kube-state-metrics
   146  // https://github.com/kubernetes/kube-state-metrics/blob/1d69c1e637564aec4591b5b03522fa8b5fca6597/internal/store/utils.go#L60
   147  // kubeLabelsToPrometheusLabels ensures that the labels including key and value are accepted by prometheus
   148  // We keep the function name (sanitizeLabelName and escapeString as well) the same as the one from kube-state-metrics for easy comparison
   149  func kubeLabelsToPrometheusLabels(labels map[string]string, prefix string) ([]string, []string) {
   150  	labelKeys := make([]string, 0, len(labels))
   151  	for k := range labels {
   152  		labelKeys = append(labelKeys, k)
   153  	}
   154  	sort.Strings(labelKeys)
   155  
   156  	labelValues := make([]string, 0, len(labels))
   157  	for i, k := range labelKeys {
   158  		labelKeys[i] = fmt.Sprintf("%s%s", prefix, sanitizeLabelName(k))
   159  		labelValues = append(labelValues, escapeString(labels[k]))
   160  	}
   161  	return labelKeys, labelValues
   162  }
   163  
   164  func sanitizeLabelName(s string) string {
   165  	return invalidLabelCharRE.ReplaceAllString(s, "_")
   166  }
   167  
   168  // https://github.com/kubernetes/kube-state-metrics/blob/1d69c1e637564aec4591b5b03522fa8b5fca6597/pkg/metric/metric.go#L96
   169  func escapeString(v string) string {
   170  	return escapeWithDoubleQuote.Replace(v)
   171  }