Skip to content

Commit c681cb6

Browse files
authored
exporters/prometheus: sort labels (#2278)
Some prometheus implementations require the labels to be sorted, or they will reject write requests
1 parent 0bb2618 commit c681cb6

File tree

3 files changed

+80
-4
lines changed

3 files changed

+80
-4
lines changed

runtimes/core/src/metrics/exporter/prometheus.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ impl Prometheus {
166166
value: metric_name,
167167
});
168168

169+
// Sort labels lexicographically by name, as required by some Prometheus implementations.
170+
labels.sort_unstable_by(|a, b| a.name.cmp(&b.name));
171+
169172
// Convert metric value to float64
170173
let value = match metric.value {
171174
MetricValue::CounterU64(val) => val as f64,
@@ -212,3 +215,66 @@ fn from_time(t: SystemTime) -> i64 {
212215
mod prompb {
213216
include!(concat!(env!("OUT_DIR"), "/prometheus.rs"));
214217
}
218+
219+
#[cfg(test)]
220+
mod tests {
221+
use super::*;
222+
use crate::encore::runtime::v1 as pb;
223+
use std::time::SystemTime;
224+
225+
/// Test that labels are sorted lexicographically by name.
226+
/// Some Prometheus implementations require this or they will reject the request
227+
/// with "out of order labels" error.
228+
#[tokio::test]
229+
async fn test_labels_are_sorted() {
230+
let env = pb::Environment::default();
231+
let container_meta_client = ContainerMetaClient::new(env, reqwest::Client::new());
232+
233+
// Create a metric with labels in non-sorted order: "zebra", "apple", "middle"
234+
let key = metrics::Key::from_parts(
235+
"test_metric",
236+
vec![
237+
metrics::Label::new("zebra", "last"),
238+
metrics::Label::new("apple", "first"),
239+
metrics::Label::new("middle", "mid"),
240+
],
241+
);
242+
243+
let collected = vec![CollectedMetric {
244+
key,
245+
value: MetricValue::CounterU64(42),
246+
registered_at: SystemTime::now(),
247+
}];
248+
249+
let prometheus = Prometheus {
250+
client: reqwest::Client::new(),
251+
remote_write_url: Url::parse("http://localhost:9090/api/v1/write").unwrap(),
252+
container_meta_client,
253+
container_labels: OnceCell::new(),
254+
};
255+
256+
let time_series = prometheus.get_metric_data(collected).await;
257+
assert_eq!(time_series.len(), 1);
258+
259+
let label_names: Vec<&str> = time_series[0]
260+
.labels
261+
.iter()
262+
.map(|l| l.name.as_str())
263+
.collect();
264+
265+
// Labels should be sorted lexicographically
266+
assert_eq!(
267+
label_names,
268+
vec![
269+
"__name__",
270+
"apple",
271+
"env_name",
272+
"instance_id",
273+
"middle",
274+
"revision_id",
275+
"service_id",
276+
"zebra"
277+
]
278+
);
279+
}
280+
}

runtimes/go/appruntime/infrasdk/metrics/prometheus/prometheus.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"context"
88
"fmt"
99
"net/http"
10+
"slices"
11+
"strings"
1012
"time"
1113

1214
"github.com/golang/protobuf/proto"
@@ -78,6 +80,10 @@ func (x *Exporter) getMetricData(now time.Time, collected []metrics.CollectedMet
7880
copy(labels, baseLabels)
7981
labels[len(baseLabels)] = &prompb.Label{Name: "__name__", Value: metricName}
8082
labels[len(baseLabels)+1] = &prompb.Label{Name: "service", Value: x.svcs[svcIdx]}
83+
// Sort labels lexicographically by name, as required by some Prometheus implementations.
84+
slices.SortFunc(labels, func(a, b *prompb.Label) int {
85+
return strings.Compare(a.Name, b.Name)
86+
})
8187
data = append(data, &prompb.TimeSeries{
8288
Labels: labels,
8389
Samples: []*prompb.Sample{
@@ -166,6 +172,10 @@ func (x *Exporter) getSysMetrics(now time.Time) []*prompb.TimeSeries {
166172
Name: "__name__",
167173
Value: metricName,
168174
}
175+
// Sort labels lexicographically by name, as required by some Prometheus implementations.
176+
slices.SortFunc(labels, func(a, b *prompb.Label) int {
177+
return strings.Compare(a.Name, b.Name)
178+
})
169179
return labels
170180
}
171181

runtimes/go/appruntime/infrasdk/metrics/prometheus/prometheus_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ func TestGetMetricData(t *testing.T) {
107107
data: []*prompb.TimeSeries{
108108
{
109109
Labels: []*prompb.Label{
110-
{
111-
Name: "key",
112-
Value: "value",
113-
},
114110
{
115111
Name: "__name__",
116112
Value: "test_labels",
117113
},
114+
{
115+
Name: "key",
116+
Value: "value",
117+
},
118118
{
119119
Name: "service",
120120
Value: "foo",

0 commit comments

Comments
 (0)