/**
 * Copyright 2018, 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.
 */ import { AggregationType , DistributionData , ExporterConfig , logger , Logger , Measurement , StatsEventListener , TagKey , TagValue , View , } from '@opencensus/core' ; import * as express from 'express' ; import * as http from 'http' ; import { Counter , Gauge , Histogram , labelValues , Metric , Registry , } from 'prom-client' ; export interface PrometheusExporterOptions extends ExporterConfig { /** App prefix for metrics, if needed - default opencensus */ prefix ? : string ; /**
     * Port number for Prometheus exporter server
     * Default registered port is 9464:
     * https://github.com/prometheus/prometheus/wiki/Default-port-allocations
     */ port ? : number ; /**
     * Define if the Prometheus exporter server will be started - default false
     */ startServer ? : boolean ; } /** Format and sends Stats to Prometheus exporter */ export class PrometheusStatsExporter implements StatsEventListener { static readonly DEFAULT_OPTIONS = { port : 9464 , startServer : false , contentType : 'text/plain; text/plain; version=0.0.15; charset=utf-8' , prefix : '' , } ; private logger : Logger ; private prefix : string ; private port : number ; private app = express ( ) ; private server ? : http . Server ; private registry = new Registry ( ) ; private static readonly RESERVED_HISTOGRAM_LABEL = 'le' ; constructor ( options ) { this . logger = options . logger || logger . logger ( ) ; this . port = options . port || PrometheusStatsExporter . DEFAULT_OPTIONS . port ; this . prefix = options . prefix || PrometheusStatsExporter . DEFAULT_OPTIONS . prefix ; /** Start the server if the startServer option is true */ if ( options . startServer ) { this . startServer ( ) ; } } param view onRegisterView ( view ) { } param views param measurement param tags onRecord ( views , measurement , tags ) { for ( const view of views ) { this . updateMetric ( view , measurement , tags ) ; } } /**
     * Starts the Prometheus exporter that polls Metric from Metrics library and
     * send batched data to backend.
     */ start ( ) { } /** Stops the exporter. */ stop ( ) { this . stopServer ( ) ; } private getLabelValues ( columns , tags ) { const labels = { } ; const tagsWithStringKey = new Map ( ) ; for ( const key of tags . keys ( ) ) { tagsWithStringKey . set ( key . name , tags . get ( key ) ) ; } columns . forEach ( tagKey => { if ( tagsWithStringKey . has ( tagKey . name ) ) { const tagValue = tagsWithStringKey . get ( tagKey . name ) ; if ( tagValue ) { labels [ tagKey . name ] = tagValue . value ; } } } ) ; return labels ; } param view param labels private registerMetric ( view , tags ) { const metricName = this . getPrometheusMetricName ( view ) ; /** Get metric if already registered */ let metric = this . registry . getSingleMetric ( metricName ) ; if ( metric ) { return metric ; } const labelNames = view . getColumns ( ) . map ( tagKey => tagKey . name ) ; const metricObj = { name : metricName , help : view . description , labelNames } ; switch ( view . aggregation ) { case AggregationType . COUNT : metric = new Counter ( metricObj ) ; break ; case AggregationType . SUM : case AggregationType . LAST_VALUE : metric = new Gauge ( metricObj ) ; break ; case AggregationType . DISTRIBUTION : this . validateDisallowedLeLabelForHistogram ( labelNames ) ; const distribution = { name : metricName , help : view . description , labelNames , buckets : this . getBoundaries ( view , tags ) , } ; metric = new Histogram ( distribution ) ; break ; default : this . logger . error ( `Aggregation ${ view . aggregation } is not supported` ) ; } this . registry . registerMetric ( metric ) ; return metric ; } param view param measurement private updateMetric ( view , measurement , tags ) { const metric = this . registerMetric ( view , tags ) ; const labelValues = this . getLabelValues ( view . getColumns ( ) , tags ) ; if ( metric instanceof Counter ) { metric . inc ( labelValues ) ; } else if ( view . aggregation === AggregationType . SUM && metric instanceof Gauge ) { metric . inc ( labelValues , measurement . value ) ; } else if ( view . aggregation === AggregationType . LAST_VALUE && metric instanceof Gauge ) { metric . set ( labelValues , measurement . value ) ; } else if ( metric instanceof Histogram ) { metric . observe ( labelValues , measurement . value ) ; } else { this . logger . error ( 'Metric not supported' ) ; } } param view private getPrometheusMetricName ( view ) { let metricName ; if ( this . prefix ) { metricName = `${ this . prefix }_${ view . name }` ; } else { metricName = view . name ; } return this . sanitizePrometheusMetricName ( metricName ) ; } param name private sanitizePrometheusMetricName ( name ) { return name . replace ( /\W/g , '_' ) ; } /**
     * Throws an error labels contain "le" label name in histogram label names.
     */ private validateDisallowedLeLabelForHistogram ( labels ) { labels . forEach ( label => { if ( label === PrometheusStatsExporter . RESERVED_HISTOGRAM_LABEL ) { throw new Error ( `${ PrometheusStatsExporter . RESERVED_HISTOGRAM_LABEL } is a reserved label keyword` ) ; } } ) ; } param view param tags private getBoundaries ( view , tags ) { const tagValues = view . getColumns ( ) . map ( tagKey => tags . get ( tagKey ) || null ) ; const data = view . getSnapshot ( tagValues ) as DistributionData ; return data . buckets ; } /**
     * Start the Prometheus exporter server
     */ startServer ( callback ? ) { const self = this ; this . app . get ( '/metrics' , ( req , res ) => { res . set ( 'Content-Type' , PrometheusStatsExporter . DEFAULT_OPTIONS . contentType ) ; res . end ( this . registry . metrics ( ) ) ; } ) ; this . server = this . app . listen ( this . port , ( ) => { self . logger . debug ( 'Prometheus Exporter started on port ' + self . port ) ; if ( callback ) { callback ( ) ; } } ) ; } param callback stopServer ( callback ? ) { if ( this . server ) { this . registry . clear ( ) ; this . server . close ( callback ) ; this . logger . debug ( 'Prometheus Exporter shutdown' ) ; } } }