Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions logger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package logger

import (
"fmt"
"os"
)

const (
Expand All @@ -33,6 +34,9 @@ type Options struct {

// OutputLevel is the level of logging
OutputLevel string

// OutputFile is the destination file path for logs.
OutputFile string
}

// SetOutputLevel sets the log output level.
Expand Down Expand Up @@ -62,6 +66,11 @@ func (o *Options) AttachCmdFlags(
"log-level",
defaultOutputLevel,
"Options are debug, info, warn, error, or fatal (default info)")
stringVar(
&o.OutputFile,
"log-file",
"",
"Path to a file where logs will be written")
}

if boolVar != nil {
Expand All @@ -79,6 +88,7 @@ func DefaultOptions() Options {
JSONFormatEnabled: defaultJSONOutput,
appID: undefinedAppID,
OutputLevel: defaultOutputLevel,
OutputFile: "",
}
}

Expand All @@ -104,5 +114,16 @@ func ApplyOptionsToLoggers(options *Options) error {
v.SetOutputLevel(daprLogLevel)
}

if options.OutputFile != "" {
file, err := os.OpenFile(options.OutputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we relax permissions a bit and allow reads?

Suggested change
file, err := os.OpenFile(options.OutputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
file, err := os.OpenFile(options.OutputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)

if err != nil {
return fmt.Errorf("failed to open log file %q: %w", options.OutputFile, err)
}

for _, v := range internalLoggers {
v.SetOutput(file)
}
}
Comment on lines +117 to +126
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplyOptionsToLoggers opens the configured log file and sets it as output for all loggers, but the file handle is never tracked/closed. If options are re-applied (eg during a reload) this will leak file descriptors, and clearing/changing OutputFile won’t revert outputs or close the previous file. Consider storing the current output writer/file in package-level state (protected by a mutex), closing/replacing it on subsequent calls, and explicitly resetting output back to the default (stdout) when OutputFile is empty.

Copilot uses AI. Check for mistakes.

return nil
}
42 changes: 42 additions & 0 deletions logger/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ limitations under the License.
package logger

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -26,6 +28,7 @@ func TestOptions(t *testing.T) {
assert.Equal(t, defaultJSONOutput, o.JSONFormatEnabled)
assert.Equal(t, undefinedAppID, o.appID)
assert.Equal(t, defaultOutputLevel, o.OutputLevel)
assert.Empty(t, o.OutputFile)
})

t.Run("set dapr ID", func(t *testing.T) {
Expand All @@ -40,10 +43,15 @@ func TestOptions(t *testing.T) {
o := DefaultOptions()

logLevelAsserted := false
logFileAsserted := false
testStringVarFn := func(p *string, name string, value string, usage string) {
if name == "log-level" && value == defaultOutputLevel {
logLevelAsserted = true
}

if name == "log-file" && value == "" {
logFileAsserted = true
}
}

logAsJSONAsserted := false
Expand All @@ -57,6 +65,7 @@ func TestOptions(t *testing.T) {

// assert
assert.True(t, logLevelAsserted)
assert.True(t, logFileAsserted)
assert.True(t, logAsJSONAsserted)
})
}
Expand Down Expand Up @@ -92,3 +101,36 @@ func TestApplyOptionsToLoggers(t *testing.T) {
(l.(*daprLogger)).logger.Logger.GetLevel())
}
}

func TestApplyOptionsToLoggersFileOutput(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "dapr.log")

testOptions := Options{
OutputLevel: "debug",
OutputFile: logPath,
}

l := NewLogger("testLoggerFileOutput")

require.NoError(t, ApplyOptionsToLoggers(&testOptions))

dl, ok := l.(*daprLogger)
require.True(t, ok)
fileOut, ok := dl.logger.Logger.Out.(*os.File)
require.True(t, ok)
assert.Equal(t, logPath, fileOut.Name())
t.Cleanup(func() {
for _, logger := range getLoggers() {
logger.SetOutput(os.Stdout)
}

require.NoError(t, fileOut.Close())
})

msg := "log-file-test-message"
l.Info(msg)

b, err := os.ReadFile(logPath)
require.NoError(t, err)
assert.Contains(t, string(b), msg)
}
Loading