Skip to content
Draft
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
33 changes: 33 additions & 0 deletions internal/composer/debug/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package debug

import "sync/atomic"

// nolint:gochecknoglobals // global provider follows the OTEL provider pattern.
var global = newGlobal()

type globalState struct {
ptr atomic.Pointer[Debugger]
}

func newGlobal() *globalState {
g := &globalState{}
var d Debugger = noopDebuggerSingleton
g.ptr.Store(&d)
return g
}

// GetDebugger returns the global Debugger.
// If no debugger has been set, a no-op Debugger is returned.
// GetDebugger is safe for concurrent use and is designed to be called on every
// FlowComponent invocation with minimal overhead.
func GetDebugger() Debugger {
return *global.ptr.Load()
}

// SetDebugger replaces the global Debugger.
// This should be called once during application initialization before any
// FlowComponent begins handling requests.
// SetDebugger is safe for concurrent use.
func SetDebugger(d Debugger) {
global.ptr.Store(&d)
}
74 changes: 74 additions & 0 deletions internal/composer/debug/debug_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package debug_test

import (
"context"
"testing"
"time"

"github.com/trebent/kerberos/internal/composer/debug"
)

func TestGetDebugger_defaultIsNoop(t *testing.T) {
d := debug.GetDebugger()
if d == nil {
t.Fatal("expected non-nil Debugger")
}

// The default noop debugger must not panic.
action := d.StartAction(context.Background(), "test-component")
if action == nil {
t.Fatal("expected non-nil Action")
}

action.End(debug.Outcome{StatusCode: 200, Duration: time.Millisecond})
}

func TestSetDebugger_replacesGlobal(t *testing.T) {
original := debug.GetDebugger()
t.Cleanup(func() { debug.SetDebugger(original) })

called := false
debug.SetDebugger(&spyDebugger{onStartAction: func() { called = true }})

debug.GetDebugger().StartAction(context.Background(), "component").
End(debug.Outcome{StatusCode: 200})

if !called {
t.Fatal("expected custom debugger to be called")
}
}

func TestSetDebugger_nilNoPanic(t *testing.T) {
original := debug.GetDebugger()
t.Cleanup(func() { debug.SetDebugger(original) })

// Setting a nil Debugger should not panic; retrieving and calling it may
// panic depending on usage, but the set operation itself must be safe.
debug.SetDebugger(nil)
}

// spyDebugger is a test helper that records StartAction calls.
type spyDebugger struct {
onStartAction func()
}

func (s *spyDebugger) StartAction(_ context.Context, _ string) debug.Action {
if s.onStartAction != nil {
s.onStartAction()
}
return &spyAction{}
}

type spyAction struct{}

func (s *spyAction) End(_ debug.Outcome) {}

// BenchmarkNoopDebugger verifies that the noop path causes zero heap allocations.
func BenchmarkNoopDebugger(b *testing.B) {
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
action := debug.GetDebugger().StartAction(ctx, "test-component")
action.End(debug.Outcome{StatusCode: 200, Duration: time.Millisecond})
}
}
29 changes: 29 additions & 0 deletions internal/composer/debug/debugger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package debug

import (
"context"
"time"
)

// Outcome describes the result of a single FlowComponent invocation.
type Outcome struct {
// StatusCode is the HTTP response status code produced by the component.
StatusCode int
// Duration is the time the component took to process the request.
Duration time.Duration
}

// Action represents a single flow component debug recording session.
// End must be called exactly once after the component finishes processing.
type Action interface {
// End records the outcome of the flow component invocation.
End(outcome Outcome)
}

// Debugger records diagnostic information about FlowComponent invocations.
// Implementations must be safe for concurrent use.
type Debugger interface {
// StartAction begins recording a single flow component invocation.
// The returned Action must have its End method called when the component finishes.
StartAction(ctx context.Context, componentName string) Action
}
24 changes: 24 additions & 0 deletions internal/composer/debug/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package debug

import "context"

// nolint:gochecknoglobals // singleton instances avoid allocations on the noop hot-path.
var (
noopDebuggerSingleton = &noopDebugger{}
noopActionSingleton = &noopAction{}
)

var (
_ Debugger = noopDebuggerSingleton
_ Action = noopActionSingleton
)

type noopDebugger struct{}

func (n *noopDebugger) StartAction(_ context.Context, _ string) Action {
return noopActionSingleton
}

type noopAction struct{}

func (n *noopAction) End(_ Outcome) {}