@@ -93,59 +93,81 @@ type forgeCollector struct {
9393// Describe implements prometheus.Collector. Intentionally emits no descriptors.
9494func (c * forgeCollector ) Describe (chan <- * prometheus.Desc ) {}
9595
96+ type series struct {
97+ value any
98+ labels map [string ]string
99+ }
100+
96101// Collect implements prometheus.Collector.
97102func (c * forgeCollector ) Collect (ch chan <- prometheus.Metric ) {
98103 if c .snapshot == nil {
99104 return
100105 }
101106
107+ families := make (map [string ][]series ) // fqName -> series
102108 for key , value := range c .snapshot () {
103109 name , labels := parseMetricKey (key )
104110 fqName := buildFQName (c .namespace , name )
111+ families [fqName ] = append (families [fqName ], series {value : value , labels : labels })
112+ }
105113
106- switch v := value .(type ) {
107- case float64 :
108- c .emitScalar (ch , fqName , prometheus .GaugeValue , "gauge" , v , labels )
109- case int64 :
110- c .emitScalar (ch , fqName , prometheus .GaugeValue , "gauge" , float64 (v ), labels )
111- case uint64 :
112- c .emitScalar (ch , fqName , prometheus .GaugeValue , "gauge" , float64 (v ), labels )
113- case map [string ]any :
114- c .emitComplex (ch , fqName , v , labels )
114+ for fqName , list := range families {
115+ keys := unionKeys (list ) // sanitized, sorted union of label keys
116+ seen := make (map [string ]bool ) // dedup by joined label values
117+ for _ , s := range list {
118+ vals := alignValues (keys , s .labels )
119+ sig := strings .Join (vals , "\x1f " )
120+ if seen [sig ] {
121+ continue
122+ }
123+ seen [sig ] = true
124+ c .emit (ch , fqName , keys , vals , s .value )
115125 }
116- // Unknown shapes are skipped.
126+ }
127+ }
128+
129+ func (c * forgeCollector ) emit (ch chan <- prometheus.Metric , fqName string ,
130+ keys , vals []string , value any ) {
131+ switch v := value .(type ) {
132+ case float64 :
133+ c .emitScalar (ch , fqName , keys , vals , prometheus .GaugeValue , "gauge" , v )
134+ case int64 :
135+ c .emitScalar (ch , fqName , keys , vals , prometheus .GaugeValue , "gauge" , float64 (v ))
136+ case uint64 :
137+ c .emitScalar (ch , fqName , keys , vals , prometheus .GaugeValue , "gauge" , float64 (v ))
138+ case map [string ]any :
139+ c .emitComplex (ch , fqName , keys , vals , v )
117140 }
118141}
119142
120143func (c * forgeCollector ) emitScalar (ch chan <- prometheus.Metric , fqName string ,
121- vt prometheus.ValueType , kind string , value float64 , labels map [string ]string ) {
122- keys , vals := sortedLabels (labels )
144+ keys , vals []string , vt prometheus.ValueType , kind string , value float64 ) {
123145 desc := prometheus .NewDesc (fqName , helpFor (kind , fqName ), keys , nil )
124146 ch <- prometheus .MustNewConstMetric (desc , vt , value , vals ... )
125147}
126148
127149func (c * forgeCollector ) emitComplex (ch chan <- prometheus.Metric , fqName string ,
128- v map [ string ] any , labels map [string ]string ) {
150+ keys , vals [] string , v map [string ]any ) {
129151 if t , _ := v ["_type" ].(string ); t == "counter" {
130152 if val , ok := toFloat (v ["value" ]); ok {
131- c .emitScalar (ch , fqName , prometheus .CounterValue , "counter" , val , labels )
153+ c .emitScalar (ch , fqName , keys , vals , prometheus .CounterValue , "counter" , val )
132154 }
133155 return
134156 }
135157
136158 if raw , ok := v ["buckets" ].(map [float64 ]uint64 ); ok {
137- c .emitHistogram (ch , fqName , v , raw , labels )
159+ c .emitHistogram (ch , fqName , keys , vals , v , raw )
138160 return
139161 }
140162
141163 if _ , ok := v ["count" ]; ok {
142- c .emitTimer (ch , fqName , v , labels )
164+ c .emitTimer (ch , fqName , keys , vals , v )
143165 return
144166 }
145167}
146168
147169func (c * forgeCollector ) emitTimer (ch chan <- prometheus.Metric , fqName string ,
148- v map [ string ] any , labels map [string ]string ) {
170+ keys , vals [] string , v map [string ]any ) {
149171 count , _ := toUint64 (v ["count" ])
150172
151173 quantiles := make (map [float64 ]float64 )
@@ -160,7 +182,6 @@ func (c *forgeCollector) emitTimer(ch chan<- prometheus.Metric, fqName string,
160182 sum = mean * float64 (count )
161183 }
162184
163- keys , vals := sortedLabels (labels )
164185 desc := prometheus .NewDesc (fqName , helpFor ("summary" , fqName ), keys , nil )
165186 ch <- prometheus .MustNewConstSummary (desc , count , sum , quantiles , vals ... )
166187}
@@ -179,7 +200,7 @@ func durationSeconds(v any) (float64, bool) {
179200}
180201
181202func (c * forgeCollector ) emitHistogram (ch chan <- prometheus.Metric , fqName string ,
182- v map [string ]any , perBucket map [float64 ]uint64 , labels map [ string ] string ) {
203+ keys , vals [] string , v map [string ]any , perBucket map [float64 ]uint64 ) {
183204 bounds := make ([]float64 , 0 , len (perBucket ))
184205 for b := range perBucket {
185206 bounds = append (bounds , b )
@@ -199,7 +220,6 @@ func (c *forgeCollector) emitHistogram(ch chan<- prometheus.Metric, fqName strin
199220 }
200221 sum , _ := toFloat (v ["sum" ])
201222
202- keys , vals := sortedLabels (labels )
203223 desc := prometheus .NewDesc (fqName , helpFor ("histogram" , fqName ), keys , nil )
204224 ch <- prometheus .MustNewConstHistogram (desc , count , sum , cumulative , vals ... )
205225}
@@ -276,24 +296,33 @@ func sanitizeName(s string) string {
276296 return b .String ()
277297}
278298
279- // sortedLabels returns label keys (sanitized, sorted) and values in matching order.
280- func sortedLabels (labels map [string ]string ) ([]string , []string ) {
281- if len (labels ) == 0 {
282- return nil , nil
299+ // unionKeys returns the sanitized, sorted union of label keys across all series.
300+ func unionKeys (list []series ) []string {
301+ set := make (map [string ]struct {})
302+ for _ , s := range list {
303+ for k := range s .labels {
304+ set [sanitizeName (k )] = struct {}{}
305+ }
283306 }
284- keys := make ([]string , 0 , len (labels ))
285- for k := range labels {
307+ keys := make ([]string , 0 , len (set ))
308+ for k := range set {
286309 keys = append (keys , k )
287310 }
288311 sort .Strings (keys )
312+ return keys
313+ }
289314
290- outKeys := make ([]string , len (keys ))
291- outVals := make ([]string , len (keys ))
315+ // alignValues returns label values ordered to match keys, "" for absent keys.
316+ func alignValues (keys []string , labels map [string ]string ) []string {
317+ sanitized := make (map [string ]string , len (labels ))
318+ for k , v := range labels {
319+ sanitized [sanitizeName (k )] = v
320+ }
321+ vals := make ([]string , len (keys ))
292322 for i , k := range keys {
293- outKeys [i ] = sanitizeName (k )
294- outVals [i ] = labels [k ]
323+ vals [i ] = sanitized [k ]
295324 }
296- return outKeys , outVals
325+ return vals
297326}
298327
299328func helpFor (kind , fqName string ) string {
0 commit comments