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

feat: add detected-fields command to logcli #12739

Merged
merged 6 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
74 changes: 74 additions & 0 deletions cmd/logcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"gopkg.in/alecthomas/kingpin.v2"

"github.com/grafana/loki/v3/pkg/logcli/client"
"github.com/grafana/loki/v3/pkg/logcli/detected"
"github.com/grafana/loki/v3/pkg/logcli/index"
"github.com/grafana/loki/v3/pkg/logcli/labelquery"
"github.com/grafana/loki/v3/pkg/logcli/output"
Expand Down Expand Up @@ -253,6 +254,39 @@ Example:
'my-query'
`)
volumeRangeQuery = newVolumeQuery(true, volumeRangeCmd)

detectedFieldsCmd = app.Command("detected-fields", `Run a query for detected fields..

The "detected-fields" command will return information about fields detected using either
the "logfmt" or "json" parser against the log lines returned by the provided query for the
provided time range.

The "detected-fields" command will output extra information about the query
and its results, such as the API URL, set of common labels, and set
of excluded labels. This extra information can be suppressed with the
--quiet flag.

By default we look over the last hour of data; use --since to modify
or provide specific start and end times with --from and --to respectively.

Notice that when using --from and --to then ensure to use RFC3339Nano
time format, but without timezone at the end. The local timezone will be added
automatically or if using --timezone flag.

Example:

logcli detected-fields
--timezone=UTC
--from="2021-01-19T10:00:00Z"
--to="2021-01-19T20:00:00Z"
--output=jsonl
'my-query'

The output is limited to 100 fields by default; use --field-limit to increase.
The query is limited to processing 1000 lines per subquery; use --line-limit to increase.
`)

detectedFieldsQuery = newDetectedFieldsQuery(detectedFieldsCmd)
)

func main() {
Expand Down Expand Up @@ -388,6 +422,8 @@ func main() {
} else {
index.GetVolume(volumeQuery, queryClient, out, *statistics)
}
case detectedFieldsCmd.FullCommand():
detectedFieldsQuery.Do(queryClient, *outputMode)
}
}

Expand Down Expand Up @@ -652,3 +688,41 @@ func newVolumeQuery(rangeQuery bool, cmd *kingpin.CmdClause) *volume.Query {

return q
}

func newDetectedFieldsQuery(cmd *kingpin.CmdClause) *detected.FieldsQuery {
// calculate query range from cli params
var from, to string
var since time.Duration

q := &detected.FieldsQuery{}

// executed after all command flags are parsed
cmd.Action(func(c *kingpin.ParseContext) error {
defaultEnd := time.Now()
defaultStart := defaultEnd.Add(-since)

q.Start = mustParse(from, defaultStart)
q.End = mustParse(to, defaultEnd)

q.Quiet = *quiet

return nil
})

cmd.Flag("field-limit", "Limit on number of fields to return.").
Default("100").
IntVar(&q.FieldLimit)
cmd.Flag("line-limit", "Limit the number of lines each subquery is allowed to process.").
Default("1000").
IntVar(&q.LineLimit)
cmd.Arg("query", "eg '{foo=\"bar\",baz=~\".*blip\"} |~ \".*error.*\"'").
Required().
StringVar(&q.QueryString)
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, for metric queries. Evaluate the query at the specified step over the time range.").
DurationVar(&q.Step)

return q
}
53 changes: 42 additions & 11 deletions pkg/logcli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@ import (
)

const (
queryPath = "/loki/api/v1/query"
queryRangePath = "/loki/api/v1/query_range"
labelsPath = "/loki/api/v1/labels"
labelValuesPath = "/loki/api/v1/label/%s/values"
seriesPath = "/loki/api/v1/series"
tailPath = "/loki/api/v1/tail"
statsPath = "/loki/api/v1/index/stats"
volumePath = "/loki/api/v1/index/volume"
volumeRangePath = "/loki/api/v1/index/volume_range"
defaultAuthHeader = "Authorization"
queryPath = "/loki/api/v1/query"
queryRangePath = "/loki/api/v1/query_range"
labelsPath = "/loki/api/v1/labels"
labelValuesPath = "/loki/api/v1/label/%s/values"
seriesPath = "/loki/api/v1/series"
tailPath = "/loki/api/v1/tail"
statsPath = "/loki/api/v1/index/stats"
volumePath = "/loki/api/v1/index/volume"
volumeRangePath = "/loki/api/v1/index/volume_range"
detectedFieldsPath = "/loki/api/v1/detected_fields"
defaultAuthHeader = "Authorization"
)

var userAgent = fmt.Sprintf("loki-logcli/%s", build.Version)
Expand All @@ -54,6 +55,7 @@ type Client interface {
GetStats(queryStr string, start, end time.Time, quiet bool) (*logproto.IndexStatsResponse, error)
GetVolume(query *volume.Query) (*loghttp.QueryResponse, error)
GetVolumeRange(query *volume.Query) (*loghttp.QueryResponse, error)
GetDetectedFields(queryStr string, fieldLimit, lineLimit int, start, end time.Time, step time.Duration, quiet bool) (*loghttp.DetectedFieldsResponse, error)
}

// Tripperware can wrap a roundtripper.
Expand Down Expand Up @@ -224,7 +226,36 @@ func (c *DefaultClient) getVolume(path string, query *volume.Query) (*loghttp.Qu
return &resp, nil
}

func (c *DefaultClient) doQuery(path string, query string, quiet bool) (*loghttp.QueryResponse, error) {
func (c *DefaultClient) GetDetectedFields(
queryStr string,
fieldLimit, lineLimit int,
start, end time.Time,
step time.Duration,
quiet bool,
) (*loghttp.DetectedFieldsResponse, error) {
qsb := util.NewQueryStringBuilder()
qsb.SetString("query", queryStr)
qsb.SetInt("field_limit", int64(fieldLimit))
qsb.SetInt("line_limit", int64(lineLimit))
qsb.SetInt("start", start.UnixNano())
qsb.SetInt("end", end.UnixNano())
qsb.SetString("step", step.String())

var err error
var r loghttp.DetectedFieldsResponse

if err = c.doRequest(detectedFieldsPath, qsb.Encode(), quiet, &r); err != nil {
return nil, err
}

return &r, nil
}

func (c *DefaultClient) doQuery(
path string,
query string,
quiet bool,
) (*loghttp.QueryResponse, error) {
var err error
var r loghttp.QueryResponse

Expand Down
17 changes: 14 additions & 3 deletions pkg/logcli/client/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,17 +190,28 @@ func (f *FileClient) GetOrgID() string {
}

func (f *FileClient) GetStats(_ string, _, _ time.Time, _ bool) (*logproto.IndexStatsResponse, error) {
// TODO(trevorwhitney): could we teach logcli to read from an actual index file?
// TODO(twhitney): could we teach logcli to read from an actual index file?
return nil, ErrNotSupported
}

func (f *FileClient) GetVolume(_ *volume.Query) (*loghttp.QueryResponse, error) {
// TODO(trevorwhitney): could we teach logcli to read from an actual index file?
// TODO(twhitney): could we teach logcli to read from an actual index file?
return nil, ErrNotSupported
}

func (f *FileClient) GetVolumeRange(_ *volume.Query) (*loghttp.QueryResponse, error) {
// TODO(trevorwhitney): could we teach logcli to read from an actual index file?
// TODO(twhitney): could we teach logcli to read from an actual index file?
return nil, ErrNotSupported
}

func (f *FileClient) GetDetectedFields(
_ string,
_, _ int,
_, _ time.Time,
_ time.Duration,
_ bool,
) (*loghttp.DetectedFieldsResponse, error) {
// TODO(twhitney): could we teach logcli to do this?
return nil, ErrNotSupported
}

Expand Down
56 changes: 56 additions & 0 deletions pkg/logcli/detected/fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package detected

import (
"encoding/json"
"fmt"
"log"
"slices"
"strings"
"time"

"github.com/fatih/color"
"github.com/grafana/loki/v3/pkg/logcli/client"
"github.com/grafana/loki/v3/pkg/loghttp"
)

type FieldsQuery struct {
QueryString string
Start time.Time
End time.Time
FieldLimit int
LineLimit int
Step time.Duration
Quiet bool
ColoredOutput bool
}

// DoQuery executes the query and prints out the results
func (q *FieldsQuery) Do(c client.Client, outputMode string) {
var resp *loghttp.DetectedFieldsResponse
var err error

resp, err = c.GetDetectedFields(q.QueryString, q.FieldLimit, q.LineLimit, q.Start, q.End, q.Step, q.Quiet)
if err != nil {
log.Fatalf("Error doing request: %+v", err)
}

switch outputMode {
case "raw":
out, err := json.Marshal(resp)
if err != nil {
log.Fatalf("Error marshalling response: %+v", err)
}
fmt.Println(string(out))
default:
output := make([]string, len(resp.Fields))
for i, field := range resp.Fields {
bold := color.New(color.Bold)
output[i] = fmt.Sprintf("label: %s\t\t", bold.Sprintf("%s", field.Label)) +
fmt.Sprintf("type: %s\t\t", bold.Sprintf("%s", field.Type)) +
fmt.Sprintf("cardinality: %s", bold.Sprintf("%d", field.Cardinality))
}

slices.Sort(output)
fmt.Println(strings.Join(output, "\n"))
}
}
10 changes: 10 additions & 0 deletions pkg/logcli/query/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,16 @@ func (t *testQueryClient) GetVolumeRange(_ *volume.Query) (*loghttp.QueryRespons
panic("not implemented")
}

func (t *testQueryClient) GetDetectedFields(
_ string,
_, _ int,
_, _ time.Time,
_ time.Duration,
_ bool,
) (*loghttp.DetectedFieldsResponse, error) {
panic("not implemented")
}

var legacySchemaConfigContents = `schema_config:
configs:
- from: 2020-05-15
Expand Down
14 changes: 14 additions & 0 deletions pkg/loghttp/detected.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package loghttp

import "github.com/grafana/loki/v3/pkg/logproto"

// LabelResponse represents the http json response to a label query
type DetectedFieldsResponse struct {
Fields []DetectedField `json:"fields,omitempty"`
}

type DetectedField struct {
Label string `json:"label,omitempty"`
Type logproto.DetectedFieldType `json:"type,omitempty"`
Cardinality uint64 `json:"cardinality,omitempty"`
}
Loading