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+ string ? 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 : '_' )
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 . TypeInfo
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+ && value . Equals ( other . value )
271+ && labels . Count == other . labels . Count
272+ && labels . All ( kv => other . labels . TryGetValue ( kv . Key , out string ? otherValue ) && kv . Value == otherValue ) ;
273+ }
274+
275+ public override int GetHashCode ( )
276+ {
277+ var hash = new HashCode ( ) ;
278+ hash . Add ( Name ) ;
279+ hash . Add ( value ) ;
280+
281+ foreach ( var kv in labels )
282+ {
283+ hash . Add ( kv . Key ) ;
284+ hash . Add ( kv . Value ) ;
285+ }
286+
287+ return hash . ToHashCode ( ) ;
288+ }
289+
290+ public override string ToString ( )
291+ {
292+ string labelStr = labels . Count > 0
293+ ? $ "{{{string.Join(", ", labels. Select ( kvp => $ "{ Escape ( kvp . Key ) } =\" { Escape ( kvp . Value ) } \" ") ) } } } "
294+ : string . Empty ;
295+ return $ "{ Name } { labelStr } { value } ";
296+
297+ static string Escape ( string s ) =>
298+ s . Replace ( "\\ " , @"\\" )
299+ . Replace ( "\" " , "\\ \" " )
300+ . Replace ( "\n " , "\\ n" )
301+ . Replace ( "\r " , "\\ r" )
302+ . Replace ( "\t " , "\\ t" ) ;
303+ }
304+ }
305+ }
0 commit comments