From 2ed721e4570e13cb65f45049fea5c8fe87f8f9e6 Mon Sep 17 00:00:00 2001
From: Aliaksandr Valialkin <valyala@gmail.com>
Date: Fri, 27 Nov 2020 14:53:27 +0200
Subject: [PATCH] lib/protoparser/prometheus: properly parse OpenMetrics
 timestamps

OpenMetrics timestamps are floating-point numbers, that represent Unix timestamp in seconds.
This differs from Prometheus exposition format, where timestamps are integer numbers representing Unix timestamp in milliseconds.
---
 docs/CHANGELOG.md                             |  3 ++
 lib/protoparser/prometheus/parser.go          | 11 ++++--
 lib/protoparser/prometheus/parser_test.go     | 34 ++++++++++++-------
 .../prometheus/streamparser_test.go           |  6 ++--
 4 files changed, 37 insertions(+), 17 deletions(-)

diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 3fc22ec55c..b26e747782 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 # tip
 
+* BUGFIX: properly parse timestamps in OpenMetrics format - they are exposed as floating-point number in seconds instead of integer milliseconds
+  unlike in Prometheus exposition format. See [the docs](https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md#timestamps).
+
 
 # [v1.48.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.48.0)
 
diff --git a/lib/protoparser/prometheus/parser.go b/lib/protoparser/prometheus/parser.go
index 9d27538262..9938ba2292 100644
--- a/lib/protoparser/prometheus/parser.go
+++ b/lib/protoparser/prometheus/parser.go
@@ -166,11 +166,18 @@ func (r *Row) unmarshal(s string, tagsPool []Tag, noEscapes bool) ([]Tag, error)
 		// There is no timestamp - just a whitespace after the value.
 		return tagsPool, nil
 	}
-	ts, err := fastfloat.ParseInt64(s)
+	ts, err := fastfloat.Parse(s)
 	if err != nil {
 		return tagsPool, fmt.Errorf("cannot parse timestamp %q: %w", s, err)
 	}
-	r.Timestamp = ts
+	if ts >= -1<<31 && ts < 1<<31 {
+		// This looks like OpenMetrics timestamp in Unix seconds.
+		// Convert it to milliseconds.
+		//
+		// See https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md#timestamps
+		ts *= 1000
+	}
+	r.Timestamp = int64(ts)
 	return tagsPool, nil
 }
 
diff --git a/lib/protoparser/prometheus/parser_test.go b/lib/protoparser/prometheus/parser_test.go
index 7451397332..fb5a783654 100644
--- a/lib/protoparser/prometheus/parser_test.go
+++ b/lib/protoparser/prometheus/parser_test.go
@@ -178,14 +178,14 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
 		Rows: []Row{{
 			Metric:    "foobar",
 			Value:     123.456,
-			Timestamp: 789,
+			Timestamp: 789000,
 		}},
 	})
-	f("foobar{} 123.456 789\n", &Rows{
+	f("foobar{} 123.456 789.4354\n", &Rows{
 		Rows: []Row{{
 			Metric:    "foobar",
 			Value:     123.456,
-			Timestamp: 789,
+			Timestamp: 789435,
 		}},
 	})
 	f(`#                                    _                                            _
@@ -225,7 +225,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 			{
 				Metric:    "abc",
 				Value:     123,
-				Timestamp: 456,
+				Timestamp: 456000,
 			},
 			{
 				Metric: "foo",
@@ -274,7 +274,8 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 		},
 	})
 
-	// Timestamp bigger than 1<<31
+	// Timestamp bigger than 1<<31.
+	// It should be parsed in milliseconds.
 	f("aaa 1123 429496729600", &Rows{
 		Rows: []Row{{
 			Metric:    "aaa",
@@ -283,6 +284,15 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 		}},
 	})
 
+	// Floating-point timestamps in OpenMetric format.
+	f("aaa 1123 42949.567", &Rows{
+		Rows: []Row{{
+			Metric:    "aaa",
+			Value:     1123,
+			Timestamp: 42949567,
+		}},
+	})
+
 	// Tags
 	f(`foo{bar="baz"} 1 2`, &Rows{
 		Rows: []Row{{
@@ -292,7 +302,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 				Value: "baz",
 			}},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		}},
 	})
 	f(`foo{bar="b\"a\\z"} -1.2`, &Rows{
@@ -324,7 +334,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 				},
 			},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		}},
 	})
 
@@ -338,7 +348,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 				Value: "baz",
 			}},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		}},
 	})
 
@@ -348,7 +358,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 			{
 				Metric:    "foo",
 				Value:     0.3,
-				Timestamp: 2,
+				Timestamp: 2000,
 			},
 			{
 				Metric: "aaa",
@@ -357,7 +367,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 			{
 				Metric:    "bar.baz",
 				Value:     0.34,
-				Timestamp: 43,
+				Timestamp: 43000,
 			},
 		},
 	})
@@ -368,12 +378,12 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 			{
 				Metric:    "foo",
 				Value:     0.3,
-				Timestamp: 2,
+				Timestamp: 2000,
 			},
 			{
 				Metric:    "bar.baz",
 				Value:     0.34,
-				Timestamp: 43,
+				Timestamp: 43000,
 			},
 		},
 	})
diff --git a/lib/protoparser/prometheus/streamparser_test.go b/lib/protoparser/prometheus/streamparser_test.go
index 6d60cb5ea6..d2b88ede6c 100644
--- a/lib/protoparser/prometheus/streamparser_test.go
+++ b/lib/protoparser/prometheus/streamparser_test.go
@@ -82,13 +82,13 @@ func TestParseStream(t *testing.T) {
 	f("foo 123 456", []Row{{
 		Metric:    "foo",
 		Value:     123,
-		Timestamp: 456,
+		Timestamp: 456000,
 	}})
 	f(`foo{bar="baz"} 1 2`+"\n"+`aaa{} 3 4`, []Row{
 		{
 			Metric:    "aaa",
 			Value:     3,
-			Timestamp: 4,
+			Timestamp: 4000,
 		},
 		{
 			Metric: "foo",
@@ -97,7 +97,7 @@ func TestParseStream(t *testing.T) {
 				Value: "baz",
 			}},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		},
 	})
 	f("foo 23", []Row{{