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
This commit is contained in:
Luis Neto 2024-11-29 09:08:42 -08:00
parent 28796c659e
commit b3304bf05b
6 changed files with 108 additions and 26 deletions

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -11,14 +12,12 @@ import (
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
) )
const configurationEndpoint = "diag/configuration"
type httpClient struct { type httpClient struct {
http.Client http.Client
baseURL url.URL baseURL *url.URL
} }
func NewHTTPClient(baseURL url.URL) *httpClient { func NewHTTPClient() *httpClient {
httpTransport := http.Transport{ httpTransport := http.Transport{
TLSHandshakeTimeout: defaultTimeout, TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout, ResponseHeaderTimeout: defaultTimeout,
@ -29,12 +28,21 @@ func NewHTTPClient(baseURL url.URL) *httpClient {
Transport: &httpTransport, Transport: &httpTransport,
Timeout: defaultTimeout, Timeout: defaultTimeout,
}, },
baseURL, nil,
} }
} }
func (client *httpClient) GET(ctx context.Context, url string) (*http.Response, error) { func (client *httpClient) SetBaseURL(baseURL *url.URL) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 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 { if err != nil {
return nil, fmt.Errorf("error creating GET request: %w", err) 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) { func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) {
endpoint, err := url.JoinPath(client.baseURL.String(), configurationEndpoint) response, err := client.GET(ctx, configurationEndpoint)
if err != nil {
return nil, fmt.Errorf("error parsing URL: %w", err)
}
response, err := client.GET(ctx, endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -93,9 +96,79 @@ func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfigur
return &LogConfiguration{"", logDirectory, uid}, nil 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 { 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
} }

View File

@ -13,4 +13,12 @@ const (
logFilename = "cloudflared_logs.txt" // name of the output log file 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 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) 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"
) )

View File

@ -17,4 +17,6 @@ var (
ErrKeyNotFound = errors.New("key not found") ErrKeyNotFound = errors.New("key not found")
// Error used when there is no disk volume information available. // Error used when there is no disk volume information available.
ErrNoVolumeFound = errors.New("no disk volume information found") 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")
) )

View File

@ -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) { func (handler *Handler) SystemHandler(writer http.ResponseWriter, request *http.Request) {
logger := handler.log.With().Str(collectorField, systemCollectorName).Logger() logger := handler.log.With().Str(collectorField, systemCollectorName).Logger()
logger.Info().Msg("Collection started") 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"` TunnelID uuid.UUID `json:"tunnelID,omitempty"`
ConnectorID uuid.UUID `json:"connectorID,omitempty"` ConnectorID uuid.UUID `json:"connectorID,omitempty"`
Connections []tunnelstate.IndexedConnectionInfo `json:"connections,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") defer log.Info().Msg("Collection finished")
body := tunnelStateResponse{ body := TunnelState{
handler.tunnelID, handler.tunnelID,
handler.connectorID, handler.connectorID,
handler.tracker.GetActiveConnections(), handler.tracker.GetActiveConnections(),

View File

@ -186,12 +186,7 @@ func TestTunnelStateHandler(t *testing.T) {
handler.TunnelStateHandler(recorder, nil) handler.TunnelStateHandler(recorder, nil)
decoder := json.NewDecoder(recorder.Body) decoder := json.NewDecoder(recorder.Body)
var response struct { var response diagnostic.TunnelState
TunnelID uuid.UUID `json:"tunnelID,omitempty"`
ConnectorID uuid.UUID `json:"connectorID,omitempty"`
Connections []tunnelstate.IndexedConnectionInfo `json:"connections,omitempty"`
}
err := decoder.Decode(&response) err := decoder.Decode(&response)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, http.StatusOK, recorder.Code)

View File

@ -94,9 +94,7 @@ func newMetricsHandler(
}) })
} }
router.HandleFunc("/diag/configuration", config.DiagnosticHandler.ConfigurationHandler) config.DiagnosticHandler.InstallEndpoints(router)
router.HandleFunc("/diag/tunnel", config.DiagnosticHandler.TunnelStateHandler)
router.HandleFunc("/diag/system", config.DiagnosticHandler.SystemHandler)
return router return router
} }