Skip to content

Commit 6cfc985

Browse files
authored
Add (optional) debug logging for console requests (#3440)
* Add (optional) debug logging for console requests * Check vulnerabilities with 1.22.7
1 parent 1c47685 commit 6cfc985

File tree

4 files changed

+131
-2
lines changed

4 files changed

+131
-2
lines changed

.github/workflows/vulncheck.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Go
2020
uses: actions/setup-go@v5
2121
with:
22-
go-version: 1.22.5
22+
go-version: 1.22.7
2323
check-latest: true
2424
- name: Get official govulncheck
2525
run: go install golang.org/x/vuln/cmd/govulncheck@latest
@@ -33,7 +33,7 @@ jobs:
3333
runs-on: ubuntu-latest
3434
strategy:
3535
matrix:
36-
go-version: [ 1.22.5 ]
36+
go-version: [ 1.22.7 ]
3737
os: [ ubuntu-latest ]
3838
steps:
3939
- name: Check out code

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,27 @@ export CONSOLE_MINIO_SERVER=https://localhost:9000
205205

206206
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
207207

208+
## Debug logging
209+
210+
In some cases it may be convenient to log all HTTP requests. This can be enabled by setting
211+
the `CONSOLE_DEBUG_LOGLEVEL` environment variable to one of the following values:
212+
213+
- `0` (default) uses no logging.
214+
- `1` log single line per request for server-side errors (status-code 5xx).
215+
- `2` log single line per request for client-side and server-side errors (status-code 4xx/5xx).
216+
- `3` log single line per request for all requests (status-code 4xx/5xx).
217+
- `4` log details per request for server-side errors (status-code 5xx).
218+
- `5` log details per request for client-side and server-side errors (status-code 4xx/5xx).
219+
- `6` log details per request for all requests (status-code 4xx/5xx).
220+
221+
A single line logging has the following information:
222+
- Remote endpoint (IP + port) of the request. Note that reverse proxies may hide the actual remote endpoint of the client's browser.
223+
- HTTP method and URL
224+
- Status code of the response (websocket connections are hijacked, so no response is shown)
225+
- Duration of the request
226+
227+
The detailed logging also includes all request and response headers (if any).
228+
208229
# Contribute to console Project
209230

210231
Please follow console [Contributor's Guide](https://github.com/minio/console/blob/master/CONTRIBUTING.md)

api/configure_console.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"path"
3232
"path/filepath"
3333
"regexp"
34+
"sort"
35+
"strconv"
3436
"strings"
3537
"sync"
3638
"time"
@@ -216,6 +218,97 @@ func AuditLogMiddleware(next http.Handler) http.Handler {
216218
})
217219
}
218220

221+
func DebugLogMiddleware(next http.Handler) http.Handler {
222+
debugLogLevel, _ := env.GetInt("CONSOLE_DEBUG_LOGLEVEL", 0)
223+
if debugLogLevel == 0 {
224+
return next
225+
}
226+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
227+
rw := logger.NewResponseWriter(w)
228+
next.ServeHTTP(rw, r)
229+
debugLog(debugLogLevel, r, rw)
230+
})
231+
}
232+
233+
func debugLog(debugLogLevel int, r *http.Request, rw *logger.ResponseWriter) {
234+
switch debugLogLevel {
235+
case 1:
236+
// Log server errors only (summary)
237+
if rw.StatusCode >= 500 {
238+
debugLogSummary(r, rw)
239+
}
240+
case 2:
241+
// Log server and client errors (summary)
242+
if rw.StatusCode >= 400 {
243+
debugLogSummary(r, rw)
244+
}
245+
case 3:
246+
// Log all requests (summary)
247+
debugLogSummary(r, rw)
248+
case 4:
249+
// Log server errors only (including headers)
250+
if rw.StatusCode >= 500 {
251+
debugLogDetails(r, rw)
252+
}
253+
case 5:
254+
// Log server and client errors (including headers)
255+
if rw.StatusCode >= 400 {
256+
debugLogDetails(r, rw)
257+
}
258+
case 6:
259+
// Log all requests (including headers)
260+
debugLogDetails(r, rw)
261+
}
262+
}
263+
264+
func debugLogSummary(r *http.Request, rw *logger.ResponseWriter) {
265+
statusCode := strconv.Itoa(rw.StatusCode)
266+
if rw.Hijacked {
267+
statusCode = "hijacked"
268+
}
269+
logger.Info(fmt.Sprintf("%s %s %s %s %dms", r.RemoteAddr, r.Method, r.URL, statusCode, time.Since(rw.StartTime).Milliseconds()))
270+
}
271+
272+
func debugLogDetails(r *http.Request, rw *logger.ResponseWriter) {
273+
var sb strings.Builder
274+
sb.WriteString(fmt.Sprintf("- Method/URL: %s %s\n", r.Method, r.URL))
275+
sb.WriteString(fmt.Sprintf(" Remote endpoint: %s\n", r.RemoteAddr))
276+
if rw.Hijacked {
277+
sb.WriteString(" Status code: <hijacked, probably a websocket>\n")
278+
} else {
279+
sb.WriteString(fmt.Sprintf(" Status code: %d\n", rw.StatusCode))
280+
}
281+
sb.WriteString(fmt.Sprintf(" Duration (ms): %d\n", time.Since(rw.StartTime).Milliseconds()))
282+
sb.WriteString(" Request headers: ")
283+
debugLogHeaders(&sb, r.Header)
284+
sb.WriteString(" Response headers: ")
285+
debugLogHeaders(&sb, rw.Header())
286+
logger.Info(sb.String())
287+
}
288+
289+
func debugLogHeaders(sb *strings.Builder, h http.Header) {
290+
keys := make([]string, 0, len(h))
291+
for key := range h {
292+
keys = append(keys, key)
293+
}
294+
sort.Strings(keys)
295+
first := true
296+
for _, key := range keys {
297+
values := h[key]
298+
for _, value := range values {
299+
if !first {
300+
sb.WriteString(" ")
301+
} else {
302+
first = false
303+
}
304+
sb.WriteString(fmt.Sprintf("%s: %s\n", key, value))
305+
}
306+
}
307+
if first {
308+
sb.WriteRune('\n')
309+
}
310+
}
311+
219312
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
220313
// So this is a good place to plug in a panic handling middleware, logger and metrics
221314
func setupGlobalMiddleware(handler http.Handler) http.Handler {
@@ -228,6 +321,8 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler {
228321
next = ContextMiddleware(next)
229322
// handle cookie or authorization header for session
230323
next = AuthenticationMiddleware(next)
324+
// handle debug logging
325+
next = DebugLogMiddleware(next)
231326

232327
sslHostFn := secure.SSLHostFunc(func(host string) string {
233328
xhost, err := xnet.ParseHost(host)

pkg/logger/audit.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package logger
1818

1919
import (
20+
"bufio"
2021
"bytes"
2122
"context"
23+
"errors"
2224
"fmt"
2325
"io"
26+
"net"
2427
"net/http"
2528
"strconv"
2629
"sync/atomic"
@@ -35,6 +38,7 @@ import (
3538
type ResponseWriter struct {
3639
http.ResponseWriter
3740
StatusCode int
41+
Hijacked bool
3842
// Log body of 4xx or 5xx responses
3943
LogErrBody bool
4044
// Log body of all responses
@@ -61,6 +65,15 @@ func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
6165
}
6266
}
6367

68+
func (lrw *ResponseWriter) Hijack() (conn net.Conn, rw *bufio.ReadWriter, err error) {
69+
hijack, ok := lrw.ResponseWriter.(http.Hijacker)
70+
if !ok {
71+
return nil, nil, errors.New("base response writer doesn't implement hijacker")
72+
}
73+
lrw.Hijacked = true
74+
return hijack.Hijack()
75+
}
76+
6477
func (lrw *ResponseWriter) Write(p []byte) (int, error) {
6578
if !lrw.headersLogged {
6679
// We assume the response code to be '200 OK' when WriteHeader() is not called,

0 commit comments

Comments
 (0)