11import { Context } from '../context' ;
22
3- type MetricType = 'counter' | 'gauge' ;
3+ type MetricType = 'counter' | 'gauge' | 'histogram' ;
44type LabelValuesKey = string ;
55
66function getLabelKey ( labels ?: MetricLabels ) : LabelValuesKey {
@@ -21,11 +21,26 @@ function parseLabelKey(key: string): MetricLabels {
2121 return labels ;
2222}
2323
24- export interface MetricSample {
24+ export interface NumericMetricSample {
2525 labels : MetricLabels ;
2626 value : number ;
2727}
2828
29+ export interface BucketMetricSample {
30+ labels : MetricLabels ;
31+ count : number ;
32+ sum : number ;
33+ buckets : Array < { le : number | '+Inf' ; count : number } > ;
34+ }
35+
36+ export type MetricSample = NumericMetricSample | BucketMetricSample ;
37+
38+ const isNumericMetricSample = ( sample : MetricSample ) : sample is NumericMetricSample =>
39+ 'value' in sample ;
40+
41+ const isBucketMetricSample = ( sample : MetricSample ) : sample is BucketMetricSample =>
42+ 'buckets' in sample ;
43+
2944export interface CollectedMetric {
3045 name : string ;
3146 help : string ;
@@ -50,7 +65,7 @@ class CounterImpl implements Counter {
5065 }
5166
5267 collect ( ) : CollectedMetric {
53- const samples = [ ...this . values . entries ( ) ] . map ( ( [ key , value ] ) => ( {
68+ const samples : NumericMetricSample [ ] = [ ...this . values . entries ( ) ] . map ( ( [ key , value ] ) => ( {
5469 labels : parseLabelKey ( key ) ,
5570 value,
5671 } ) ) ;
@@ -91,7 +106,7 @@ class GaugeImpl implements Gauge {
91106 }
92107
93108 collect ( ) : CollectedMetric {
94- const samples = [ ...this . values . entries ( ) ] . map ( ( [ key , value ] ) => ( {
109+ const samples : NumericMetricSample [ ] = [ ...this . values . entries ( ) ] . map ( ( [ key , value ] ) => ( {
95110 labels : parseLabelKey ( key ) ,
96111 value,
97112 } ) ) ;
@@ -107,6 +122,86 @@ class GaugeImpl implements Gauge {
107122 }
108123}
109124
125+ interface HistogramData {
126+ count : number ;
127+ sum : number ;
128+ buckets : Map < number , number > ;
129+ }
130+
131+ class HistogramImpl implements Histogram {
132+ private values = new Map < LabelValuesKey , HistogramData > ( ) ;
133+
134+ private buckets : number [ ] ;
135+
136+ constructor ( private opts : BucketMetricOptions ) {
137+ const buckets = opts . buckets || [ 0.005 , 0.01 , 0.025 , 0.05 , 0.1 , 0.25 , 0.5 , 1 , 2.5 , 5 , 10 ] ;
138+ const sortedBuckets = [ ...new Set ( buckets . filter ( ( b ) => b !== Infinity ) ) ] . sort ( ( a , b ) => a - b ) ;
139+ this . buckets = [ ...sortedBuckets , Infinity ] ;
140+ }
141+
142+ restore ( sample : BucketMetricSample ) : void {
143+ const key = getLabelKey ( sample . labels ) ;
144+ const data : HistogramData = {
145+ count : sample . count ,
146+ sum : sample . sum ,
147+ buckets : new Map (
148+ sample . buckets . map ( ( b ) => [ b . le === '+Inf' ? Infinity : ( b . le as number ) , b . count ] ) ,
149+ ) ,
150+ } ;
151+ this . values . set ( key , data ) ;
152+ }
153+
154+ observe ( value : number , labels ?: MetricLabels ) : void {
155+ const key = getLabelKey ( labels ) ;
156+ let data = this . values . get ( key ) ;
157+
158+ if ( ! data ) {
159+ const buckets = new Map < number , number > ( ) ;
160+ for ( const bucket of this . buckets ) {
161+ buckets . set ( bucket , 0 ) ;
162+ }
163+
164+ data = {
165+ count : 0 ,
166+ sum : 0 ,
167+ buckets,
168+ } ;
169+ this . values . set ( key , data ) ;
170+ }
171+
172+ data . count ++ ;
173+ data . sum += value ;
174+
175+ for ( const bucket of this . buckets ) {
176+ if ( value <= bucket ) {
177+ const currentCount = data . buckets . get ( bucket ) ! ;
178+ data . buckets . set ( bucket , currentCount + 1 ) ;
179+ }
180+ }
181+ }
182+
183+ collect ( ) : CollectedMetric {
184+ const samples : BucketMetricSample [ ] = Array . from ( this . values . entries ( ) ) . map ( ( [ key , data ] ) => ( {
185+ labels : parseLabelKey ( key ) ,
186+ count : data . count ,
187+ sum : data . sum ,
188+ buckets : Array . from ( data . buckets . entries ( ) ) . map ( ( [ le , count ] ) => ( {
189+ le : le === Infinity ? '+Inf' : le ,
190+ count,
191+ } ) ) ,
192+ } ) ) ;
193+
194+ this . values . clear ( ) ;
195+
196+ return {
197+ name : this . opts . name ,
198+ help : this . opts . help ,
199+ type : 'histogram' ,
200+ samples,
201+ } ;
202+ }
203+ }
204+
110205export type MetricLabels = Record < string , string > ;
111206
112207export interface Counter {
@@ -119,6 +214,11 @@ export interface Gauge {
119214 set ( value : number , labels ?: MetricLabels ) : void ;
120215}
121216
217+ export interface Histogram {
218+ observe ( value : number , labels ?: MetricLabels ) : void ;
219+ restore ( sample : BucketMetricSample ) : void ;
220+ }
221+
122222export interface ImpactMetricsDataSource {
123223 collect ( ) : CollectedMetric [ ] ;
124224 restore ( metrics : CollectedMetric [ ] ) : void ;
@@ -127,15 +227,19 @@ export interface ImpactMetricsDataSource {
127227export interface ImpactMetricRegistry {
128228 getCounter ( counterName : string ) : Counter | undefined ;
129229 getGauge ( gaugeName : string ) : Gauge | undefined ;
230+ getHistogram ( histogramName : string ) : Histogram | undefined ;
130231 counter ( opts : MetricOptions ) : Counter ;
131232 gauge ( opts : MetricOptions ) : Gauge ;
233+ histogram ( opts : BucketMetricOptions ) : Histogram ;
132234}
133235
134236export class InMemoryMetricRegistry implements ImpactMetricsDataSource , ImpactMetricRegistry {
135237 private counters = new Map < string , Counter & CollectibleMetric > ( ) ;
136238
137239 private gauges = new Map < string , Gauge & CollectibleMetric > ( ) ;
138240
241+ private histograms = new Map < string , Histogram & CollectibleMetric > ( ) ;
242+
139243 getCounter ( counterName : string ) : Counter | undefined {
140244 return this . counters . get ( counterName ) ;
141245 }
@@ -144,6 +248,10 @@ export class InMemoryMetricRegistry implements ImpactMetricsDataSource, ImpactMe
144248 return this . gauges . get ( gaugeName ) ;
145249 }
146250
251+ getHistogram ( histogramName : string ) : Histogram | undefined {
252+ return this . histograms . get ( histogramName ) ;
253+ }
254+
147255 counter ( opts : MetricOptions ) : Counter {
148256 const key = opts . name ;
149257 if ( ! this . counters . has ( key ) ) {
@@ -160,10 +268,19 @@ export class InMemoryMetricRegistry implements ImpactMetricsDataSource, ImpactMe
160268 return this . gauges . get ( key ) ! ;
161269 }
162270
271+ histogram ( opts : BucketMetricOptions ) : Histogram {
272+ const key = opts . name ;
273+ if ( ! this . histograms . has ( key ) ) {
274+ this . histograms . set ( key , new HistogramImpl ( opts ) ) ;
275+ }
276+ return this . histograms . get ( key ) ! ;
277+ }
278+
163279 collect ( ) : CollectedMetric [ ] {
164280 const allCounters = [ ...this . counters . values ( ) ] . map ( ( c ) => c . collect ( ) ) ;
165281 const allGauges = [ ...this . gauges . values ( ) ] . map ( ( g ) => g . collect ( ) ) ;
166- const allMetrics = [ ...allCounters , ...allGauges ] ;
282+ const allHistograms = [ ...this . histograms . values ( ) ] . map ( ( h ) => h . collect ( ) ) ;
283+ const allMetrics = [ ...allCounters , ...allGauges , ...allHistograms ] ;
167284
168285 const nonEmpty = allMetrics . filter ( ( metric ) => metric . samples . length > 0 ) ;
169286 return nonEmpty . length > 0 ? nonEmpty : [ ] ;
@@ -175,15 +292,38 @@ export class InMemoryMetricRegistry implements ImpactMetricsDataSource, ImpactMe
175292 case 'counter' : {
176293 const counter = this . counter ( { name : metric . name , help : metric . help } ) ;
177294 for ( const sample of metric . samples ) {
178- counter . inc ( sample . value , sample . labels ) ;
295+ if ( isNumericMetricSample ( sample ) ) {
296+ counter . inc ( sample . value , sample . labels ) ;
297+ }
179298 }
180299 break ;
181300 }
182301
183302 case 'gauge' : {
184303 const gauge = this . gauge ( { name : metric . name , help : metric . help } ) ;
185304 for ( const sample of metric . samples ) {
186- gauge . set ( sample . value , sample . labels ) ;
305+ if ( isNumericMetricSample ( sample ) ) {
306+ gauge . set ( sample . value , sample . labels ) ;
307+ }
308+ }
309+ break ;
310+ }
311+
312+ case 'histogram' : {
313+ const firstSample = metric . samples . find ( isBucketMetricSample ) ;
314+ if ( firstSample ) {
315+ const buckets = firstSample . buckets . map ( ( b ) =>
316+ b . le === '+Inf' ? Infinity : ( b . le as number ) ,
317+ ) ;
318+ const histogram = this . histogram ( {
319+ name : metric . name ,
320+ help : metric . help ,
321+ buckets,
322+ } ) ;
323+
324+ metric . samples . filter ( isBucketMetricSample ) . forEach ( ( sample ) => {
325+ histogram . restore ( sample ) ;
326+ } ) ;
187327 }
188328 break ;
189329 }
@@ -198,6 +338,10 @@ export interface MetricOptions {
198338 labelNames ?: string [ ] ;
199339}
200340
341+ export interface BucketMetricOptions extends MetricOptions {
342+ buckets : number [ ] ;
343+ }
344+
201345export interface MetricFlagContext {
202346 flagNames : string [ ] ;
203347 context : Context ;
0 commit comments