Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
loki/pkg/loghttp/params.go

251 lines
6.5 KiB

package loghttp
import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
"github.com/c2h5oh/datasize"
"github.com/pkg/errors"
"github.com/prometheus/common/model"
"github.com/grafana/loki/v3/pkg/logproto"
"github.com/grafana/loki/v3/pkg/logql/log"
"github.com/grafana/loki/v3/pkg/logql/syntax"
)
const (
defaultQueryLimit = 100
defaultLimit = 1000
defaultSince = 1 * time.Hour
defaultDirection = logproto.BACKWARD
)
func limit(r *http.Request) (uint32, error) {
l, err := parseInt(r.Form.Get("limit"), defaultQueryLimit)
if err != nil {
return 0, err
}
if l <= 0 {
return 0, errors.New("limit must be a positive value")
}
return uint32(l), nil
}
func lineLimit(r *http.Request) (uint32, error) {
l, err := parseInt(r.Form.Get("line_limit"), defaultQueryLimit)
if err != nil {
return 0, err
}
if l <= 0 {
return 0, errors.New("limit must be a positive value")
}
return uint32(l), nil
}
func detectedFieldsLimit(r *http.Request) (uint32, error) {
limit := r.Form.Get("limit")
if limit == "" {
// for backwards compatibility
limit = r.Form.Get("field_limit")
}
l, err := parseInt(limit, defaultLimit)
if err != nil {
return 0, err
}
if l <= 0 {
return 0, errors.New("limit must be a positive value")
}
return uint32(l), nil
}
func query(r *http.Request) string {
return r.Form.Get("query")
}
func ts(r *http.Request) (time.Time, error) {
return parseTimestamp(r.Form.Get("time"), time.Now())
}
func direction(r *http.Request) (logproto.Direction, error) {
return parseDirection(r.Form.Get("direction"), defaultDirection)
}
func shards(r *http.Request) []string {
return r.Form["shards"]
}
func bounds(r *http.Request) (time.Time, time.Time, error) {
now := time.Now()
start := r.Form.Get("start")
end := r.Form.Get("end")
since := r.Form.Get("since")
return determineBounds(now, start, end, since)
}
func determineBounds(now time.Time, startString, endString, sinceString string) (time.Time, time.Time, error) {
since := defaultSince
if sinceString != "" {
d, err := model.ParseDuration(sinceString)
if err != nil {
return time.Time{}, time.Time{}, errors.Wrap(err, "could not parse 'since' parameter")
}
since = time.Duration(d)
}
end, err := parseTimestamp(endString, now)
if err != nil {
return time.Time{}, time.Time{}, errors.Wrap(err, "could not parse 'end' parameter")
}
// endOrNow is used to apply a default for the start time or an offset if 'since' is provided.
// we want to use the 'end' time so long as it's not in the future as this should provide
// a more intuitive experience when end time is in the future.
endOrNow := end
if end.After(now) {
endOrNow = now
}
start, err := parseTimestamp(startString, endOrNow.Add(-since))
if err != nil {
return time.Time{}, time.Time{}, errors.Wrap(err, "could not parse 'start' parameter")
}
return start, end, nil
}
func step(r *http.Request, start, end time.Time) (time.Duration, error) {
value := r.Form.Get("step")
if value == "" {
return time.Duration(defaultQueryRangeStep(start, end)) * time.Second, nil
}
return parseSecondsOrDuration(value)
}
func interval(r *http.Request) (time.Duration, error) {
value := r.Form.Get("interval")
if value == "" {
return 0, nil
}
return parseSecondsOrDuration(value)
}
// defaultQueryRangeStep returns the default step used in the query range API,
// which is dynamically 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.Form.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)
}
// parseTimestamp 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
}
func parseSecondsOrDuration(value string) (time.Duration, error) {
if d, err := strconv.ParseFloat(value, 64); err == nil {
ts := d * float64(time.Second)
if ts > float64(math.MaxInt64) || ts < float64(math.MinInt64) {
return 0, errors.Errorf("cannot parse %q to a valid duration. It overflows int64", value)
}
return time.Duration(ts), nil
}
if d, err := model.ParseDuration(value); err == nil {
return time.Duration(d), nil
}
return 0, errors.Errorf("cannot parse %q to a valid duration", value)
}
// parseRegexQuery parses regex and query querystring from httpRequest and returns the combined LogQL query.
// This is used only to keep regexp query string support until it gets fully deprecated.
func parseRegexQuery(httpRequest *http.Request) (string, error) {
query := httpRequest.Form.Get("query")
regexp := httpRequest.Form.Get("regexp")
if regexp != "" {
expr, err := syntax.ParseLogSelector(query, true)
if err != nil {
return "", err
}
newExpr, err := syntax.AddFilterExpr(expr, log.LineMatchRegexp, "", regexp)
if err != nil {
return "", err
}
query = newExpr.String()
}
return query, nil
}
func parseBytes(r *http.Request, field string, optional bool) (val datasize.ByteSize, err error) {
s := r.Form.Get(field)
if s == "" {
if !optional {
return 0, fmt.Errorf("missing %s", field)
}
return val, nil
}
if err := val.UnmarshalText([]byte(s)); err != nil {
return 0, errors.Wrapf(err, "invalid %s: %s", field, s)
}
return val, nil
}