Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SEARCH-8423 Fix time field type #34

Merged
merged 1 commit into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions pkg/plugin/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,13 @@ func (d *Datasource) query(_ context.Context, _ backend.PluginContext, dataQuery
// Grab the keys and values from the event and populate fields in the frame
for fieldName, value := range event {
if fieldName == CRIBL_TIME_FIELD {
// _time is in seconds, and Grafana needs it in ISO format. If the conversion is successful
// (which it will be most of the time), we use Grafana's well-known "time" field name instead.
// If the conversion fails, _time is something other than seconds, and it will pass-through
// as is with the "_time" field name.
if ok, isoString := timeToIsoString(value); ok {
// Two things are happening here:
// 1. Instead of our "_time" we use Grafana's well-known "time" field name.
// 2. Convert from seconds to time.Time struct. If the conversion fails, _time must be something
// other than seconds, and it will pass-through as is with the original "_time" field name.
if ok, time := criblTimeToGrafanaTime(value); ok {
fieldName = GRAFANA_TIME_FIELD_NAME
value = isoString
value = time
}
}

Expand Down
14 changes: 8 additions & 6 deletions pkg/plugin/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,24 @@ func collapseToSingleLine(query string) string {
return regexp.MustCompile("[\r\n\t]+").ReplaceAllString(query, " ")
}

// Convert the value of the "_time" field to an ISO timestamp string
func timeToIsoString(timeValue interface{}) (bool, string) {
// Convert the value of the "_time" field (expected to be in seconds) to a time.Time in UTC
func criblTimeToGrafanaTime(timeValue interface{}) (bool, time.Time) {
var seconds float64
switch v := timeValue.(type) {
case float64:
seconds = v
case string:
s, err := strconv.ParseFloat(v, 64)
if err != nil {
return false, ""
return false, time.Time{}
}
seconds = s
default:
return false, ""
return false, time.Time{}
}
wholeSec := int64(seconds)
nanoSec := int64(math.Round((seconds-float64(wholeSec))*1000.0)) * 1000000 // ms precision
return true, time.Unix(wholeSec, nanoSec).UTC().Format(time.RFC3339Nano)
nanoSec := int64(math.Round((seconds-float64(wholeSec))*1000000.0)) * 1000 // microsec precision
return true, time.Unix(wholeSec, nanoSec).UTC()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: I changed this to microsec because we can, and it provides additional granularly should it be needed. I tried nanoseconds and was getting some floating point precision garbage (i.e. 122999 instead of 123000), so I figured this is good enough for ~100% of the likely scenarios.

}

// Grafana's data.NewField() is super finicky. You're force to supply an array of values,
Expand All @@ -90,6 +90,8 @@ func makeEmptyConcreteTypeArray(val interface{}) (interface{}, error) {
return []float64{}, nil
case bool:
return []bool{}, nil
case time.Time:
return []time.Time{}, nil
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really key. As you populate fields in a data frame, Grafana's data layer infers the field type from this array type.

default:
return nil, fmt.Errorf("unsupported type: %T (%v)", t, t)
}
Expand Down
26 changes: 15 additions & 11 deletions pkg/plugin/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,42 +63,46 @@ func TestCollapseToSingleLine(t *testing.T) {
assert.Equal(t, `hello there aw yeah`, collapseToSingleLine("hello\nthere\taw\r\nyeah"))
}

func TestTimeToIsoString(t *testing.T) {
func TestCriblTimeToGrafanaTime(t *testing.T) {
for _, test := range []struct {
In interface{}
Expected string
Expected int64
}{
{
In: nil,
Expected: "",
Expected: 0,
},
{
In: false,
Expected: "",
Expected: 0,
},
{
In: true,
Expected: "",
Expected: 0,
},
{
In: "whatever",
Expected: "",
Expected: 0,
},
{
In: float64(1728744793),
Expected: "2024-10-12T14:53:13Z",
Expected: 1728744793000000,
},
{
In: float64(1728744793.123),
Expected: "2024-10-12T14:53:13.123Z",
Expected: 1728744793123000,
},
{
In: float64(1728744793.123456),
Expected: 1728744793123456,
},
} {
ok, out := timeToIsoString(test.In)
if len(test.Expected) == 0 {
ok, out := criblTimeToGrafanaTime(test.In)
if test.Expected == 0 {
assert.Equal(t, false, ok, fmt.Sprintf("input %v produced ok=%v, out=%v", test.In, ok, out))
} else {
assert.Equal(t, true, ok, fmt.Sprintf("input %v produced ok=%v, out=%v", test.In, ok, out))
assert.Equal(t, test.Expected, out, test.In)
assert.Equal(t, test.Expected, out.UnixMicro(), test.In)
}
}
}
Expand Down
Loading