Add debug logging to all HTTP requests

This commit is contained in:
Brian Picciano 2024-11-19 14:12:17 +01:00
parent 8e1dd2b2e9
commit a696f0ded6
6 changed files with 147 additions and 81 deletions

View File

@ -1,14 +1,9 @@
package main package main
import ( import (
"fmt"
"isle/bootstrap" "isle/bootstrap"
) )
func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) { func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) {
res, err := ctx.getDaemonRPC().GetHosts(ctx) return ctx.getDaemonRPC().GetHosts(ctx)
if err != nil {
return nil, fmt.Errorf("calling GetHosts: %w", err)
}
return res, nil
} }

View File

@ -8,6 +8,7 @@ import (
"isle/daemon" "isle/daemon"
"isle/daemon/jsonrpc2" "isle/daemon/jsonrpc2"
"isle/jsonutil" "isle/jsonutil"
"isle/toolkit"
"os" "os"
"strings" "strings"
@ -95,10 +96,16 @@ func usagePrefix(subCmdNames []string) string {
func (ctx subCmdCtx) getDaemonRPC() daemon.RPC { func (ctx subCmdCtx) getDaemonRPC() daemon.RPC {
if ctx.opts.daemonRPC == nil { if ctx.opts.daemonRPC == nil {
// TODO Close is not being called on the HTTPClient
httpClient, baseURL := toolkit.NewUnixHTTPClient(
ctx.logger.WithNamespace("http-client"),
daemon.HTTPSocketPath(),
)
baseURL.Path = daemonHTTPRPCPath
ctx.opts.daemonRPC = daemon.RPCFromClient( ctx.opts.daemonRPC = daemon.RPCFromClient(
jsonrpc2.NewUnixHTTPClient( jsonrpc2.NewHTTPClient(httpClient, baseURL.String()),
daemon.HTTPSocketPath(), daemonHTTPRPCPath,
),
) )
} }
return ctx.opts.daemonRPC return ctx.opts.daemonRPC

View File

@ -6,50 +6,28 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"isle/toolkit"
"net/http" "net/http"
"net/url"
"github.com/tv42/httpunix"
) )
type httpClient struct { // HTTPClient makes JSONRPC2 requests over an HTTP endpoint.
c *http.Client //
// Close should be called once the HTTPClient is not longer needed.
type HTTPClient struct {
c toolkit.HTTPClient
url string url string
} }
// NewHTTPClient returns a Client which will use HTTP POST requests against the // NewHTTPClient returns an HTTPClient which will use HTTP POST requests against
// given URL as a transport for JSONRPC2 method calls. // the given URL as a transport for JSONRPC2 method calls.
func NewHTTPClient(urlStr string) Client { func NewHTTPClient(httpClient toolkit.HTTPClient, urlStr string) *HTTPClient {
return &httpClient{ return &HTTPClient{
c: &http.Client{ c: httpClient,
Transport: http.DefaultTransport.(*http.Transport).Clone(),
},
url: urlStr, url: urlStr,
} }
} }
// NewUnixHTTPClient returns a Client which will use HTTP POST requests against func (c *HTTPClient) Call(
// the given unix socket as a transport for JSONRPC2 method calls. The given
// path will be used as the path portion of the HTTP request.
func NewUnixHTTPClient(unixSocketPath, reqPath string) Client {
const host = "uds"
u := &url.URL{
Scheme: httpunix.Scheme,
Host: host,
Path: reqPath,
}
transport := new(httpunix.Transport)
transport.RegisterLocation(host, unixSocketPath)
return &httpClient{
c: &http.Client{Transport: transport},
url: u.String(),
}
}
func (c *httpClient) Call(
ctx context.Context, rcv any, method string, args ...any, ctx context.Context, rcv any, method string, args ...any,
) error { ) error {
var ( var (
@ -88,3 +66,7 @@ func (c *httpClient) Call(
return nil return nil
} }
func (c *HTTPClient) Close() error {
return c.c.Close()
}

View File

@ -12,6 +12,8 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
type DivideParams struct { type DivideParams struct {
@ -216,13 +218,17 @@ func TestReadWriter(t *testing.T) {
} }
func TestHTTP(t *testing.T) { func TestHTTP(t *testing.T) {
logger := toolkit.NewTestLogger(t)
server := httptest.NewServer(NewHTTPHandler(testHandler(t))) server := httptest.NewServer(NewHTTPHandler(testHandler(t)))
t.Cleanup(server.Close) t.Cleanup(server.Close)
testClient(t, NewHTTPClient(server.URL)) httpClient := toolkit.NewHTTPClient(logger)
t.Cleanup(func() { assert.NoError(t, httpClient.Close()) })
testClient(t, NewHTTPClient(httpClient, server.URL))
} }
func TestUnixHTTP(t *testing.T) { func TestUnixHTTP(t *testing.T) {
var ( var (
logger = toolkit.NewTestLogger(t)
unixSocketPath = filepath.Join(t.TempDir(), "test.sock") unixSocketPath = filepath.Join(t.TempDir(), "test.sock")
server = httptest.NewUnstartedServer(NewHTTPHandler(testHandler(t))) server = httptest.NewUnstartedServer(NewHTTPHandler(testHandler(t)))
) )
@ -235,5 +241,8 @@ func TestUnixHTTP(t *testing.T) {
server.Start() server.Start()
t.Cleanup(server.Close) t.Cleanup(server.Close)
testClient(t, NewUnixHTTPClient(unixSocketPath, "/")) httpClient, baseURL := toolkit.NewUnixHTTPClient(logger, unixSocketPath)
t.Cleanup(func() { assert.NoError(t, httpClient.Close()) })
testClient(t, NewHTTPClient(httpClient, baseURL.String()))
} }

View File

@ -6,8 +6,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"isle/toolkit"
"net/http" "net/http"
"net/http/httputil"
"net/netip" "net/netip"
"time" "time"
@ -51,35 +51,31 @@ func (e AdminClientError) Error() string {
// interface. // interface.
type AdminClient struct { type AdminClient struct {
logger *mlog.Logger logger *mlog.Logger
c *http.Client c toolkit.HTTPClient
addr string addr string
adminToken string adminToken string
transport *http.Transport
} }
// NewAdminClient initializes and returns an AdminClient which will use the // NewAdminClient initializes and returns an AdminClient which will use the
// given address and adminToken for all requests made. // given address and adminToken for all requests made.
// //
// If Logger is nil then logs will be suppressed. // If Logger is nil then logs will be suppressed.
func NewAdminClient(logger *mlog.Logger, addr, adminToken string) *AdminClient { func NewAdminClient(
transport := http.DefaultTransport.(*http.Transport).Clone() logger *mlog.Logger,
addr, adminToken string,
) *AdminClient {
httpClient := toolkit.NewHTTPClient(logger.WithNamespace("http"))
return &AdminClient{ return &AdminClient{
logger: logger, logger: logger,
c: &http.Client{ c: httpClient,
Transport: transport,
},
addr: addr, addr: addr,
adminToken: adminToken, adminToken: adminToken,
transport: transport,
} }
} }
// Close cleans up all resources held by the lient. // Close cleans up all resources held by the lient.
func (c *AdminClient) Close() error { func (c *AdminClient) Close() error {
c.transport.CloseIdleConnections() return c.c.Close()
return nil
} }
// do performs an HTTP request with the given method (GET, POST) and path, and // do performs an HTTP request with the given method (GET, POST) and path, and
@ -88,7 +84,6 @@ func (c *AdminClient) Close() error {
func (c *AdminClient) do( func (c *AdminClient) do(
ctx context.Context, rcv any, method, path string, body any, ctx context.Context, rcv any, method, path string, body any,
) error { ) error {
var bodyR io.Reader var bodyR io.Reader
if body != nil { if body != nil {
@ -109,31 +104,10 @@ func (c *AdminClient) do(
req.Header.Set("Authorization", "Bearer "+c.adminToken) req.Header.Set("Authorization", "Bearer "+c.adminToken)
if c.logger.MaxLevel() >= mlog.LevelDebug.Int() {
reqB, err := httputil.DumpRequestOut(req, true)
if err != nil {
c.logger.Error(ctx, "failed to dump http request", err)
} else {
c.logger.Debug(ctx, "------ request ------\n"+string(reqB)+"\n")
}
}
res, err := c.c.Do(req) res, err := c.c.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("performing http request: %w", err) return fmt.Errorf("performing http request: %w", err)
} }
if c.logger.MaxLevel() >= mlog.LevelDebug.Int() {
resB, err := httputil.DumpResponse(res, true)
if err != nil {
c.logger.Error(ctx, "failed to dump http response", err)
} else {
c.logger.Debug(ctx, "------ response ------\n"+string(resB)+"\n")
}
}
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode >= 300 { if res.StatusCode >= 300 {

99
go/toolkit/http.go Normal file
View File

@ -0,0 +1,99 @@
package toolkit
import (
"net/http"
"net/http/httputil"
"net/url"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"github.com/tv42/httpunix"
)
// HTTPClient is an interface around the default http.Client type.
type HTTPClient interface {
Do(*http.Request) (*http.Response, error)
// Close cleans up any resources being held by the HTTPClient.
Close() error
}
type httpClient struct {
logger *mlog.Logger
*http.Client
}
// NewHTTPClient returns a new HTTPClient using a clone of
// [http.DefaultTransport].
func NewHTTPClient(logger *mlog.Logger) HTTPClient {
return &httpClient{
logger,
&http.Client{
Transport: http.DefaultTransport.(*http.Transport).Clone(),
},
}
}
// NewUnixHTTPClient returns an HTTPClient which will use the given
// unixSocketPath to serve requests. The base URL (scheme and host) which should
// be used for requests is returned as well.
func NewUnixHTTPClient(
logger *mlog.Logger, unixSocketPath string,
) (
HTTPClient, *url.URL,
) {
const host = "uds"
u := &url.URL{
Scheme: httpunix.Scheme,
Host: host,
}
transport := new(httpunix.Transport)
transport.RegisterLocation(host, unixSocketPath)
return &httpClient{logger, &http.Client{Transport: transport}}, u
}
func (c *httpClient) Do(req *http.Request) (*http.Response, error) {
if c.logger.MaxLevel() < mlog.LevelDebug.Int() {
return c.Client.Do(req)
}
var (
ctx = req.Context()
origScheme = req.URL.Scheme
)
// httputil.DumpRequestOut doesn't like the httpunix.Scheme, so we
// temporarily switch it.
if req.URL.Scheme == httpunix.Scheme {
req.URL.Scheme = "http"
}
reqB, err := httputil.DumpRequestOut(req, true)
if err != nil {
c.logger.Error(ctx, "failed to dump http request", err)
} else {
c.logger.Debug(ctx, "------ request ------\n"+string(reqB)+"\n")
}
req.URL.Scheme = origScheme
res, err := c.Client.Do(req)
if res != nil {
resB, err := httputil.DumpResponse(res, true)
if err != nil {
c.logger.Error(ctx, "failed to dump http response", err)
} else {
c.logger.Debug(ctx, "------ response ------\n"+string(resB)+"\n")
}
}
return res, err
}
func (c *httpClient) Close() error {
c.CloseIdleConnections()
return nil
}