Skip to content

Commit d68362b

Browse files
authored
Merge pull request #113 from quickwit-oss/ddelemeny/revamp-parse-time
Revamp time parser function
2 parents d35a8bf + a6688f3 commit d68362b

File tree

6 files changed

+164
-48
lines changed

6 files changed

+164
-48
lines changed

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ require (
3838
github.com/hashicorp/go-plugin v1.4.9 // indirect
3939
github.com/hashicorp/yamux v0.1.1 // indirect
4040
github.com/invopop/yaml v0.1.0 // indirect
41+
github.com/itchyny/timefmt-go v0.1.5
4142
github.com/josharian/intern v1.0.0 // indirect
4243
github.com/json-iterator/go v1.1.12 // indirect
4344
github.com/klauspost/compress v1.13.1 // indirect

Diff for: go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg
211211
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
212212
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
213213
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
214+
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
215+
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
214216
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
215217
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
216218
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=

Diff for: pkg/quickwit/response_parser.go

+2-40
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import (
1212

1313
"github.com/grafana/grafana-plugin-sdk-go/backend"
1414
"github.com/grafana/grafana-plugin-sdk-go/data"
15-
"golang.org/x/exp/slices"
1615

1716
es "github.com/quickwit-oss/quickwit-datasource/pkg/quickwit/client"
1817
"github.com/quickwit-oss/quickwit-datasource/pkg/quickwit/simplejson"
18+
utils "github.com/quickwit-oss/quickwit-datasource/pkg/utils"
1919
)
2020

2121
const (
@@ -247,7 +247,7 @@ func processDocsToDataFrameFields(docs []map[string]interface{}, propNames []str
247247
if propName == configuredFields.TimeField {
248248
timeVector := make([]*time.Time, size)
249249
for i, doc := range docs {
250-
timeValue, err := ParseToTime(doc[configuredFields.TimeField], configuredFields.TimeOutputFormat)
250+
timeValue, err := utils.ParseTime(doc[configuredFields.TimeField], configuredFields.TimeOutputFormat)
251251
if err != nil {
252252
continue
253253
}
@@ -291,44 +291,6 @@ func processDocsToDataFrameFields(docs []map[string]interface{}, propNames []str
291291
return allFields
292292
}
293293

294-
// Parses a value into Time given a timeOutputFormat. The conversion
295-
// only works with float64 as this is what we get when parsing a response.
296-
// TODO: understand why we get a float64?
297-
func ParseToTime(value interface{}, timeOutputFormat string) (time.Time, error) {
298-
299-
if timeOutputFormat == Iso8601 || timeOutputFormat == Rfc3339 {
300-
value_string := value.(string)
301-
timeValue, err := time.Parse(time.RFC3339, value_string)
302-
if err != nil {
303-
return time.Time{}, err
304-
}
305-
return timeValue, nil
306-
} else if timeOutputFormat == Rfc2822 {
307-
value_string := value.(string)
308-
timeValue, err := time.Parse(time.RFC822Z, value_string)
309-
if err != nil {
310-
return time.Time{}, err
311-
}
312-
return timeValue, nil
313-
} else if slices.Contains([]string{TimestampSecs, TimestampMillis, TimestampMicros, TimestampNanos}, timeOutputFormat) {
314-
typed_value, ok := value.(float64)
315-
if !ok {
316-
return time.Time{}, errors.New("parse time only accepts float64 with timestamp based format")
317-
}
318-
int64_value := int64(typed_value)
319-
if timeOutputFormat == TimestampSecs {
320-
return time.Unix(int64_value, 0), nil
321-
} else if timeOutputFormat == TimestampMillis {
322-
return time.Unix(0, int64_value*1_000_000), nil
323-
} else if timeOutputFormat == TimestampMicros {
324-
return time.Unix(0, int64_value*1_000), nil
325-
} else if timeOutputFormat == TimestampNanos {
326-
return time.Unix(0, int64_value), nil
327-
}
328-
}
329-
return time.Time{}, fmt.Errorf("timeOutputFormat not supported yet %s", timeOutputFormat)
330-
}
331-
332294
func processBuckets(aggs map[string]interface{}, target *Query,
333295
queryResult *backend.DataResponse, props map[string]string, depth int) error {
334296
var err error

Diff for: pkg/quickwit/response_parser_qw_test.go

-8
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,3 @@ func TestProcessLogsResponseWithDifferentTimeOutputFormat(t *testing.T) {
278278
require.Equal(t, &expectedTimeValue, logsFieldMap["testtime"].At(0))
279279
})
280280
}
281-
282-
func TestConvertToTime(t *testing.T) {
283-
t.Run("Test parse unix timestamps nanosecs of float type", func(t *testing.T) {
284-
inputValue := interface{}(1234567890000000000.0)
285-
value, _ := ParseToTime(inputValue, "unix_timestamp_nanos")
286-
require.Equal(t, time.Unix(1234567890, 0), value)
287-
})
288-
}

Diff for: pkg/utils/parse_time.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package utils
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"reflect"
7+
"time"
8+
9+
timefmt "github.com/itchyny/timefmt-go"
10+
)
11+
12+
const (
13+
Iso8601 string = "iso8601"
14+
Rfc2822 string = "rfc2822" // timezone name
15+
Rfc2822z string = "rfc2822z" // explicit timezone
16+
Rfc3339 string = "rfc3339"
17+
TimestampSecs string = "unix_timestamp_secs"
18+
TimestampMillis string = "unix_timestamp_millis"
19+
TimestampMicros string = "unix_timestamp_micros"
20+
TimestampNanos string = "unix_timestamp_nanos"
21+
)
22+
23+
const Rfc2822Layout string = "%a, %d %b %Y %T %Z"
24+
const Rfc2822zLayout string = "%a, %d %b %Y %T %z"
25+
26+
// Parses a value into Time given a timeOutputFormat. The conversion
27+
// only works with float64 as this is what we get when parsing a response.
28+
func ParseTime(value any, timeOutputFormat string) (time.Time, error) {
29+
switch timeOutputFormat {
30+
case Iso8601, Rfc3339:
31+
value_string := value.(string)
32+
timeValue, err := time.Parse(time.RFC3339, value_string)
33+
if err != nil {
34+
return time.Time{}, err
35+
}
36+
return timeValue, nil
37+
38+
case Rfc2822:
39+
// XXX: the time package's layout for RFC2822 is bogus, don't use that.
40+
value_string := value.(string)
41+
timeValue, err := timefmt.Parse(value_string, Rfc2822Layout)
42+
if err != nil {
43+
return time.Time{}, err
44+
}
45+
return timeValue, nil
46+
case Rfc2822z:
47+
// XXX: the time package's layout for RFC2822 is bogus, don't use that.
48+
value_string := value.(string)
49+
timeValue, err := timefmt.Parse(value_string, Rfc2822zLayout)
50+
if err != nil {
51+
return time.Time{}, err
52+
}
53+
return timeValue, nil
54+
55+
case TimestampSecs, TimestampMillis, TimestampMicros, TimestampNanos:
56+
var value_i64 int64
57+
switch value.(type) {
58+
case int, int8, int16, int32, int64:
59+
value_i64 = reflect.ValueOf(value).Int()
60+
case float32, float64:
61+
value_f64 := reflect.ValueOf(value).Float()
62+
value_i64 = int64(value_f64)
63+
default:
64+
return time.Time{}, errors.New("parseTime only accepts float64 or int64 values with timestamp based formats")
65+
}
66+
67+
if timeOutputFormat == TimestampSecs {
68+
return time.Unix(value_i64, 0), nil
69+
} else if timeOutputFormat == TimestampMillis {
70+
return time.Unix(0, value_i64*1_000_000), nil
71+
} else if timeOutputFormat == TimestampMicros {
72+
return time.Unix(0, value_i64*1_000), nil
73+
} else if timeOutputFormat == TimestampNanos {
74+
return time.Unix(0, value_i64), nil
75+
}
76+
default:
77+
value_string := value.(string)
78+
timeValue, err := timefmt.Parse(value_string, timeOutputFormat)
79+
if err != nil {
80+
return time.Time{}, err
81+
}
82+
return timeValue, nil
83+
}
84+
return time.Time{}, fmt.Errorf("timeOutputFormat not supported yet %s", timeOutputFormat)
85+
}

Diff for: pkg/utils/parse_time_test.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
const (
11+
testYear int = 2024
12+
testMonth int = 3
13+
testDay int = 28
14+
testHour int = 12
15+
testMinute int = 34
16+
testSecond int = 56
17+
testUnixSeconds int = 1711629296
18+
testMilli int = testUnixSeconds*1000 + 987
19+
testMicro int = testMilli*1000 + 654
20+
testNano int = testMicro*1000 + 321
21+
)
22+
23+
var successTests = []struct {
24+
value any
25+
timeOutputFormat string
26+
}{
27+
// RFC3339
28+
{"2024-03-28T12:34:56.987Z", Rfc3339},
29+
// RFC2822
30+
{"Thu, 28 Mar 2024 12:34:56 GMT", Rfc2822},
31+
{"Thu, 28 Mar 2024 12:34:56 +0000", Rfc2822z},
32+
// Custom layout
33+
{"2024-03-28 12:34:56", "%Y-%m-%d %H:%M:%S"},
34+
{"2024-03-28 12:34:56.987", "%Y-%m-%d %H:%M:%S.%f"},
35+
// Int timestamps
36+
{1711629296, TimestampSecs},
37+
{1711629296987, TimestampMillis},
38+
{1711629296987654, TimestampMicros},
39+
{1711629296987654321, TimestampNanos},
40+
// Float timestamps
41+
{1711629296., TimestampSecs},
42+
{1711629296987., TimestampMillis},
43+
{1711629296987654., TimestampMicros},
44+
// {1711629296987654321., TimestampNanos}, // Float precision fail
45+
}
46+
47+
func TestParseTime(t *testing.T) {
48+
assert := assert.New(t)
49+
for _, tt := range successTests {
50+
t.Run(fmt.Sprintf("Parse %s", tt.timeOutputFormat), func(t *testing.T) {
51+
time, err := ParseTime(tt.value, tt.timeOutputFormat)
52+
assert.Nil(err)
53+
assert.NotNil(time)
54+
// Check day
55+
assert.Equal(testYear, int(time.UTC().Year()), "Year mismatch")
56+
assert.Equal(testMonth, int(time.UTC().Month()), "Month mismatch")
57+
assert.Equal(testDay, int(time.UTC().Day()), "Day mismatch")
58+
assert.Equal(testHour, int(time.UTC().Hour()), "Hour mismatch")
59+
assert.Equal(testMinute, int(time.UTC().Minute()), "Minute mismatch")
60+
assert.Equal(testSecond, int(time.UTC().Second()), "Second mismatch")
61+
62+
switch tt.timeOutputFormat {
63+
case TimestampNanos:
64+
assert.Equal(testNano, int(time.UTC().UnixNano()), "Nanosecond mismatch")
65+
fallthrough
66+
case TimestampMicros:
67+
assert.Equal(testMicro, int(time.UTC().UnixMicro()), "Microsecond mismatch")
68+
fallthrough
69+
case Rfc3339, TimestampMillis:
70+
assert.Equal(testMilli, int(time.UTC().UnixMilli()), "Millisecond mismatch")
71+
}
72+
})
73+
}
74+
}

0 commit comments

Comments
 (0)