Skip to content

Commit ec772bc

Browse files
committed
Add http debugging
When developing things (like prometheus#962), it's useful to be able to debug the http roundtrips. This adds a an --http.debug flag which logs the HTTP requests while redacting credentials. Signed-off-by: Phil Dibowitz <phil@ipom.com>
1 parent f6ad6a6 commit ec772bc

2 files changed

Lines changed: 191 additions & 0 deletions

File tree

config/http_debug.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package config
15+
16+
import (
17+
"fmt"
18+
"io"
19+
"net/http"
20+
"net/http/httptrace"
21+
"strings"
22+
"time"
23+
)
24+
25+
const debugBodyPreviewLimit = 512
26+
27+
var redactedDebugRequestHeaders = map[string]struct{}{
28+
"authorization": {},
29+
"cf-access-client-id": {},
30+
"cf-access-client-secret": {},
31+
"cf-access-token": {},
32+
"proxy-authorization": {},
33+
}
34+
35+
// NewDebugRoundTripper returns a RoundTripper that writes outgoing HTTP
36+
// requests and their responses to out. Credential-bearing request headers are
37+
// redacted, and response body previews are limited to 512 bytes.
38+
func NewDebugRoundTripper(out io.Writer, next http.RoundTripper) http.RoundTripper {
39+
return &debugRoundTripper{out: out, next: next}
40+
}
41+
42+
type debugRoundTripper struct {
43+
out io.Writer
44+
next http.RoundTripper
45+
}
46+
47+
// RoundTrip implements http.RoundTripper.
48+
func (rt *debugRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
49+
fmt.Fprintf(rt.out, "--> %s %s\n", req.Method, req.URL)
50+
51+
trace := &httptrace.ClientTrace{
52+
WroteHeaderField: func(key string, values []string) {
53+
fmt.Fprintf(rt.out, " %s: %s\n", key, redactDebugHeader(key, values))
54+
},
55+
}
56+
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
57+
58+
start := time.Now()
59+
resp, err := rt.next.RoundTrip(req)
60+
elapsed := time.Since(start)
61+
if err != nil {
62+
fmt.Fprintf(rt.out, "<-- error after %s: %v\n", elapsed, err)
63+
return resp, err
64+
}
65+
66+
fmt.Fprintf(rt.out, "<-- %s in %s (content-type: %s)\n", resp.Status, elapsed, resp.Header.Get("Content-Type"))
67+
68+
if resp.Body != nil {
69+
preview := make([]byte, debugBodyPreviewLimit)
70+
n, _ := io.ReadFull(resp.Body, preview)
71+
resp.Body = &previewedBody{
72+
preview: preview[:n],
73+
rest: resp.Body,
74+
}
75+
if n > 0 {
76+
fmt.Fprintf(rt.out, " body preview: %q\n", preview[:n])
77+
}
78+
}
79+
80+
return resp, nil
81+
}
82+
83+
func (rt *debugRoundTripper) CloseIdleConnections() {
84+
if ci, ok := rt.next.(closeIdler); ok {
85+
ci.CloseIdleConnections()
86+
}
87+
}
88+
89+
func redactDebugHeader(name string, values []string) string {
90+
if _, ok := redactedDebugRequestHeaders[strings.ToLower(name)]; ok {
91+
return "<redacted>"
92+
}
93+
return strings.Join(values, ", ")
94+
}
95+
96+
type previewedBody struct {
97+
preview []byte
98+
off int
99+
rest io.ReadCloser
100+
}
101+
102+
func (b *previewedBody) Read(p []byte) (int, error) {
103+
if b.off < len(b.preview) {
104+
n := copy(p, b.preview[b.off:])
105+
b.off += n
106+
return n, nil
107+
}
108+
return b.rest.Read(p)
109+
}
110+
111+
func (b *previewedBody) Close() error {
112+
return b.rest.Close()
113+
}

config/http_debug_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package config
15+
16+
import (
17+
"bytes"
18+
"errors"
19+
"io"
20+
"net/http"
21+
"net/http/httptest"
22+
"strings"
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestDebugRoundTripper(t *testing.T) {
29+
const responseBody = "complete response body"
30+
31+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
32+
w.Header().Set("Content-Type", "text/plain")
33+
_, err := io.WriteString(w, responseBody)
34+
require.NoError(t, err)
35+
}))
36+
t.Cleanup(server.Close)
37+
38+
var output bytes.Buffer
39+
client := &http.Client{Transport: NewDebugRoundTripper(&output, http.DefaultTransport)}
40+
req, err := http.NewRequest(http.MethodGet, server.URL+"/alerts", http.NoBody)
41+
require.NoError(t, err)
42+
req.Header.Set("Authorization", "Bearer authorization-secret")
43+
req.Header.Set("Cf-Access-Token", "cf-access-secret")
44+
req.Header.Set("X-Debug-Test", "visible")
45+
46+
resp, err := client.Do(req)
47+
require.NoError(t, err)
48+
t.Cleanup(func() { require.NoError(t, resp.Body.Close()) })
49+
50+
body, err := io.ReadAll(resp.Body)
51+
require.NoError(t, err)
52+
require.Equal(t, responseBody, string(body))
53+
54+
log := output.String()
55+
require.Contains(t, log, "--> GET "+server.URL+"/alerts")
56+
require.Contains(t, log, "Authorization: <redacted>")
57+
require.Contains(t, log, "Cf-Access-Token: <redacted>")
58+
require.Contains(t, log, "X-Debug-Test: visible")
59+
require.Contains(t, log, "<-- 200 OK")
60+
require.Contains(t, log, `body preview: "complete response body"`)
61+
require.NotContains(t, log, "authorization-secret")
62+
require.NotContains(t, log, "cf-access-secret")
63+
}
64+
65+
func TestDebugRoundTripperError(t *testing.T) {
66+
expectedErr := errors.New("request failed")
67+
next := NewRoundTripCheckRequest(func(*http.Request) {}, nil, expectedErr)
68+
var output bytes.Buffer
69+
rt := NewDebugRoundTripper(&output, next)
70+
req, err := http.NewRequest(http.MethodGet, "https://example.com/alerts", http.NoBody)
71+
require.NoError(t, err)
72+
73+
_, err = rt.RoundTrip(req)
74+
require.ErrorIs(t, err, expectedErr)
75+
require.True(t, strings.HasPrefix(output.String(), "--> GET https://example.com/alerts\n"))
76+
require.Contains(t, output.String(), "<-- error after ")
77+
require.Contains(t, output.String(), expectedErr.Error())
78+
}

0 commit comments

Comments
 (0)