Skip to content

Commit 607ab99

Browse files
getevoclaude
andcommitted
Show caller location when shutdown hooks fire
Each registered hook now captures its call site (file:line) at registration time by walking the stack past the shutdown package and the evo.OnShutdown wrapper. When the hook fires during shutdown, the location is printed: shutting down /path/to/app/server.go:112 The stack walk skips all frames inside lib/shutdown and the root-package evo.OnShutdown shim so the path always points to the actual registering code, regardless of how many wrapper layers are in between. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent da8989d commit 607ab99

File tree

1 file changed

+49
-7
lines changed

1 file changed

+49
-7
lines changed

lib/shutdown/shutdown.go

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,74 @@
44
// without creating import cycles.
55
package shutdown
66

7-
import "sync"
7+
import (
8+
"fmt"
9+
"runtime"
10+
"strings"
11+
"sync"
12+
)
13+
14+
// hook pairs a registered function with the source location that registered it.
15+
type hook struct {
16+
fn func()
17+
caller string // "file.go:line"
18+
}
819

920
var (
1021
mu sync.Mutex
11-
hooks []func()
22+
hooks []hook
1223
)
1324

1425
// Register appends fn to the list of functions that will be called by Run.
26+
// The call site (file:line) is captured at registration time and printed
27+
// when the hook fires during shutdown.
1528
// Hooks are invoked in registration order.
1629
func Register(fn func()) {
30+
caller := captureCallerOutsidePackage()
1731
mu.Lock()
1832
defer mu.Unlock()
19-
hooks = append(hooks, fn)
33+
hooks = append(hooks, hook{fn: fn, caller: caller})
2034
}
2135

22-
// Run calls every registered hook in registration order.
36+
// Run calls every registered hook in registration order, printing the
37+
// originating source location before each one.
2338
// It is safe to call from multiple goroutines; only the first call executes
2439
// the hooks — subsequent calls are no-ops.
2540
func Run() {
2641
mu.Lock()
27-
fns := make([]func(), len(hooks))
42+
fns := make([]hook, len(hooks))
2843
copy(fns, hooks)
2944
hooks = nil // prevent double-execution
3045
mu.Unlock()
3146

32-
for _, fn := range fns {
33-
fn()
47+
for _, h := range fns {
48+
fmt.Printf("shutting down %s\n", h.caller)
49+
h.fn()
50+
}
51+
}
52+
53+
// captureCallerOutsidePackage walks the call stack and returns the file:line
54+
// of the first frame that belongs neither to this package nor to the evo
55+
// root-package wrapper (evo.OnShutdown). This means the location always
56+
// points to the actual user/connector code that registered the hook.
57+
func captureCallerOutsidePackage() string {
58+
pcs := make([]uintptr, 16)
59+
// Skip runtime.Callers + captureCallerOutsidePackage + Register itself.
60+
n := runtime.Callers(3, pcs)
61+
frames := runtime.CallersFrames(pcs[:n])
62+
for {
63+
frame, more := frames.Next()
64+
fn := frame.Function
65+
// Skip frames that are part of the shutdown registry itself or the
66+
// thin evo.OnShutdown wrapper so the location always points to the
67+
// caller's code.
68+
if !strings.Contains(fn, "getevo/evo/v2/lib/shutdown") &&
69+
fn != "github.com/getevo/evo/v2.OnShutdown" {
70+
return fmt.Sprintf("%s:%d", frame.File, frame.Line)
71+
}
72+
if !more {
73+
break
74+
}
3475
}
76+
return "unknown"
3577
}

0 commit comments

Comments
 (0)