From fd85b1f6058ce0f98e6c7cc395b8c5cd19f883eb Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 00:30:22 -0500 Subject: [PATCH 01/12] feat: add vector integration This currently is missing a few features: - global sinks - logrotation But works fairly well otherwise. --- docs/deployment/logs.md | 88 ++++++- plugins/common/docker.go | 30 +++ plugins/common/log.go | 16 ++ plugins/logs/.gitignore | 5 + plugins/logs/Makefile | 5 +- plugins/logs/functions.go | 247 ++++++++++++++++++++ plugins/logs/go.mod | 3 + plugins/logs/go.sum | 14 ++ plugins/logs/logs.go | 9 + plugins/logs/report.go | 25 ++ plugins/logs/src/commands/commands.go | 7 +- plugins/logs/src/subcommands/subcommands.go | 28 +++ plugins/logs/src/triggers/triggers.go | 36 +++ plugins/logs/subcommands.go | 79 +++++++ plugins/logs/triggers.go | 41 ++++ 15 files changed, 628 insertions(+), 5 deletions(-) create mode 100644 plugins/logs/functions.go create mode 100644 plugins/logs/report.go create mode 100644 plugins/logs/src/triggers/triggers.go create mode 100644 plugins/logs/triggers.go diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index fb251842b..05d2797c7 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -2,7 +2,12 @@ ``` logs [-h] [-t] [-n num] [-q] [-p process] # Display recent log output -logs:failed [--parallel count] [--all|] # Shows the last failed deploy logs +logs:failed [--all|] # Shows the last failed deploy logs +logs:report [] [] # Displays a logs report for one or more apps +logs:set # Set or clear a logs property for an app +logs:vector-logs # Tail the logs of the vector container +logs:vector-start # Start the vector logging container +logs:vector-stop # Stop the vector logging container ``` ## Usage @@ -40,7 +45,7 @@ will show logs continually from the web process. In some cases, it may be useful to retrieve the logs from a previously failed deploy. -You can retrieve these logs by using the `logs:failed` command +You can retrieve these logs by using the `logs:failed` command. ```shell dokku logs:failed node-js-app @@ -51,3 +56,82 @@ You may also fetch all failed app logs by using the `--all` flag. ```shell dokku logs:failed --all ``` + +### Vector Logging + +Vector is an open-source, lightweight and ultra-fast tool for building observability pipelines. Dokku integrates with it for shipping container logs for the `docker-local` scheduler. Users may configure log-shipping on a per-app or global basis, neither of which interfere with the `dokku logs` commands. + +#### Starting the Vector container + +> Warning: While the default vector image may be updated over time, this will not impact running vector containers. Users are encouraged to view any Dokku and Vector changelogs to ensure their system will continue running as expected. + +Vector may be started via the `logs:vector-start` command. + +```shell +dokku logs:vector-start +``` + +This will start a new container named `vector` with Dokku's vector config mounted and ready for use. If a running container already exists, this command will do nothing. Additionally, if a container exists but is not running, this command will attempt to start the container. + +By default, the vector container will do nothing and may exit immediately. It is necessary to configure a sink in order to ensure the vector container runs as necessary. + +While the default vector image is hardcoded, users may specify an alternative via the `--vector-image` flag: + +```shell +dokku logs:vector-start --vector-image timberio/vector:latest-debian +``` + +The `vector` container will be started with the following volume mounts: + +- `/var/lib/dokku/data/logs/vector.json:/etc/vector/vector.json` +- `/var/run/docker.sock:/var/run/docker.sock` +- `/var/log/dokku/apps:/var/log/dokku/apps` + +The final volume mount - `/var/log/dokku/apps` - may be used for users that wish to ship logs to a file on disk that may be later logrotated. This directory is owned by the `dokku` user and group, with permissions set to `0755`. At this time, log-rotation is not configured for this directory. + +#### Stopping the Vector container + +Vector may be stopped via the `logs:vector-stop` command. + +```shell +dokku logs:vector-stop +``` + +The `vector` container will be stopped and removed from the system. If the container is not running, this command will do nothing. + +#### Configuring a log sink + +Vector uses the concept of log "sinks" to send logs to a given endpoint. Log sinks may be configured on a per-app basis by specifying a `sink` in DSN form with the `logs:set` command. Specifying a sink value will reload any running vector container. + +```shell +# setting the sink value in quotes is encouraged to avoid +# issues with ampersand encoding in shell commands +dokku logs:set node-js-app sink "console://?encoding[codec]=json" +``` + +A sink may be removed by setting an empty value, which will also reload the running vector container. + +```shell +dokku logs:set node-js-app sink +``` + +Only one sink may be specified on a per-app basis at a given time. + +##### Log Sink DSN Format + +The DSN form of a sink is as follows: + +``` +SINK_TYPE://?SINK_OPTIONS +``` + +Valid values for `SINK_TYPE` include all log vector log sinks, while `SINK_OPTIONS` is a query-string form for the sink's options. The following is a short description on how to set various values: + +- `bool`: form: `key=bool` +- `string`: form: `key=string` +- `int`: form: `key=int` +- `[string]`: form: `key[]=string` +- `[int]`: form: `key[]=int` +- `table`: form: `option[key]=value + +Please read the [sink documentation](https://vector.dev/docs/reference/sinks/) for your sink of choice to configure the sink as desired. diff --git a/plugins/common/docker.go b/plugins/common/docker.go index ec80b6acd..18df7ecd5 100644 --- a/plugins/common/docker.go +++ b/plugins/common/docker.go @@ -20,6 +20,31 @@ func ContainerIsRunning(containerID string) bool { return strings.TrimSpace(string(b[:])) == "true" } +// ContainerStart runs 'docker container start' against an existing container +// whether that container is running or not +func ContainerStart(containerID string) bool { + cmd := sh.Command(DockerBin(), "container", "start", containerID) + cmd.Stdout = nil + cmd.Stderr = nil + if err := cmd.Run(); err != nil { + return false + } + + return true +} + +// ContainerExists checks to see if a container exists +func ContainerExists(containerID string) bool { + cmd := sh.Command(DockerBin(), "container", "inspect", containerID) + cmd.Stdout = nil + cmd.Stderr = nil + if err := cmd.Run(); err != nil { + return false + } + + return true +} + // CopyFromImage copies a file from named image to destination func CopyFromImage(appName string, image string, source string, destination string) error { if !VerifyImage(image) { @@ -219,6 +244,11 @@ func DockerInspect(containerOrImageID, format string) (output string, err error) return } +// DockerInspect runs an inspect command with a given format against a container ID +func DockerSignal(containerID, signal string) (err error) { + return sh.Command(DockerBin(), "kill", fmt.Sprintf("--signal=%s", signal), containerID).Run() +} + // IsImageHerokuishBased returns true if app image is based on herokuish func IsImageHerokuishBased(image string, appName string) bool { output, err := DockerInspect(image, "{{range .Config.Env}}{{if eq . \"USER=herokuishuser\" }}{{println .}}{{end}}{{end}}") diff --git a/plugins/common/log.go b/plugins/common/log.go index e939cd1b2..e7a988031 100644 --- a/plugins/common/log.go +++ b/plugins/common/log.go @@ -1,6 +1,7 @@ package common import ( + "bufio" "fmt" "os" "strings" @@ -93,6 +94,21 @@ func LogVerboseQuietContainerLogs(containerID string) { } } +// LogVerboseQuietContainerLogs is the verbose log formatter for container logs +func LogVerboseQuietContainerLogsTail(containerID string) { + sc := NewShellCmdWithArgs(DockerBin(), "container", "logs", containerID, "--follow", "--tail", "10") + stdout, _ := sc.Command.StdoutPipe() + sc.Command.Start() + + scanner := bufio.NewScanner(stdout) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + m := scanner.Text() + fmt.Println(m) + } + sc.Command.Wait() +} + // LogWarn is the warning log formatter func LogWarn(text string) { fmt.Fprintln(os.Stderr, fmt.Sprintf(" ! %s", text)) diff --git a/plugins/logs/.gitignore b/plugins/logs/.gitignore index 0f6770b1e..291036bd4 100644 --- a/plugins/logs/.gitignore +++ b/plugins/logs/.gitignore @@ -1,2 +1,7 @@ /commands /subcommands/* +/triggers/* +/triggers +/install +/post-* +/report diff --git a/plugins/logs/Makefile b/plugins/logs/Makefile index abb84bed4..bcd96e90f 100644 --- a/plugins/logs/Makefile +++ b/plugins/logs/Makefile @@ -1,5 +1,6 @@ -SUBCOMMANDS = subcommands/failed -BUILD = commands subcommands +SUBCOMMANDS = subcommands/failed subcommands/report subcommands/set subcommands/vector-logs subcommands/vector-start subcommands/vector-stop +TRIGGERS = triggers/install triggers/post-delete triggers/report +BUILD = commands subcommands triggers PLUGIN_NAME = logs include ../../common.mk diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go new file mode 100644 index 000000000..4040ad04e --- /dev/null +++ b/plugins/logs/functions.go @@ -0,0 +1,247 @@ +package logs + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "path/filepath" + "strings" + "time" + + "github.com/dokku/dokku/plugins/common" + "github.com/joncalhoun/qson" + "github.com/xo/dburl" +) + +type vectorConfig struct { + Sources map[string]vectorSource `json:"sources"` + Sinks map[string]vectorSink `json:"sinks"` +} + +type vectorSource struct { + Type string `json:"type"` + IncludeLabels []string `json:"include_labels,omitempty"` +} + +type vectorSink map[string]interface{} + +const vectorContainerName = "vector" + +func init() { + sinks := []string{ + "aws_cloudwatch_logs", + "aws_kinesis_firehose", + "aws_kinesis_streams", + "aws_s3", "aws_sqs", + "azure_monitor_logs", + "blackhole", + "clickhouse", + "console", + "datadog_logs", + "elasticsearch", + "file", + "gcp_cloud_storage", + "gcp_pubsub", + "gcp_stackdriver_logs", + "honeycomb", + "http", + "humio_logs", + "influxdb_logs", + "kafka", + "logdna", + "loki", + "nats", + "new_relic_logs", + "papertrail", + "pulsar", + "sematext_logs", + "socket", + "splunk_hec", + "vector", + } + for _, sink := range sinks { + dburl.Register(dburl.Scheme{ + Driver: sink, + Generator: genOpaqueWithMissingPath, + Proto: 0, + Opaque: false, + Aliases: []string{}, + Override: "", + }) + } +} + +func genOpaqueWithMissingPath(u *dburl.URL) (string, error) { + return u.Opaque + genQueryOptions(u.Query()), nil +} + +func genQueryOptions(q url.Values) string { + if s := q.Encode(); s != "" { + return "?" + s + } + return "" +} + +func killVectorContainer() error { + if !common.ContainerExists(vectorContainerName) { + return nil + } + + if err := stopVectorContainer(); err != nil { + return err + } + + time.Sleep(10 * time.Second) + if err := removeVectorContainer(); err != nil { + return err + } + + return nil +} + +func removeVectorContainer() error { + if !common.ContainerExists(vectorContainerName) { + return nil + } + + cmd := common.NewShellCmd(strings.Join([]string{ + common.DockerBin(), "container", "rm", "-f", vectorContainerName}, " ")) + + return common.SuppressOutput(func() error { + if cmd.Execute() { + return nil + } + + if common.ContainerExists(vectorContainerName) { + return errors.New("Unable to remove vector container") + } + + return nil + }) +} + +func startVectorContainer(vectorImage string) error { + cmd := common.NewShellCmd(strings.Join([]string{ + common.DockerBin(), + "container", + "run", "--detach", "--name", vectorContainerName, common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS"), + "--volume", "/var/lib/dokku/data/logs/vector.json:/etc/vector/vector.json", + "--volume", "/var/run/docker.sock:/var/run/docker.sock", + vectorImage, + "--config", "/etc/vector/vector.json"}, " ")) + + if !cmd.Execute() { + return errors.New("Unable to start vector container") + } + + return nil +} + +func stopVectorContainer() error { + if !common.ContainerExists(vectorContainerName) { + return nil + } + + if !common.ContainerIsRunning(vectorContainerName) { + return nil + } + + cmd := common.NewShellCmd(strings.Join([]string{ + common.DockerBin(), "container", "stop", vectorContainerName}, " ")) + + return common.SuppressOutput(func() error { + if cmd.Execute() { + return nil + } + + if common.ContainerIsRunning(vectorContainerName) { + return errors.New("Unable to stop vector container") + } + + return nil + }) +} + +func valueToConfig(appName string, value string) (vectorSink, error) { + var data vectorSink + u, err := dburl.Parse(value) + if err != nil { + return data, err + } + + if u.Query().Get("sinks") != "" { + return data, errors.New("Invalid option sinks") + } + + t := fmt.Sprintf("type=%s", u.Scheme) + i := fmt.Sprintf("inputs[]=docker-source:%s", appName) + initialQuery := fmt.Sprintf("%s&%s", t, i) + query := u.RawQuery + if query == "" { + query = initialQuery + } else if strings.HasPrefix(query, "&") { + query = fmt.Sprintf("%s%s", initialQuery, query) + } else { + query = fmt.Sprintf("%s&%s", initialQuery, query) + } + + b, err := qson.ToJSON(query) + if err != nil { + return data, err + } + + if err := json.Unmarshal(b, &data); err != nil { + return data, err + } + + return data, nil +} + +func writeVectorConfig() error { + apps, err := common.DokkuApps() + if err != nil { + return err + } + + data := vectorConfig{ + Sources: map[string]vectorSource{}, + Sinks: map[string]vectorSink{}, + } + for _, appName := range apps { + value := common.PropertyGet("logs", appName, "vector-sink") + if value == "" { + continue + } + + sink, err := valueToConfig(appName, value) + if err != nil { + return err + } + + data.Sources[fmt.Sprintf("docker-source:%s", appName)] = vectorSource{ + Type: "docker_logs", + IncludeLabels: []string{fmt.Sprintf("com.dokku.app-name=%s", appName)}, + } + + data.Sinks[fmt.Sprintf("docker-sink:%s", appName)] = sink + } + + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + + vectorConfig := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "logs", "vector.json") + if err := common.WriteSliceToFile(vectorConfig, []string{string(b)}); err != nil { + return err + } + + if !common.ContainerExists(vectorContainerName) { + return nil + } + + return common.SuppressOutput(func() error { + return common.DockerSignal(vectorContainerName, "HUP") + }) +} diff --git a/plugins/logs/go.mod b/plugins/logs/go.mod index e29b5eddc..fda43da75 100644 --- a/plugins/logs/go.mod +++ b/plugins/logs/go.mod @@ -3,10 +3,13 @@ module github.com/dokku/dokku/plugins/logs go 1.15 require ( + github.com/RaveNoX/go-jsonmerge v1.0.0 github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 + github.com/joncalhoun/qson v0.0.0-20200422171543-84433dcd3da0 github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/xo/dburl v0.1.0 ) replace github.com/dokku/dokku/plugins/common => ../common diff --git a/plugins/logs/go.sum b/plugins/logs/go.sum index 58c904707..77eb3234a 100644 --- a/plugins/logs/go.sum +++ b/plugins/logs/go.sum @@ -1,10 +1,24 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/RaveNoX/go-jsonmerge v1.0.0 h1:2e0nqnadoGUP8rAvcA0hkQelZreVO5X3BHomT2XMrAk= +github.com/RaveNoX/go-jsonmerge v1.0.0/go.mod h1:qYM/NA77LhO4h51JJM7Z+xBU3ovqrNIACZe+SkSNVFo= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 h1:HHUr4P/aKh4quafGxDT9LDasjGdlGkzLbfmmrlng3kA= github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/joncalhoun/qson v0.0.0-20200422171543-84433dcd3da0 h1:ct2XA1aDw8A07Dr8gtrrZgIgLKcZNAl2o9nn0WRMK4Y= +github.com/joncalhoun/qson v0.0.0-20200422171543-84433dcd3da0/go.mod h1:DFXrEwSRX0p/aSvxE21319menCBFeQO0jXpRj7LEZUA= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 h1:utdYOikI1XjNtTFGCwSM6OmFJblU4ld4gACoJsbadJg= github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/xo/dburl v0.1.0 h1:WsW5ujxrapzhTflR4WoI+ZhynjE8T412ZWyoZtNNuJI= +github.com/xo/dburl v0.1.0/go.mod h1:TM8VMBT+LWqC3MBOulZjb8FAthcvZq0t/qvDLwS6skU= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/plugins/logs/logs.go b/plugins/logs/logs.go index 5d2bb4636..b343a8846 100644 --- a/plugins/logs/logs.go +++ b/plugins/logs/logs.go @@ -6,6 +6,15 @@ import ( "github.com/dokku/dokku/plugins/common" ) +var ( + // DefaultProperties is a map of all valid ps properties with corresponding default property values + DefaultProperties = map[string]string{ + "vector-sink": "", + } +) + +const VectorImage = "timberio/vector:0.11.X-debian" + // GetFailedLogs outputs failed deploy logs for a given app func GetFailedLogs(appName string) error { common.LogInfo2Quiet(fmt.Sprintf("%s failed deploy logs", appName)) diff --git a/plugins/logs/report.go b/plugins/logs/report.go new file mode 100644 index 000000000..c887a5073 --- /dev/null +++ b/plugins/logs/report.go @@ -0,0 +1,25 @@ +package logs + +import ( + "github.com/dokku/dokku/plugins/common" +) + +// ReportSingleApp is an internal function that displays the logs report for one or more apps +func ReportSingleApp(appName, infoFlag string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + flags := map[string]common.ReportFunc{ + "--logs-vector-sink": reportVectorSink, + } + + trimPrefix := false + uppercaseFirstCharacter := true + infoFlags := common.CollectReport(appName, infoFlag, flags) + return common.ReportSingleApp("logs", appName, infoFlag, infoFlags, trimPrefix, uppercaseFirstCharacter) +} + +func reportVectorSink(appName string) string { + return common.PropertyGet("logs", appName, "vector-sink") +} diff --git a/plugins/logs/src/commands/commands.go b/plugins/logs/src/commands/commands.go index 15ca282da..dd6e72e45 100644 --- a/plugins/logs/src/commands/commands.go +++ b/plugins/logs/src/commands/commands.go @@ -23,7 +23,12 @@ Additional commands:` helpContent = ` logs [-h] [-t|--tail] [-n|--num num] [-q|--quiet] [-p|--ps process] , Display recent log output - logs:failed [], Shows the last failed deploy logs + logs:failed [--all|], Shows the last failed deploy logs + logs:report [] [], Displays a logs report for one or more apps + logs:set , Set or clear a logs property for an app + logs:vector-logs, Tail the logs of the vector container + logs:vector-start, Start the vector logging container + logs:vector-stop, Stop the vector logging container ` ) diff --git a/plugins/logs/src/subcommands/subcommands.go b/plugins/logs/src/subcommands/subcommands.go index 9d09b974a..bcadf8335 100644 --- a/plugins/logs/src/subcommands/subcommands.go +++ b/plugins/logs/src/subcommands/subcommands.go @@ -24,6 +24,34 @@ func main() { args.Parse(os.Args[2:]) appName := args.Arg(0) err = logs.CommandFailed(appName, *allApps) + case "report": + args := flag.NewFlagSet("logs:report", flag.ExitOnError) + osArgs, infoFlag, err := common.ParseReportArgs("logs", os.Args[2:]) + if err == nil { + args.Parse(osArgs) + appName := args.Arg(0) + err = logs.CommandReport(appName, infoFlag) + } + case "set": + args := flag.NewFlagSet("logs:set", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + property := args.Arg(1) + value := args.Arg(2) + err = logs.CommandSet(appName, property, value) + case "vector-logs": + args := flag.NewFlagSet("logs:vector-logs", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = logs.CommandVectorLogs() + case "vector-start": + args := flag.NewFlagSet("logs:vector-start", flag.ExitOnError) + vectorImage := args.String("vector-image", logs.VectorImage, "--vector-image: the name of the docker image to run for vector") + args.Parse(os.Args[2:]) + err = logs.CommandVectorStart(*vectorImage) + case "vector-stop": + args := flag.NewFlagSet("logs:vector-stop", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = logs.CommandVectorStop() default: common.LogFail(fmt.Sprintf("Invalid plugin subcommand call: %s", subcommand)) } diff --git a/plugins/logs/src/triggers/triggers.go b/plugins/logs/src/triggers/triggers.go new file mode 100644 index 000000000..d09fa5c80 --- /dev/null +++ b/plugins/logs/src/triggers/triggers.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/logs" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "install": + err = logs.TriggerInstall() + case "post-delete": + appName := flag.Arg(0) + err = logs.TriggerPostDelete(appName) + case "report": + appName := flag.Arg(0) + err = logs.ReportSingleApp(appName, "") + default: + common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger)) + } + + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/logs/subcommands.go b/plugins/logs/subcommands.go index a1256151b..ffabc7be9 100644 --- a/plugins/logs/subcommands.go +++ b/plugins/logs/subcommands.go @@ -1,7 +1,9 @@ package logs import ( + "errors" "fmt" + "path/filepath" "strconv" "github.com/dokku/dokku/plugins/common" @@ -45,3 +47,80 @@ func CommandFailed(appName string, allApps bool) error { return GetFailedLogs(appName) } + +// CommandReport displays a logs report for one or more apps +func CommandReport(appName string, infoFlag string) error { + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + return err + } + for _, appName := range apps { + if err := ReportSingleApp(appName, infoFlag); err != nil { + return err + } + } + return nil + } + + return ReportSingleApp(appName, infoFlag) +} + +// CommandSet sets or clears a logs property for an app +func CommandSet(appName string, property string, value string) error { + if property == "vector-sink" && value != "" { + _, err := valueToConfig(appName, value) + if err != nil { + return err + } + } + + common.CommandPropertySet("logs", appName, property, value, DefaultProperties) + if property == "vector-sink" { + common.LogVerboseQuiet(fmt.Sprintf("Writing updated vector config to %s", filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "logs", "vector.json"))) + return writeVectorConfig() + } + return nil +} + +// CommandVectorLogs tails the log output for the vector container +func CommandVectorLogs() error { + if !common.ContainerExists(vectorContainerName) { + return errors.New("Vector container does not exist") + } + + if !common.ContainerIsRunning(vectorContainerName) { + return errors.New("Vector container is not running") + } + + common.LogInfo1Quiet("Tailing vector container logs") + common.LogVerboseQuietContainerLogsTail(vectorContainerName) + + return nil +} + +// CommandVectorStart starts a new vector container +// or starts an existing one if it already exists +func CommandVectorStart(vectorImage string) error { + common.LogInfo2("Starting vector container") + if !common.ContainerExists(vectorContainerName) { + return startVectorContainer(vectorImage) + } + + if common.ContainerIsRunning(vectorContainerName) { + common.LogVerbose("Container already running") + return nil + } + + if !common.ContainerStart(vectorContainerName) { + return errors.New("Unable to start vector container") + } + + return nil +} + +// CommandVectorStop stops and removes an existing vector container +func CommandVectorStop() error { + common.LogInfo2Quiet("StoppingĀ and removing vector container") + return killVectorContainer() +} diff --git a/plugins/logs/triggers.go b/plugins/logs/triggers.go new file mode 100644 index 000000000..a43c9c884 --- /dev/null +++ b/plugins/logs/triggers.go @@ -0,0 +1,41 @@ +package logs + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/dokku/dokku/plugins/common" +) + +// TriggerInstall initializes app restart policies +func TriggerInstall() error { + if err := common.PropertySetup("logs"); err != nil { + return fmt.Errorf("Unable to install the logs plugin: %s", err.Error()) + } + + directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "logs") + if err := os.MkdirAll(directory, 0755); err != nil { + return err + } + + if err := common.SetPermissions(directory, 0755); err != nil { + return err + } + + logDirectory := filepath.Join(common.MustGetEnv("DOKKU_LOGS_DIR"), "apps") + if err := os.MkdirAll(logDirectory, 0755); err != nil { + return err + } + + if err := common.SetPermissions(logDirectory, 0755); err != nil { + return err + } + + return nil +} + +// TriggerPostDelete destroys the network property for a given app container +func TriggerPostDelete(appName string) error { + return common.PropertyDestroy("logs", appName) +} From f5fa94cc9ce1b4a6924d1067f91a8fde8b95516e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 00:42:13 -0500 Subject: [PATCH 02/12] chore: remove dburl usage dburl adds aliases for everything, which breaks if two schemes are similar enough (anything with `aws_` as a prefix broke scheme registration). It does not appear to add anything, so we'll drop that dependency. Also remove go-jsonmerge, as this was refactored to use structs instead of byte manipulation... --- plugins/logs/functions.go | 58 +-------------------------------------- plugins/logs/go.mod | 2 -- plugins/logs/go.sum | 12 -------- 3 files changed, 1 insertion(+), 71 deletions(-) diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go index 4040ad04e..35856b520 100644 --- a/plugins/logs/functions.go +++ b/plugins/logs/functions.go @@ -11,7 +11,6 @@ import ( "github.com/dokku/dokku/plugins/common" "github.com/joncalhoun/qson" - "github.com/xo/dburl" ) type vectorConfig struct { @@ -28,61 +27,6 @@ type vectorSink map[string]interface{} const vectorContainerName = "vector" -func init() { - sinks := []string{ - "aws_cloudwatch_logs", - "aws_kinesis_firehose", - "aws_kinesis_streams", - "aws_s3", "aws_sqs", - "azure_monitor_logs", - "blackhole", - "clickhouse", - "console", - "datadog_logs", - "elasticsearch", - "file", - "gcp_cloud_storage", - "gcp_pubsub", - "gcp_stackdriver_logs", - "honeycomb", - "http", - "humio_logs", - "influxdb_logs", - "kafka", - "logdna", - "loki", - "nats", - "new_relic_logs", - "papertrail", - "pulsar", - "sematext_logs", - "socket", - "splunk_hec", - "vector", - } - for _, sink := range sinks { - dburl.Register(dburl.Scheme{ - Driver: sink, - Generator: genOpaqueWithMissingPath, - Proto: 0, - Opaque: false, - Aliases: []string{}, - Override: "", - }) - } -} - -func genOpaqueWithMissingPath(u *dburl.URL) (string, error) { - return u.Opaque + genQueryOptions(u.Query()), nil -} - -func genQueryOptions(q url.Values) string { - if s := q.Encode(); s != "" { - return "?" + s - } - return "" -} - func killVectorContainer() error { if !common.ContainerExists(vectorContainerName) { return nil @@ -165,7 +109,7 @@ func stopVectorContainer() error { func valueToConfig(appName string, value string) (vectorSink, error) { var data vectorSink - u, err := dburl.Parse(value) + u, err := url.Parse(value) if err != nil { return data, err } diff --git a/plugins/logs/go.mod b/plugins/logs/go.mod index fda43da75..5f0bdacb7 100644 --- a/plugins/logs/go.mod +++ b/plugins/logs/go.mod @@ -3,13 +3,11 @@ module github.com/dokku/dokku/plugins/logs go 1.15 require ( - github.com/RaveNoX/go-jsonmerge v1.0.0 github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 github.com/joncalhoun/qson v0.0.0-20200422171543-84433dcd3da0 github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/xo/dburl v0.1.0 ) replace github.com/dokku/dokku/plugins/common => ../common diff --git a/plugins/logs/go.sum b/plugins/logs/go.sum index 77eb3234a..00274255e 100644 --- a/plugins/logs/go.sum +++ b/plugins/logs/go.sum @@ -1,24 +1,12 @@ -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/RaveNoX/go-jsonmerge v1.0.0 h1:2e0nqnadoGUP8rAvcA0hkQelZreVO5X3BHomT2XMrAk= -github.com/RaveNoX/go-jsonmerge v1.0.0/go.mod h1:qYM/NA77LhO4h51JJM7Z+xBU3ovqrNIACZe+SkSNVFo= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 h1:HHUr4P/aKh4quafGxDT9LDasjGdlGkzLbfmmrlng3kA= github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/joncalhoun/qson v0.0.0-20200422171543-84433dcd3da0 h1:ct2XA1aDw8A07Dr8gtrrZgIgLKcZNAl2o9nn0WRMK4Y= github.com/joncalhoun/qson v0.0.0-20200422171543-84433dcd3da0/go.mod h1:DFXrEwSRX0p/aSvxE21319menCBFeQO0jXpRj7LEZUA= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 h1:utdYOikI1XjNtTFGCwSM6OmFJblU4ld4gACoJsbadJg= github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/xo/dburl v0.1.0 h1:WsW5ujxrapzhTflR4WoI+ZhynjE8T412ZWyoZtNNuJI= -github.com/xo/dburl v0.1.0/go.mod h1:TM8VMBT+LWqC3MBOulZjb8FAthcvZq0t/qvDLwS6skU= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From e1425a85e41aa5b06a887ed618f070a27bad2400 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 00:50:14 -0500 Subject: [PATCH 03/12] feat: add ability to set global properties Not all properties can be set globally, and the "appName" in the global case is currently defaulted to `--global`. This is okay because domain label names cannot start with hyphens, and all app names must be valid domain label names. --- plugins/common/properties.go | 11 ++++++++--- plugins/logs/logs.go | 3 +++ plugins/logs/subcommands.go | 2 +- plugins/network/network.go | 3 +++ plugins/network/subcommands.go | 2 +- plugins/ps/ps.go | 3 +++ plugins/ps/subcommands.go | 2 +- 7 files changed, 20 insertions(+), 6 deletions(-) diff --git a/plugins/common/properties.go b/plugins/common/properties.go index c3b8a748b..fa17f40f4 100644 --- a/plugins/common/properties.go +++ b/plugins/common/properties.go @@ -12,9 +12,14 @@ import ( ) // CommandPropertySet is a generic function that will set a property for a given plugin/app combination -func CommandPropertySet(pluginName, appName, property, value string, properties map[string]string) { - if err := VerifyAppName(appName); err != nil { - LogFail(err.Error()) +func CommandPropertySet(pluginName, appName, property, value string, properties map[string]string, globalProperties map[string]bool) { + if appName != "--global" { + if err := VerifyAppName(appName); err != nil { + LogFail(err.Error()) + } + } + if !globalProperties[property] { + LogFail("Property cannot be specified globally") } if property == "" { LogFail("No property specified") diff --git a/plugins/logs/logs.go b/plugins/logs/logs.go index b343a8846..2ad2a1cec 100644 --- a/plugins/logs/logs.go +++ b/plugins/logs/logs.go @@ -11,6 +11,9 @@ var ( DefaultProperties = map[string]string{ "vector-sink": "", } + + // GlobalProperties is a map of all valid global logs properties + GlobalProperties = map[string]bool{} ) const VectorImage = "timberio/vector:0.11.X-debian" diff --git a/plugins/logs/subcommands.go b/plugins/logs/subcommands.go index ffabc7be9..ba59c17ff 100644 --- a/plugins/logs/subcommands.go +++ b/plugins/logs/subcommands.go @@ -75,7 +75,7 @@ func CommandSet(appName string, property string, value string) error { } } - common.CommandPropertySet("logs", appName, property, value, DefaultProperties) + common.CommandPropertySet("logs", appName, property, value, DefaultProperties, GlobalProperties) if property == "vector-sink" { common.LogVerboseQuiet(fmt.Sprintf("Writing updated vector config to %s", filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "logs", "vector.json"))) return writeVectorConfig() diff --git a/plugins/network/network.go b/plugins/network/network.go index e2f36ac47..954083f1e 100644 --- a/plugins/network/network.go +++ b/plugins/network/network.go @@ -22,6 +22,9 @@ var ( "attach-post-deploy": "", "tld": "", } + + // GlobalProperties is a map of all valid global network properties + GlobalProperties = map[string]bool{} ) // BuildConfig builds network config files diff --git a/plugins/network/subcommands.go b/plugins/network/subcommands.go index 5cf6e4d16..028ae0d9c 100644 --- a/plugins/network/subcommands.go +++ b/plugins/network/subcommands.go @@ -169,6 +169,6 @@ func CommandSet(appName string, property string, value string) error { } } - common.CommandPropertySet("network", appName, property, value, DefaultProperties) + common.CommandPropertySet("network", appName, property, value, DefaultProperties, GlobalProperties) return nil } diff --git a/plugins/ps/ps.go b/plugins/ps/ps.go index 84fc40996..7ca3dc5de 100644 --- a/plugins/ps/ps.go +++ b/plugins/ps/ps.go @@ -16,6 +16,9 @@ var ( DefaultProperties = map[string]string{ "restart-policy": "on-failure:10", } + + // GlobalProperties is a map of all valid global ps properties + GlobalProperties = map[string]bool{} ) // Rebuild rebuilds app from base image diff --git a/plugins/ps/subcommands.go b/plugins/ps/subcommands.go index dec35936a..008ca6201 100644 --- a/plugins/ps/subcommands.go +++ b/plugins/ps/subcommands.go @@ -163,7 +163,7 @@ func CommandSet(appName string, property string, value string) error { return dockeroptions.SetDockerOptionForPhases(appName, []string{"deploy"}, "restart", value) } - common.CommandPropertySet("ps", appName, property, value, DefaultProperties) + common.CommandPropertySet("ps", appName, property, value, DefaultProperties, GlobalProperties) return nil } From 43d6a2ad94b64dcbbbae56a4e430f9a95f69ff01 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 01:11:49 -0500 Subject: [PATCH 04/12] feat: add ability to set a global vector sink --- docs/deployment/logs.md | 20 ++++++++++++++++++-- plugins/logs/functions.go | 18 ++++++++++++++++++ plugins/logs/logs.go | 4 +++- plugins/logs/report.go | 7 ++++++- plugins/logs/src/subcommands/subcommands.go | 6 ++++++ 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index 05d2797c7..df7998894 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -4,7 +4,7 @@ logs [-h] [-t] [-n num] [-q] [-p process] # Display recent log output logs:failed [--all|] # Shows the last failed deploy logs logs:report [] [] # Displays a logs report for one or more apps -logs:set # Set or clear a logs property for an app +logs:set [--global|] # Set or clear a logs property for an app logs:vector-logs # Tail the logs of the vector container logs:vector-start # Start the vector logging container logs:vector-stop # Stop the vector logging container @@ -101,7 +101,9 @@ The `vector` container will be stopped and removed from the system. If the conta #### Configuring a log sink -Vector uses the concept of log "sinks" to send logs to a given endpoint. Log sinks may be configured on a per-app basis by specifying a `sink` in DSN form with the `logs:set` command. Specifying a sink value will reload any running vector container. +> If no sinks are specified, the vector container will exit. + +Vector uses the concept of log "sinks" to send logs to a given endpoint. Log sinks may be configured globally or on a per-app basis by specifying a `sink` in DSN form with the `logs:set` command. Specifying a sink value will reload any running vector container. ```shell # setting the sink value in quotes is encouraged to avoid @@ -117,6 +119,20 @@ dokku logs:set node-js-app sink Only one sink may be specified on a per-app basis at a given time. +Log sinks can also be specified globally by specifying the `--global` flag to `logs:set` with no app name specified: + +```shell +dokku logs:set --global sink "console://?encoding[codec]=json" +``` + +As with app-specific sink settings, the global value may also be cleared by setting no value. + +```shell +dokku logs:set --global sink +``` + +If no sink is specified, the vector container may exit prematurely. + ##### Log Sink DSN Format The DSN form of a sink is as follows: diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go index 35856b520..99c1db2c1 100644 --- a/plugins/logs/functions.go +++ b/plugins/logs/functions.go @@ -120,6 +120,9 @@ func valueToConfig(appName string, value string) (vectorSink, error) { t := fmt.Sprintf("type=%s", u.Scheme) i := fmt.Sprintf("inputs[]=docker-source:%s", appName) + if appName == "--global" { + i = "inputs[]=docker-global-source" + } initialQuery := fmt.Sprintf("%s&%s", t, i) query := u.RawQuery if query == "" { @@ -171,6 +174,21 @@ func writeVectorConfig() error { data.Sinks[fmt.Sprintf("docker-sink:%s", appName)] = sink } + value := common.PropertyGet("logs", "--global", "vector-sink") + if value != "" { + sink, err := valueToConfig("--global", value) + if err != nil { + return err + } + + data.Sources["docker-global-source"] = vectorSource{ + Type: "docker_logs", + IncludeLabels: []string{"com.dokku.app-name"}, + } + + data.Sinks["docker-global-sink"] = sink + } + b, err := json.MarshalIndent(data, "", " ") if err != nil { return err diff --git a/plugins/logs/logs.go b/plugins/logs/logs.go index 2ad2a1cec..c966ba25a 100644 --- a/plugins/logs/logs.go +++ b/plugins/logs/logs.go @@ -13,7 +13,9 @@ var ( } // GlobalProperties is a map of all valid global logs properties - GlobalProperties = map[string]bool{} + GlobalProperties = map[string]bool{ + "vector-sink": true, + } ) const VectorImage = "timberio/vector:0.11.X-debian" diff --git a/plugins/logs/report.go b/plugins/logs/report.go index c887a5073..1234e6e55 100644 --- a/plugins/logs/report.go +++ b/plugins/logs/report.go @@ -11,7 +11,8 @@ func ReportSingleApp(appName, infoFlag string) error { } flags := map[string]common.ReportFunc{ - "--logs-vector-sink": reportVectorSink, + "--logs-vector-sink": reportVectorSink, + "--logs-global-vector-sink": reportGlobalVectorSink, } trimPrefix := false @@ -23,3 +24,7 @@ func ReportSingleApp(appName, infoFlag string) error { func reportVectorSink(appName string) string { return common.PropertyGet("logs", appName, "vector-sink") } + +func reportGlobalVectorSink(appName string) string { + return common.PropertyGet("logs", "--global", "vector-sink") +} diff --git a/plugins/logs/src/subcommands/subcommands.go b/plugins/logs/src/subcommands/subcommands.go index bcadf8335..b10ce4912 100644 --- a/plugins/logs/src/subcommands/subcommands.go +++ b/plugins/logs/src/subcommands/subcommands.go @@ -34,10 +34,16 @@ func main() { } case "set": args := flag.NewFlagSet("logs:set", flag.ExitOnError) + global := args.Bool("global", false, "--global: set a global property") args.Parse(os.Args[2:]) appName := args.Arg(0) property := args.Arg(1) value := args.Arg(2) + if *global { + appName = "--global" + property = args.Arg(0) + value = args.Arg(1) + } err = logs.CommandSet(appName, property, value) case "vector-logs": args := flag.NewFlagSet("logs:vector-logs", flag.ExitOnError) From 94696e81fa44e5b751fa85d327d9be9164e81593 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 01:12:25 -0500 Subject: [PATCH 05/12] fix: ensure the vector container does not exit when no configured sinks are in place --- docs/deployment/logs.md | 6 ------ plugins/logs/functions.go | 22 ++++++++++++++++++++++ plugins/logs/subcommands.go | 5 +++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index df7998894..466ebac33 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -73,8 +73,6 @@ dokku logs:vector-start This will start a new container named `vector` with Dokku's vector config mounted and ready for use. If a running container already exists, this command will do nothing. Additionally, if a container exists but is not running, this command will attempt to start the container. -By default, the vector container will do nothing and may exit immediately. It is necessary to configure a sink in order to ensure the vector container runs as necessary. - While the default vector image is hardcoded, users may specify an alternative via the `--vector-image` flag: ```shell @@ -101,8 +99,6 @@ The `vector` container will be stopped and removed from the system. If the conta #### Configuring a log sink -> If no sinks are specified, the vector container will exit. - Vector uses the concept of log "sinks" to send logs to a given endpoint. Log sinks may be configured globally or on a per-app basis by specifying a `sink` in DSN form with the `logs:set` command. Specifying a sink value will reload any running vector container. ```shell @@ -131,8 +127,6 @@ As with app-specific sink settings, the global value may also be cleared by sett dokku logs:set --global sink ``` -If no sink is specified, the vector container may exit prematurely. - ##### Log Sink DSN Format The DSN form of a sink is as follows: diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go index 99c1db2c1..50c2b5e71 100644 --- a/plugins/logs/functions.go +++ b/plugins/logs/functions.go @@ -123,6 +123,10 @@ func valueToConfig(appName string, value string) (vectorSink, error) { if appName == "--global" { i = "inputs[]=docker-global-source" } + if appName == "--null" { + i = "inputs[]=docker-null-source" + } + initialQuery := fmt.Sprintf("%s&%s", t, i) query := u.RawQuery if query == "" { @@ -189,6 +193,24 @@ func writeVectorConfig() error { data.Sinks["docker-global-sink"] = sink } + if len(data.Sources) == 0 { + // pull from no containers + data.Sources["docker-null-source"] = vectorSource{ + Type: "docker_logs", + IncludeLabels: []string{"com.dokku.vector-null"}, + } + } + + if len(data.Sinks) == 0 { + // write logs to a blackhole + sink, err := valueToConfig("--null", "blackhole://?print_amount=1") + if err != nil { + return err + } + + data.Sinks["docker-null-sink"] = sink + } + b, err := json.MarshalIndent(data, "", " ") if err != nil { return err diff --git a/plugins/logs/subcommands.go b/plugins/logs/subcommands.go index ba59c17ff..1dd4e0975 100644 --- a/plugins/logs/subcommands.go +++ b/plugins/logs/subcommands.go @@ -104,6 +104,11 @@ func CommandVectorLogs() error { func CommandVectorStart(vectorImage string) error { common.LogInfo2("Starting vector container") if !common.ContainerExists(vectorContainerName) { + common.LogVerbose("Ensuring vector configuration exists") + if err := writeVectorConfig(); err != nil { + return err + } + return startVectorContainer(vectorImage) } From d6acf5530e14e54ca7f023883d5cbd5f8bff97ed Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 01:14:40 -0500 Subject: [PATCH 06/12] fix: add some comments --- plugins/common/docker.go | 2 +- plugins/common/log.go | 2 +- plugins/logs/logs.go | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/common/docker.go b/plugins/common/docker.go index 18df7ecd5..51f24f5b2 100644 --- a/plugins/common/docker.go +++ b/plugins/common/docker.go @@ -244,7 +244,7 @@ func DockerInspect(containerOrImageID, format string) (output string, err error) return } -// DockerInspect runs an inspect command with a given format against a container ID +// DockerSignal sends a given signal (in string form) to a docker container func DockerSignal(containerID, signal string) (err error) { return sh.Command(DockerBin(), "kill", fmt.Sprintf("--signal=%s", signal), containerID).Run() } diff --git a/plugins/common/log.go b/plugins/common/log.go index e7a988031..1a64ca241 100644 --- a/plugins/common/log.go +++ b/plugins/common/log.go @@ -94,7 +94,7 @@ func LogVerboseQuietContainerLogs(containerID string) { } } -// LogVerboseQuietContainerLogs is the verbose log formatter for container logs +// LogVerboseQuietContainerLogsTail is the verbose log formatter for container logs with tail mode enabled func LogVerboseQuietContainerLogsTail(containerID string) { sc := NewShellCmdWithArgs(DockerBin(), "container", "logs", containerID, "--follow", "--tail", "10") stdout, _ := sc.Command.StdoutPipe() diff --git a/plugins/logs/logs.go b/plugins/logs/logs.go index c966ba25a..7af0000af 100644 --- a/plugins/logs/logs.go +++ b/plugins/logs/logs.go @@ -18,6 +18,7 @@ var ( } ) +// VectorImage contains the default vector image to run const VectorImage = "timberio/vector:0.11.X-debian" // GetFailedLogs outputs failed deploy logs for a given app From 6f9a6eb294e58f57c1a7dbb95a5b8864d0630622 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 12:43:15 -0500 Subject: [PATCH 07/12] fix: only check for global properties when appName is --global --- plugins/common/properties.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/common/properties.go b/plugins/common/properties.go index fa17f40f4..89d561017 100644 --- a/plugins/common/properties.go +++ b/plugins/common/properties.go @@ -18,7 +18,7 @@ func CommandPropertySet(pluginName, appName, property, value string, properties LogFail(err.Error()) } } - if !globalProperties[property] { + if appName == "--global" && !globalProperties[property] { LogFail("Property cannot be specified globally") } if property == "" { From 1aec869f9e4bf2bab2f35cec54ec36a6a473dff5 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 21:40:51 -0500 Subject: [PATCH 08/12] fix: do not delete apps log subdir This is a new directory introduced for vector logging should not be deleted so vector logging still works. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c87e78ae0..37bb77814 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,4 +53,4 @@ RUN \ && rm -f /etc/nginx/sites-enabled/default /usr/share/nginx/html/index.html /etc/my_init.d/10_syslog-ng.init \ && rm -f /usr/local/openresty/nginx/conf/sites-enabled/default /usr/share/openresty/html/index.html \ && sed -i '/imklog/d' /etc/rsyslog.conf \ - && rm -f /var/log/btmp /var/log/wtmp /var/log/*log /var/log/apt/* /var/log/dokku/* /var/log/nginx/* /var/log/openresty/* + && rm -f /var/log/btmp /var/log/wtmp /var/log/*log /var/log/apt/* /var/log/dokku/*.log /var/log/nginx/* /var/log/openresty/* From 0b3a6687f69e73ad25ac0adb1f214c2b12fdcbd7 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 21:45:26 -0500 Subject: [PATCH 09/12] feat: actually mount the apps log dir Also fix it so that the log directory is mountable from a custom path on the host if running dokku within a container. --- docs/configuration/environment-variables.md | 1 + dokku | 1 + plugins/logs/functions.go | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/configuration/environment-variables.md b/docs/configuration/environment-variables.md index 839883ee6..b3e9e9030 100644 --- a/docs/configuration/environment-variables.md +++ b/docs/configuration/environment-variables.md @@ -87,6 +87,7 @@ The following config variables have special meanings and can be set in a variety | `PLUGIN_CORE_AVAILABLE_PATH` | `$PLUGIN_CORE_PATH/available"` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | The directory that stores all available core plugins. | | `PLUGIN_CORE_ENABLED_PATH` | `$PLUGIN_CORE_PATH/enabled"` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | The directory that stores all enabled core plugins. | | `DOKKU_LOGS_DIR` | `/var/log/dokku` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | Where dokku logs should be written to. | +| `DOKKU_LOGS_HOST_DIR` | `$DOKKU_LOGS_DIR` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | A path on the host that will be mounted into the vector logging container. | | `DOKKU_EVENTS_LOGFILE` | `$DOKKU_LOGS_DIR/events.log` | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*` | Where the events log file is written to. | | `DOKKU_APP_NAME` | none | `--app APP` flag | Name of application to work on. Respected by core plugins. | | `DOKKU_APPS_FORCE_DELETE` | none | `--force` flag | Whether to force delete an application. Also used by other plugins for destructive actions. | diff --git a/dokku b/dokku index 5e10bbb97..32683ce30 100755 --- a/dokku +++ b/dokku @@ -59,6 +59,7 @@ export DOKKU_VALID_EXIT=0 export DOKKU_PID="$BASHPID" export DOKKU_LOGS_DIR=${DOKKU_LOGS_DIR:="/var/log/dokku"} +export DOKKU_LOGS_HOST_DIR=${DOKKU_LOGS_HOST_DIR:=$DOKKU_LOGS_DIR} export DOKKU_EVENTS_LOGFILE=${DOKKU_EVENTS_LOGFILE:="$DOKKU_LOGS_DIR/events.log"} export DOKKU_CONTAINER_LABEL=dokku diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go index 50c2b5e71..87f6b348b 100644 --- a/plugins/logs/functions.go +++ b/plugins/logs/functions.go @@ -72,6 +72,7 @@ func startVectorContainer(vectorImage string) error { "run", "--detach", "--name", vectorContainerName, common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS"), "--volume", "/var/lib/dokku/data/logs/vector.json:/etc/vector/vector.json", "--volume", "/var/run/docker.sock:/var/run/docker.sock", + "--volume", common.MustGetEnv("DOKKU_LOGS_HOST_DIR") + ":/var/logs/dokku/apps", vectorImage, "--config", "/etc/vector/vector.json"}, " ")) From 711945147bdba4387b87013a135f363ff5513b0f Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 21:48:28 -0500 Subject: [PATCH 10/12] refactor: use --watch-config instead of signaling the container to reload config --- plugins/common/docker.go | 5 ----- plugins/logs/functions.go | 10 ++-------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/plugins/common/docker.go b/plugins/common/docker.go index 51f24f5b2..492e754c6 100644 --- a/plugins/common/docker.go +++ b/plugins/common/docker.go @@ -244,11 +244,6 @@ func DockerInspect(containerOrImageID, format string) (output string, err error) return } -// DockerSignal sends a given signal (in string form) to a docker container -func DockerSignal(containerID, signal string) (err error) { - return sh.Command(DockerBin(), "kill", fmt.Sprintf("--signal=%s", signal), containerID).Run() -} - // IsImageHerokuishBased returns true if app image is based on herokuish func IsImageHerokuishBased(image string, appName string) bool { output, err := DockerInspect(image, "{{range .Config.Env}}{{if eq . \"USER=herokuishuser\" }}{{println .}}{{end}}{{end}}") diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go index 87f6b348b..2587d3341 100644 --- a/plugins/logs/functions.go +++ b/plugins/logs/functions.go @@ -74,7 +74,7 @@ func startVectorContainer(vectorImage string) error { "--volume", "/var/run/docker.sock:/var/run/docker.sock", "--volume", common.MustGetEnv("DOKKU_LOGS_HOST_DIR") + ":/var/logs/dokku/apps", vectorImage, - "--config", "/etc/vector/vector.json"}, " ")) + "--config", "/etc/vector/vector.json", "--watch-config"}, " ")) if !cmd.Execute() { return errors.New("Unable to start vector container") @@ -222,11 +222,5 @@ func writeVectorConfig() error { return err } - if !common.ContainerExists(vectorContainerName) { - return nil - } - - return common.SuppressOutput(func() error { - return common.DockerSignal(vectorContainerName, "HUP") - }) + return nil } From 09ed45534d6bd72312a3db4a5dca7594a0d9f613 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 21:48:38 -0500 Subject: [PATCH 11/12] refactor: always write out the vector configuration --- plugins/logs/subcommands.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/logs/subcommands.go b/plugins/logs/subcommands.go index 1dd4e0975..06b25aaf8 100644 --- a/plugins/logs/subcommands.go +++ b/plugins/logs/subcommands.go @@ -103,12 +103,12 @@ func CommandVectorLogs() error { // or starts an existing one if it already exists func CommandVectorStart(vectorImage string) error { common.LogInfo2("Starting vector container") - if !common.ContainerExists(vectorContainerName) { - common.LogVerbose("Ensuring vector configuration exists") - if err := writeVectorConfig(); err != nil { - return err - } + common.LogVerbose("Ensuring vector configuration exists") + if err := writeVectorConfig(); err != nil { + return err + } + if !common.ContainerExists(vectorContainerName) { return startVectorContainer(vectorImage) } From 2d07d89d603fa37401b70041f059f740fc40f097 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 4 Jan 2021 22:56:03 -0500 Subject: [PATCH 12/12] docs: add new as of banner [ci skip] --- docs/deployment/logs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index 466ebac33..553b3f719 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -59,6 +59,8 @@ dokku logs:failed --all ### Vector Logging +> New as of 0.22.6 + Vector is an open-source, lightweight and ultra-fast tool for building observability pipelines. Dokku integrates with it for shipping container logs for the `docker-local` scheduler. Users may configure log-shipping on a per-app or global basis, neither of which interfere with the `dokku logs` commands. #### Starting the Vector container