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:
parent
28796c659e
commit
b3304bf05b
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue