-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(docker-logql): make
docker-logql
part of oteldb
- Loading branch information
Showing
8 changed files
with
668 additions
and
4 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
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 |
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,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) | ||
``` |
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,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) | ||
} |
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,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) | ||
} |
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,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 | ||
} |
Oops, something went wrong.