Skip to content

feat!: decouple public logger from lib-observability (fixes #24)#26

Closed
gandalf-at-lerian wants to merge 2 commits into
LerianStudio:developfrom
gandalf-at-lerian:fix/logger-abstraction-leak-24
Closed

feat!: decouple public logger from lib-observability (fixes #24)#26
gandalf-at-lerian wants to merge 2 commits into
LerianStudio:developfrom
gandalf-at-lerian:fix/logger-abstraction-leak-24

Conversation

@gandalf-at-lerian

@gandalf-at-lerian gandalf-at-lerian commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Requested by: Guilherme Rodrigues (fixes #24 - logger abstraction leak)

🎯 Release target: v1.2.0 (first tag: v1.2.0-beta.1)

  • Breaking change — minor-version bump to v1.2.0 (pre-v1.0 compat is not
    guaranteed and the module is still v1, so no module path change and no
    /v2 suffix
    : the import path stays github.com/LerianStudio/lib-service-discovery).
  • Migration: replace the log.Logger you pass to WithLogger() or set on
    Config.Logger with a *slog.Logger (e.g. slog.Default() or any
    slog-compatible logger). No adapter wrapper is needed on the caller side.
  • lib-observability is no longer required as a direct dependency for consumers
    of this lib — you pass a stdlib *slog.Logger directly. (lib-observability
    remains an internal implementation detail of this lib, bridged by a private
    adapter, and never appears in the public API.)
  • CHANGELOG.md updated with a BREAKING entry under the unreleased/1.2.0 section.

Problem

The public API leaked lib-observability's log.Logger through Config.Logger and WithLogger(). Because log.Logger's method set references concrete lib-observability types (log.Level, log.Field), consumers had to pin the exact lib-observability version or hand-roll an adapter wrapper.

Fix

  • New public interface libsd.Logger — minimal, stdlib-only, slog-compatible:
    InfoContext/WarnContext/ErrorContext/DebugContext(ctx, msg, ...any).
  • Config.Logger and WithLogger() now take libsd.Logger.
  • Internal adapter obsLoggerAdapter bridges libsd.Loggerlib-observability log.Logger, so no internal call site changes; lib-observability stays a private implementation detail.
  • lib-observability removed from the public API surface.
  • Demo (services/main.go), README and tests updated to use *slog.Logger.

⚠️ Breaking change (intentional, per #24)

Config.Logger / WithLogger() no longer accept lib-observability's log.Logger (it uses Log(ctx, level, msg, ...Field) and does not structurally satisfy the new interface). Consumers now pass any slog-compatible logger (e.g. the stdlib *slog.Logger) directly, with no lib-observability dependency or adapter. This is precisely what removes the coupling.

Changed files

  • logger.go (new) — Logger interface + internal adapter
  • logger_test.go, logger_testhelpers_test.go (new) — adapter/level/field tests + shared nop logger
  • config.go, manager.go — public types switch to libsd.Logger
  • services/main.go — demo uses *slog.Logger
  • README.md — documents the interface + breaking change
  • CHANGELOG.md — BREAKING entry for the logger decoupling (v1.2.0)
  • test files — updated captureLogger/nop logger to the new interface

Verification

  • go build ./... — OK
  • go vet -tags unit ./... and go vet -tags integration ./... — clean
  • go test -tags unit ./... — all pass
  • golangci-lint run --build-tags unit ./... — 0 issues
  • go.mod module path unchanged (github.com/LerianStudio/lib-service-discovery, no /v2)

Requested by: Guilherme Rodrigues (fixes LerianStudio#24 - logger abstraction leak)

The public API leaked lib-observability's log.Logger through Config.Logger
and WithLogger(), forcing consumers to pin the exact lib-observability
version or hand-roll an adapter.

- Add a minimal, stdlib-only, slog-compatible libsd.Logger interface
  (InfoContext/WarnContext/ErrorContext/DebugContext).
- Switch Config.Logger and WithLogger() to libsd.Logger.
- Add an internal obsLoggerAdapter bridging libsd.Logger -> lib-observability
  log.Logger, so no internal call site changes; lib-observability stays an
  implementation detail.
- Remove lib-observability from the public API surface.
- Update the demo, docs and tests to use *slog.Logger.

BREAKING CHANGE: Config.Logger and WithLogger() now take libsd.Logger instead
of lib-observability's log.Logger. A log.Logger no longer satisfies the API
(it uses Log(ctx, level, msg, ...Field)); pass any slog-compatible logger
(e.g. *slog.Logger) directly, with no lib-observability dependency.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 97fc257f-3f23-40b8-a170-6d0ca1c51b77

📥 Commits

Reviewing files that changed from the base of the PR and between 20a4a33 and 31b097e.

📒 Files selected for processing (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

The PR introduces a local libsd.Logger interface, adapts it to the existing internal logger, updates configuration and manager wiring, migrates tests and the example service to slog, and documents the breaking logging API change.

Changes

Logger API migration

Layer / File(s) Summary
Logger contract and adapter
logger.go, logger_test.go
Adds context-aware structured logging methods and an adapter for level mapping, fields, groups, nil loggers, and synchronization behavior, with focused adapter tests.
Configuration and manager wiring
config.go, manager.go, *_test.go
Changes Config.Logger and WithLogger to use libsd.Logger, wraps loggers internally, and updates unit and integration test logger setup and assertions.
Consumer migration and documentation
services/main.go, README.md, CHANGELOG.md
Replaces the example service’s observability logger with slog and documents the new logger interface and breaking API change.

Possibly related PRs

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
managed_resolver_test.go (1)

146-210: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Make these modified unit tests parallel-safe.

These tests do not call t.Parallel(). Because they inspect process-wide goroutine counts, first replace that global measurement with scoped synchronization/cleanup that remains isolated under parallel execution, then call t.Parallel().

As per coding guidelines, “Every test and subtest must call t.Parallel().”

Also applies to: 222-260, 540-585

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@managed_resolver_test.go` around lines 146 - 210, Update
TestManagedResolver_CloseDuringInFlightSeedDoesNotLeak and the other modified
tests to call t.Parallel(), replacing process-wide runtime.NumGoroutine
baseline/assertions with test-scoped synchronization and cleanup that verifies
watcher shutdown without relying on global goroutine counts. Preserve the
existing bounded waits and leak-detection behavior while ensuring parallel tests
cannot observe or affect one another.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@logger.go`:
- Around line 38-43: The toObsLogger function must detect typed-nil Logger
implementations before creating obsLoggerAdapter; add the appropriate typed-nil
check while preserving the existing plain-nil behavior. In logger_test.go,
extend the existing nil logger coverage with a regression test using a typed-nil
*slog.Logger and verify it produces the no-op logger without panicking.

---

Outside diff comments:
In `@managed_resolver_test.go`:
- Around line 146-210: Update
TestManagedResolver_CloseDuringInFlightSeedDoesNotLeak and the other modified
tests to call t.Parallel(), replacing process-wide runtime.NumGoroutine
baseline/assertions with test-scoped synchronization and cleanup that verifies
watcher shutdown without relying on global goroutine counts. Preserve the
existing bounded waits and leak-detection behavior while ensuring parallel tests
cannot observe or affect one another.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 092ef718-8b21-4962-9667-9e4875e271a0

📥 Commits

Reviewing files that changed from the base of the PR and between 658ce70 and 20a4a33.

📒 Files selected for processing (12)
  • README.md
  • close_test.go
  • config.go
  • consul_dual_integration_test.go
  • consul_integration_test.go
  • logger.go
  • logger_test.go
  • logger_testhelpers_test.go
  • managed_resolver_test.go
  • manager.go
  • manager_test.go
  • services/main.go

Comment thread logger.go
Comment on lines +38 to +43
func toObsLogger(l Logger) log.Logger {
if l == nil {
return log.NewNop()
}

return &obsLoggerAdapter{l: l}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== logger.go ==\n'
sed -n '1,180p' logger.go

printf '\n== logger_test.go ==\n'
sed -n '1,140p' logger_test.go

printf '\n== search for Logger interface/type defs ==\n'
rg -n "type Logger|toObsLogger|obsLoggerAdapter|slog.Logger|NewNop" -S .

Repository: LerianStudio/lib-service-discovery

Length of output: 8461


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== logger.go ==\n'
sed -n '1,220p' logger.go

printf '\n== logger_test.go ==\n'
sed -n '1,220p' logger_test.go

printf '\n== logger-related declarations ==\n'
rg -n "type Logger|obsLoggerAdapter|toObsLogger|NewNop|slog.Logger" -S --glob '!**/vendor/**' .

Repository: LerianStudio/lib-service-discovery

Length of output: 8451


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "type Logger|toObsLogger|obsLoggerAdapter|NewNop|slog.Logger" -S .

Repository: LerianStudio/lib-service-discovery

Length of output: 2473


🌐 Web query:

Go standard library slog.Logger nil receiver panic InfoContext nil pointer method behavior

💡 Result:

Calling methods on a nil *slog.Logger will cause a runtime panic [1]. In Go, calling a method on a nil pointer is technically permitted by the language specification [2][3], but it only succeeds if the method does not dereference the receiver [4][3]. The methods of *slog.Logger (such as Info, InfoContext, Log, etc.) dereference the receiver to access its underlying handler [5][6][7]. Because these methods internally invoke l.Handler or otherwise access the Logger's fields, calling them on a nil *slog.Logger results in a nil pointer dereference panic [1]. Regarding nil contexts passed to methods like InfoContext: - Passing a nil context.Context to slog methods is discouraged [8][9], even though it is technically permitted by the function signatures [10]. - Internally, slog handles nil contexts by converting them into context.Background before performing any operations [6][7][9]. This behavior is designed for resilience [8], but developers are still advised to provide a valid context whenever one is available to ensure proper tracing and cancellation propagation [10][11]. In summary, while you should not pass a nil context, the slog package handles that gracefully [6][7][9]; however, a nil *slog.Logger itself is not usable and will cause a crash if any logging method is called on it [1].

Citations:


Handle typed-nil public loggers before wrapping them. A nil *slog.Logger can still satisfy Logger as a non-nil interface value, so toObsLogger will wrap it and the first Log call will panic. Add a typed-nil regression test alongside the existing nil case.

📍 Affects 2 files
  • logger.go#L38-L43 (this comment)
  • logger_test.go#L39-L48
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logger.go` around lines 38 - 43, The toObsLogger function must detect
typed-nil Logger implementations before creating obsLoggerAdapter; add the
appropriate typed-nil check while preserving the existing plain-nil behavior. In
logger_test.go, extend the existing nil logger coverage with a regression test
using a typed-nil *slog.Logger and verify it produces the no-op logger without
panicking.

@guimoreirar
guimoreirar changed the base branch from main to develop July 22, 2026 17:17
@guimoreirar guimoreirar self-assigned this Jul 22, 2026
@gandalf-at-lerian

Copy link
Copy Markdown
Collaborator Author

Superseded by #27, which recreates this exact change on a branch in the upstream repo (now that write access is available). Continuing the review there. This cross-fork PR is being closed to consolidate on #27.

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.

Logger abstraction leak: public API exposes lib-observability log.Logger

2 participants