Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 21 additions & 1 deletion services/httpd/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,12 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user meta.U

epoch := strings.TrimSpace(r.FormValue("epoch"))

// timeFormat should default to "epoch"
timeFormat := strings.TrimSpace(r.FormValue("time_format"))
if timeFormat == "" {
timeFormat = "epoch"
}

Comment on lines +597 to +610
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it should return an error if an invalid value is given for time_format. This will save a lot of support time.

p := influxql.NewParser(qr)
db := r.FormValue("db")

Expand Down Expand Up @@ -747,10 +753,14 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user meta.U
}

// if requested, convert result timestamps to epoch
if epoch != "" {
if epoch != "" && timeFormat == "epoch" {
convertToEpoch(r, epoch)
}

if timeFormat == "rfc3339" {
convertToRfc3339(r)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like we are pretty consistent in calling ns-precision RFC3339 simply RFC3339 in our docs and in the API. I wonder if we should allow rfc3339nano as a synonym?

Comment on lines -750 to +770
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are these two mutually exclusive? If so, it should probably be an if-then-else.

It seems like setting time_format=rfc3339 and epoch=foo should give the user an error to avoid support calls of people trying to combine them to get RFC3339 with second precision or such.


// Write out result immediately if chunked.
if chunked {
n, _ := rw.WriteResponse(Response{
Expand Down Expand Up @@ -1812,6 +1822,16 @@ func convertToEpoch(r *query.Result, epoch string) {
}
}

func convertToRfc3339(r *query.Result) {
for _, s := range r.Series {
for _, v := range s.Values {
if ts, ok := v[0].(time.Time); ok {
v[0] = ts.Format(time.RFC3339Nano)
}
}
}
}

// servePromWrite receives data in the Prometheus remote write protocol and writes it
// to the database
func (h *Handler) servePromWrite(w http.ResponseWriter, r *http.Request, user meta.User) {
Expand Down
103 changes: 103 additions & 0 deletions services/httpd/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/stretchr/testify/require"
"io"
"log"
"math"
Expand Down Expand Up @@ -596,6 +597,108 @@ func TestHandler_Query_CloseNotify(t *testing.T) {
}
}

// Ensure the handler returns results with RFC3339 timestamp format when requested.
func TestHandler_Query_RFC3339(t *testing.T) {
h := NewHandler(false)
testTime := time.Date(2021, 1, 1, 12, 0, 0, 0, time.UTC)

h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx *query.ExecutionContext) error {
ctx.Results <- &query.Result{
StatementID: 1,
Series: []*models.Row{{
Name: "series0",
Columns: []string{"time", "value"},
Values: [][]interface{}{
{testTime, 42},
},
}},
}
return nil
}

w := httptest.NewRecorder()
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar&time_format=rfc3339", nil))
require.Equal(t, w.Code, http.StatusOK, "response status")

expectedRFC3339 := testTime.Format(time.RFC3339Nano)
expectedBody := fmt.Sprintf(`{"results":[{"statement_id":1,"series":[{"name":"series0","columns":["time","value"],"values":[["%s",42]]}]}]}`, expectedRFC3339)

body := strings.TrimSpace(w.Body.String())
require.Equal(t, expectedBody, body, "response body")
}

// Ensure the handler returns results with RFC3339 timestamp format for multiple series.
func TestHandler_Query_RFC3339_MultipleSeries(t *testing.T) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it was me, I would combine the two test functions to avoid copying and pasting the boilerplate.

h := NewHandler(false)
testTime1 := time.Date(2021, 1, 1, 12, 0, 0, 0, time.UTC)
testTime2 := time.Date(2021, 1, 2, 12, 0, 0, 0, time.UTC)

h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx *query.ExecutionContext) error {
ctx.Results <- &query.Result{
StatementID: 1,
Series: []*models.Row{
{
Name: "series0",
Columns: []string{"time", "value"},
Values: [][]interface{}{
{testTime1, 42},
{testTime2, 43},
},
},
{
Name: "series1",
Columns: []string{"time", "value"},
Values: [][]interface{}{
{testTime1, 100},
},
},
},
}
return nil
}

w := httptest.NewRecorder()
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar&time_format=rfc3339", nil))
require.Equal(t, w.Code, http.StatusOK, "response code")

firstExpectedRFC3339 := testTime1.Format(time.RFC3339Nano)
secondExpectedRFC3339 := testTime2.Format(time.RFC3339Nano)
expectedBody := fmt.Sprintf(`{"results":[{"statement_id":1,"series":[{"name":"series0","columns":["time","value"],"values":[["%s",42],["%s",43]]},{"name":"series1","columns":["time","value"],"values":[["%s",100]]}]}]}`, firstExpectedRFC3339, secondExpectedRFC3339, firstExpectedRFC3339)

body := strings.TrimSpace(w.Body.String())
require.Equal(t, expectedBody, body, "response body")
}

// Ensure the handler returns results with RFC3339 timestamp format using nanosecond precision.
func TestHandler_Query_RFC3339_Nanoseconds(t *testing.T) {
h := NewHandler(false)
testTime := time.Date(2021, 1, 1, 12, 0, 0, 123456789, time.UTC)

h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx *query.ExecutionContext) error {
ctx.Results <- &query.Result{
StatementID: 1,
Series: []*models.Row{{
Name: "series0",
Columns: []string{"time", "value"},
Values: [][]interface{}{
{testTime, 42},
},
}},
}
return nil
}

w := httptest.NewRecorder()
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar&time_format=rfc3339", nil))
require.Equal(t, w.Code, http.StatusOK, "response code")

expectedRFC3339 := testTime.Format(time.RFC3339Nano)
expectedBody := fmt.Sprintf(`{"results":[{"statement_id":1,"series":[{"name":"series0","columns":["time","value"],"values":[["%s",42]]}]}]}`, expectedRFC3339)

body := strings.TrimSpace(w.Body.String())
require.Equal(t, expectedBody, body, "response body")
}

// Ensure the handler returns an appropriate 401 status when authentication
// fails on ping endpoints.
func TestHandler_Ping_ErrAuthorize(t *testing.T) {
Expand Down