From 76c23ed5af4e382f4ac088be86eae1587c02d3e9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Jun 2026 14:41:11 +0000 Subject: [PATCH] Add support for customizing log file directory (logsDir) in Multi-App Run template Log files for app and daprd were always written to /.dapr/logs with no way to change the location. This writes constant filesystem events into the project tree, which degrades dev servers that recursively watch it (e.g. uvicorn --reload via watchfiles), and prevents centralizing logs. This change adds an optional 'logsDir' field to the Multi-App Run template, available under 'common' and per app. Precedence: apps[i].logsDir > common.logsDir > default (/.dapr/logs). Relative paths are resolved against the app directory (per-app) or the run file directory (common); '~' is expanded. The directory is created on use. Default behavior is unchanged when the field is not set. Signed-off-by: YOUR NAME --- pkg/runfileconfig/run_file_config.go | 10 +++- pkg/runfileconfig/run_file_config_parser.go | 30 ++++++++++ .../run_file_config_parser_test.go | 55 +++++++++++++++++++ .../testdata/test_run_config_logs_dir.yaml | 12 ++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 pkg/runfileconfig/testdata/test_run_config_logs_dir.yaml diff --git a/pkg/runfileconfig/run_file_config.go b/pkg/runfileconfig/run_file_config.go index 575237600..1369f63c5 100644 --- a/pkg/runfileconfig/run_file_config.go +++ b/pkg/runfileconfig/run_file_config.go @@ -51,6 +51,7 @@ type App struct { standalone.RunConfig `yaml:",inline"` ContainerConfiguration `yaml:",inline"` AppDirPath string `yaml:"appDirPath"` + LogsDir string `yaml:"logsDir,omitempty"` AppLogFileName string DaprdLogFileName string AppLogWriteCloser io.WriteCloser @@ -60,10 +61,17 @@ type App struct { // Common represents the configuration options for the common section in the run file. type Common struct { standalone.SharedRunConfig `yaml:",inline"` + LogsDir string `yaml:"logsDir,omitempty"` } +// GetLogsDir returns the directory where the app and daprd log files are written. +// It defaults to '/.dapr/logs' unless a custom 'logsDir' is provided +// in the run template file. The directory is created if it does not exist. func (a *App) GetLogsDir() string { - logsPath := filepath.Join(a.AppDirPath, standalone.DefaultDaprDirName, logsDir) + logsPath := a.LogsDir + if logsPath == "" { + logsPath = filepath.Join(a.AppDirPath, standalone.DefaultDaprDirName, logsDir) + } os.MkdirAll(logsPath, 0o755) return logsPath } diff --git a/pkg/runfileconfig/run_file_config_parser.go b/pkg/runfileconfig/run_file_config_parser.go index 126334674..dcc64f6c8 100644 --- a/pkg/runfileconfig/run_file_config_parser.go +++ b/pkg/runfileconfig/run_file_config_parser.go @@ -72,6 +72,12 @@ func (a *RunFileConfig) validateRunConfig(runFilePath string) error { a.Common.ResourcesPaths = append(a.Common.ResourcesPaths, a.Common.ResourcesPath) } + // Resolve common's section LogsDir to an absolute path. The directory is not + // required to exist, it is created when the log files are written. + if err := a.resolvePathToAbs(baseDir, &a.Common.LogsDir); err != nil { + return err + } + for i := range len(a.Apps) { if a.Apps[i].AppDirPath == "" { return errors.New("required field 'appDirPath' not found in the provided app config file") @@ -100,6 +106,16 @@ func (a *RunFileConfig) validateRunConfig(runFilePath string) error { a.Apps[i].ResourcesPaths = append(a.Apps[i].ResourcesPaths, a.Apps[i].ResourcesPath) } + // Resolve the app's LogsDir to an absolute path relative to AppDirPath. + // The directory is not required to exist, it is created when the log files are written. + // Precedence order -> apps[i].logsDir > common.logsDir > default (/.dapr/logs). + if err := a.resolvePathToAbs(a.Apps[i].AppDirPath, &a.Apps[i].LogsDir); err != nil { + return err + } + if a.Apps[i].LogsDir == "" { + a.Apps[i].LogsDir = a.Common.LogsDir + } + // Check containerImagePullPolicy is valid. if a.Apps[i].ContainerImagePullPolicy != "" { if !utils.Contains(imagePullPolicyValuesAllowed, a.Apps[i].ContainerImagePullPolicy) { @@ -211,6 +227,20 @@ func (a *RunFileConfig) getBasePathFromAbsPath(appDirPath string) (string, error return "", fmt.Errorf("error in getting the base path from the provided appDirPath %q: ", appDirPath) } +// resolvePathToAbs resolves a relative path in the run file to an absolute path +// without validating that it exists. +func (a *RunFileConfig) resolvePathToAbs(baseDir string, path *string) error { + if *path == "" { + return nil + } + resolved, err := utils.ResolveHomeDir(*path) + if err != nil { + return err + } + *path = utils.GetAbsPath(baseDir, resolved) + return nil +} + // resolvePathToAbsAndValidate resolves the relative paths in run file to absolute path and validates the file path. func (a *RunFileConfig) resolvePathToAbsAndValidate(baseDir string, paths ...*string) error { var err error diff --git a/pkg/runfileconfig/run_file_config_parser_test.go b/pkg/runfileconfig/run_file_config_parser_test.go index dc0a648ba..e395e5c07 100644 --- a/pkg/runfileconfig/run_file_config_parser_test.go +++ b/pkg/runfileconfig/run_file_config_parser_test.go @@ -33,6 +33,7 @@ var ( runFileForPrecedenceRuleDaprDir = filepath.Join(".", "testdata", "test_run_config_precedence_rule_dapr_dir.yaml") runFileForLogDestination = filepath.Join(".", "testdata", "test_run_config_log_destination.yaml") runFileForMultiResourcePaths = filepath.Join(".", "testdata", "test_run_config_multiple_resources_paths.yaml") + runFileForLogsDir = filepath.Join(".", "testdata", "test_run_config_logs_dir.yaml") runFileForContainerImagePullPolicy = filepath.Join(".", "testdata", "test_run_config_container_image_pull_policy.yaml") runFileForContainerImagePullPolicyInvalid = filepath.Join(".", "testdata", "test_run_config_container_image_pull_policy_invalid.yaml") @@ -405,3 +406,57 @@ func getResourcesAndConfigFilePaths(t *testing.T, daprInstallPath string) []stri result[1] = standalone.GetDaprConfigPath(daprDirPath) return result } + +func TestLogsDirPrecedenceAndResolution(t *testing.T) { + config := RunFileConfig{} + apps, err := config.GetApps(runFileForLogsDir) + assert.NoError(t, err) + assert.Len(t, apps, 3) + + testDataDir, err := filepath.Abs(filepath.Join(".", "testdata")) + assert.NoError(t, err) + + // common's relative logsDir is resolved against the run file's directory. + expectedCommonLogsDir := filepath.Join(testDataDir, "central-logs") + // app_2's relative logsDir is resolved against its appDirPath. + expectedApp2LogsDir := filepath.Join(testDataDir, "webapp", "custom-logs") + + t.Run("app without logsDir inherits common's logsDir", func(t *testing.T) { + assert.Equal(t, expectedCommonLogsDir, apps[0].LogsDir) + assert.Equal(t, expectedCommonLogsDir, apps[0].GetLogsDir()) + }) + + t.Run("app's relative logsDir overrides common and resolves against appDirPath", func(t *testing.T) { + assert.Equal(t, expectedApp2LogsDir, apps[1].LogsDir) + assert.Equal(t, expectedApp2LogsDir, apps[1].GetLogsDir()) + }) + + t.Run("app's absolute logsDir is used as is", func(t *testing.T) { + assert.Equal(t, "/tmp/dapr-abs-logs", apps[2].LogsDir) + assert.Equal(t, "/tmp/dapr-abs-logs", apps[2].GetLogsDir()) + }) + + t.Run("logs directory is created on use", func(t *testing.T) { + logsDir := apps[0].GetLogsDir() + stat, err := os.Stat(logsDir) + assert.NoError(t, err) + assert.True(t, stat.IsDir()) + }) + + t.Cleanup(func() { + os.RemoveAll(expectedCommonLogsDir) + os.RemoveAll(expectedApp2LogsDir) + os.RemoveAll("/tmp/dapr-abs-logs") + }) +} + +func TestLogsDirDefault(t *testing.T) { + config := RunFileConfig{} + apps, err := config.GetApps(validRunFilePath) + assert.NoError(t, err) + for _, app := range apps { + assert.Empty(t, app.LogsDir) + expected := filepath.Join(app.AppDirPath, standalone.DefaultDaprDirName, "logs") + assert.Equal(t, expected, app.GetLogsDir()) + } +} diff --git a/pkg/runfileconfig/testdata/test_run_config_logs_dir.yaml b/pkg/runfileconfig/testdata/test_run_config_logs_dir.yaml new file mode 100644 index 000000000..d9f967df2 --- /dev/null +++ b/pkg/runfileconfig/testdata/test_run_config_logs_dir.yaml @@ -0,0 +1,12 @@ +version: 1 +common: + logsDir: ./central-logs/ +apps: + - appDirPath: ./webapp/ + appID: app_1 + - appDirPath: ./webapp/ + appID: app_2 + logsDir: ./custom-logs/ + - appDirPath: ./backend/ + appID: app_3 + logsDir: /tmp/dapr-abs-logs