Skip to content

Commit b9a0155

Browse files
authored
feat: Adds time_format param for httpd (#26596) (#26833)
Closes FR#615
1 parent c168cc5 commit b9a0155

File tree

2 files changed

+114
-1
lines changed

2 files changed

+114
-1
lines changed

services/httpd/handler.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/prometheus/client_golang/prometheus/promhttp"
4343
"github.com/prometheus/prometheus/prompb"
4444
"go.uber.org/zap"
45+
"golang.org/x/exp/slices"
4546
)
4647

4748
var ErrDiagnosticsValueMissing = errors.New("expected diagnostic value missing")
@@ -594,6 +595,19 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user meta.U
594595

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

598+
timeFormats := []string{
599+
"rfc3339",
600+
"epoch",
601+
}
602+
// timeFormat should default to "epoch"
603+
timeFormat := strings.TrimSpace(r.FormValue("time_format"))
604+
if timeFormat == "" {
605+
timeFormat = "epoch"
606+
} else if !slices.Contains(timeFormats, timeFormat) {
607+
h.httpError(rw, fmt.Sprintf("Time format must be one of the following: %s", strings.Join(timeFormats, ",")), http.StatusBadRequest)
608+
return
609+
}
610+
597611
p := influxql.NewParser(qr)
598612
db := r.FormValue("db")
599613

@@ -747,8 +761,12 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user meta.U
747761
}
748762

749763
// if requested, convert result timestamps to epoch
750-
if epoch != "" {
764+
if epoch != "" && timeFormat == "epoch" {
751765
convertToEpoch(r, epoch)
766+
} else if timeFormat == "rfc3339" {
767+
if err := convertToTimeFormat(r, time.RFC3339Nano); err != nil {
768+
h.httpError(rw, fmt.Sprintf("error converting time to RFC3339Nano: %s", err.Error()), http.StatusBadRequest)
769+
}
752770
}
753771

754772
// Write out result immediately if chunked.
@@ -1812,6 +1830,23 @@ func convertToEpoch(r *query.Result, epoch string) {
18121830
}
18131831
}
18141832

1833+
func convertToTimeFormat(r *query.Result, format string) error {
1834+
for _, s := range r.Series {
1835+
for _, v := range s.Values {
1836+
switch format {
1837+
case time.RFC3339Nano:
1838+
if ts, ok := v[0].(time.Time); ok {
1839+
v[0] = ts.Format(time.RFC3339Nano)
1840+
}
1841+
default:
1842+
return fmt.Errorf("unknown time format: %s", format)
1843+
}
1844+
}
1845+
}
1846+
1847+
return nil
1848+
}
1849+
18151850
// servePromWrite receives data in the Prometheus remote write protocol and writes it
18161851
// to the database
18171852
func (h *Handler) servePromWrite(w http.ResponseWriter, r *http.Request, user meta.User) {

services/httpd/handler_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"github.com/stretchr/testify/require"
910
"io"
1011
"log"
1112
"math"
@@ -596,6 +597,83 @@ func TestHandler_Query_CloseNotify(t *testing.T) {
596597
}
597598
}
598599

600+
// Ensure the handler returns results with RFC3339 timestamp format when requested.
601+
func TestHandler_Query_RFC3339(t *testing.T) {
602+
testTime1 := time.Date(2021, 1, 1, 12, 0, 0, 0, time.UTC)
603+
testTime2 := time.Date(2021, 1, 2, 12, 0, 0, 0, time.UTC)
604+
testTimeNano := time.Date(2021, 1, 1, 12, 0, 0, 123456789, time.UTC)
605+
606+
tests := []struct {
607+
name string
608+
series []*models.Row
609+
expectedResult string
610+
}{
611+
{
612+
name: "single series",
613+
series: []*models.Row{{
614+
Name: "series0",
615+
Columns: []string{"time", "value"},
616+
Values: [][]interface{}{
617+
{testTime1, 42},
618+
},
619+
}},
620+
expectedResult: fmt.Sprintf(`{"results":[{"statement_id":1,"series":[{"name":"series0","columns":["time","value"],"values":[["%s",42]]}]}]}`, testTime1.Format(time.RFC3339Nano)),
621+
},
622+
{
623+
name: "multiple series",
624+
series: []*models.Row{
625+
{
626+
Name: "series0",
627+
Columns: []string{"time", "value"},
628+
Values: [][]interface{}{
629+
{testTime1, 42},
630+
{testTime2, 43},
631+
},
632+
},
633+
{
634+
Name: "series1",
635+
Columns: []string{"time", "value"},
636+
Values: [][]interface{}{
637+
{testTime1, 100},
638+
},
639+
},
640+
},
641+
expectedResult: 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]]}]}]}`, testTime1.Format(time.RFC3339Nano), testTime2.Format(time.RFC3339Nano), testTime1.Format(time.RFC3339Nano)),
642+
},
643+
{
644+
name: "nanosecond precision",
645+
series: []*models.Row{{
646+
Name: "series0",
647+
Columns: []string{"time", "value"},
648+
Values: [][]interface{}{
649+
{testTimeNano, 42},
650+
},
651+
}},
652+
expectedResult: fmt.Sprintf(`{"results":[{"statement_id":1,"series":[{"name":"series0","columns":["time","value"],"values":[["%s",42]]}]}]}`, testTimeNano.Format(time.RFC3339Nano)),
653+
},
654+
}
655+
656+
for _, tt := range tests {
657+
t.Run(tt.name, func(t *testing.T) {
658+
h := NewHandler(false)
659+
h.StatementExecutor.ExecuteStatementFn = func(stmt influxql.Statement, ctx *query.ExecutionContext) error {
660+
ctx.Results <- &query.Result{
661+
StatementID: 1,
662+
Series: tt.series,
663+
}
664+
return nil
665+
}
666+
667+
w := httptest.NewRecorder()
668+
h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SELECT+*+FROM+bar&time_format=rfc3339", nil))
669+
require.Equal(t, http.StatusOK, w.Code, "response status")
670+
671+
body := strings.TrimSpace(w.Body.String())
672+
require.Equal(t, tt.expectedResult, body, "response body")
673+
})
674+
}
675+
}
676+
599677
// Ensure the handler returns an appropriate 401 status when authentication
600678
// fails on ping endpoints.
601679
func TestHandler_Ping_ErrAuthorize(t *testing.T) {

0 commit comments

Comments
 (0)