Skip to content

Commit

Permalink
feat(logging): Allow overriding message key for structured logging (i…
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgokhale authored Dec 5, 2024
1 parent bec49c2 commit 304ab2e
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 20 deletions.
4 changes: 4 additions & 0 deletions cmd/telegraf/agent.conf
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
## "structured" or, on Windows, "eventlog".
# logformat = "text"

## Message key for structured logs, to override the default of "msg".
## Ignored if `logformat` is not "structured".
# structured_log_message_key = "message"

## Name of the file to be logged to or stderr if unset or empty. This
## setting is ignored for the "eventlog" format.
# logfile = ""
Expand Down
19 changes: 10 additions & 9 deletions cmd/telegraf/telegraf.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,15 +364,16 @@ func (t *Telegraf) runAgent(ctx context.Context, reloadConfig bool) error {

// Setup logging as configured.
logConfig := &logger.Config{
Debug: c.Agent.Debug || t.debug,
Quiet: c.Agent.Quiet || t.quiet,
LogTarget: c.Agent.LogTarget,
LogFormat: c.Agent.LogFormat,
Logfile: c.Agent.Logfile,
RotationInterval: time.Duration(c.Agent.LogfileRotationInterval),
RotationMaxSize: int64(c.Agent.LogfileRotationMaxSize),
RotationMaxArchives: c.Agent.LogfileRotationMaxArchives,
LogWithTimezone: c.Agent.LogWithTimezone,
Debug: c.Agent.Debug || t.debug,
Quiet: c.Agent.Quiet || t.quiet,
LogTarget: c.Agent.LogTarget,
LogFormat: c.Agent.LogFormat,
Logfile: c.Agent.Logfile,
StructuredLogMessageKey: c.Agent.StructuredLogMessageKey,
RotationInterval: time.Duration(c.Agent.LogfileRotationInterval),
RotationMaxSize: int64(c.Agent.LogfileRotationMaxSize),
RotationMaxArchives: c.Agent.LogfileRotationMaxArchives,
LogWithTimezone: c.Agent.LogWithTimezone,
}

if err := logger.SetupLogging(logConfig); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ type AgentConfig struct {
// Name of the file to be logged to or stderr if empty. Ignored for "eventlog" format.
Logfile string `toml:"logfile"`

// Message key for structured logs, to override the default of "msg".
// Ignored if "logformat" is not "structured".
StructuredLogMessageKey string `toml:"structured_log_message_key"`

// The file will be rotated after the time interval specified. When set
// to 0 no time based rotation is performed.
LogfileRotationInterval Duration `toml:"logfile_rotation_interval"`
Expand Down
4 changes: 4 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ The agent table configures Telegraf and the defaults used across all plugins.
"structured" or, on Windows, "eventlog". The output file (if any) is
determined by the `logfile` setting.

- **structured_log_message_key**:
Message key for structured logs, to override the default of "msg".
Ignored if `logformat` is not "structured".

- **logfile**:
Name of the file to be logged to or stderr if unset or empty. This
setting is ignored for the "eventlog" format.
Expand Down
2 changes: 2 additions & 0 deletions logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ type Config struct {
LogWithTimezone string
// Logger instance name
InstanceName string
// Structured logging message key
StructuredLogMessageKey string

// internal log-level
logLevel telegraf.LogLevel
Expand Down
36 changes: 25 additions & 11 deletions logger/structured_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,19 @@ func (l *structuredLogger) Print(level telegraf.LogLevel, ts time.Time, _ string
}
}

var defaultStructuredHandlerOptions = &slog.HandlerOptions{
Level: slog.Level(-99),
ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr {
// Translate the Telegraf log-levels to strings
if attr.Key == slog.LevelKey {
if level, ok := attr.Value.Any().(slog.Level); ok {
attr.Value = slog.StringValue(telegraf.LogLevel(level).String())
}
var defaultReplaceAttr = func(_ []string, attr slog.Attr) slog.Attr {
// Translate the Telegraf log-levels to strings
if attr.Key == slog.LevelKey {
if level, ok := attr.Value.Any().(slog.Level); ok {
attr.Value = slog.StringValue(telegraf.LogLevel(level).String())
}
return attr
},
}
return attr
}

var defaultStructuredHandlerOptions = &slog.HandlerOptions{
Level: slog.Level(-99),
ReplaceAttr: defaultReplaceAttr,
}

func init() {
Expand All @@ -70,8 +72,20 @@ func init() {
writer = w
}

structuredHandlerOptions := defaultStructuredHandlerOptions

if cfg.StructuredLogMessageKey != "" {
structuredHandlerOptions.ReplaceAttr = func(groups []string, attr slog.Attr) slog.Attr {
if attr.Key == slog.MessageKey {
attr.Key = cfg.StructuredLogMessageKey
}

return defaultReplaceAttr(groups, attr)
}
}

return &structuredLogger{
handler: slog.NewJSONHandler(writer, defaultStructuredHandlerOptions),
handler: slog.NewJSONHandler(writer, structuredHandlerOptions),
output: writer,
errlog: log.New(os.Stderr, "", 0),
}, nil
Expand Down
38 changes: 38 additions & 0 deletions logger/structured_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,44 @@ func TestStructuredWriteToFileInRotation(t *testing.T) {
require.Len(t, files, 2)
}

func TestStructuredLogMessageKey(t *testing.T) {
instance = defaultHandler()

tmpfile, err := os.CreateTemp("", "")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())

cfg := &Config{
Logfile: tmpfile.Name(),
LogFormat: "structured",
RotationMaxArchives: -1,
Debug: true,
StructuredLogMessageKey: "message",
}
require.NoError(t, SetupLogging(cfg))

l := New("testing", "test", "")
l.Info("TEST")

buf, err := os.ReadFile(tmpfile.Name())
require.NoError(t, err)

expected := map[string]interface{}{
"level": "INFO",
"message": "TEST",
"category": "testing",
"plugin": "test",
}

var actual map[string]interface{}
require.NoError(t, json.Unmarshal(buf, &actual))

require.Contains(t, actual, "time")
require.NotEmpty(t, actual["time"])
delete(actual, "time")
require.Equal(t, expected, actual)
}

func BenchmarkTelegrafStructuredLogWrite(b *testing.B) {
// Discard all logging output
l := &structuredLogger{
Expand Down

0 comments on commit 304ab2e

Please sign in to comment.