1
+ using System . Collections . Generic ;
2
+ using System . Collections . Immutable ;
3
+ using System . Linq ;
4
+ using BenchmarkDotNet . Loggers ;
5
+ using BenchmarkDotNet . Parameters ;
6
+ using BenchmarkDotNet . Reports ;
7
+ using BenchmarkDotNet . Running ;
8
+ using System ;
9
+ using System . Text ;
10
+ using BenchmarkDotNet . Engines ;
11
+ using BenchmarkDotNet . Extensions ;
12
+ using BenchmarkDotNet . Mathematics ;
13
+
14
+ namespace BenchmarkDotNet . Exporters . OpenMetrics ;
15
+
16
+ public class OpenMetricsExporter : ExporterBase
17
+ {
18
+ private const string MetricPrefix = "benchmark_" ;
19
+ protected override string FileExtension => "metrics" ;
20
+ protected override string FileCaption => "openmetrics" ;
21
+
22
+ public static readonly IExporter Default = new OpenMetricsExporter ( ) ;
23
+
24
+ public override void ExportToLog ( Summary summary , ILogger logger )
25
+ {
26
+ var metricsSet = new HashSet < OpenMetric > ( ) ;
27
+
28
+ foreach ( var report in summary . Reports )
29
+ {
30
+ var benchmark = report . BenchmarkCase ;
31
+ var gcStats = report . GcStats ;
32
+ var descriptor = benchmark . Descriptor ;
33
+ var parameters = benchmark . Parameters ;
34
+
35
+ var stats = report . ResultStatistics ;
36
+ var metrics = report . Metrics ;
37
+ if ( stats == null )
38
+ continue ;
39
+
40
+ AddCommonMetrics ( metricsSet , descriptor , parameters , stats , gcStats ) ;
41
+ AddAdditionalMetrics ( metricsSet , metrics , descriptor , parameters ) ;
42
+ }
43
+
44
+ WriteMetricsToLogger ( logger , metricsSet ) ;
45
+ }
46
+
47
+ private static void AddCommonMetrics ( HashSet < OpenMetric > metricsSet , Descriptor descriptor , ParameterInstances parameters , Statistics stats , GcStats gcStats )
48
+ {
49
+ metricsSet . AddRange ( [
50
+ // Mean
51
+ OpenMetric . FromStatistics (
52
+ $ "{ MetricPrefix } mean_nanoseconds",
53
+ "Mean execution time in nanoseconds." ,
54
+ "gauge" ,
55
+ descriptor ,
56
+ parameters ,
57
+ stats . Mean ) ,
58
+ // Error
59
+ OpenMetric . FromStatistics (
60
+ $ "{ MetricPrefix } error_nanoseconds",
61
+ "Standard error of the mean execution time in nanoseconds." ,
62
+ "gauge" ,
63
+ descriptor ,
64
+ parameters ,
65
+ stats . StandardError ) ,
66
+ // Standard Deviation
67
+ OpenMetric . FromStatistics (
68
+ $ "{ MetricPrefix } stddev_nanoseconds",
69
+ "Standard deviation of execution time in nanoseconds." ,
70
+ "gauge" ,
71
+ descriptor ,
72
+ parameters ,
73
+ stats . StandardDeviation ) ,
74
+ // GC Stats Gen0
75
+ OpenMetric . FromStatistics (
76
+ $ "{ MetricPrefix } gc_gen0_collections",
77
+ "Number of Gen 0 garbage collections during the benchmark execution." ,
78
+ "gauge" ,
79
+ descriptor ,
80
+ parameters ,
81
+ gcStats . Gen0Collections ) ,
82
+ // GC Stats Gen1
83
+ OpenMetric . FromStatistics (
84
+ $ "{ MetricPrefix } gc_gen1_collections",
85
+ "Number of Gen 1 garbage collections during the benchmark execution." ,
86
+ "gauge" ,
87
+ descriptor ,
88
+ parameters ,
89
+ gcStats . Gen1Collections ) ,
90
+ // GC Stats Gen2
91
+ OpenMetric . FromStatistics (
92
+ $ "{ MetricPrefix } gc_gen2_collections",
93
+ "Number of Gen 2 garbage collections during the benchmark execution." ,
94
+ "gauge" ,
95
+ descriptor ,
96
+ parameters ,
97
+ gcStats . Gen2Collections ) ,
98
+ // Total GC Operations
99
+ OpenMetric . FromStatistics (
100
+ $ "{ MetricPrefix } gc_total_operations",
101
+ "Total number of garbage collection operations during the benchmark execution." ,
102
+ "gauge" ,
103
+ descriptor ,
104
+ parameters ,
105
+ gcStats . TotalOperations ) ,
106
+ // P90
107
+ OpenMetric . FromStatistics (
108
+ $ "{ MetricPrefix } p90_nanoseconds",
109
+ "90th percentile execution time in nanoseconds." ,
110
+ "gauge" ,
111
+ descriptor ,
112
+ parameters ,
113
+ stats . Percentiles . P90 ) ,
114
+ // P95
115
+ OpenMetric . FromStatistics (
116
+ $ "{ MetricPrefix } p95_nanoseconds",
117
+ "95th percentile execution time in nanoseconds." ,
118
+ "gauge" ,
119
+ descriptor ,
120
+ parameters ,
121
+ stats . Percentiles . P95 )
122
+ ] ) ;
123
+ }
124
+
125
+ private static void AddAdditionalMetrics ( HashSet < OpenMetric > metricsSet , IReadOnlyDictionary < string , Metric > metrics , Descriptor descriptor , ParameterInstances parameters )
126
+ {
127
+ var reservedMetricNames = new HashSet < string >
128
+ {
129
+ $ "{ MetricPrefix } mean_nanoseconds",
130
+ $ "{ MetricPrefix } error_nanoseconds",
131
+ $ "{ MetricPrefix } stddev_nanoseconds",
132
+ $ "{ MetricPrefix } gc_gen0_collections",
133
+ $ "{ MetricPrefix } gc_gen1_collections",
134
+ $ "{ MetricPrefix } gc_gen2_collections",
135
+ $ "{ MetricPrefix } gc_total_operations",
136
+ $ "{ MetricPrefix } p90_nanoseconds",
137
+ $ "{ MetricPrefix } p95_nanoseconds"
138
+ } ;
139
+
140
+ foreach ( var metric in metrics )
141
+ {
142
+ string metricName = SanitizeMetricName ( metric . Key ) ;
143
+ string fullMetricName = $ "{ MetricPrefix } { metricName } ";
144
+
145
+ if ( reservedMetricNames . Contains ( fullMetricName ) )
146
+ continue ;
147
+
148
+ metricsSet . Add ( OpenMetric . FromMetric (
149
+ fullMetricName ,
150
+ metric ,
151
+ "gauge" , // Assuming all additional metrics are of type "gauge"
152
+ descriptor ,
153
+ parameters ) ) ;
154
+ }
155
+ }
156
+
157
+ private static void WriteMetricsToLogger ( ILogger logger , HashSet < OpenMetric > metricsSet )
158
+ {
159
+ var emittedHelpType = new HashSet < string > ( ) ;
160
+
161
+ foreach ( var metric in metricsSet . OrderBy ( m => m . Name ) )
162
+ {
163
+ if ( ! emittedHelpType . Contains ( metric . Name ) )
164
+ {
165
+ logger . WriteLine ( $ "# HELP { metric . Name } { metric . Help } ") ;
166
+ logger . WriteLine ( $ "# TYPE { metric . Name } { metric . Type } ") ;
167
+ emittedHelpType . Add ( metric . Name ) ;
168
+ }
169
+
170
+ logger . WriteLine ( metric . ToString ( ) ) ;
171
+ }
172
+
173
+ logger . WriteLine ( "# EOF" ) ;
174
+ }
175
+
176
+ private static string SanitizeMetricName ( string name )
177
+ {
178
+ var builder = new StringBuilder ( ) ;
179
+ bool lastWasUnderscore = false ;
180
+
181
+ foreach ( char c in name . ToLowerInvariant ( ) )
182
+ {
183
+ if ( char . IsLetterOrDigit ( c ) || c == '_' )
184
+ {
185
+ builder . Append ( c ) ;
186
+ lastWasUnderscore = false ;
187
+ }
188
+ else if ( ! lastWasUnderscore )
189
+ {
190
+ builder . Append ( '_' ) ;
191
+ lastWasUnderscore = true ;
192
+ }
193
+ }
194
+
195
+ var result = builder . ToString ( ) . Trim ( '_' ) ; // <-- Trim here
196
+
197
+ if ( result . Length > 0 && char . IsDigit ( result [ 0 ] ) )
198
+ result = "_" + result ;
199
+
200
+ return result ;
201
+ }
202
+
203
+ private class OpenMetric : IEquatable < OpenMetric >
204
+ {
205
+ internal string Name { get ; }
206
+ internal string Help { get ; }
207
+ internal string Type { get ; }
208
+ private readonly ImmutableSortedDictionary < string , string > labels ;
209
+ private readonly double value ;
210
+
211
+ private OpenMetric ( string name , string help , string type , ImmutableSortedDictionary < string , string > labels , double value )
212
+ {
213
+ if ( string . IsNullOrWhiteSpace ( name ) ) throw new ArgumentException ( "Metric name cannot be null or empty." ) ;
214
+ if ( string . IsNullOrWhiteSpace ( type ) ) throw new ArgumentException ( "Metric type cannot be null or empty." ) ;
215
+
216
+ Name = name ;
217
+ Help = help ;
218
+ Type = type ;
219
+ this . labels = labels ?? throw new ArgumentNullException ( nameof ( labels ) ) ;
220
+ this . value = value ;
221
+ }
222
+
223
+ public static OpenMetric FromStatistics ( string name , string help , string type , Descriptor descriptor , ParameterInstances parameters , double value )
224
+ {
225
+ var labels = BuildLabelDict ( descriptor , parameters ) ;
226
+ return new OpenMetric ( name , help , type , labels , value ) ;
227
+ }
228
+
229
+ public static OpenMetric FromMetric ( string fullMetricName , KeyValuePair < string , Metric > metric , string type , Descriptor descriptor , ParameterInstances parameters )
230
+ {
231
+ string help = $ "Additional metric { metric . Key } ";
232
+ var labels = BuildLabelDict ( descriptor , parameters ) ;
233
+ return new OpenMetric ( fullMetricName , help , type , labels , metric . Value . Value ) ;
234
+ }
235
+
236
+ private static readonly Dictionary < string , string > NormalizedLabelKeyCache = new ( ) ;
237
+ private static string NormalizeLabelKey ( string key )
238
+ {
239
+ string normalized = new ( key
240
+ . ToLowerInvariant ( )
241
+ . Select ( c => char . IsLetterOrDigit ( c ) || c == '_' ? c : '_' )
242
+ . ToArray ( ) ) ;
243
+ return normalized ;
244
+ }
245
+
246
+ private static ImmutableSortedDictionary < string , string > BuildLabelDict ( Descriptor descriptor , ParameterInstances parameters )
247
+ {
248
+ var dict = new SortedDictionary < string , string >
249
+ {
250
+ [ "method" ] = descriptor . WorkloadMethod . Name ,
251
+ [ "type" ] = descriptor . Type . Name
252
+ } ;
253
+ foreach ( var param in parameters . Items )
254
+ {
255
+ string key = NormalizeLabelKey ( param . Name ) ;
256
+ string value = param . Value . ToString ( ) . Replace ( "\\ " , @"\\" ) . Replace ( "\" " , "\\ \" " ) ;
257
+ dict [ key ] = value ;
258
+ }
259
+ return dict . ToImmutableSortedDictionary ( ) ;
260
+ }
261
+
262
+ public override bool Equals ( object ? obj ) => Equals ( obj as OpenMetric ) ;
263
+
264
+ public bool Equals ( OpenMetric ? other )
265
+ {
266
+ if ( other is null )
267
+ return false ;
268
+
269
+ return Name == other . Name
270
+ && Type == other . Type
271
+ && Help == other . Help
272
+ && value . Equals ( other . value )
273
+ && labels . Count == other . labels . Count
274
+ && labels . All ( kv => other . labels . TryGetValue ( kv . Key , out string ? otherValue ) && kv . Value == otherValue ) ;
275
+ }
276
+
277
+ public override int GetHashCode ( )
278
+ {
279
+ var hash = new HashCode ( ) ;
280
+ hash . Add ( Name ) ;
281
+ hash . Add ( Help ) ;
282
+ hash . Add ( Type ) ;
283
+ hash . Add ( value ) ;
284
+
285
+ foreach ( var kv in labels )
286
+ {
287
+ hash . Add ( kv . Key ) ;
288
+ hash . Add ( kv . Value ) ;
289
+ }
290
+
291
+ return hash . ToHashCode ( ) ;
292
+ }
293
+
294
+ public override string ToString ( )
295
+ {
296
+ string labelStr = labels . Count > 0
297
+ ? $ "{{{string.Join(", ", labels. Select ( kvp => $ "{ Escape ( kvp . Key ) } =\" { Escape ( kvp . Value ) } \" ") ) } } } "
298
+ : string . Empty ;
299
+ return $ "{ Name } { labelStr } { value } ";
300
+
301
+ static string Escape ( string s ) =>
302
+ s . Replace ( "\\ " , @"\\" )
303
+ . Replace ( "\" " , "\\ \" " )
304
+ . Replace ( "\n " , "\\ n" )
305
+ . Replace ( "\r " , "\\ r" )
306
+ . Replace ( "\t " , "\\ t" ) ;
307
+ }
308
+ }
309
+ }
0 commit comments