-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Moves request parsing into the loghttp package (#1171)
* 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
1 parent
de040ca
commit 1abe884
Showing
10 changed files
with
658 additions
and
329 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.