4646import java .io .UncheckedIOException ;
4747import java .io .Writer ;
4848import java .nio .charset .StandardCharsets ;
49+ import java .util .ArrayList ;
4950import java .util .Collection ;
5051import java .util .Collections ;
52+ import java .util .HashSet ;
5153import java .util .LinkedHashMap ;
54+ import java .util .LinkedHashSet ;
5255import java .util .List ;
5356import java .util .Map ;
5457import java .util .Optional ;
58+ import java .util .Set ;
5559import java .util .concurrent .TimeUnit ;
5660import java .util .function .BiConsumer ;
5761import java .util .function .Predicate ;
58- import java .util .stream .Collectors ;
5962import javax .annotation .Nullable ;
6063
6164/** Serializes metrics into Prometheus exposition formats. */
6265// Adapted from
6366// https://github.com/prometheus/client_java/blob/master/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java
6467abstract class Serializer {
65-
6668 static Serializer create (@ Nullable String acceptHeader , Predicate <String > filter ) {
6769 if (acceptHeader == null ) {
6870 return new Prometheus004Serializer (filter );
@@ -100,61 +102,64 @@ abstract void writeExemplar(
100102
101103 abstract void writeEof (Writer writer ) throws IOException ;
102104
103- final void write (Collection <MetricData > metrics , OutputStream output ) throws IOException {
104- Map <InstrumentationScopeInfo , List <MetricData >> metricsByScope =
105- metrics .stream ()
106- // Not supported in specification yet.
107- .filter (metric -> metric .getType () != MetricDataType .EXPONENTIAL_HISTOGRAM )
108- // PrometheusHttpServer#getAggregationTemporality specifies cumulative temporality for
109- // all instruments, but non-SDK MetricProducers may not conform. We drop delta
110- // temporality metrics to avoid the complexity of stateful transformation to cumulative.
111- .filter (metric -> !isDeltaTemporality (metric ))
112- .filter (metric -> metricNameFilter .test (metricName (metric )))
113- .collect (
114- Collectors .groupingBy (
115- MetricData ::getInstrumentationScopeInfo ,
116- LinkedHashMap ::new ,
117- Collectors .toList ()));
105+ final Set <String > write (Collection <MetricData > metrics , OutputStream output ) throws IOException {
106+ Set <String > conflictMetricNames = new HashSet <>();
107+ Map <String , List <MetricData >> metricsByName = new LinkedHashMap <>();
108+ Set <InstrumentationScopeInfo > scopes = new LinkedHashSet <>();
109+ // Iterate through metrics, filtering and grouping by headerName
110+ for (MetricData metric : metrics ) {
111+ // Not supported in specification yet.
112+ if (metric .getType () == MetricDataType .EXPONENTIAL_HISTOGRAM ) {
113+ continue ;
114+ }
115+ // PrometheusHttpServer#getAggregationTemporality specifies cumulative temporality for
116+ // all instruments, but non-SDK MetricProducers may not conform. We drop delta
117+ // temporality metrics to avoid the complexity of stateful transformation to cumulative.
118+ if (isDeltaTemporality (metric )) {
119+ continue ;
120+ }
121+ PrometheusType prometheusType = PrometheusType .forMetric (metric );
122+ String metricName = metricName (metric .getName (), prometheusType );
123+ // Skip metrics which do not pass metricNameFilter
124+ if (!metricNameFilter .test (metricName )) {
125+ continue ;
126+ }
127+ List <MetricData > metricsWithHeaderName =
128+ metricsByName .computeIfAbsent (metricName , unused -> new ArrayList <>());
129+ // Skip metrics with the same name but different type
130+ if (metricsWithHeaderName .size () > 0
131+ && prometheusType != PrometheusType .forMetric (metricsWithHeaderName .get (0 ))) {
132+ conflictMetricNames .add (metricName );
133+ continue ;
134+ }
135+
136+ metricsWithHeaderName .add (metric );
137+ scopes .add (metric .getInstrumentationScopeInfo ());
138+ }
139+
118140 Optional <Resource > optResource = metrics .stream ().findFirst ().map (MetricData ::getResource );
119141 try (Writer writer =
120142 new BufferedWriter (new OutputStreamWriter (output , StandardCharsets .UTF_8 ))) {
121143 if (optResource .isPresent ()) {
122144 writeResource (optResource .get (), writer );
123145 }
124- for (Map .Entry <InstrumentationScopeInfo , List <MetricData >> entry :
125- metricsByScope .entrySet ()) {
146+ for (InstrumentationScopeInfo scope : scopes ) {
147+ writeScopeInfo (scope , writer );
148+ }
149+ for (Map .Entry <String , List <MetricData >> entry : metricsByName .entrySet ()) {
126150 write (entry .getValue (), entry .getKey (), writer );
127151 }
128152 writeEof (writer );
129153 }
154+ return conflictMetricNames ;
130155 }
131156
132- private void write (
133- List <MetricData > metrics , InstrumentationScopeInfo instrumentationScopeInfo , Writer writer )
134- throws IOException {
135- writeScopeInfo (instrumentationScopeInfo , writer );
136- // Group metrics with the scope, name, but different types. This is a semantic error which the
137- // SDK warns about but passes through to exporters to handle.
138- Map <String , List <MetricData >> metricsByName =
139- metrics .stream ()
140- .collect (
141- Collectors .groupingBy (
142- metric ->
143- headerName (
144- NameSanitizer .INSTANCE .apply (metric .getName ()),
145- PrometheusType .forMetric (metric )),
146- LinkedHashMap ::new ,
147- Collectors .toList ()));
148-
149- for (Map .Entry <String , List <MetricData >> entry : metricsByName .entrySet ()) {
150- write (entry .getValue (), entry .getKey (), writer );
151- }
152- }
153-
154- private void write (List <MetricData > metrics , String headerName , Writer writer )
157+ private void write (List <MetricData > metrics , String metricName , Writer writer )
155158 throws IOException {
156159 // Write header based on first metric
157- PrometheusType type = PrometheusType .forMetric (metrics .get (0 ));
160+ MetricData first = metrics .get (0 );
161+ PrometheusType type = PrometheusType .forMetric (first );
162+ String headerName = headerName (NameSanitizer .INSTANCE .apply (first .getName ()), type );
158163 String description = metrics .get (0 ).getDescription ();
159164
160165 writer .write ("# TYPE " );
@@ -171,21 +176,19 @@ private void write(List<MetricData> metrics, String headerName, Writer writer)
171176
172177 // Then write the metrics.
173178 for (MetricData metric : metrics ) {
174- write (metric , writer );
179+ write (metric , metricName , writer );
175180 }
176181 }
177182
178- private void write (MetricData metric , Writer writer ) throws IOException {
179- String name = metricName (metric );
180-
183+ private void write (MetricData metric , String metricName , Writer writer ) throws IOException {
181184 for (PointData point : getPoints (metric )) {
182185 switch (metric .getType ()) {
183186 case DOUBLE_SUM :
184187 case DOUBLE_GAUGE :
185188 writePoint (
186189 writer ,
187190 metric .getInstrumentationScopeInfo (),
188- name ,
191+ metricName ,
189192 ((DoublePointData ) point ).getValue (),
190193 point .getAttributes (),
191194 point .getEpochNanos ());
@@ -195,18 +198,18 @@ private void write(MetricData metric, Writer writer) throws IOException {
195198 writePoint (
196199 writer ,
197200 metric .getInstrumentationScopeInfo (),
198- name ,
201+ metricName ,
199202 (double ) ((LongPointData ) point ).getValue (),
200203 point .getAttributes (),
201204 point .getEpochNanos ());
202205 break ;
203206 case HISTOGRAM :
204207 writeHistogram (
205- writer , metric .getInstrumentationScopeInfo (), name , (HistogramPointData ) point );
208+ writer , metric .getInstrumentationScopeInfo (), metricName , (HistogramPointData ) point );
206209 break ;
207210 case SUMMARY :
208211 writeSummary (
209- writer , metric .getInstrumentationScopeInfo (), name , (SummaryPointData ) point );
212+ writer , metric .getInstrumentationScopeInfo (), metricName , (SummaryPointData ) point );
210213 break ;
211214 case EXPONENTIAL_HISTOGRAM :
212215 throw new IllegalArgumentException ("Can't happen" );
@@ -648,9 +651,8 @@ static Collection<? extends PointData> getPoints(MetricData metricData) {
648651 return Collections .emptyList ();
649652 }
650653
651- private static String metricName (MetricData metric ) {
652- PrometheusType type = PrometheusType .forMetric (metric );
653- String name = NameSanitizer .INSTANCE .apply (metric .getName ());
654+ private static String metricName (String rawMetricName , PrometheusType type ) {
655+ String name = NameSanitizer .INSTANCE .apply (rawMetricName );
654656 if (type == PrometheusType .COUNTER ) {
655657 name = name + "_total" ;
656658 }
0 commit comments