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 }