diff --git a/docs/deployment/logs.md b/docs/deployment/logs.md index ee58053f9..3bd658f24 100644 --- a/docs/deployment/logs.md +++ b/docs/deployment/logs.md @@ -178,6 +178,37 @@ Setting this to an empty string will reset the version to the version currently dokku logs:set --global vector-image ``` +#### Attaching Vector to additional Docker networks + +By default, the Vector container runs with `network_mode: bridge` and can only reach app containers that are also on the default bridge network. Apps deployed onto a per-app network or a custom network - typically via `dokku network:set initial-network ` - are not reachable from Vector over Docker's internal DNS, so sinks that need to talk to those apps directly (for example, an in-host log search service such as [Logpond](https://github.com/dokku/logpond)) would have to route traffic out through the external proxy. + +The global `vector-networks` property accepts a comma-separated list of Docker networks that Vector should additionally join. + +```shell +dokku logs:set --global vector-networks dokku-logs +``` + +Multiple networks may be specified by separating them with a comma. + +```shell +dokku logs:set --global vector-networks dokku-logs,observability +``` + +Each network must already exist; setting a non-existent network or the reserved `bridge` value will fail. The list can be cleared by setting an empty value, which restores the default `network_mode: bridge` configuration. + +```shell +dokku logs:set --global vector-networks +``` + +Network attachments are reconciled by `docker compose` on every `logs:vector-start`, so after changing the value the Vector container must be cycled. + +```shell +dokku logs:vector-stop +dokku logs:vector-start +``` + +Once attached, an app on `dokku-logs` (for example via `dokku network:set node-js-app initial-network dokku-logs`) is reachable from Vector at `.:` over the shared network without round-tripping through the external proxy. + #### Configuring a log sink 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 `vector-sink` in DSN form with the `logs:set` command. Specifying a sink value will reload any running vector container. diff --git a/plugins/logs/functions.go b/plugins/logs/functions.go index f091d4939..95b522329 100644 --- a/plugins/logs/functions.go +++ b/plugins/logs/functions.go @@ -24,9 +24,10 @@ type vectorSource struct { } type vectorTemplateData struct { - DokkuLibRoot string - DokkuLogsDir string - VectorImage string + DokkuLibRoot string + DokkuLogsDir string + VectorImage string + VectorNetworks []string } const vectorContainerName = "vector-vector-1" @@ -89,9 +90,10 @@ func startVectorContainer(vectorImage string) error { } data := vectorTemplateData{ - DokkuLibRoot: dokkuLibRoot, - DokkuLogsDir: dokkuLogsDir, - VectorImage: vectorImage, + DokkuLibRoot: dokkuLibRoot, + DokkuLogsDir: dokkuLogsDir, + VectorImage: vectorImage, + VectorNetworks: getVectorNetworks(), } if err := tmpl.Execute(tmpFile, data); err != nil { @@ -104,6 +106,24 @@ func startVectorContainer(vectorImage string) error { }) } +func getVectorNetworks() []string { + value := common.PropertyGet("logs", "--global", "vector-networks") + if value == "" { + return nil + } + + networks := []string{} + for _, name := range strings.Split(value, ",") { + name = strings.TrimSpace(name) + if name == "" { + continue + } + networks = append(networks, name) + } + + return networks +} + func getComputedVectorImage() string { return common.PropertyGetDefault("logs", "--global", "vector-image", getDefaultVectorImage()) } @@ -151,9 +171,10 @@ func stopVectorContainer() error { } data := vectorTemplateData{ - DokkuLibRoot: dokkuLibRoot, - DokkuLogsDir: dokkuLogsDir, - VectorImage: getComputedVectorImage(), + DokkuLibRoot: dokkuLibRoot, + DokkuLogsDir: dokkuLogsDir, + VectorImage: getComputedVectorImage(), + VectorNetworks: getVectorNetworks(), } if err := tmpl.Execute(tmpFile, data); err != nil { diff --git a/plugins/logs/logs.go b/plugins/logs/logs.go index 8a21bbc84..6753e0d1f 100644 --- a/plugins/logs/logs.go +++ b/plugins/logs/logs.go @@ -35,6 +35,7 @@ var ( "app-label-alias": true, "max-size": true, "vector-image": true, + "vector-networks": true, "vector-sink": true, } ) diff --git a/plugins/logs/report.go b/plugins/logs/report.go index 714f09470..af7953b26 100644 --- a/plugins/logs/report.go +++ b/plugins/logs/report.go @@ -24,6 +24,7 @@ func ReportSingleApp(appName string, format string, infoFlag string) error { "--logs-global-max-size": reportGlobalMaxSize, "--logs-global-vector-sink": reportGlobalVectorSink, "--logs-vector-global-image": reportVectorGlobalImage, + "--logs-vector-global-networks": reportVectorGlobalNetworks, } } else { flags = map[string]common.ReportFunc{ @@ -35,6 +36,7 @@ func ReportSingleApp(appName string, format string, infoFlag string) error { "--logs-app-label-alias": reportAppLabelAlias, "--logs-max-size": reportMaxSize, "--logs-vector-global-image": reportVectorGlobalImage, + "--logs-vector-global-networks": reportVectorGlobalNetworks, "--logs-vector-sink": reportVectorSink, } } @@ -84,6 +86,10 @@ func reportVectorGlobalImage(appName string) string { return getComputedVectorImage() } +func reportVectorGlobalNetworks(appName string) string { + return common.PropertyGet("logs", "--global", "vector-networks") +} + func reportGlobalVectorSink(appName string) string { value := common.PropertyGet("logs", "--global", "vector-sink") if value == "" { diff --git a/plugins/logs/set.go b/plugins/logs/set.go index 11e4945b8..436a67da8 100644 --- a/plugins/logs/set.go +++ b/plugins/logs/set.go @@ -4,6 +4,9 @@ import ( "errors" "fmt" "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" ) func validateSetValue(appName string, key string, value string) error { @@ -11,6 +14,10 @@ func validateSetValue(appName string, key string, value string) error { return validateMaxSize(appName, value) } + if key == "vector-networks" { + return validateVectorNetworks(appName, value) + } + if key == "vector-sink" { return validateVectorSink(appName, value) } @@ -56,3 +63,30 @@ func validateVectorSink(appName string, value string) error { return nil } + +func validateVectorNetworks(appName string, value string) error { + if value == "" { + return nil + } + + for _, name := range strings.Split(value, ",") { + name = strings.TrimSpace(name) + if name == "" { + return errors.New("Invalid vector-networks value, empty entry in comma-separated list") + } + + if name == "bridge" { + return errors.New("Invalid vector-networks value, \"bridge\" is attached by default and must not be listed") + } + + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: common.DockerBin(), + Args: []string{"network", "inspect", name}, + }) + if err != nil || result.ExitCode != 0 { + return fmt.Errorf("Network %q does not exist", name) + } + } + + return nil +} diff --git a/plugins/logs/templates/compose.yml.tmpl b/plugins/logs/templates/compose.yml.tmpl index 70f033667..1bd7b1bb1 100644 --- a/plugins/logs/templates/compose.yml.tmpl +++ b/plugins/logs/templates/compose.yml.tmpl @@ -13,7 +13,15 @@ services: org.label-schema.schema-version: "1.0" org.label-schema.vendor: dokku + {{- if $.VectorNetworks }} + networks: + bridge: {} + {{- range $name := $.VectorNetworks }} + {{ $name }}: {} + {{- end }} + {{- else }} network_mode: bridge + {{- end }} restart: unless-stopped @@ -21,3 +29,15 @@ services: - "{{ $.DokkuLibRoot }}/data/logs:/etc/vector" - "/var/run/docker.sock:/var/run/docker.sock:ro" - "{{ $.DokkuLogsDir }}/apps:/var/log/dokku/apps" +{{- if $.VectorNetworks }} + +networks: + bridge: + name: bridge + external: true + {{- range $name := $.VectorNetworks }} + {{ $name }}: + name: {{ $name }} + external: true + {{- end }} +{{- end }} diff --git a/tests/unit/logs.bats b/tests/unit/logs.bats index b7fc1c2ae..2958e83af 100644 --- a/tests/unit/logs.bats +++ b/tests/unit/logs.bats @@ -9,6 +9,8 @@ setup() { teardown() { destroy_app + docker network rm test-vector-net-a >/dev/null || true + docker network rm test-vector-net-b >/dev/null || true global_teardown } @@ -54,7 +56,7 @@ teardown() { echo "status: $status" assert_failure assert_output_contains "$TEST_APP logs information" 0 - assert_output_contains "Invalid flag passed, valid flags: --logs-app-label-alias, --logs-computed-app-label-alias, --logs-computed-max-size, --logs-global-app-label-alias, --logs-global-max-size, --logs-global-vector-sink, --logs-max-size, --logs-vector-global-image, --logs-vector-sink" + assert_output_contains "Invalid flag passed, valid flags: --logs-app-label-alias, --logs-computed-app-label-alias, --logs-computed-max-size, --logs-global-app-label-alias, --logs-global-max-size, --logs-global-vector-sink, --logs-max-size, --logs-vector-global-image, --logs-vector-global-networks, --logs-vector-sink" run /bin/bash -c "dokku logs:report $TEST_APP --logs-vector-sink 2>&1" echo "output: $output" @@ -458,6 +460,143 @@ teardown() { assert_output "10m" } +@test "(logs) logs:set --global vector-networks" { + docker network create test-vector-net-a >/dev/null + docker network create test-vector-net-b >/dev/null + + run create_app + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:set $TEST_APP vector-networks test-vector-net-a 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "dokku logs:set --global vector-networks does-not-exist 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "Network \"does-not-exist\" does not exist" + + run /bin/bash -c "dokku logs:set --global vector-networks bridge 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "\"bridge\" is attached by default and must not be listed" + + run /bin/bash -c "dokku logs:set --global vector-networks 'test-vector-net-a,' 2>&1" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "empty entry in comma-separated list" + + run /bin/bash -c "dokku logs:set --global vector-networks test-vector-net-a 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting vector-networks" + + run /bin/bash -c "dokku logs:report --global --logs-vector-global-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "test-vector-net-a" + + run /bin/bash -c "dokku logs:set --global vector-networks test-vector-net-a,test-vector-net-b 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Setting vector-networks" + + run /bin/bash -c "dokku logs:report --global --logs-vector-global-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "test-vector-net-a,test-vector-net-b" + + run /bin/bash -c "dokku logs:set --global vector-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Unsetting vector-networks" + + run /bin/bash -c "dokku logs:report --global --logs-vector-global-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists +} + +@test "(logs) logs:vector-start attaches configured networks" { + docker network create test-vector-net-a >/dev/null + docker network create test-vector-net-b >/dev/null + + run /bin/bash -c "dokku logs:set --global vector-networks test-vector-net-a,test-vector-net-b 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-start 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Vector container is running" + + run /bin/bash -c "sudo docker inspect --format='{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}' vector-vector-1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "bridge" + assert_output_contains "test-vector-net-a" + assert_output_contains "test-vector-net-b" + + run /bin/bash -c "dokku logs:vector-stop 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-start 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Vector container is running" + + run /bin/bash -c "sudo docker inspect --format='{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}' vector-vector-1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "test-vector-net-a" + assert_output_contains "test-vector-net-b" + + run /bin/bash -c "dokku logs:set --global vector-networks 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-stop 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku logs:vector-start 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "sudo docker inspect --format='{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}' vector-vector-1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "bridge" + + run /bin/bash -c "dokku logs:vector-stop 2>&1" + echo "output: $output" + echo "status: $status" + assert_success +} + @test "(logs) logs:set app-label-alias" { run create_app echo "output: $output"