Skip to content

Commit 61ae816

Browse files
authored
fix timestamp representation (#1292)
Fixes #1277 --------- Signed-off-by: Gregor Zeitlinger <[email protected]>
1 parent 6106004 commit 61ae816

File tree

13 files changed

+219
-36
lines changed

13 files changed

+219
-36
lines changed

benchmarks/src/main/java/io/prometheus/metrics/benchmarks/TextFormatUtilBenchmark.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ public class TextFormatUtilBenchmark {
5050
}
5151

5252
private static final ExpositionFormatWriter OPEN_METRICS_TEXT_FORMAT_WRITER =
53-
new OpenMetricsTextFormatWriter(false, false);
53+
OpenMetricsTextFormatWriter.create();
5454
private static final ExpositionFormatWriter PROMETHEUS_TEXT_FORMAT_WRITER =
55-
new PrometheusTextFormatWriter(false);
55+
PrometheusTextFormatWriter.create();
5656

5757
@State(Scope.Benchmark)
5858
public static class WriterState {

mise.toml

+7-3
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@ env = { REQUIRE_PROTO_UP_TO_DATE = "true" }
1919
description = "format source code"
2020
run = "./mvnw spotless:apply"
2121

22+
[tasks.compile]
23+
description = "bare compile, ignoring formatting and linters"
24+
run = "./mvnw install -DskipTests -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
25+
2226
[tasks.test]
2327
description = "run unit tests, ignoring formatting and linters"
24-
run = "./mvnw test -DskipTests=true -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
28+
run = "./mvnw test -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
2529

2630
[tasks.test-all]
2731
description = "run all tests"
2832
run = "./mvnw verify"
2933

3034
[tasks.build]
3135
description = "build all modules wihthout tests"
32-
run = "./mvnw clean install -DskipTests"
36+
run = "./mvnw install -DskipTests"
3337

3438
[tasks.acceptance-test]
3539
description = "Run OATs acceptance tests"
@@ -39,7 +43,7 @@ run = "oats -timeout 5m examples/"
3943
[tasks.native-test]
4044
depends = "build"
4145
tools.java = "graalvm-22.3.3+java17"
42-
run = "../../mvnw test -PnativeTest"
46+
run = "../../mvnw test -PnativeTest"
4347
dir = "integration-tests/it-spring-boot-smoke-test"
4448

4549
[settings]

prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterProperties.java

+26-3
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@
66
public class ExporterProperties {
77

88
private static final String INCLUDE_CREATED_TIMESTAMPS = "includeCreatedTimestamps";
9+
// milliseconds is the default - we only provide a boolean flag to avoid a breaking change
10+
private static final String PROMETHEUS_TIMESTAMPS_IN_MS = "prometheusTimestampsInMs";
911
private static final String EXEMPLARS_ON_ALL_METRIC_TYPES = "exemplarsOnAllMetricTypes";
1012
private static final String PREFIX = "io.prometheus.exporter";
1113

1214
private final Boolean includeCreatedTimestamps;
15+
private final Boolean prometheusTimestampsInMs;
1316
private final Boolean exemplarsOnAllMetricTypes;
1417

15-
private ExporterProperties(Boolean includeCreatedTimestamps, Boolean exemplarsOnAllMetricTypes) {
18+
private ExporterProperties(
19+
Boolean includeCreatedTimestamps,
20+
Boolean prometheusTimestampsInMs,
21+
Boolean exemplarsOnAllMetricTypes) {
1622
this.includeCreatedTimestamps = includeCreatedTimestamps;
23+
this.prometheusTimestampsInMs = prometheusTimestampsInMs;
1724
this.exemplarsOnAllMetricTypes = exemplarsOnAllMetricTypes;
1825
}
1926

@@ -22,6 +29,11 @@ public boolean getIncludeCreatedTimestamps() {
2229
return includeCreatedTimestamps != null && includeCreatedTimestamps;
2330
}
2431

32+
/** Use milliseconds for timestamps in prometheus text format? Default is {@code false}. */
33+
public boolean getPrometheusTimestampsInMs() {
34+
return prometheusTimestampsInMs != null && prometheusTimestampsInMs;
35+
}
36+
2537
/**
2638
* Allow Exemplars on all metric types in OpenMetrics format? Default is {@code false}, which
2739
* means Exemplars will only be added for Counters and Histogram buckets.
@@ -38,9 +50,12 @@ static ExporterProperties load(Map<Object, Object> properties)
3850
throws PrometheusPropertiesException {
3951
Boolean includeCreatedTimestamps =
4052
Util.loadBoolean(PREFIX + "." + INCLUDE_CREATED_TIMESTAMPS, properties);
53+
Boolean timestampsInMs =
54+
Util.loadBoolean(PREFIX + "." + PROMETHEUS_TIMESTAMPS_IN_MS, properties);
4155
Boolean exemplarsOnAllMetricTypes =
4256
Util.loadBoolean(PREFIX + "." + EXEMPLARS_ON_ALL_METRIC_TYPES, properties);
43-
return new ExporterProperties(includeCreatedTimestamps, exemplarsOnAllMetricTypes);
57+
return new ExporterProperties(
58+
includeCreatedTimestamps, timestampsInMs, exemplarsOnAllMetricTypes);
4459
}
4560

4661
public static Builder builder() {
@@ -51,6 +66,7 @@ public static class Builder {
5166

5267
private Boolean includeCreatedTimestamps;
5368
private Boolean exemplarsOnAllMetricTypes;
69+
boolean prometheusTimestampsInMs;
5470

5571
private Builder() {}
5672

@@ -66,8 +82,15 @@ public Builder exemplarsOnAllMetricTypes(boolean exemplarsOnAllMetricTypes) {
6682
return this;
6783
}
6884

85+
/** See {@link #getPrometheusTimestampsInMs()}. */
86+
public Builder prometheusTimestampsInMs(boolean prometheusTimestampsInMs) {
87+
this.prometheusTimestampsInMs = prometheusTimestampsInMs;
88+
return this;
89+
}
90+
6991
public ExporterProperties build() {
70-
return new ExporterProperties(includeCreatedTimestamps, exemplarsOnAllMetricTypes);
92+
return new ExporterProperties(
93+
includeCreatedTimestamps, prometheusTimestampsInMs, exemplarsOnAllMetricTypes);
7194
}
7295
}
7396
}

prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ void builder() {
4848
ExporterProperties.builder()
4949
.includeCreatedTimestamps(true)
5050
.exemplarsOnAllMetricTypes(true)
51+
.prometheusTimestampsInMs(false)
5152
.build();
5253
assertThat(properties.getIncludeCreatedTimestamps()).isTrue();
5354
assertThat(properties.getExemplarsOnAllMetricTypes()).isTrue();

prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java

+30-3
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class PushGateway {
8080

8181
private final URL url;
8282
private final ExpositionFormatWriter writer;
83+
private final boolean prometheusTimestampsInMs;
8384
private final Map<String, String> requestHeaders;
8485
private final PrometheusRegistry registry;
8586
private final HttpConnectionFactory connectionFactory;
@@ -89,20 +90,25 @@ private PushGateway(
8990
Format format,
9091
URL url,
9192
HttpConnectionFactory connectionFactory,
92-
Map<String, String> requestHeaders) {
93+
Map<String, String> requestHeaders,
94+
boolean prometheusTimestampsInMs) {
9395
this.registry = registry;
9496
this.url = url;
9597
this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders));
9698
this.connectionFactory = connectionFactory;
99+
this.prometheusTimestampsInMs = prometheusTimestampsInMs;
97100
writer = getWriter(format);
98101
if (!writer.isAvailable()) {
99102
throw new RuntimeException(writer.getClass() + " is not available");
100103
}
101104
}
102105

106+
@SuppressWarnings("deprecation")
103107
private ExpositionFormatWriter getWriter(Format format) {
104108
if (format == Format.PROMETHEUS_TEXT) {
105-
return new PrometheusTextFormatWriter(false);
109+
return PrometheusTextFormatWriter.builder()
110+
.setTimestampsInMs(this.prometheusTimestampsInMs)
111+
.build();
106112
} else {
107113
// use reflection to avoid a compile-time dependency on the expositionformats module
108114
return new PrometheusProtobufWriter();
@@ -264,6 +270,7 @@ public static class Builder {
264270
private String address;
265271
private Scheme scheme;
266272
private String job;
273+
private boolean prometheusTimestampsInMs;
267274
private final Map<String, String> requestHeaders = new HashMap<>();
268275
private PrometheusRegistry registry = PrometheusRegistry.defaultRegistry;
269276
private HttpConnectionFactory connectionFactory = new DefaultHttpConnectionFactory();
@@ -380,6 +387,21 @@ public Builder registry(PrometheusRegistry registry) {
380387
return this;
381388
}
382389

390+
/**
391+
* Use milliseconds for timestamps in text format? Default is {@code false}. Can be overwritten
392+
* at runtime with the {@code io.prometheus.exporter.timestampsInMs} property.
393+
*/
394+
public Builder prometheusTimestampsInMs(boolean prometheusTimestampsInMs) {
395+
this.prometheusTimestampsInMs = prometheusTimestampsInMs;
396+
return this;
397+
}
398+
399+
private boolean getPrometheusTimestampsInMs() {
400+
// accept either to opt in to timestamps in milliseconds
401+
return config.getExporterProperties().getPrometheusTimestampsInMs()
402+
|| this.prometheusTimestampsInMs;
403+
}
404+
383405
private Scheme getScheme(ExporterPushgatewayProperties properties) {
384406
if (properties != null && properties.getScheme() != null) {
385407
return Scheme.valueOf(properties.getScheme());
@@ -453,7 +475,12 @@ public PushGateway build() {
453475
config == null ? null : config.getExporterPushgatewayProperties();
454476
try {
455477
return new PushGateway(
456-
registry, getFormat(), makeUrl(properties), connectionFactory, requestHeaders);
478+
registry,
479+
getFormat(),
480+
makeUrl(properties),
481+
connectionFactory,
482+
requestHeaders,
483+
getPrometheusTimestampsInMs());
457484
} catch (MalformedURLException e) {
458485
throw new PrometheusPropertiesException(
459486
address + ": Invalid address. Expecting <host>:<port>");

prometheus-metrics-exporter-pushgateway/src/test/java/io/prometheus/metrics/exporter/pushgateway/BasicAuthPushGatewayTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public void setUp() {
2929
.address("localhost:" + mockServerClient.getPort())
3030
.basicAuth("testUser", "testPwd")
3131
.registry(registry)
32+
.prometheusTimestampsInMs(true)
3233
.job("j")
3334
.build();
3435
}

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@ public static ExpositionFormats init() {
2222
return init(PrometheusProperties.get().getExporterProperties());
2323
}
2424

25+
@SuppressWarnings("deprecation")
2526
public static ExpositionFormats init(ExporterProperties properties) {
2627
return new ExpositionFormats(
2728
new PrometheusProtobufWriter(),
28-
new PrometheusTextFormatWriter(properties.getIncludeCreatedTimestamps()),
29-
new OpenMetricsTextFormatWriter(
30-
properties.getIncludeCreatedTimestamps(), properties.getExemplarsOnAllMetricTypes()));
29+
PrometheusTextFormatWriter.builder()
30+
.setIncludeCreatedTimestamps(properties.getIncludeCreatedTimestamps())
31+
.setTimestampsInMs(properties.getPrometheusTimestampsInMs())
32+
.build(),
33+
OpenMetricsTextFormatWriter.builder()
34+
.setCreatedTimestampsEnabled(properties.getIncludeCreatedTimestamps())
35+
.setExemplarsOnAllMetricTypesEnabled(properties.getExemplarsOnAllMetricTypes())
36+
.build());
3137
}
3238

3339
public ExpositionFormatWriter findWriter(String acceptHeader) {

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java

+44-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedLabelValue;
55
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels;
66
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
7-
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeTimestamp;
7+
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp;
88

99
import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
1010
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
@@ -37,21 +37,58 @@
3737
*/
3838
public class OpenMetricsTextFormatWriter implements ExpositionFormatWriter {
3939

40+
public static class Builder {
41+
boolean createdTimestampsEnabled;
42+
boolean exemplarsOnAllMetricTypesEnabled;
43+
44+
private Builder() {}
45+
46+
/**
47+
* @param createdTimestampsEnabled whether to include the _created timestamp in the output
48+
*/
49+
public Builder setCreatedTimestampsEnabled(boolean createdTimestampsEnabled) {
50+
this.createdTimestampsEnabled = createdTimestampsEnabled;
51+
return this;
52+
}
53+
54+
/**
55+
* @param exemplarsOnAllMetricTypesEnabled whether to include exemplars in the output for all
56+
* metric types
57+
*/
58+
public Builder setExemplarsOnAllMetricTypesEnabled(boolean exemplarsOnAllMetricTypesEnabled) {
59+
this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
60+
return this;
61+
}
62+
63+
public OpenMetricsTextFormatWriter build() {
64+
return new OpenMetricsTextFormatWriter(
65+
createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled);
66+
}
67+
}
68+
4069
public static final String CONTENT_TYPE =
4170
"application/openmetrics-text; version=1.0.0; charset=utf-8";
4271
private final boolean createdTimestampsEnabled;
4372
private final boolean exemplarsOnAllMetricTypesEnabled;
4473

4574
/**
46-
* @param createdTimestampsEnabled defines if {@code _created} timestamps should be included in
47-
* the output or not.
75+
* @param createdTimestampsEnabled whether to include the _created timestamp in the output - This
76+
* will produce an invalid OpenMetrics output, but is kept for backwards compatibility.
4877
*/
4978
public OpenMetricsTextFormatWriter(
5079
boolean createdTimestampsEnabled, boolean exemplarsOnAllMetricTypesEnabled) {
5180
this.createdTimestampsEnabled = createdTimestampsEnabled;
5281
this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
5382
}
5483

84+
public static Builder builder() {
85+
return new Builder();
86+
}
87+
88+
public static OpenMetricsTextFormatWriter create() {
89+
return builder().build();
90+
}
91+
5592
@Override
5693
public boolean accepts(String acceptHeader) {
5794
if (acceptHeader == null) {
@@ -299,10 +336,10 @@ private void writeCreated(Writer writer, MetricMetadata metadata, DataPointSnaps
299336
throws IOException {
300337
if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
301338
writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels());
302-
writeTimestamp(writer, data.getCreatedTimestampMillis());
339+
writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
303340
if (data.hasScrapeTimestamp()) {
304341
writer.write(' ');
305-
writeTimestamp(writer, data.getScrapeTimestampMillis());
342+
writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
306343
}
307344
writer.write('\n');
308345
}
@@ -335,7 +372,7 @@ private void writeScrapeTimestampAndExemplar(
335372
Writer writer, DataPointSnapshot data, Exemplar exemplar) throws IOException {
336373
if (data.hasScrapeTimestamp()) {
337374
writer.write(' ');
338-
writeTimestamp(writer, data.getScrapeTimestampMillis());
375+
writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
339376
}
340377
if (exemplar != null) {
341378
writer.write(" # ");
@@ -344,7 +381,7 @@ private void writeScrapeTimestampAndExemplar(
344381
writeDouble(writer, exemplar.getValue());
345382
if (exemplar.hasTimestamp()) {
346383
writer.write(' ');
347-
writeTimestamp(writer, exemplar.getTimestampMillis());
384+
writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis());
348385
}
349386
}
350387
writer.write('\n');

0 commit comments

Comments
 (0)