// Copyright 2017, OpenCensus Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package prometheus contains a Prometheus exporter that supports exporting // OpenCensus views as Prometheus metrics. package prometheus // import "contrib.go.opencensus.io/exporter/prometheus" import ( "context" "fmt" "log" "net/http" "sync" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "go.opencensus.io/metric/metricdata" "go.opencensus.io/metric/metricexport" "go.opencensus.io/stats/view" ) // Exporter exports stats to Prometheus, users need // to register the exporter as an http.Handler to be // able to export. type Exporter struct { opts Options g prometheus.Gatherer c *collector handler http.Handler } // Options contains options for configuring the exporter. type Options struct { Namespace string Registry *prometheus.Registry Registerer prometheus.Registerer Gatherer prometheus.Gatherer OnError func(err error) ConstLabels prometheus.Labels // ConstLabels will be set as labels on all views. } // NewExporter returns an exporter that exports stats to Prometheus. func NewExporter(o Options) (*Exporter, error) { if o.Registry == nil { o.Registry = prometheus.NewRegistry() } if o.Registerer == nil { o.Registerer = o.Registry } if o.Gatherer == nil { o.Gatherer = o.Registry } collector := newCollector(o, o.Registerer) e := &Exporter{ opts: o, g: o.Gatherer, c: collector, handler: promhttp.HandlerFor(o.Gatherer, promhttp.HandlerOpts{}), } collector.ensureRegisteredOnce() return e, nil } var _ http.Handler = (*Exporter)(nil) // ensureRegisteredOnce invokes reg.Register on the collector itself // exactly once to ensure that we don't get errors such as // cannot register the collector: descriptor Desc{fqName: *} // already exists with the same fully-qualified name and const label values // which is documented by Prometheus at // https://github.com/prometheus/client_golang/blob/fcc130e101e76c5d303513d0e28f4b6d732845c7/prometheus/registry.go#L89-L101 func (c *collector) ensureRegisteredOnce() { c.registerOnce.Do(func() { if err := c.reg.Register(c); err != nil { c.opts.onError(fmt.Errorf("cannot register the collector: %v", err)) } }) } func (o *Options) onError(err error) { if o.OnError != nil { o.OnError(err) } else { log.Printf("Failed to export to Prometheus: %v", err) } } // ExportView exports to the Prometheus if view data has one or more rows. // Each OpenCensus AggregationData will be converted to // corresponding Prometheus Metric: SumData will be converted // to Untyped Metric, CountData will be a Counter Metric, // DistributionData will be a Histogram Metric. // // Deprecated: in lieu of metricexport.Reader interface. func (e *Exporter) ExportView(vd *view.Data) { } // ServeHTTP serves the Prometheus endpoint. func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) { e.handler.ServeHTTP(w, r) } // collector implements prometheus.Collector type collector struct { opts Options registerOnce sync.Once // reg helps collector register views dynamically. reg prometheus.Registerer // reader reads metrics from all registered producers. reader *metricexport.Reader } func (c *collector) Describe(ch chan<- *prometheus.Desc) { de := &descExporter{c: c, descCh: ch} c.reader.ReadAndExport(de) } // Collect fetches the statistics from OpenCensus // and delivers them as Prometheus Metrics. // Collect is invoked every time a prometheus.Gatherer is run // for example when the HTTP endpoint is invoked by Prometheus. func (c *collector) Collect(ch chan<- prometheus.Metric) { me := &metricExporter{c: c, metricCh: ch} c.reader.ReadAndExport(me) } func newCollector(opts Options, registrar prometheus.Registerer) *collector { return &collector{ reg: registrar, opts: opts, reader: metricexport.NewReader()} } func (c *collector) toDesc(metric *metricdata.Metric) *prometheus.Desc { var labels prometheus.Labels switch { case metric.Resource == nil: labels = c.opts.ConstLabels case c.opts.ConstLabels == nil: labels = metric.Resource.Labels default: labels = prometheus.Labels{} for k, v := range c.opts.ConstLabels { labels[k] = v } // Resource labels overwrite const labels. for k, v := range metric.Resource.Labels { labels[k] = v } } return prometheus.NewDesc( metricName(c.opts.Namespace, metric), metric.Descriptor.Description, toPromLabels(metric.Descriptor.LabelKeys), labels) } type metricExporter struct { c *collector metricCh chan<- prometheus.Metric } // ExportMetrics exports to the Prometheus. // Each OpenCensus Metric will be converted to // corresponding Prometheus Metric: // TypeCumulativeInt64 and TypeCumulativeFloat64 will be a Counter Metric, // TypeCumulativeDistribution will be a Histogram Metric. // TypeGaugeFloat64 and TypeGaugeInt64 will be a Gauge Metric func (me *metricExporter) ExportMetrics(ctx context.Context, metrics []*metricdata.Metric) error { for _, metric := range metrics { desc := me.c.toDesc(metric) for _, ts := range metric.TimeSeries { tvs := toLabelValues(ts.LabelValues) for _, point := range ts.Points { metric, err := toPromMetric(desc, metric, point, tvs) if err != nil { me.c.opts.onError(err) } else if metric != nil { me.metricCh <- metric } } } } return nil } type descExporter struct { c *collector descCh chan<- *prometheus.Desc } // ExportMetrics exports descriptor to the Prometheus. // It is invoked when request to scrape descriptors is received. func (me *descExporter) ExportMetrics(ctx context.Context, metrics []*metricdata.Metric) error { for _, metric := range metrics { desc := me.c.toDesc(metric) me.descCh <- desc } return nil } func toPromLabels(mls []metricdata.LabelKey) (labels []string) { for _, ml := range mls { labels = append(labels, sanitize(ml.Key)) } return labels } func metricName(namespace string, m *metricdata.Metric) string { var name string if namespace != "" { name = namespace + "_" } return name + sanitize(m.Descriptor.Name) } func toPromMetric( desc *prometheus.Desc, metric *metricdata.Metric, point metricdata.Point, labelValues []string) (prometheus.Metric, error) { switch metric.Descriptor.Type { case metricdata.TypeCumulativeFloat64, metricdata.TypeCumulativeInt64: pv, err := toPromValue(point) if err != nil { return nil, err } return prometheus.NewConstMetric(desc, prometheus.CounterValue, pv, labelValues...) case metricdata.TypeGaugeFloat64, metricdata.TypeGaugeInt64: pv, err := toPromValue(point) if err != nil { return nil, err } return prometheus.NewConstMetric(desc, prometheus.GaugeValue, pv, labelValues...) case metricdata.TypeCumulativeDistribution: switch v := point.Value.(type) { case *metricdata.Distribution: points := make(map[float64]uint64) // Histograms are cumulative in Prometheus. // Get cumulative bucket counts. cumCount := uint64(0) for i, b := range v.BucketOptions.Bounds { cumCount += uint64(v.Buckets[i].Count) points[b] = cumCount } return prometheus.NewConstHistogram(desc, uint64(v.Count), v.Sum, points, labelValues...) default: return nil, typeMismatchError(point) } case metricdata.TypeSummary: // TODO: [rghetia] add support for TypeSummary. return nil, nil default: return nil, fmt.Errorf("aggregation %T is not yet supported", metric.Descriptor.Type) } } func toLabelValues(labelValues []metricdata.LabelValue) (values []string) { for _, lv := range labelValues { if lv.Present { values = append(values, lv.Value) } else { values = append(values, "") } } return values } func typeMismatchError(point metricdata.Point) error { return fmt.Errorf("point type %T does not match metric type", point) } func toPromValue(point metricdata.Point) (float64, error) { switch v := point.Value.(type) { case float64: return v, nil case int64: return float64(v), nil default: return 0.0, typeMismatchError(point) } }