Skip to content

Commit ee9bbb0

Browse files
feat(log): print traceback when panics occur (#2166)
Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
1 parent d1bf713 commit ee9bbb0

6 files changed

+122
-9
lines changed

THIRD-PARTY-LICENSES.md

-2
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ github.com/emirpasic/gods|https://github.com/emirpasic/gods/blob/v1.18.1/LICENSE
147147
github.com/evanphx/json-patch|https://github.com/evanphx/json-patch/blob/v5.6.0/LICENSE|BSD-3-Clause
148148
github.com/exponent-io/jsonpath|https://github.com/exponent-io/jsonpath/blob/d6023ce2651d/LICENSE|MIT
149149
github.com/fatih/color|https://github.com/fatih/color/blob/v1.15.0/LICENSE.md|MIT
150-
github.com/felixge/httpsnoop|https://github.com/felixge/httpsnoop/blob/v1.0.4/LICENSE.txt|MIT
151150
github.com/fsnotify/fsnotify|https://github.com/fsnotify/fsnotify/blob/v1.7.0/LICENSE|BSD-3-Clause
152151
github.com/go-asn1-ber/asn1-ber|https://github.com/go-asn1-ber/asn1-ber/blob/v1.5.5/LICENSE|MIT
153152
github.com/go-chi/chi|https://github.com/go-chi/chi/blob/v4.1.2/LICENSE|MIT
@@ -198,7 +197,6 @@ github.com/google/uuid|https://github.com/google/uuid/blob/v1.4.0/LICENSE|BSD-3-
198197
github.com/google/wire|https://github.com/google/wire/blob/v0.5.0/LICENSE|Apache-2.0
199198
github.com/googleapis/enterprise-certificate-proxy/client|https://github.com/googleapis/enterprise-certificate-proxy/blob/v0.3.2/LICENSE|Apache-2.0
200199
github.com/googleapis/gax-go/v2|https://github.com/googleapis/gax-go/blob/v2.12.0/v2/LICENSE|BSD-3-Clause
201-
github.com/gorilla/handlers|https://github.com/gorilla/handlers/blob/v1.5.2/LICENSE|BSD-3-Clause
202200
github.com/gorilla/mux|https://github.com/gorilla/mux/blob/v1.8.0/LICENSE|BSD-3-Clause
203201
github.com/gorilla/schema|https://github.com/gorilla/schema/blob/v1.2.0/LICENSE|BSD-3-Clause
204202
github.com/gorilla/securecookie|https://github.com/gorilla/securecookie/blob/v1.1.2/LICENSE|BSD-3-Clause

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ require (
5050
github.com/aws/aws-secretsmanager-caching-go v1.1.3
5151
github.com/containers/image/v5 v5.29.0
5252
github.com/google/go-github/v52 v52.0.0
53-
github.com/gorilla/handlers v1.5.2
5453
github.com/gorilla/securecookie v1.1.2
5554
github.com/gorilla/sessions v1.2.2
5655
github.com/migueleliasweb/go-github-mock v0.0.22
@@ -124,7 +123,6 @@ require (
124123
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
125124
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
126125
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
127-
github.com/felixge/httpsnoop v1.0.4 // indirect
128126
github.com/go-errors/errors v1.4.2 // indirect
129127
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
130128
github.com/go-ini/ini v1.67.0 // indirect
@@ -139,6 +137,7 @@ require (
139137
github.com/google/licenseclassifier/v2 v2.0.0 // indirect
140138
github.com/google/s2a-go v0.1.7 // indirect
141139
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
140+
github.com/gorilla/handlers v1.5.2 // indirect
142141
github.com/gosuri/uitable v0.0.4 // indirect
143142
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
144143
github.com/jmoiron/sqlx v1.3.5 // indirect

go.sum

-1
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,6 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF
670670
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
671671
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
672672
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
673-
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
674673
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
675674
github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
676675
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=

pkg/api/controller.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"syscall"
1515
"time"
1616

17-
"github.com/gorilla/handlers"
1817
"github.com/gorilla/mux"
1918
"github.com/zitadel/oidc/pkg/client/rp"
2019

@@ -117,9 +116,8 @@ func (c *Controller) Run() error {
117116

118117
engine.Use(
119118
SessionLogger(c),
120-
handlers.RecoveryHandler(
121-
handlers.PrintRecoveryStack(true),
122-
))
119+
RecoveryHandler(c.Log),
120+
)
123121

124122
if c.Audit != nil {
125123
engine.Use(SessionAuditLogger(c.Audit))

pkg/api/controller_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import (
6161
"zotregistry.io/zot/pkg/storage"
6262
storageConstants "zotregistry.io/zot/pkg/storage/constants"
6363
"zotregistry.io/zot/pkg/storage/gc"
64+
storageTypes "zotregistry.io/zot/pkg/storage/types"
6465
authutils "zotregistry.io/zot/pkg/test/auth"
6566
test "zotregistry.io/zot/pkg/test/common"
6667
"zotregistry.io/zot/pkg/test/deprecated"
@@ -959,6 +960,39 @@ func TestBlobReferenced(t *testing.T) {
959960
})
960961
}
961962

963+
func TestPrintTracebackOnPanic(t *testing.T) {
964+
Convey("Run server on unavailable port", t, func() {
965+
port := test.GetFreePort()
966+
baseURL := test.GetBaseURL(port)
967+
968+
conf := config.New()
969+
conf.HTTP.Port = port
970+
971+
logFile, err := os.CreateTemp("", "zot-log*.txt")
972+
So(err, ShouldBeNil)
973+
conf.Log.Output = logFile.Name()
974+
defer os.Remove(logFile.Name()) // clean up
975+
976+
ctlr := makeController(conf, t.TempDir())
977+
cm := test.NewControllerManager(ctlr)
978+
979+
cm.StartAndWait(port)
980+
defer cm.StopServer()
981+
982+
ctlr.StoreController.SubStore = make(map[string]storageTypes.ImageStore)
983+
ctlr.StoreController.SubStore["/a"] = nil
984+
985+
resp, err := resty.R().Get(baseURL + "/v2/_catalog")
986+
So(err, ShouldBeNil)
987+
So(resp, ShouldNotBeNil)
988+
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
989+
990+
data, err := os.ReadFile(logFile.Name())
991+
So(err, ShouldBeNil)
992+
So(string(data), ShouldContainSubstring, "invalid memory address or nil pointer dereference")
993+
})
994+
}
995+
962996
func TestInterruptedBlobUpload(t *testing.T) {
963997
Convey("Successfully cleaning interrupted blob uploads", t, func() {
964998
port := test.GetFreePort()

pkg/api/recovery.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"runtime"
7+
8+
"github.com/gorilla/mux"
9+
10+
"zotregistry.io/zot/pkg/log"
11+
)
12+
13+
type Stack struct {
14+
Frames []Frame `json:"stack"`
15+
}
16+
17+
type Frame struct {
18+
Name string `json:"function"`
19+
File string `json:"file"`
20+
Line int `json:"line"`
21+
}
22+
23+
// RecoveryHandler is a HTTP middleware that recovers from a panic.
24+
// It logs the panic and its traceback in json format, writes http.StatusInternalServerError
25+
// and continues to the next handler.
26+
func RecoveryHandler(log log.Logger) mux.MiddlewareFunc {
27+
return func(next http.Handler) http.Handler {
28+
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
29+
defer func() {
30+
if err := recover(); err != nil {
31+
response.WriteHeader(http.StatusInternalServerError)
32+
33+
stack := Stack{
34+
Frames: getStacktrace(),
35+
}
36+
37+
recoveredErr, ok := err.(error)
38+
if ok {
39+
buf, err := json.Marshal(stack)
40+
if err == nil {
41+
log.Error().Err(recoveredErr).RawJSON("traceback", buf).Msg("panic recovered") //nolint: check-logs
42+
}
43+
}
44+
}
45+
}()
46+
47+
// Process request
48+
next.ServeHTTP(response, request)
49+
})
50+
}
51+
}
52+
53+
func getStacktrace() []Frame {
54+
stack := []Frame{}
55+
//nolint: varnamelen
56+
pc := make([]uintptr, 64)
57+
58+
n := runtime.Callers(0, pc)
59+
if n == 0 {
60+
return []Frame{}
61+
}
62+
63+
// first three frames are from this file, don't need them.
64+
pc = pc[3:]
65+
frames := runtime.CallersFrames(pc)
66+
67+
// loop to get frames.
68+
for {
69+
frame, more := frames.Next()
70+
71+
// store this frame
72+
stack = append(stack, Frame{
73+
Name: frame.Function,
74+
File: frame.File,
75+
Line: frame.Line,
76+
})
77+
78+
// check whether there are more frames to process after this one.
79+
if !more {
80+
break
81+
}
82+
}
83+
84+
return stack
85+
}

0 commit comments

Comments
 (0)