Skip to content

Commit 1a992c2

Browse files
Copilotmaansaake
authored andcommitted
feat: add internal/composer/debug package for flow component debugging
Agent-Logs-Url: https://github.com/trebent/kerberos/sessions/39bcaa63-bf51-431e-89ff-d6e9e7ef85ef Co-authored-by: maansaake <15028979+maansaake@users.noreply.github.com>
1 parent d4ac556 commit 1a992c2

4 files changed

Lines changed: 160 additions & 0 deletions

File tree

internal/composer/debug/debug.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package debug
2+
3+
import "sync/atomic"
4+
5+
// nolint:gochecknoglobals // global provider follows the OTEL provider pattern.
6+
var global = newGlobal()
7+
8+
type globalState struct {
9+
ptr atomic.Pointer[Debugger]
10+
}
11+
12+
func newGlobal() *globalState {
13+
g := &globalState{}
14+
var d Debugger = noopDebuggerSingleton
15+
g.ptr.Store(&d)
16+
return g
17+
}
18+
19+
// GetDebugger returns the global Debugger.
20+
// If no debugger has been set, a no-op Debugger is returned.
21+
// GetDebugger is safe for concurrent use and is designed to be called on every
22+
// FlowComponent invocation with minimal overhead.
23+
func GetDebugger() Debugger {
24+
return *global.ptr.Load()
25+
}
26+
27+
// SetDebugger replaces the global Debugger.
28+
// This should be called once during application initialization before any
29+
// FlowComponent begins handling requests.
30+
// SetDebugger is safe for concurrent use.
31+
func SetDebugger(d Debugger) {
32+
global.ptr.Store(&d)
33+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package debug_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/trebent/kerberos/internal/composer/debug"
9+
)
10+
11+
func TestGetDebugger_defaultIsNoop(t *testing.T) {
12+
d := debug.GetDebugger()
13+
if d == nil {
14+
t.Fatal("expected non-nil Debugger")
15+
}
16+
17+
// The default noop debugger must not panic.
18+
action := d.StartAction(context.Background(), "test-component")
19+
if action == nil {
20+
t.Fatal("expected non-nil Action")
21+
}
22+
23+
action.End(debug.Outcome{StatusCode: 200, Duration: time.Millisecond})
24+
}
25+
26+
func TestSetDebugger_replacesGlobal(t *testing.T) {
27+
original := debug.GetDebugger()
28+
t.Cleanup(func() { debug.SetDebugger(original) })
29+
30+
called := false
31+
debug.SetDebugger(&spyDebugger{onStartAction: func() { called = true }})
32+
33+
debug.GetDebugger().StartAction(context.Background(), "component").
34+
End(debug.Outcome{StatusCode: 200})
35+
36+
if !called {
37+
t.Fatal("expected custom debugger to be called")
38+
}
39+
}
40+
41+
func TestSetDebugger_nilNoPanic(t *testing.T) {
42+
original := debug.GetDebugger()
43+
t.Cleanup(func() { debug.SetDebugger(original) })
44+
45+
// Setting a nil Debugger should not panic; retrieving and calling it may
46+
// panic depending on usage, but the set operation itself must be safe.
47+
debug.SetDebugger(nil)
48+
}
49+
50+
// spyDebugger is a test helper that records StartAction calls.
51+
type spyDebugger struct {
52+
onStartAction func()
53+
}
54+
55+
func (s *spyDebugger) StartAction(_ context.Context, _ string) debug.Action {
56+
if s.onStartAction != nil {
57+
s.onStartAction()
58+
}
59+
return &spyAction{}
60+
}
61+
62+
type spyAction struct{}
63+
64+
func (s *spyAction) End(_ debug.Outcome) {}
65+
66+
// BenchmarkNoopDebugger verifies that the noop path causes zero heap allocations.
67+
func BenchmarkNoopDebugger(b *testing.B) {
68+
ctx := context.Background()
69+
b.ReportAllocs()
70+
for b.Loop() {
71+
action := debug.GetDebugger().StartAction(ctx, "test-component")
72+
action.End(debug.Outcome{StatusCode: 200, Duration: time.Millisecond})
73+
}
74+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package debug
2+
3+
import (
4+
"context"
5+
"time"
6+
)
7+
8+
// Outcome describes the result of a single FlowComponent invocation.
9+
type Outcome struct {
10+
// StatusCode is the HTTP response status code produced by the component.
11+
StatusCode int
12+
// Duration is the time the component took to process the request.
13+
Duration time.Duration
14+
}
15+
16+
// Action represents a single flow component debug recording session.
17+
// End must be called exactly once after the component finishes processing.
18+
type Action interface {
19+
// End records the outcome of the flow component invocation.
20+
End(outcome Outcome)
21+
}
22+
23+
// Debugger records diagnostic information about FlowComponent invocations.
24+
// Implementations must be safe for concurrent use.
25+
type Debugger interface {
26+
// StartAction begins recording a single flow component invocation.
27+
// The returned Action must have its End method called when the component finishes.
28+
StartAction(ctx context.Context, componentName string) Action
29+
}

internal/composer/debug/noop.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package debug
2+
3+
import "context"
4+
5+
// nolint:gochecknoglobals // singleton instances avoid allocations on the noop hot-path.
6+
var (
7+
noopDebuggerSingleton = &noopDebugger{}
8+
noopActionSingleton = &noopAction{}
9+
)
10+
11+
var (
12+
_ Debugger = noopDebuggerSingleton
13+
_ Action = noopActionSingleton
14+
)
15+
16+
type noopDebugger struct{}
17+
18+
func (n *noopDebugger) StartAction(_ context.Context, _ string) Action {
19+
return noopActionSingleton
20+
}
21+
22+
type noopAction struct{}
23+
24+
func (n *noopAction) End(_ Outcome) {}

0 commit comments

Comments
 (0)