import { FetchFn } from '.' ; import { Matcher } from '../types' ; import { labelMatchersToString } from '../parser' ; import LRUCache from 'lru-cache' ; export interface MetricMetadata { type : string ; help : string ; } export interface PrometheusClient { labelNames ( metricName ? ) : Promise < string [ ] > ; labelValues ( labelName , metricName ? , matchers ? ) : Promise < string [ ] > ; metricMetadata ( ) : Promise < Record < string , MetricMetadata [ ] > > ; series ( metricName , matchers ? , labelName ? ) : Promise < Map < string , string > [ ] > ; metricNames ( prefix ? ) : Promise < string [ ] > ; } export interface CacheConfig { maxAge ? : number ; initialMetricList ? : string [ ] ; } export interface PrometheusConfig { url ? : string ; lookbackInterval ? : number ; httpErrorHandler ? : ( error : any ) => void ; fetchFn ? : FetchFn ; cache ? : CacheConfig ; httpMethod ? : 'POST' | 'GET' ; apiPrefix ? : string ; } interface APIResponse < T > { status : 'success' | 'error' ; data ? : T ; error ? : string ; warnings ? : string [ ] ; } const badRequest = 400 ; const unprocessableEntity = 422 ; const serviceUnavailable = 503 ; export class HTTPPrometheusClient implements PrometheusClient { private readonly lookbackInterval = 60 * 60 * 1000 * 12 ; private readonly url : string ; private readonly errorHandler ? : ( error : any ) => void ; private readonly httpMethod : 'POST' | 'GET' = 'POST' ; private readonly apiPrefix : string = '/api/v1' ; private readonly fetchFn : FetchFn = ( input , init ? ) : Promise < Response > => fetch ( input , init ) ; constructor ( config ) { this . url = config . url ? config . url : '' ; this . errorHandler = config . httpErrorHandler ; if ( config . lookbackInterval ) { this . lookbackInterval = config . lookbackInterval ; } if ( config . fetchFn ) { this . fetchFn = config . fetchFn ; } if ( config . httpMethod ) { this . httpMethod = config . httpMethod ; } if ( config . apiPrefix ) { this . apiPrefix = config . apiPrefix ; } } labelNames ( metricName ? ) { const end = new Date ( ) ; const start = new Date ( end . getTime ( ) - this . lookbackInterval ) ; if ( metricName === undefined || metricName === '' ) { const request = this . buildRequest ( this . labelsEndpoint ( ) , new URLSearchParams ( { start : start . toISOString ( ) , end : end . toISOString ( ) , } ) ) ; return this . fetchAPI < string [ ] > ( request . uri , { method : this . httpMethod , body : request . body , } ) . catch ( ( error ) => { if ( this . errorHandler ) { this . errorHandler ( error ) ; } return [ ] ; } ) ; } return this . series ( metricName ) . then ( ( series ) => { const labelNames = new Set < string > ( ) ; for ( const labelSet of series ) { for ( const [ key ] of Object . entries ( labelSet ) ) { if ( key === '__name__' ) { continue ; } labelNames . add ( key ) ; } } return Array . from ( labelNames ) ; } ) ; } labelValues ( labelName , metricName ? , matchers ? ) { const end = new Date ( ) ; const start = new Date ( end . getTime ( ) - this . lookbackInterval ) ; if ( ! metricName || metricName . length === 0 ) { const params = new URLSearchParams ( { start : start . toISOString ( ) , end : end . toISOString ( ) , } ) ; return this . fetchAPI < string [ ] > ( `${ this . labelValuesEndpoint ( ) . replace ( /:name/gi , labelName ) }?${ params }` ) . catch ( ( error ) => { if ( this . errorHandler ) { this . errorHandler ( error ) ; } return [ ] ; } ) ; } return this . series ( metricName , matchers , labelName ) . then ( ( series ) => { const labelValues = new Set < string > ( ) ; for ( const labelSet of series ) { for ( const [ key , value ] of Object . entries ( labelSet ) ) { if ( key === '__name__' ) { continue ; } if ( key === labelName ) { labelValues . add ( value ) ; } } } return Array . from ( labelValues ) ; } ) ; } metricMetadata ( ) { return this . fetchAPI < Record < string , MetricMetadata [ ] > > ( this . metricMetadataEndpoint ( ) ) . catch ( ( error ) => { if ( this . errorHandler ) { this . errorHandler ( error ) ; } return { } ; } ) ; } series ( metricName , matchers ? , labelName ? ) { const end = new Date ( ) ; const start = new Date ( end . getTime ( ) - this . lookbackInterval ) ; const request = this . buildRequest ( this . seriesEndpoint ( ) , new URLSearchParams ( { start : start . toISOString ( ) , end : end . toISOString ( ) , 'match[]' : labelMatchersToString ( metricName , matchers , labelName ) , } ) ) ; return this . fetchAPI < Map < string , string > [ ] > ( request . uri , { method : this . httpMethod , body : request . body , } ) . catch ( ( error ) => { if ( this . errorHandler ) { this . errorHandler ( error ) ; } return [ ] ; } ) ; } metricNames ( ) { return this . labelValues ( '__name__' ) ; } private fetchAPI < T > ( resource , init ? ) { return this . fetchFn ( this . url + resource , init ) . then ( ( res ) => { if ( ! res . ok && ! [ badRequest , unprocessableEntity , serviceUnavailable ] . includes ( res . status ) ) { throw new Error ( res . statusText ) ; } return res ; } ) . then ( ( res ) => res . json ( ) ) . then ( ( apiRes ) => { if ( apiRes . status === 'error' ) { throw new Error ( apiRes . error !== undefined ? apiRes . error : 'missing "error" field in response JSON' ) ; } if ( apiRes . data === undefined ) { throw new Error ( 'missing "data" field in response JSON' ) ; } return apiRes . data ; } ) ; } private buildRequest ( endpoint , params ) { let uri = endpoint ; let body = params ; if ( this . httpMethod === 'GET' ) { uri = `${ uri }?${ params }` ; body = null ; } return { uri , body } ; } private labelsEndpoint ( ) { return `${ this . apiPrefix }/labels` ; } private labelValuesEndpoint ( ) { return `${ this . apiPrefix }/label/:name/values` ; } private seriesEndpoint ( ) { return `${ this . apiPrefix }/series` ; } private metricMetadataEndpoint ( ) { return `${ this . apiPrefix }/metadata` ; } } class Cache { private readonly completeAssociation : LRUCache < string , Map < string , Set < string > > > ; private metricMetadata : Record < string , MetricMetadata [ ] > ; private labelValues : LRUCache < string , string [ ] > ; private labelNames : string [ ] ; constructor ( config ? ) { const maxAge = config && config . maxAge ? config . maxAge : 5 * 60 * 1000 ; this . completeAssociation = new LRUCache < string , Map < string , Set < string > > > ( maxAge ) ; this . metricMetadata = { } ; this . labelValues = new LRUCache < string , string [ ] > ( maxAge ) ; this . labelNames = [ ] ; if ( config ?. initialMetricList ) { this . setLabelValues ( '__name__' , config . initialMetricList ) ; } } setAssociations ( metricName , series ) { series . forEach ( ( labelSet ) => { let currentAssociation = this . completeAssociation . get ( metricName ) ; if ( ! currentAssociation ) { currentAssociation = new Map < string , Set < string > > ( ) ; this . completeAssociation . set ( metricName , currentAssociation ) ; } for ( const [ key , value ] of Object . entries ( labelSet ) ) { if ( key === '__name__' ) { continue ; } const labelValues = currentAssociation . get ( key ) ; if ( labelValues === undefined ) { currentAssociation . set ( key , new Set < string > ( [ value ] ) ) ; } else { labelValues . add ( value ) ; } } } ) ; } setMetricMetadata ( metadata ) { this . metricMetadata = metadata ; } getMetricMetadata ( ) { return this . metricMetadata ; } setLabelNames ( labelNames ) { this . labelNames = labelNames ; } getLabelNames ( metricName ? ) { if ( ! metricName || metricName . length === 0 ) { return this . labelNames ; } const labelSet = this . completeAssociation . get ( metricName ) ; return labelSet ? Array . from ( labelSet . keys ( ) ) : [ ] ; } setLabelValues ( labelName , labelValues ) { this . labelValues . set ( labelName , labelValues ) ; } getLabelValues ( labelName , metricName ? ) { if ( ! metricName || metricName . length === 0 ) { const result = this . labelValues . get ( labelName ) ; return result ? result : [ ] ; } const labelSet = this . completeAssociation . get ( metricName ) ; if ( labelSet ) { const labelValues = labelSet . get ( labelName ) ; return labelValues ? Array . from ( labelValues ) : [ ] ; } return [ ] ; } } export class CachedPrometheusClient implements PrometheusClient { private readonly cache : Cache ; private readonly client : PrometheusClient ; constructor ( client , config ? ) { this . client = client ; this . cache = new Cache ( config ) ; } labelNames ( metricName ? ) { const cachedLabel = this . cache . getLabelNames ( metricName ) ; if ( cachedLabel && cachedLabel . length > 0 ) { return Promise . resolve ( cachedLabel ) ; } if ( metricName === undefined || metricName === '' ) { return this . client . labelNames ( ) . then ( ( labelNames ) => { this . cache . setLabelNames ( labelNames ) ; return labelNames ; } ) ; } return this . series ( metricName ) . then ( ( ) => { return this . cache . getLabelNames ( metricName ) ; } ) ; } labelValues ( labelName , metricName ? ) { const cachedLabel = this . cache . getLabelValues ( labelName , metricName ) ; if ( cachedLabel && cachedLabel . length > 0 ) { return Promise . resolve ( cachedLabel ) ; } if ( metricName === undefined || metricName === '' ) { return this . client . labelValues ( labelName ) . then ( ( labelValues ) => { this . cache . setLabelValues ( labelName , labelValues ) ; return labelValues ; } ) ; } return this . series ( metricName ) . then ( ( ) => { return this . cache . getLabelValues ( labelName , metricName ) ; } ) ; } metricMetadata ( ) { const cachedMetadata = this . cache . getMetricMetadata ( ) ; if ( cachedMetadata && Object . keys ( cachedMetadata ) . length > 0 ) { return Promise . resolve ( cachedMetadata ) ; } return this . client . metricMetadata ( ) . then ( ( metadata ) => { this . cache . setMetricMetadata ( metadata ) ; return this . cache . getMetricMetadata ( ) ; } ) ; } series ( metricName ) { return this . client . series ( metricName ) . then ( ( series ) => { this . cache . setAssociations ( metricName , series ) ; return series ; } ) ; } metricNames ( ) { return this . labelValues ( '__name__' ) ; } }