Skip to content

Commit

Permalink
feat(docker-logql): make docker-logql part of oteldb
Browse files Browse the repository at this point in the history
  • Loading branch information
tdakkota committed Aug 18, 2024
1 parent c10f915 commit 954b9f7
Show file tree
Hide file tree
Showing 8 changed files with 668 additions and 4 deletions.
4 changes: 4 additions & 0 deletions cmd/docker-logql/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
install:
mkdir -pv ~/.docker/cli-plugins/
go build -v -o ~/.docker/cli-plugins/docker-logql ./
.PHONY: install
45 changes: 45 additions & 0 deletions cmd/docker-logql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# docker-logql

A simple Docker CLI plugin to run LogQL queries over docker container logs.

## Installation

1. Build `docker-logql` binary.
- **NOTE**: `docker-` prefix is important, docker would not find plugin without it.
2. Add binary to [plugin directory](https://github.com/docker/cli/blob/34797d167891c11d2e10c1339b072166b77a3378/cli-plugins/manager/manager_unix.go#L5-L8)
- `~/.docker/cli-plugins` for current user
- `/usr/local/libexec/docker/cli-plugins` for system-wide installation

Or use `make install`, it would build and add plugin to `~/.docker/cli-plugins` directory.

```console
git clone --depth 1 https://github.com/go-faster/oteldb
cd oteldb/cmd/docker-logql
make install
```

## Query logs

```console
$ docker logql query --help

Usage: docker logql query <logql>

Examples:
# Get logs from all containers.
docker logql query '{}'

# Get logs for last 24h from container "registry" that contains "info".
docker logql query --since=1d '{container="registry"} |= "info"'

Options:
--color Enable color (default true)
-c, --container Show container name (default true)
-d, --direction string Direction of sorting (default "asc")
--end lokiapi.LokiTime End of query range, defaults to now (default now)
-l, --limit int Limit result (default -1)
--since start A duration used to calculate start relative to `end` (default 6h)
--start lokiapi.LokiTime Start of query range (default `end - since`)
--step lokiapi.PrometheusDuration Query resolution step
-t, --timestamp Show timestamps (default true)
```
33 changes: 33 additions & 0 deletions cmd/docker-logql/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"fmt"
"strconv"
)

var (
names = []string{
"grey",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
}

resetColor = ansi("0")
colors = func() map[string]string {
m := map[string]string{}
for i, name := range names {
m[name] = ansi(strconv.Itoa(30 + i))
m["intense_"+name] = ansi(strconv.Itoa(30+i) + ";1")
}
return m
}()
)

func ansi(code string) string {
return fmt.Sprintf("\033[%sm", code)
}
52 changes: 52 additions & 0 deletions cmd/docker-logql/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Command main implements Docker CLI plugin to run LogQL queries.
package main

import (
"fmt"

"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"

"github.com/go-faster/oteldb/internal/cliversion"
)

func rootCmd(dcli command.Cli) *cobra.Command {
root := &cobra.Command{
Use: "logql",
}
root.AddCommand(queryCmd(dcli))
root.AddCommand(&cobra.Command{
Use: "version",
Short: "Print plugin version",
Run: func(cmd *cobra.Command, _ []string) {
info, _ := cliversion.GetInfo("github.com/go-faster/oteldb")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "docker-logql %s\n", info)
},
})
return root
}

func getVersion() string {
info, _ := cliversion.GetInfo("github.com/go-faster/oteldb")
switch {
case info.Version != "":
return info.Version
case info.Commit != "":
return "dev-" + info.Commit
default:
return "unknown"
}
}

func main() {
meta := manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "go-faster",
Version: getVersion(),
ShortDescription: "A simple Docker CLI plugin to run LogQL queries over docker container logs.",
URL: "https://github.com/go-faster/oteldb/cmd/docker-logql",
}
plugin.Run(rootCmd, meta)
}
190 changes: 190 additions & 0 deletions cmd/docker-logql/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package main

import (
"fmt"
"math"
"strconv"
"strings"
"time"

"github.com/go-faster/errors"
"github.com/prometheus/common/model"
"github.com/spf13/pflag"

"github.com/go-faster/oteldb/internal/logql/logqlengine"
"github.com/go-faster/oteldb/internal/lokiapi"
)

// APIFlag is [pflag.Value] wrapping ogen optional.
type APIFlag[
O interface {
SetTo(S)
},
S ~string,
] struct {
Val O
DefaultVal S
}

func apiFlagFor[
T interface {
Get() (S, bool)
},
P interface {
*T
SetTo(S)
},
S ~string,
](defaultVal S) APIFlag[P, S] {
var zero T
return APIFlag[P, S]{
Val: &zero,
DefaultVal: defaultVal,
}
}

var _ pflag.Value = (*APIFlag[*lokiapi.OptLokiTime, lokiapi.LokiTime])(nil)

// String implements [pflag.Value].
func (f *APIFlag[O, S]) String() string {
return string(f.DefaultVal)
}

// Set implements [pflag.Value].
func (f *APIFlag[O, S]) Set(val string) error {
f.Val.SetTo(S(val))
return nil
}

// Type implements [pflag.Value].
func (f *APIFlag[O, S]) Type() string {
var zero S
return fmt.Sprintf("%T", zero)
}

// parseTimeRange parses optional parameters and returns time range
//
// Default values:
//
// - since = 6 * time.Hour
// - end = now
// - start = end.Add(-since)
func parseTimeRange(
now time.Time,
startParam lokiapi.OptLokiTime,
endParam lokiapi.OptLokiTime,
sinceParam lokiapi.OptPrometheusDuration,
) (start, end time.Time, err error) {
since := 6 * time.Hour
if v, ok := sinceParam.Get(); ok {
d, err := model.ParseDuration(string(v))
if err != nil {
return start, end, errors.Wrap(err, "parse since")
}
since = time.Duration(d)
}

endValue := endParam.Or("")
end, err = parseTimestamp(endValue, now)
if err != nil {
return start, end, errors.Wrapf(err, "parse end %q", endValue)
}

endOrNow := end
if end.After(now) {
endOrNow = now
}

startValue := startParam.Or("")
start, err = parseTimestamp(startValue, endOrNow.Add(-since))
if err != nil {
return start, end, errors.Wrapf(err, "parse start %q", startValue)
}
return start, end, nil
}

func parseTimestamp(lt lokiapi.LokiTime, def time.Time) (time.Time, error) {
value := string(lt)
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 {
return time.Parse(time.RFC3339Nano, value)
}
if len(value) <= 10 {
return time.Unix(nanos, 0), nil
}
return time.Unix(0, nanos), nil
}

func parseStep(param lokiapi.OptPrometheusDuration, start, end time.Time) (time.Duration, error) {
v, ok := param.Get()
if !ok {
return defaultStep(start, end), nil
}
return parseDuration(v)
}

func defaultStep(start, end time.Time) time.Duration {
seconds := math.Max(
math.Floor(end.Sub(start).Seconds()/250),
1,
)
return time.Duration(seconds) * time.Second
}

func parseDuration(param lokiapi.PrometheusDuration) (time.Duration, error) {
value := string(param)
if !strings.ContainsAny(value, "smhdwy") {
f, err := strconv.ParseFloat(value, 64)
if err == nil {
d := f * float64(time.Second)
return time.Duration(d), nil
}
}
md, err := model.ParseDuration(value)
return time.Duration(md), err
}

var directionMap = func() map[string]logqlengine.Direction {
m := map[string]logqlengine.Direction{}
for _, s := range []struct {
dir logqlengine.Direction
values []string
}{
{
logqlengine.DirectionBackward,
[]string{"desc", "descending"},
},
{
logqlengine.DirectionForward,
[]string{"asc", "ascending"},
},
} {
m[s.dir.String()] = s.dir
for _, v := range s.values {
m[v] = s.dir
}
}
return m
}()

func parseDirection(s string) (logqlengine.Direction, error) {
orig := s
s = strings.ToLower(s)

d, ok := directionMap[s]
if !ok {
return "", errors.Errorf("unexpected direction %q", orig)
}
return d, nil
}
Loading

0 comments on commit 954b9f7

Please sign in to comment.