Skip to content

feat: rewrite logger on log/slog and add a structured logging API - #165

Open
JoshVanL wants to merge 2 commits into
dapr:mainfrom
JoshVanL:structured-logging
Open

feat: rewrite logger on log/slog and add a structured logging API#165
JoshVanL wants to merge 2 commits into
dapr:mainfrom
JoshVanL:structured-logging

Conversation

@JoshVanL

Copy link
Copy Markdown
Contributor

kit/logger was a thin wrapper over logrus. Call sites built their message eagerly with printf verbs, so the output carried no fields that could be filtered on beyond the fixed schema, and logrus allocated a fresh map per entry.

Replace the backend with log/slog and add a structured API alongside the existing one:

    log := logger.New("dapr.kit")
    log.Info("component loaded", "component", name)
    log.Error("failed to load component", logger.Err(err))

Output is byte-identical to logrus in both text and JSON. The log schema is documented public API and user log pipelines match on it, so the encoder reproduces logrus exactly: alphabetical field ordering, the "warning" level label, omission of an empty message in text but not in JSON, bare rendering of empty values, and HTML escaping in JSON. TestGoldenOutput pins all of it against output captured from logrus before it was removed.

Benchmarked against the previous logrus configuration, same machine, -count=3:

    text            1840 ns, 19 allocs,  953 B    810 ns, 3 allocs, 400 B
    json            2080 ns, 30 allocs, 1500 B   1070 ns, 4 allocs, 689 B
    disabled          21 ns,  1 alloc              20 ns, 1 alloc

The emitting path is roughly twice as fast for a sixth of the allocations. The disabled path is at parity, since both still box arguments at the call site; LogAttrs is the form that avoids it entirely at 0 allocations.

On the API:

  • Log cannot implement Logger, because the embedded slog methods take a leading message string andnds. Use (*Log).Legacy() where a Logger is required.
  • logger.FromLogger(Logger) This is the migration path for code that receives a Logger through a signature that cannot change, notablnstructor.
  • Only the printf methods are marked Deprecated. The Logger interface and NewLogger are not, becrameter types for the duration of the migration, and deprecating them would flag thousands of legitimate us
  • Configuration is per logger name and shared between the Logger and the Log of that name, matching each name owned its own logrus instance. A component setting its level from metadata therefore does noole process.

Two existing bugs are fixed

  • EnableJSONOutput rebuilt t a second call silently dropped app_id and any fields added with WithFields.
  • Loggers created after Applilt-in defaults of text output at info level instead of the configured options.

Also converts kit's own call sites to the structured API, drops the logrus dependency, configurease keys and
reject the reserved schema keys, and fixes a depguard rule that denied github.com/Sirupsen/logrus, since the
project was renamed, so it had never matched anything.

kit/logger was a thin wrapper over logrus. Call sites built their
message eagerly with printf verbs, so the output carried no fields that
could be filtered on beyond the fixed schema, and logrus allocated a
fresh map per entry.

Replace the backend with log/slog and add a structured API alongside the
existing one:

```
    log := logger.New("dapr.kit")
    log.Info("component loaded", "component", name)
    log.Error("failed to load component", logger.Err(err))
```

Output is byte-identical to logrus in both text and JSON. The log schema
is documented public API and user log pipelines match on it, so the
encoder reproduces logrus exactly: alphabetical field ordering, the
"warning" level label, omission of an empty message in text but not in
JSON, bare rendering of empty values, and HTML escaping in JSON.
TestGoldenOutput pins all of it against output captured from logrus
before it was removed.

Benchmarked against the previous logrus configuration, same machine,
-count=3:

```
    text            1840 ns, 19 allocs,  953 B    810 ns, 3 allocs, 400 B
    json            2080 ns, 30 allocs, 1500 B   1070 ns, 4 allocs, 689 B
    disabled          21 ns,  1 alloc              20 ns, 1 alloc
```

The emitting path is roughly twice as fast for a sixth of the
allocations. The disabled path is at parity, since both still box
arguments at the call site; LogAttrs is the form that avoids it entirely
at 0 allocations.

On the API:

* Log cannot implement Logger, because the embedded slog methods take a
  leading message string andnds. Use
  (*Log).Legacy() where a Logger is required.
* logger.FromLogger(Logger) This is the
  migration path for code that receives a Logger through a signature
  that cannot change, notablnstructor.
* Only the printf methods are marked Deprecated. The Logger interface
  and NewLogger are not, becrameter types
  for the duration of the migration, and deprecating them would flag
  thousands of legitimate us
* Configuration is per logger name and shared between the Logger and the
  Log of that name, matching each name
  owned its own logrus instance. A component setting its level from
  metadata therefore does noole process.

Two existing bugs are fixed

* EnableJSONOutput rebuilt t a second call
  silently dropped app_id and any fields added with WithFields.
* Loggers created after Applilt-in defaults
  of text output at info level instead of the configured options.

Also converts kit's own call sites to the structured API, drops the
logrus dependency, configurease keys and
reject the reserved schema keys, and fixes a depguard rule that denied
github.com/Sirupsen/logrus, since the
project was renamed, so it had never matched anything.

Signed-off-by: joshvanl <me@joshvanl.dev>
@JoshVanL
JoshVanL requested review from a team as code owners August 13, 2026 17:51
@JoshVanL
JoshVanL requested a balanced review from Copilot and removed request for a team August 13, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Replaces the logrus-backed logger with log/slog, adds structured logging, and preserves the existing Dapr log schema.

Changes:

  • Adds structured logging, adapters, dynamic configuration, and compatibility encoding.
  • Migrates internal call sites to structured attributes.
  • Adds tests, benchmarks, lint rules, and removes logrus.

Reviewed changes

Copilot reviewed 26 out of 28 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
.golangci.yml Configures slog linting and logrus bans.
concurrency/closer.go Migrates shutdown logging.
concurrency/dir/dir.go Migrates directory operation logging.
concurrency/dir/switch_unix.go Migrates Unix switch logging.
crypto/spiffe/spiffe.go Adds structured SPIFFE logging.
crypto/spiffe/trustanchors/file/file.go Migrates trust-anchor logging.
errors/errors.go Migrates error-detail logging.
go.mod Removes logrus dependency.
go.sum Refreshes dependency checksums.
jwkscache/cache.go Migrates JWKS cache logging.
logger/attrs.go Adds standard structured attributes.
logger/bench_test.go Adds logger benchmarks.
logger/bridge.go Adapts third-party legacy loggers.
logger/dapr_logger.go Reimplements legacy API with slog.
logger/dapr_logger_test.go Updates legacy logger tests.
logger/golden_capture_test.go Pins output compatibility.
logger/handler.go Implements custom schema encoders.
logger/levels.go Maps Dapr and slog levels.
logger/log.go Adds the structured logging API.
logger/log_test.go Tests structured logging behavior.
logger/logger.go Adds shared named configuration.
logger/nop_logger.go Exposes a no-op logger.
logger/options.go Applies persistent runtime configuration.
logger/options_test.go Tests defaults and file output.
logger/state.go Stores concurrent logger state.
signals/signals.go Migrates shutdown signal logging.
signals/signals_posix.go Migrates restart signal logging.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread logger/handler.go Outdated
Comment thread logger/handler.go Outdated
Comment thread logger/options.go Outdated
Comment thread logger/state.go Outdated
Comment thread logger/handler.go
Comment thread logger/handler.go
Comment thread logger/handler.go Outdated
Comment thread errors/errors.go Outdated
Signed-off-by: joshvanl <me@joshvanl.dev>

@mikeee mikeee left a comment

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.

extraneous file committed

Comment thread foo

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.

foo

@m3nax

m3nax commented Aug 20, 2026

Copy link
Copy Markdown

Afternoon @JoshVanL, I've opened a small PR that adds support for registering additional handlers alongside the primary one. The idea is to make OTLP log export easier to plug in later on.

PR: JoshVanL#1

Let me know if this approach makes sense and whether it could be merged into your existing work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants