Skip to content
Merged
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
66 changes: 44 additions & 22 deletions logger/dapr_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package logger
import (
"io"
"os"
"time"

"github.com/sirupsen/logrus"
)
Expand All @@ -27,6 +26,10 @@ type daprLogger struct {
name string
// loger is the instance of logrus logger
logger *logrus.Entry
// jsonOutput indicates whether the logger emits JSON formatted logs
jsonOutput bool
Comment thread
JoshVanL marked this conversation as resolved.
// timestampFormat is the format used for log timestamps
timestampFormat string
Comment thread
JoshVanL marked this conversation as resolved.
}

var DaprVersion = "unknown"
Expand All @@ -41,6 +44,7 @@ func newDaprLogger(name string) *daprLogger {
logFieldScope: name,
logFieldType: LogTypeLog,
}),
timestampFormat: defaultTimestampFormat,
}

dl.EnableJSONOutput(defaultJSONOutput)
Expand All @@ -50,16 +54,6 @@ func newDaprLogger(name string) *daprLogger {

// EnableJSONOutput enables JSON formatted output log.
func (l *daprLogger) EnableJSONOutput(enabled bool) {
var formatter logrus.Formatter

fieldMap := logrus.FieldMap{
// If time field name is conflicted, logrus adds "fields." prefix.
// So rename to unused field @time to avoid the confliction.
logrus.FieldKeyTime: logFieldTimeStamp,
logrus.FieldKeyLevel: logFieldLevel,
logrus.FieldKeyMsg: logFieldMessage,
}

hostname, _ := os.Hostname()
l.logger.Data = logrus.Fields{
logFieldScope: l.logger.Data[logFieldScope],
Expand All @@ -68,19 +62,19 @@ func (l *daprLogger) EnableJSONOutput(enabled bool) {
logFieldDaprVer: DaprVersion,
}

if enabled {
formatter = &logrus.JSONFormatter{ //nolint: exhaustruct
TimestampFormat: time.RFC3339Nano,
FieldMap: fieldMap,
}
} else {
formatter = &logrus.TextFormatter{ //nolint: exhaustruct
TimestampFormat: time.RFC3339Nano,
FieldMap: fieldMap,
}
l.jsonOutput = enabled
l.applyFormatter()
}

// SetTimestampFormat sets the format used for log timestamps. An empty format
// resets it to the default (RFC3339 with nanoseconds).
func (l *daprLogger) SetTimestampFormat(format string) {
if format == "" {
format = defaultTimestampFormat
}

l.logger.Logger.SetFormatter(formatter)
l.timestampFormat = format
l.applyFormatter()
}

// SetAppID sets app_id field in the log. Default value is empty string.
Expand Down Expand Up @@ -174,3 +168,31 @@ func (l *daprLogger) Fatal(args ...any) {
func (l *daprLogger) Fatalf(format string, args ...any) {
l.logger.Fatalf(format, args...)
}

// applyFormatter builds and applies the logrus formatter based on the current
// output format and timestamp format.
func (l *daprLogger) applyFormatter() {
var formatter logrus.Formatter

fieldMap := logrus.FieldMap{
// If time field name is conflicted, logrus adds "fields." prefix.
// So rename to unused field @time to avoid the confliction.
logrus.FieldKeyTime: logFieldTimeStamp,
logrus.FieldKeyLevel: logFieldLevel,
logrus.FieldKeyMsg: logFieldMessage,
}

if l.jsonOutput {
formatter = &logrus.JSONFormatter{ //nolint: exhaustruct
TimestampFormat: l.timestampFormat,
FieldMap: fieldMap,
}
} else {
formatter = &logrus.TextFormatter{ //nolint: exhaustruct
TimestampFormat: l.timestampFormat,
FieldMap: fieldMap,
}
}

l.logger.Logger.SetFormatter(formatter)
}
83 changes: 83 additions & 0 deletions logger/dapr_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,89 @@ func TestEnableJSON(t *testing.T) {
assert.Equal(t, expectedHost, testLogger.logger.Data[logFieldInstance])
}

func TestSetTimestampFormat(t *testing.T) {
const customFormat = "2006/01/02 15:04:05.000"

t.Run("default is RFC3339Nano", func(t *testing.T) {
var buf bytes.Buffer

testLogger := getTestLogger(&buf)
testLogger.EnableJSONOutput(true)

formatter, ok := testLogger.logger.Logger.Formatter.(*logrus.JSONFormatter)
require.True(t, ok)
assert.Equal(t, time.RFC3339Nano, formatter.TimestampFormat)
})

t.Run("custom format applied to JSON formatter", func(t *testing.T) {
var buf bytes.Buffer

testLogger := getTestLogger(&buf)
testLogger.EnableJSONOutput(true)
testLogger.SetTimestampFormat(customFormat)

formatter, ok := testLogger.logger.Logger.Formatter.(*logrus.JSONFormatter)
require.True(t, ok)
assert.Equal(t, customFormat, formatter.TimestampFormat)

testLogger.Info("Hello, dapr")

b, _ := buf.ReadBytes('\n')

var o map[string]any
require.NoError(t, json.Unmarshal(b, &o))

timeVal, ok := o[logFieldTimeStamp].(string)
require.True(t, ok)

_, err := time.Parse(customFormat, timeVal)
require.NoError(t, err)
})

t.Run("custom format applied to text formatter", func(t *testing.T) {
var buf bytes.Buffer

testLogger := getTestLogger(&buf)
testLogger.EnableJSONOutput(false)
testLogger.SetTimestampFormat(customFormat)

formatter, ok := testLogger.logger.Logger.Formatter.(*logrus.TextFormatter)
require.True(t, ok)
assert.Equal(t, customFormat, formatter.TimestampFormat)
})

t.Run("format is preserved when toggling JSON output", func(t *testing.T) {
var buf bytes.Buffer

testLogger := getTestLogger(&buf)
testLogger.SetTimestampFormat(customFormat)
testLogger.EnableJSONOutput(true)

jsonFormatter, ok := testLogger.logger.Logger.Formatter.(*logrus.JSONFormatter)
require.True(t, ok)
assert.Equal(t, customFormat, jsonFormatter.TimestampFormat)

testLogger.EnableJSONOutput(false)

textFormatter, ok := testLogger.logger.Logger.Formatter.(*logrus.TextFormatter)
require.True(t, ok)
assert.Equal(t, customFormat, textFormatter.TimestampFormat)
})

t.Run("empty format resets to default", func(t *testing.T) {
var buf bytes.Buffer

testLogger := getTestLogger(&buf)
testLogger.EnableJSONOutput(true)
testLogger.SetTimestampFormat(customFormat)
testLogger.SetTimestampFormat("")

formatter, ok := testLogger.logger.Logger.Formatter.(*logrus.JSONFormatter)
require.True(t, ok)
assert.Equal(t, time.RFC3339Nano, formatter.TimestampFormat)
})
}

func TestJSONLoggerFields(t *testing.T) {
tests := []struct {
name string
Expand Down
3 changes: 3 additions & 0 deletions logger/nop_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ func (n *nopLogger) SetOutputLevel(_ LogLevel) {}
// SetOutput sets the destination for the logs
func (n *nopLogger) SetOutput(_ io.Writer) {}

// SetTimestampFormat sets the format used for log timestamps.
func (n *nopLogger) SetTimestampFormat(_ string) {}

// IsOutputLevelEnabled returns true if the logger will output this LogLevel.
func (n *nopLogger) IsOutputLevelEnabled(_ LogLevel) bool { return true }

Expand Down
26 changes: 23 additions & 3 deletions logger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import (
"io"
"os"
"sync"
"time"
)

const (
defaultJSONOutput = false
defaultOutputLevel = "info"
undefinedAppID = ""
defaultJSONOutput = false
defaultOutputLevel = "info"
defaultTimestampFormat = time.RFC3339Nano
undefinedAppID = ""
)

var (
Expand All @@ -45,6 +47,11 @@ type Options struct {

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

// TimestampFormat is the format used for log timestamps, expressed as a
// Go time layout. An empty value means the default (RFC3339 with
// nanoseconds).
TimestampFormat string
}

// SetOutputLevel sets the log output level.
Expand Down Expand Up @@ -79,6 +86,11 @@ func (o *Options) AttachCmdFlags(
"log-file",
"",
"Path to a file where logs will be written")
stringVar(
&o.TimestampFormat,
"log-timestamp-format",
"",
"Format for log timestamps, expressed as a Go time layout, e.g. '2006/01/02 15:04:05.000' (default RFC3339 with nanoseconds)")
}

if boolVar != nil {
Expand All @@ -97,6 +109,7 @@ func DefaultOptions() Options {
appID: undefinedAppID,
OutputLevel: defaultOutputLevel,
OutputFile: "",
TimestampFormat: "",
}
}

Expand All @@ -108,6 +121,13 @@ func ApplyOptionsToLoggers(options *Options) error {
for _, v := range internalLoggers {
v.EnableJSONOutput(options.JSONFormatEnabled)

// Applied via type assertion rather than through the Logger interface,
// so this stays a non-breaking change for external implementers of
// Logger. Both in-tree implementations provide the method.
if s, ok := v.(interface{ SetTimestampFormat(string) }); ok {
s.SetTimestampFormat(options.TimestampFormat)
}

if options.appID != undefinedAppID {
v.SetAppID(options.appID)
}
Expand Down
11 changes: 11 additions & 0 deletions logger/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TestOptions(t *testing.T) {

logLevelAsserted := false
logFileAsserted := false
logTimestampFormatAsserted := false
testStringVarFn := func(p *string, name string, value string, usage string) {
if name == "log-level" && value == defaultOutputLevel {
logLevelAsserted = true
Expand All @@ -52,6 +53,10 @@ func TestOptions(t *testing.T) {
if name == "log-file" && value == "" {
logFileAsserted = true
}

if name == "log-timestamp-format" && value == "" {
logTimestampFormatAsserted = true
}
}

logAsJSONAsserted := false
Expand All @@ -66,6 +71,7 @@ func TestOptions(t *testing.T) {
// assert
assert.True(t, logLevelAsserted)
assert.True(t, logFileAsserted)
assert.True(t, logTimestampFormatAsserted)
assert.True(t, logAsJSONAsserted)
})
}
Expand All @@ -75,6 +81,7 @@ func TestApplyOptionsToLoggers(t *testing.T) {
JSONFormatEnabled: true,
appID: "dapr-app",
OutputLevel: "debug",
TimestampFormat: "2006/01/02 15:04:05.000",
}

// Create two loggers
Expand All @@ -99,6 +106,10 @@ func TestApplyOptionsToLoggers(t *testing.T) {
t,
toLogrusLevel(DebugLevel),
(l.(*daprLogger)).logger.Logger.GetLevel())
assert.Equal(
t,
"2006/01/02 15:04:05.000",
(l.(*daprLogger)).timestampFormat)
}
}

Expand Down
Loading