Skip to content

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

Merged
guimoreirar merged 2 commits into
developfrom
fix/logger-abstraction-leak-24
Jul 22, 2026
Merged

feat!: decouple public logger from lib-observability (fixes #24)#27
guimoreirar merged 2 commits into
developfrom
fix/logger-abstraction-leak-24

Conversation

@gandalf-at-lerian

Copy link
Copy Markdown
Collaborator

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

Re-opened from #26 now that this branch lives on the upstream repo. Same change, closes #24.

🎯 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)

Closes #24

Requested by: Guilherme Rodrigues (fixes #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

📝 Walkthrough

Walkthrough

The library replaces its public observability logger dependency with a stdlib-compatible libsd.Logger, adapts it internally, updates manager and demo wiring, and migrates tests and documentation to the new contract.

Changes

Logger decoupling

Layer / File(s) Summary
Logger contract and adapter
config.go, logger.go, README.md, CHANGELOG.md
Config.Logger and WithLogger() now use the context-aware libsd.Logger interface, with internal adaptation for structured fields, levels, groups, nil values, and syncing.
Manager and service wiring
manager.go, services/main.go
Manager initialization wraps consumer loggers, while the demo uses log/slog directly for service-discovery setup and runtime messages.
Adapter and integration validation
logger_test.go, logger_testhelpers_test.go, manager_test.go, *_integration_test.go, close_test.go, managed_resolver_test.go
Tests validate logger adaptation and replace observability no-op loggers with silent slog-backed loggers.

Sequence Diagram(s)

sequenceDiagram
  participant DemoService
  participant libsd.New
  participant Manager
  participant slog.Logger
  DemoService->>slog.Logger: create slog logger
  DemoService->>libsd.New: pass logger through WithLogger
  libsd.New->>Manager: initialize adapted logger
  Manager->>slog.Logger: emit context-aware structured logs
Loading

Possibly related PRs

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/logger-abstraction-leak-24

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

@lerian-studio lerian-studio added size/M core Public API — Manager, Registry, domain types, config demo Demo services and docker-compose docs Documentation and markdown tests Unit and integration tests labels Jul 22, 2026
@lerian-studio

Copy link
Copy Markdown
Contributor

🔒 Security Scan Results — lib-service-discovery

✅ PR Mergeable — no blocking findings

Stage Status Blocking?
Filesystem Scan ✅ Clean
Docker Image Scan ➖ Skipped
Docker Hub Health Score ➖ Skipped
Pre-release Version Check ✅ Clean

Trivy

Filesystem Scan

✅ No vulnerabilities or secrets found.


Pre-release Version Check

✅ No unstable version pins found.


🔍 View full scan logs

@lerian-studio

Copy link
Copy Markdown
Contributor

🔍 PR Validation Summary

✅ PR Mergeable — no blocking failures

Check Status Blocking
Source Branch ✅ success yes
PR Title ✅ success yes
PR Description ✅ success yes
PR Size ✅ success no
Auto Labels ✅ success no
PR Metadata ✅ success no

🔍 View workflow run

@lerian-studio

Copy link
Copy Markdown
Contributor

📊 Unit Test Coverage Report: app

Metric Value
Overall Coverage 91.4% ✅ PASS
Threshold 80%

Coverage by Package

Package Coverage
github.com/LerianStudio/lib-service-discovery 93.2%

Generated by Go PR Analysis workflow

@guimoreirar
guimoreirar merged commit a6e7867 into develop Jul 22, 2026
20 of 21 checks passed
@github-actions
github-actions Bot deleted the fix/logger-abstraction-leak-24 branch July 22, 2026 17:47

@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: 3

🤖 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 `@config.go`:
- Around line 172-174: Update toObsLogger to detect both a nil Logger interface
and an interface containing a typed-nil logger, returning the silent logger
behavior in either case before wrapping or invoking methods. Add a regression
test using a nil *slog.Logger assigned to Logger and verify logging does not
panic or emit output.

In `@logger.go`:
- Around line 38-43: The toObsLogger nil check does not handle typed-nil Logger
values, and manager construction paths must consistently use that guard. Update
toObsLogger to perform a nil-aware check before creating obsLoggerAdapter, then
route both WithLogger and New in manager.go through toObsLogger; apply the
changes at logger.go lines 38-43, manager.go lines 75-83, and manager.go lines
127-130.

In `@manager_test.go`:
- Around line 204-212: Update the logger assertion in the test around New and
obsLoggerAdapter to use assert.Same instead of assert.Equal, verifying that
adapter.l is the exact nop logger instance passed to WithLogger.
🪄 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: 398057fa-3c76-45bd-9310-9fe405801a5b

📥 Commits

Reviewing files that changed from the base of the PR and between c0d2391 and 567560f.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • 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 config.go
Comment on lines +172 to +174
// It is a version-agnostic, slog-compatible interface (see libsd.Logger):
// pass any *slog.Logger or equivalent directly. A nil Logger silences output.
Logger Logger

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files | rg '(^|/)(config\.go|logger\.go|.*test.*\.go)$'

printf '\n== AST outline: config.go ==\n'
ast-grep outline config.go --view expanded || true

printf '\n== AST outline: logger.go ==\n'
ast-grep outline logger.go --view expanded || true

printf '\n== Relevant symbols/text ==\n'
rg -n "toObsLogger|withDefaults|type Logger|Logger interface|InfoContext|slog.Logger|nil Logger|silences output|New\\(" config.go logger.go . -g '!**/vendor/**'

printf '\n== config.go excerpt ==\n'
sed -n '1,260p' config.go

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

Repository: LerianStudio/lib-service-discovery

Length of output: 32276


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '\n== Files ==\n'
git ls-files | rg '(^|/)(config\.go|logger\.go|.*test.*\.go)$'
printf '\n== config.go / logger.go symbols ==\n'
rg -n "toObsLogger|withDefaults|type Logger|InfoContext|slog.Logger|nil Logger|silences output|New\\(" config.go logger.go . -g '!**/vendor/**'
printf '\n== config.go excerpt ==\n'
sed -n '1,260p' config.go
printf '\n== logger.go excerpt ==\n'
sed -n '1,260p' logger.go

Repository: LerianStudio/lib-service-discovery

Length of output: 29783


Handle typed-nil loggers as nil. toObsLogger only treats a nil interface as silent; a var l *slog.Logger = nil still wraps and can panic on the first log call because slog.Logger methods dereference the receiver. Handle the underlying nil case here and add a regression test.

🤖 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 `@config.go` around lines 172 - 174, Update toObsLogger to detect both a nil
Logger interface and an interface containing a typed-nil logger, returning the
silent logger behavior in either case before wrapping or invoking methods. Add a
regression test using a nil *slog.Logger assigned to Logger and verify logging
does not panic or emit output.

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files and inspect the surrounding code.
git ls-files | rg '(^|/)(logger\.go|manager\.go|.*logger.*\.go)$'

echo '--- logger.go ---'
wc -l logger.go
cat -n logger.go | sed -n '1,220p'

echo '--- manager.go ---'
wc -l manager.go
cat -n manager.go | sed -n '1,220p'

Repository: LerianStudio/lib-service-discovery

Length of output: 14102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the Logger interface and any typed-nil-related helper usages.
rg -n --hidden --glob '!**/vendor/**' 'type Logger interface|toObsLogger|WithLogger|NewNop|obsLoggerAdapter|slog\.Logger' .

Repository: LerianStudio/lib-service-discovery

Length of output: 3467


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the interface is defined elsewhere, inspect just that file and any adapter implementation.
files=$(rg -l --hidden --glob '!**/vendor/**' 'type Logger interface|obsLoggerAdapter|toObsLogger' . | sort -u)
printf '%s\n' "$files"

for f in $files; do
  echo "--- $f ---"
  wc -l "$f"
  # Print only the relevant slices with line numbers.
  cat -n "$f" | sed -n '1,220p'
done

Repository: LerianStudio/lib-service-discovery

Length of output: 25136


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe: determine whether a typed-nil implementation stored in the Logger interface
# compares equal to nil and whether the adapter receives a non-nil interface.
python3 - <<'PY'
print("pending repository inspection")
PY

Repository: LerianStudio/lib-service-discovery

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat >/tmp/typed_nil_probe.go <<'EOF'
package main

import "fmt"

type Logger interface {
	InfoContext()
}

type impl struct{}

func (*impl) InfoContext() {
	fmt.Println("called")
}

func main() {
	var p *impl = nil
	var l Logger = p

	fmt.Printf("interface == nil: %v\n", l == nil)
	fmt.Printf("dynamic type: %T\n", l)

	defer func() {
		if r := recover(); r != nil {
			fmt.Printf("panic: %v\n", r)
		}
	}()

	l.InfoContext()
}
EOF

go run /tmp/typed_nil_probe.go

Repository: LerianStudio/lib-service-discovery

Length of output: 228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Config.Logger contract and any nil-related documentation.
rg -n --hidden --glob '!**/vendor/**' 'Logger\s+Logger|Config\.Logger|nil Logger|WithLogger\(' config.go README.md CHANGELOG.md logger.go manager.go

Repository: LerianStudio/lib-service-discovery

Length of output: 1002


Handle typed-nil Logger values before wrapping them.

l == nil only catches an untyped nil; a typed-nil Logger can still be wrapped and will panic on the first log call.

  • logger.go#L38-L43: add a nil-aware guard in toObsLogger.
  • manager.go#L77-L83 and manager.go#L127-L130: route both WithLogger and New through the same guard.
📍 Affects 2 files
  • logger.go#L38-L43 (this comment)
  • manager.go#L75-L83
🤖 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 nil check does not handle
typed-nil Logger values, and manager construction paths must consistently use
that guard. Update toObsLogger to perform a nil-aware check before creating
obsLoggerAdapter, then route both WithLogger and New in manager.go through
toObsLogger; apply the changes at logger.go lines 38-43, manager.go lines 75-83,
and manager.go lines 127-130.

Comment thread manager_test.go
Comment on lines +204 to +212
nop := nopLogger()

m, err := New(Config{Enabled: false}, WithLogger(nop))
require.NoError(t, err)
assert.Equal(t, nop, m.logger)
// m.logger is the internal lib-observability adapter that bridges the
// public libsd.Logger; assert it wraps exactly the logger we passed.
adapter, ok := m.logger.(*obsLoggerAdapter)
require.True(t, ok)
assert.Equal(t, nop, adapter.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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the test and the logger implementations.
git ls-files | rg 'manager_test\.go$|logger|observability|obsLoggerAdapter|nopLogger'

echo '--- manager_test.go ---'
sed -n '1,260p' manager_test.go

echo '--- search for obsLoggerAdapter and nopLogger ---'
rg -n 'type obsLoggerAdapter|func nopLogger|obsLoggerAdapter|nopLogger\(' .

echo '--- possible logger interface definitions ---'
rg -n 'type .*Logger interface|interface .*Logger' .

Repository: LerianStudio/lib-service-discovery

Length of output: 9451


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- logger.go ---'
sed -n '1,180p' logger.go

echo '--- logger_testhelpers_test.go ---'
sed -n '1,120p' logger_testhelpers_test.go

echo '--- testify assert Equal/Same usage nearby ---'
rg -n 'assert\.(Equal|Same)\(' manager_test.go logger_test.go logger_testhelpers_test.go

Repository: LerianStudio/lib-service-discovery

Length of output: 8286


Assert logger identity, not deep equality.

assert.Equal(t, nop, adapter.l) can pass for a different *slog.Logger with equivalent fields, so the test doesn’t prove the exact instance was preserved. Use assert.Same(t, nop, adapter.l) instead.

🤖 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 `@manager_test.go` around lines 204 - 212, Update the logger assertion in the
test around New and obsLoggerAdapter to use assert.Same instead of assert.Equal,
verifying that adapter.l is the exact nop logger instance passed to WithLogger.

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

Labels

core Public API — Manager, Registry, domain types, config demo Demo services and docker-compose docs Documentation and markdown size/M tests Unit and integration tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants