From b3304bf05b04aa75a3ef7a94360cc82a6650b149 Mon Sep 17 00:00:00 2001 From: Luis Neto Date: Fri, 29 Nov 2024 09:08:42 -0800 Subject: [PATCH] TUN-8727: implement metrics, runtime, system, and tunnelstate in diagnostic http client ## Summary The diagnostic procedure needs to extract information available in the metrics server via HTTP calls. These changes add to the diagnostic client the remaining endpoints. Closes TUN-8727 --- diagnostic/client.go | 103 ++++++++++++++++++++++++++++++------ diagnostic/consts.go | 8 +++ diagnostic/error.go | 2 + diagnostic/handlers.go | 10 +++- diagnostic/handlers_test.go | 7 +-- metrics/metrics.go | 4 +- 6 files changed, 108 insertions(+), 26 deletions(-) diff --git a/diagnostic/client.go b/diagnostic/client.go index f22830bb..3b62ad14 100644 --- a/diagnostic/client.go +++ b/diagnostic/client.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/url" "strconv" @@ -11,14 +12,12 @@ import ( "github.com/cloudflare/cloudflared/logger" ) -const configurationEndpoint = "diag/configuration" - type httpClient struct { http.Client - baseURL url.URL + baseURL *url.URL } -func NewHTTPClient(baseURL url.URL) *httpClient { +func NewHTTPClient() *httpClient { httpTransport := http.Transport{ TLSHandshakeTimeout: defaultTimeout, ResponseHeaderTimeout: defaultTimeout, @@ -29,12 +28,21 @@ func NewHTTPClient(baseURL url.URL) *httpClient { Transport: &httpTransport, Timeout: defaultTimeout, }, - baseURL, + nil, } } -func (client *httpClient) GET(ctx context.Context, url string) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) +func (client *httpClient) SetBaseURL(baseURL *url.URL) { + client.baseURL = baseURL +} + +func (client *httpClient) GET(ctx context.Context, endpoint string) (*http.Response, error) { + if client.baseURL == nil { + return nil, ErrNoBaseUrl + } + url := client.baseURL.JoinPath(endpoint) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) if err != nil { return nil, fmt.Errorf("error creating GET request: %w", err) } @@ -56,12 +64,7 @@ type LogConfiguration struct { } func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) { - endpoint, err := url.JoinPath(client.baseURL.String(), configurationEndpoint) - if err != nil { - return nil, fmt.Errorf("error parsing URL: %w", err) - } - - response, err := client.GET(ctx, endpoint) + response, err := client.GET(ctx, configurationEndpoint) if err != nil { return nil, err } @@ -93,9 +96,79 @@ func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfigur return &LogConfiguration{"", logDirectory, uid}, nil } - return nil, ErrKeyNotFound + // No log configured may happen when cloudflared is executed as a managed service or + // when containerized + return &LogConfiguration{"", "", uid}, nil +} + +func (client *httpClient) GetMemoryDump(ctx context.Context, writer io.Writer) error { + response, err := client.GET(ctx, memoryDumpEndpoint) + if err != nil { + return err + } + + return copyToWriter(response, writer) +} + +func (client *httpClient) GetGoroutineDump(ctx context.Context, writer io.Writer) error { + response, err := client.GET(ctx, goroutineDumpEndpoint) + if err != nil { + return err + } + + return copyToWriter(response, writer) +} + +func (client *httpClient) GetTunnelState(ctx context.Context) (*TunnelState, error) { + response, err := client.GET(ctx, tunnelStateEndpoint) + if err != nil { + return nil, err + } + + defer response.Body.Close() + + var state TunnelState + if err := json.NewDecoder(response.Body).Decode(&state); err != nil { + return nil, fmt.Errorf("failed to decode body: %w", err) + } + + return &state, nil +} + +func (client *httpClient) GetSystemInformation(ctx context.Context, writer io.Writer) error { + response, err := client.GET(ctx, systemInformationEndpoint) + if err != nil { + return err + } + + return copyToWriter(response, writer) +} + +func (client *httpClient) GetMetrics(ctx context.Context, writer io.Writer) error { + response, err := client.GET(ctx, metricsEndpoint) + if err != nil { + return err + } + + return copyToWriter(response, writer) +} + +func copyToWriter(response *http.Response, writer io.Writer) error { + defer response.Body.Close() + + _, err := io.Copy(writer, response.Body) + if err != nil { + return fmt.Errorf("error writing metrics: %w", err) + } + + return nil } type HTTPClient interface { - GetLogConfiguration(ctx context.Context) (LogConfiguration, error) + GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) + GetMemoryDump(ctx context.Context, writer io.Writer) error + GetGoroutineDump(ctx context.Context, writer io.Writer) error + GetTunnelState(ctx context.Context) (*TunnelState, error) + GetSystemInformation(ctx context.Context, writer io.Writer) error + GetMetrics(ctx context.Context, writer io.Writer) error } diff --git a/diagnostic/consts.go b/diagnostic/consts.go index fb6cb3bd..e98f5e71 100644 --- a/diagnostic/consts.go +++ b/diagnostic/consts.go @@ -13,4 +13,12 @@ const ( logFilename = "cloudflared_logs.txt" // name of the output log file configurationKeyUID = "uid" // Key used to set and get the UID value from the configuration map tailMaxNumberOfLines = "10000" // maximum number of log lines from a virtual runtime (docker or kubernetes) + + // Endpoints used by the diagnostic HTTP Client. + configurationEndpoint = "diag/configuration" + tunnelStateEndpoint = "diag/tunnel" + systemInformationEndpoint = "diag/system" + memoryDumpEndpoint = "debug/pprof/heap" + goroutineDumpEndpoint = "debug/pprof/goroutine" + metricsEndpoint = "metrics" ) diff --git a/diagnostic/error.go b/diagnostic/error.go index a7cabec7..cc601b7f 100644 --- a/diagnostic/error.go +++ b/diagnostic/error.go @@ -17,4 +17,6 @@ var ( ErrKeyNotFound = errors.New("key not found") // Error used when there is no disk volume information available. ErrNoVolumeFound = errors.New("no disk volume information found") + // Error user when the base url of the diagnostic client is not provided. + ErrNoBaseUrl = errors.New("no base url") ) diff --git a/diagnostic/handlers.go b/diagnostic/handlers.go index bdb17199..e8fb7e62 100644 --- a/diagnostic/handlers.go +++ b/diagnostic/handlers.go @@ -55,6 +55,12 @@ func NewDiagnosticHandler( } } +func (handler *Handler) InstallEndpoints(router *http.ServeMux) { + router.HandleFunc(configurationEndpoint, handler.ConfigurationHandler) + router.HandleFunc(tunnelStateEndpoint, handler.TunnelStateHandler) + router.HandleFunc(systemInformationEndpoint, handler.SystemHandler) +} + func (handler *Handler) SystemHandler(writer http.ResponseWriter, request *http.Request) { logger := handler.log.With().Str(collectorField, systemCollectorName).Logger() logger.Info().Msg("Collection started") @@ -95,7 +101,7 @@ func (handler *Handler) SystemHandler(writer http.ResponseWriter, request *http. } } -type tunnelStateResponse struct { +type TunnelState struct { TunnelID uuid.UUID `json:"tunnelID,omitempty"` ConnectorID uuid.UUID `json:"connectorID,omitempty"` Connections []tunnelstate.IndexedConnectionInfo `json:"connections,omitempty"` @@ -107,7 +113,7 @@ func (handler *Handler) TunnelStateHandler(writer http.ResponseWriter, _ *http.R defer log.Info().Msg("Collection finished") - body := tunnelStateResponse{ + body := TunnelState{ handler.tunnelID, handler.connectorID, handler.tracker.GetActiveConnections(), diff --git a/diagnostic/handlers_test.go b/diagnostic/handlers_test.go index 7e8222da..cc456045 100644 --- a/diagnostic/handlers_test.go +++ b/diagnostic/handlers_test.go @@ -186,12 +186,7 @@ func TestTunnelStateHandler(t *testing.T) { handler.TunnelStateHandler(recorder, nil) decoder := json.NewDecoder(recorder.Body) - var response struct { - TunnelID uuid.UUID `json:"tunnelID,omitempty"` - ConnectorID uuid.UUID `json:"connectorID,omitempty"` - Connections []tunnelstate.IndexedConnectionInfo `json:"connections,omitempty"` - } - + var response diagnostic.TunnelState err := decoder.Decode(&response) require.NoError(t, err) assert.Equal(t, http.StatusOK, recorder.Code) diff --git a/metrics/metrics.go b/metrics/metrics.go index 4fdbc2b2..e326b3c0 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -94,9 +94,7 @@ func newMetricsHandler( }) } - router.HandleFunc("/diag/configuration", config.DiagnosticHandler.ConfigurationHandler) - router.HandleFunc("/diag/tunnel", config.DiagnosticHandler.TunnelStateHandler) - router.HandleFunc("/diag/system", config.DiagnosticHandler.SystemHandler) + config.DiagnosticHandler.InstallEndpoints(router) return router }