Skip to content

Commit

Permalink
Moves request parsing into the loghttp package (#1171)
Browse files Browse the repository at this point in the history
* refactor query requests parsing

Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com>

* add tests and documentation

Signed-off-by: Cyril Tovena <cyril.tovena@gmail.com>
  • Loading branch information
cyriltovena authored Oct 17, 2019
1 parent de040ca commit 1abe884
Show file tree
Hide file tree
Showing 10 changed files with 658 additions and 329 deletions.
21 changes: 21 additions & 0 deletions pkg/loghttp/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package loghttp

import (
"bytes"
"net/http"
"sort"
"strconv"

"github.com/gorilla/mux"
"github.com/grafana/loki/pkg/logproto"
)

// LabelResponse represents the http json response to a label query
Expand Down Expand Up @@ -37,3 +41,20 @@ func (l LabelSet) String() string {
b.WriteByte('}')
return b.String()
}

// ParseLabelQuery parses a LabelRequest request from an http request.
func ParseLabelQuery(r *http.Request) (*logproto.LabelRequest, error) {
name, ok := mux.Vars(r)["name"]
req := &logproto.LabelRequest{
Values: ok,
Name: name,
}

start, end, err := bounds(r)
if err != nil {
return nil, err
}
req.Start = &start
req.End = &end
return req, nil
}
65 changes: 65 additions & 0 deletions pkg/loghttp/labels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package loghttp

import (
"net/http"
"reflect"
"testing"
"time"

"github.com/gorilla/mux"
"github.com/grafana/loki/pkg/logproto"
)

func TestParseLabelQuery(t *testing.T) {
t.Parallel()

tests := []struct {
name string
r *http.Request
want *logproto.LabelRequest
wantErr bool
}{
{"bad start", &http.Request{URL: mustParseURL(`?&start=t`)}, nil, true},
{"bad end", &http.Request{URL: mustParseURL(`?&start=2016-06-10T21:42:24.760738998Z&end=h`)}, nil, true},
{"good no name in the pah",
requestWithVar(&http.Request{
URL: mustParseURL(`?start=2017-06-10T21:42:24.760738998Z&end=2017-07-10T21:42:24.760738998Z`),
}, "name", "test"), &logproto.LabelRequest{
Name: "test",
Values: true,
Start: timePtr(time.Date(2017, 06, 10, 21, 42, 24, 760738998, time.UTC)),
End: timePtr(time.Date(2017, 07, 10, 21, 42, 24, 760738998, time.UTC)),
}, false},
{"good with name",
&http.Request{
URL: mustParseURL(`?start=2017-06-10T21:42:24.760738998Z&end=2017-07-10T21:42:24.760738998Z`),
}, &logproto.LabelRequest{
Name: "",
Values: false,
Start: timePtr(time.Date(2017, 06, 10, 21, 42, 24, 760738998, time.UTC)),
End: timePtr(time.Date(2017, 07, 10, 21, 42, 24, 760738998, time.UTC)),
}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseLabelQuery(tt.r)
if (err != nil) != tt.wantErr {
t.Errorf("ParseLabelQuery() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseLabelQuery() = %v, want %v", got, tt.want)
}
})
}
}

func timePtr(t time.Time) *time.Time {
return &t
}

func requestWithVar(req *http.Request, name, value string) *http.Request {
return mux.SetURLVars(req, map[string]string{
name: value,
})
}
123 changes: 123 additions & 0 deletions pkg/loghttp/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package loghttp

import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"

"github.com/grafana/loki/pkg/logproto"
)

const (
defaultQueryLimit = 100
defaultSince = 1 * time.Hour
defaultStep = 1 // 1 seconds
)

func limit(r *http.Request) (uint32, error) {
l, err := parseInt(r.URL.Query().Get("limit"), defaultQueryLimit)
if err != nil {
return 0, err
}
return uint32(l), nil
}

func query(r *http.Request) string {
return r.URL.Query().Get("query")
}

func ts(r *http.Request) (time.Time, error) {
return parseTimestamp(r.URL.Query().Get("time"), time.Now())
}

func direction(r *http.Request) (logproto.Direction, error) {
return parseDirection(r.URL.Query().Get("direction"), logproto.BACKWARD)
}

func bounds(r *http.Request) (time.Time, time.Time, error) {
now := time.Now()
start, err := parseTimestamp(r.URL.Query().Get("start"), now.Add(-defaultSince))
if err != nil {
return time.Time{}, time.Time{}, err
}
end, err := parseTimestamp(r.URL.Query().Get("end"), now)
if err != nil {
return time.Time{}, time.Time{}, err
}
return start, end, nil
}

func step(r *http.Request, start, end time.Time) (time.Duration, error) {
s, err := parseInt(r.URL.Query().Get("step"), defaultQueryRangeStep(start, end))
if err != nil {
return 0, err
}
return time.Duration(s) * time.Second, nil
}

// defaultQueryRangeStep returns the default step used in the query range API,
// which is dinamically calculated based on the time range
func defaultQueryRangeStep(start time.Time, end time.Time) int {
return int(math.Max(math.Floor(end.Sub(start).Seconds()/250), 1))
}

func tailDelay(r *http.Request) (uint32, error) {
l, err := parseInt(r.URL.Query().Get("delay_for"), 0)
if err != nil {
return 0, err
}
return uint32(l), nil
}

// parseInt parses an int from a string
// if the value is empty it returns a default value passed as second parameter
func parseInt(value string, def int) (int, error) {
if value == "" {
return def, nil
}
return strconv.Atoi(value)
}

// parseUnixNano parses a ns unix timestamp from a string
// if the value is empty it returns a default value passed as second parameter
func parseTimestamp(value string, def time.Time) (time.Time, error) {
if value == "" {
return def, nil
}

if strings.Contains(value, ".") {
if t, err := strconv.ParseFloat(value, 64); err == nil {
s, ns := math.Modf(t)
ns = math.Round(ns*1000) / 1000
return time.Unix(int64(s), int64(ns*float64(time.Second))), nil
}
}
nanos, err := strconv.ParseInt(value, 10, 64)
if err != nil {
if ts, err := time.Parse(time.RFC3339Nano, value); err == nil {
return ts, nil
}
return time.Time{}, err
}
if len(value) <= 10 {
return time.Unix(nanos, 0), nil
}
return time.Unix(0, nanos), nil
}

// parseDirection parses a logproto.Direction from a string
// if the value is empty it returns a default value passed as second parameter
func parseDirection(value string, def logproto.Direction) (logproto.Direction, error) {
if value == "" {
return def, nil
}

d, ok := logproto.Direction_value[strings.ToUpper(value)]
if !ok {
return logproto.FORWARD, fmt.Errorf("invalid direction '%s'", value)
}
return logproto.Direction(d), nil
}
123 changes: 123 additions & 0 deletions pkg/loghttp/params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package loghttp

import (
"net/http/httptest"
"reflect"
"testing"
"time"

"github.com/grafana/loki/pkg/logproto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestHttp_defaultQueryRangeStep(t *testing.T) {
t.Parallel()

tests := map[string]struct {
start time.Time
end time.Time
expected int
}{
"should not be lower then 1s": {
start: time.Unix(60, 0),
end: time.Unix(60, 0),
expected: 1,
},
"should return 1s if input time range is 5m": {
start: time.Unix(60, 0),
end: time.Unix(360, 0),
expected: 1,
},
"should return 14s if input time range is 1h": {
start: time.Unix(60, 0),
end: time.Unix(3660, 0),
expected: 14,
},
}

for testName, testData := range tests {
testData := testData

t.Run(testName, func(t *testing.T) {
assert.Equal(t, testData.expected, defaultQueryRangeStep(testData.start, testData.end))
})
}
}

func TestHttp_ParseRangeQuery_Step(t *testing.T) {
t.Parallel()

tests := map[string]struct {
reqPath string
expected *RangeQuery
}{
"should set the default step based on the input time range if the step parameter is not provided": {
reqPath: "/loki/api/v1/query_range?query={}&start=0&end=3600000000000",
expected: &RangeQuery{
Query: "{}",
Start: time.Unix(0, 0),
End: time.Unix(3600, 0),
Step: 14 * time.Second,
Limit: 100,
Direction: logproto.BACKWARD,
},
},
"should use the input step parameter if provided": {
reqPath: "/loki/api/v1/query_range?query={}&start=0&end=3600000000000&step=5",
expected: &RangeQuery{
Query: "{}",
Start: time.Unix(0, 0),
End: time.Unix(3600, 0),
Step: 5 * time.Second,
Limit: 100,
Direction: logproto.BACKWARD,
},
},
}

for testName, testData := range tests {
testData := testData

t.Run(testName, func(t *testing.T) {
req := httptest.NewRequest("GET", testData.reqPath, nil)
actual, err := ParseRangeQuery(req)

require.NoError(t, err)
assert.Equal(t, testData.expected, actual)
})
}
}

func Test_parseTimestamp(t *testing.T) {

now := time.Now()

tests := []struct {
name string
value string
def time.Time
want time.Time
wantErr bool
}{
{"default", "", now, now, false},
{"unix timestamp", "1571332130", now, time.Unix(1571332130, 0), false},
{"unix nano timestamp", "1571334162051000000", now, time.Unix(0, 1571334162051000000), false},
{"unix timestamp with subseconds", "1571332130.934", now, time.Unix(1571332130, 934*1e6), false},
{"RFC3339 format", "2002-10-02T15:00:00Z", now, time.Date(2002, 10, 02, 15, 0, 0, 0, time.UTC), false},
{"RFC3339nano format", "2009-11-10T23:00:00.000000001Z", now, time.Date(2009, 11, 10, 23, 0, 0, 1, time.UTC), false},
{"invalid", "we", now, time.Time{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseTimestamp(tt.value, tt.def)
if (err != nil) != tt.wantErr {
t.Errorf("parseTimestamp() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTimestamp() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit 1abe884

Please sign in to comment.