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

logcli: added --step support to query command #1103

Merged
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
1 change: 1 addition & 0 deletions cmd/logcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ func newQuery(instant bool, cmd *kingpin.CmdClause) *query.Query {
cmd.Flag("since", "Lookback window.").Default("1h").DurationVar(&since)
cmd.Flag("from", "Start looking for logs at this absolute time (inclusive)").StringVar(&from)
cmd.Flag("to", "Stop looking for logs at this absolute time (exclusive)").StringVar(&to)
cmd.Flag("step", "Query resolution step width").DurationVar(&query.Step)
}

cmd.Flag("forward", "Scan forwards through logs.").Default("false").BoolVar(&query.Forward)
Expand Down
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ accepts the following query parameters in the URL:
- `limit`: The max number of entries to return
- `start`: The start time for the query as a nanosecond Unix epoch. Defaults to one hour ago.
- `end`: The start time for the query as a nanosecond Unix epoch. Defaults to now.
- `step`: Query resolution step width in seconds. Defaults to 1.
- `step`: Query resolution step width in seconds. Defaults to a dynamic value based on `start` and `end`.
- `direction`: Determines the sort order of logs. Supported values are `forward` or `backward`. Defaults to `backward.`

Requests against this endpoint require Loki to query the index store in order to
Expand Down
26 changes: 16 additions & 10 deletions pkg/logcli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"

Expand All @@ -21,7 +22,7 @@ import (

const (
queryPath = "/loki/api/v1/query?query=%s&limit=%d&time=%d&direction=%s"
queryRangePath = "/loki/api/v1/query_range?query=%s&limit=%d&start=%d&end=%d&direction=%s"
queryRangePath = "/loki/api/v1/query_range"
labelsPath = "/loki/api/v1/label"
labelValuesPath = "/loki/api/v1/label/%s/values"
tailPath = "/loki/api/v1/tail?query=%s&delay_for=%d&limit=%d&start=%d"
Expand Down Expand Up @@ -52,16 +53,21 @@ func (c *Client) Query(queryStr string, limit int, time time.Time, direction log
// QueryRange uses the /api/v1/query_range endpoint to execute a range query
// excluding interfacer b/c it suggests taking the interface promql.Node instead of logproto.Direction b/c it happens to have a String() method
// nolint:interfacer
func (c *Client) QueryRange(queryStr string, limit int, from, through time.Time, direction logproto.Direction, quiet bool) (*loghttp.QueryResponse, error) {
path := fmt.Sprintf(queryRangePath,
url.QueryEscape(queryStr), // query
limit, // limit
from.UnixNano(), // start
through.UnixNano(), // end
direction.String(), // direction
)
func (c *Client) QueryRange(queryStr string, limit int, from, through time.Time, direction logproto.Direction, step time.Duration, quiet bool) (*loghttp.QueryResponse, error) {
params := url.Values{}
params.Set("query", queryStr)
params.Set("limit", strconv.Itoa(limit))
params.Set("start", strconv.FormatInt(from.UnixNano(), 10))
params.Set("end", strconv.FormatInt(through.UnixNano(), 10))
params.Set("direction", direction.String())

// The step is optional, so we do set it only if provided,
// otherwise we do leverage on the API defaults
if step != 0 {
params.Set("step", strconv.FormatInt(int64(step.Seconds()), 10))
}

return c.doQuery(path, quiet)
return c.doQuery(queryRangePath+"?"+params.Encode(), quiet)
}

// ListLabelNames uses the /api/v1/label endpoint to list label names
Expand Down
3 changes: 2 additions & 1 deletion pkg/logcli/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Query struct {
End time.Time
Limit int
Forward bool
Step time.Duration
Quiet bool
NoLabels bool
IgnoreLabelsKey []string
Expand All @@ -47,7 +48,7 @@ func (q *Query) DoQuery(c *client.Client, out output.LogOutput) {
if q.isInstant() {
resp, err = c.Query(q.QueryString, q.Limit, q.Start, d, q.Quiet)
} else {
resp, err = c.QueryRange(q.QueryString, q.Limit, q.Start, q.End, d, q.Quiet)
resp, err = c.QueryRange(q.QueryString, q.Limit, q.Start, q.End, d, q.Step, q.Quiet)
}

if err != nil {
Expand Down
20 changes: 14 additions & 6 deletions pkg/querier/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const (
defaultSince = 1 * time.Hour
wsPingPeriod = 1 * time.Second
maxDelayForInTailing = 5
defaultStep = 1 // 1 seconds
)

// nolint
Expand Down Expand Up @@ -85,6 +84,12 @@ func directionParam(values url.Values, name string, def logproto.Direction) (log
return logproto.Direction(d), 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 httpRequestToInstantQueryRequest(httpRequest *http.Request) (*instantQueryRequest, error) {
params := httpRequest.URL.Query()
queryRequest := instantQueryRequest{
Expand All @@ -111,21 +116,24 @@ func httpRequestToInstantQueryRequest(httpRequest *http.Request) (*instantQueryR
}

func httpRequestToRangeQueryRequest(httpRequest *http.Request) (*rangeQueryRequest, error) {
var err error

params := httpRequest.URL.Query()
queryRequest := rangeQueryRequest{
query: params.Get("query"),
}

step, err := intParam(params, "step", defaultStep)
queryRequest.limit, queryRequest.start, queryRequest.end, err = httpRequestToLookback(httpRequest)
if err != nil {
return nil, httpgrpc.Errorf(http.StatusBadRequest, err.Error())
return nil, err
}
queryRequest.step = time.Duration(step) * time.Second

queryRequest.limit, queryRequest.start, queryRequest.end, err = httpRequestToLookback(httpRequest)
step, err := intParam(params, "step", defaultQueryRangeStep(queryRequest.start, queryRequest.end))
if err != nil {
return nil, err
return nil, httpgrpc.Errorf(http.StatusBadRequest, err.Error())
}
queryRequest.step = time.Duration(step) * time.Second

queryRequest.direction, err = directionParam(params, "direction", logproto.BACKWARD)
if err != nil {
return nil, httpgrpc.Errorf(http.StatusBadRequest, err.Error())
Expand Down
90 changes: 90 additions & 0 deletions pkg/querier/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package querier

import (
"net/http/httptest"
"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_httpRequestToRangeQueryRequest(t *testing.T) {
t.Parallel()

tests := map[string]struct {
reqPath string
expected *rangeQueryRequest
}{
"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: &rangeQueryRequest{
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: &rangeQueryRequest{
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 := httpRequestToRangeQueryRequest(req)

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