diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index 2e242c689b0..ef63fd47d25 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -57,6 +57,34 @@ You may also fetch all failed app logs by using the `--all` flag. dokku logs:failed --all ``` +### Docker Log Retention + +Docker log retention can be specified via the `logs:set` command by specifying a value for `max-size`. Log retention is set via injected docker options for all applications, but is also available via the `logs-get-property` trigger for alternative schedulers. + +```shell +dokku logs:set node-js-app max-size 20m +``` + +The default value may be set by passing an empty value for the option: + +```shell +dokku logs:set node-js-app max-size +``` + +Valid values include any integer number followed by a unit of measure (`k`, `m`, or `g`) or the string `unlimited`. Setting to `unlimited` will result in Dokku omitting the log option. + +The `max-size` property can also be set globally. The global default is `10m`, and the global value is used when no app-specific value is set. + +```shell +dokku logs:set --global max-size 20m +``` + +The default value may be set by passing an empty value for the option. + +```shell +dokku logs:set --global max-size +``` + ### Vector Logging Shipping > New as of 0.22.6 diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index 0924d087ebf..571ddaf14a6 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -699,6 +699,22 @@ if [[ ! -f "$DOKKU_ROOT/HOSTNAME" ]]; then fi ``` +### `logs-get-property` + +- Description: Fetches a given logs property value +- Invoked by: +- Arguments: `$APP` `$PROPERTY` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +APP="$1"; PROPERTY="$2" + +# TODO +``` + ### `network-build-config` - Description: Rebuilds network configuration diff --git a/plugins/common/properties.go b/plugins/common/properties.go index 89d561017be..7cacffb180f 100644 --- a/plugins/common/properties.go +++ b/plugins/common/properties.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "reflect" + "sort" "strings" ) @@ -32,6 +33,7 @@ func CommandPropertySet(pluginName, appName, property, value string, properties validPropertyList[i] = properties[i].String() } + sort.Strings(validPropertyList) LogFail(fmt.Sprintf("Invalid property specified, valid properties include: %s", strings.Join(validPropertyList, ", "))) } diff --git a/plugins/logs/.gitignore b/plugins/logs/.gitignore index 291036bd409..ce3e77e5ccf 100644 --- a/plugins/logs/.gitignore +++ b/plugins/logs/.gitignore @@ -2,6 +2,7 @@ /subcommands/* /triggers/* /triggers +/docker-args-process-deploy /install /post-* /report diff --git a/plugins/logs/Makefile b/plugins/logs/Makefile index bcd96e90f5f..eab1d71e3c5 100644 --- a/plugins/logs/Makefile +++ b/plugins/logs/Makefile @@ -1,5 +1,5 @@ SUBCOMMANDS = subcommands/failed subcommands/report subcommands/set subcommands/vector-logs subcommands/vector-start subcommands/vector-stop -TRIGGERS = triggers/install triggers/post-delete triggers/report +TRIGGERS = triggers/docker-args-process-deploy triggers/install triggers/logs-get-property triggers/post-delete triggers/report BUILD = commands subcommands triggers PLUGIN_NAME = logs diff --git a/plugins/logs/logs.go b/plugins/logs/logs.go index 7af0000af7e..780802f4702 100644 --- a/plugins/logs/logs.go +++ b/plugins/logs/logs.go @@ -6,14 +6,19 @@ import ( "github.com/dokku/dokku/plugins/common" ) +// MaxSize is the default max retention size for docker logs +const MaxSize = "10m" + var ( // DefaultProperties is a map of all valid ps properties with corresponding default property values DefaultProperties = map[string]string{ + "max-size": "", "vector-sink": "", } // GlobalProperties is a map of all valid global logs properties GlobalProperties = map[string]bool{ + "max-size": true, "vector-sink": true, } ) diff --git a/plugins/logs/report.go b/plugins/logs/report.go index 3e8a95aa9bd..c88ac504772 100644 --- a/plugins/logs/report.go +++ b/plugins/logs/report.go @@ -11,8 +11,11 @@ func ReportSingleApp(appName, infoFlag string) error { } flags := map[string]common.ReportFunc{ - "--logs-vector-sink": reportVectorSink, + "--logs-computed-max-size": reportComputedMaxSize, + "--logs-global-max-size": reportGlobalMaxSize, "--logs-global-vector-sink": reportGlobalVectorSink, + "--logs-max-size": reportMaxSize, + "--logs-vector-sink": reportVectorSink, } flagKeys := []string{} @@ -26,10 +29,27 @@ func ReportSingleApp(appName, infoFlag string) error { return common.ReportSingleApp("logs", appName, infoFlag, infoFlags, flagKeys, trimPrefix, uppercaseFirstCharacter) } -func reportVectorSink(appName string) string { - return common.PropertyGet("logs", appName, "vector-sink") +func reportComputedMaxSize(appName string) string { + value := reportMaxSize(appName) + if value == "" { + value = reportGlobalMaxSize(appName) + } + + return value +} + +func reportGlobalMaxSize(appName string) string { + return common.PropertyGetDefault("logs", "--global", "max-size", MaxSize) } func reportGlobalVectorSink(appName string) string { return common.PropertyGet("logs", "--global", "vector-sink") } + +func reportMaxSize(appName string) string { + return common.PropertyGet("logs", appName, "max-size") +} + +func reportVectorSink(appName string) string { + return common.PropertyGet("logs", appName, "vector-sink") +} diff --git a/plugins/logs/set.go b/plugins/logs/set.go new file mode 100644 index 00000000000..5878958f988 --- /dev/null +++ b/plugins/logs/set.go @@ -0,0 +1,58 @@ +package logs + +import ( + "errors" + "fmt" + "strconv" +) + +func validateSetValue(appName string, key string, value string) error { + if key == "max-size" { + return validateMaxSize(appName, value) + } + + if key == "vector-sink" { + return validateVectorSink(appName, value) + } + + return nil +} + +func validateMaxSize(appName string, value string) error { + if value == "" { + return nil + } + + if value == "unlimited" { + return nil + } + + last := value[len(value)-1:] + if last != "k" && last != "m" && last != "g" { + return errors.New("Invalid max-size unit measure, value must end in any of [k, m, g]") + } + + if len(value) < 2 { + return errors.New("Invalid max-size value, must be a number followed by a unit of measure [k, m, d]") + } + + number := value[:len(value)-1] + if _, err := strconv.Atoi(number); err != nil { + return fmt.Errorf("Invalid max-size value, unable to convert number to int: %s", err.Error()) + } + + return nil +} + +func validateVectorSink(appName string, value string) error { + if value == "" { + return nil + } + + _, err := valueToConfig(appName, value) + if err != nil { + return err + } + + return nil +} diff --git a/plugins/logs/src/subcommands/subcommands.go b/plugins/logs/src/subcommands/subcommands.go index 875dc2d67eb..f40ec834b3e 100644 --- a/plugins/logs/src/subcommands/subcommands.go +++ b/plugins/logs/src/subcommands/subcommands.go @@ -35,6 +35,7 @@ func main() { case "set": args := flag.NewFlagSet("logs:set", flag.ExitOnError) global := args.Bool("global", false, "--global: set a global property") + valueOverride := args.Bool("1", false, "-1: negative value (for max-size)") args.Parse(os.Args[2:]) appName := args.Arg(0) property := args.Arg(1) @@ -44,6 +45,11 @@ func main() { property = args.Arg(0) value = args.Arg(1) } + + if *valueOverride { + value = "-1" + } + err = logs.CommandSet(appName, property, value) case "vector-logs": args := flag.NewFlagSet("logs:vector-logs", flag.ExitOnError) diff --git a/plugins/logs/src/triggers/triggers.go b/plugins/logs/src/triggers/triggers.go index d09fa5c8027..58e9511718b 100644 --- a/plugins/logs/src/triggers/triggers.go +++ b/plugins/logs/src/triggers/triggers.go @@ -18,8 +18,15 @@ func main() { var err error switch trigger { + case "docker-args-process-deploy": + appName := flag.Arg(0) + err = logs.TriggerDockerArgsProcessDeploy(appName) case "install": err = logs.TriggerInstall() + case "logs-get-property": + appName := flag.Arg(0) + property := flag.Arg(1) + err = logs.TriggerLogsGetProperty(appName, property) case "post-delete": appName := flag.Arg(0) err = logs.TriggerPostDelete(appName) diff --git a/plugins/logs/subcommands.go b/plugins/logs/subcommands.go index d9c8ce57276..e8a9b4a2501 100644 --- a/plugins/logs/subcommands.go +++ b/plugins/logs/subcommands.go @@ -69,11 +69,8 @@ func CommandReport(appName string, infoFlag string) error { // 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 - } + if err := validateSetValue(appName, property, value); err != nil { + return err } common.CommandPropertySet("logs", appName, property, value, DefaultProperties, GlobalProperties) diff --git a/plugins/logs/triggers.go b/plugins/logs/triggers.go index a43c9c884a7..175eb0c6eb9 100644 --- a/plugins/logs/triggers.go +++ b/plugins/logs/triggers.go @@ -1,13 +1,35 @@ package logs import ( + "errors" "fmt" + "io/ioutil" "os" "path/filepath" "github.com/dokku/dokku/plugins/common" ) +// TriggerDockerArgsProcessDeploy outputs the logs plugin docker options for an app +func TriggerDockerArgsProcessDeploy(appName string) error { + stdin, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return err + } + + maxSize := common.PropertyGet("logs", appName, "max-size") + if maxSize == "" { + maxSize = common.PropertyGetDefault("logs", "--global", "max-size", MaxSize) + } + + if maxSize != "unlimited" { + fmt.Printf(" --log-opt max-size=%s ", maxSize) + } + + fmt.Print(string(stdin)) + return nil +} + // TriggerInstall initializes app restart policies func TriggerInstall() error { if err := common.PropertySetup("logs"); err != nil { @@ -35,6 +57,21 @@ func TriggerInstall() error { return nil } +// TriggerLogsGetProperty writes the logs key to stdout for a given app container +func TriggerLogsGetProperty(appName string, key string) error { + if key != "max-size" { + return errors.New("Invalid logs property specified") + } + + value := common.PropertyGet("logs", appName, "max-size") + if value == "" { + value = common.PropertyGetDefault("logs", "--global", "max-size", MaxSize) + } + + fmt.Println(value) + return nil +} + // TriggerPostDelete destroys the network property for a given app container func TriggerPostDelete(appName string) error { return common.PropertyDestroy("logs", appName) diff --git a/tests/unit/logs.bats b/tests/unit/logs.bats index 0e86f5f3a67..23547e9ed15 100644 --- a/tests/unit/logs.bats +++ b/tests/unit/logs.bats @@ -53,7 +53,7 @@ teardown() { echo "status: $status" assert_failure assert_output_contains "$TEST_APP logs information" 0 - assert_output_contains "Invalid flag passed, valid flags: --logs-global-vector-sink, --logs-vector-sink" + assert_output_contains "Invalid flag passed, valid flags: --logs-computed-max-size, --logs-global-max-size, --logs-global-vector-sink, --logs-max-size, --logs-vector-sink" run /bin/bash -c "dokku logs:report $TEST_APP --logs-vector-sink 2>&1" echo "output: $output" @@ -98,13 +98,13 @@ teardown() { echo "output: $output" echo "status: $status" assert_failure - assert_output_contains "Invalid property specified, valid properties include: vector-sink" + assert_output_contains "Invalid property specified, valid properties include: max-size, vector-sink" run /bin/bash -c "dokku logs:set $TEST_APP invalid value" 2>&1 echo "output: $output" echo "status: $status" assert_failure - assert_output_contains "Invalid property specified, valid properties include: vector-sink" + assert_output_contains "Invalid property specified, valid properties include: max-size, vector-sink" } @test "(logs) logs:set app" { @@ -144,7 +144,50 @@ teardown() { echo "status: $status" assert_success assert_output "console://?encoding[codec]=json" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-max-size 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists + + run /bin/bash -c "dokku logs:set $TEST_APP max-size" 2>&1 + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Unsetting max-size" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-max-size 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists + + run /bin/bash -c "dokku logs:set $TEST_APP max-size 20m" 2>&1 + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting max-size" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-max-size 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "20m" + + run /bin/bash -c "dokku logs:set $TEST_APP max-size unlimited" 2>&1 + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting max-size" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-max-size 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "unlimited" } + @test "(logs) logs:set global" { run create_app echo "output: $output" @@ -189,6 +232,54 @@ teardown() { assert_success assert_output_contains "Unsetting vector-sink" assert_output_contains "Writing updated vector config to /var/lib/dokku/data/logs/vector.json" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-global-vector-sink 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists + + run /bin/bash -c "dokku logs:set --global max-size" 2>&1 + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Unsetting max-size" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-global-max-size 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "10m" + + run /bin/bash -c "dokku logs:set --global max-size 20m" 2>&1 + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting max-size" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-global-max-size 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "20m" + + run /bin/bash -c "dokku logs:set --global max-size unlimited" 2>&1 + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting max-size" + + run /bin/bash -c "dokku logs:report $TEST_APP --logs-global-max-size 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "unlimited" + + run /bin/bash -c "dokku logs:set --global max-size" 2>&1 + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Unsetting max-size" } @test "(logs) logs:vector" {