Files
teleport/lib/client/debug/debug.go
T

235 lines
6.4 KiB
Go

// Teleport
// Copyright (C) 2024 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package debug
import (
"bytes"
"context"
"encoding/json"
"io"
"net"
"net/http"
"net/url"
"path/filepath"
"strconv"
"github.com/gravitational/trace"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/common/model"
"github.com/gravitational/teleport"
apidefaults "github.com/gravitational/teleport/api/defaults"
)
// SupportedProfiles list of supported pprof profiles that can be collected.
// This list is composed by runtime/pprof.Profile and http/pprof definitions.
var SupportedProfiles = map[string]struct{}{
"allocs": {},
"block": {},
"cmdline": {},
"goroutine": {},
"heap": {},
"mutex": {},
"profile": {},
"threadcreate": {},
"trace": {},
}
// Client represents the debug service client.
type Client struct {
clt *http.Client
socketPath string
}
// NewClient generates a new debug service client.
func NewClient(dataDir string) *Client {
socketPath := filepath.Join(dataDir, teleport.DebugServiceSocketName)
return &Client{
clt: &http.Client{
Timeout: apidefaults.DefaultIOTimeout,
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", socketPath)
},
DisableKeepAlives: true,
},
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return trace.Errorf("redirect via socket not allowed")
},
},
socketPath: socketPath,
}
}
// SocketPath returns the absolute path to the UNIX socket that the debug service is exposed on.
func (c *Client) SocketPath() string {
return c.socketPath
}
// SetLogLevel changes the application's log level and a change status message.
func (c *Client) SetLogLevel(ctx context.Context, level string) (string, error) {
resp, err := c.do(ctx, http.MethodPut, url.URL{Path: "/log-level"}, []byte(level))
if err != nil {
return "", trace.Wrap(err)
}
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024))
defer resp.Body.Close()
if err != nil {
return "", trace.Wrap(err)
}
if resp.StatusCode != http.StatusOK {
return "", trace.BadParameter("Unable to change log level: %s", respBody)
}
return string(respBody), nil
}
// GetLogLevel fetches the current log level.
func (c *Client) GetLogLevel(ctx context.Context) (string, error) {
resp, err := c.do(ctx, http.MethodGet, url.URL{Path: "/log-level"}, nil)
if err != nil {
return "", trace.Wrap(err)
}
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024))
defer resp.Body.Close()
if err != nil {
return "", trace.Wrap(err)
}
if resp.StatusCode != http.StatusOK {
return "", trace.BadParameter("Unable to fetch log level: %s", respBody)
}
return string(respBody), nil
}
// CollectProfile collects a pprof profile.
func (c *Client) CollectProfile(ctx context.Context, profileName string, seconds int) ([]byte, error) {
u := url.URL{
Path: "/debug/pprof/" + profileName,
}
if _, ok := SupportedProfiles[profileName]; !ok {
return nil, trace.BadParameter("%q profile not supported", profileName)
}
if seconds > 0 {
qs := url.Values{}
qs.Add("seconds", strconv.Itoa(seconds))
u.RawQuery = qs.Encode()
}
resp, err := c.do(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, trace.Wrap(err)
}
result, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, trace.Wrap(err)
}
if resp.StatusCode != http.StatusOK {
return nil, trace.BadParameter("Unable to collect profile %q: %s", profileName, result)
}
return result, nil
}
// Readiness describes the readiness of the Teleport instance.
type Readiness struct {
// Ready is true if the instance is ready.
// This field is only set by clients, based on status.
Ready bool `json:"-"`
// Status provides more detail about the readiness status.
Status string `json:"status"`
// PID is the process PID
PID int `json:"pid"`
}
// GetReadiness returns true if the Teleport service is ready.
func (c *Client) GetReadiness(ctx context.Context) (Readiness, error) {
var ready Readiness
resp, err := c.do(ctx, http.MethodGet, url.URL{Path: "/readyz"}, nil)
if err != nil {
return ready, trace.Wrap(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return ready, trace.NotFound("readiness endpoint not found")
}
ready.Ready = resp.StatusCode == http.StatusOK
err = json.NewDecoder(resp.Body).Decode(&ready)
if err != nil {
return ready, trace.Wrap(err)
}
return ready, nil
}
// GetMetrics returns prometheus metrics as a map keyed by metric name.
func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, error) {
resp, err := c.do(ctx, http.MethodGet, url.URL{Path: "/metrics"}, nil)
if err != nil {
return nil, trace.Wrap(err)
}
defer resp.Body.Close()
parser := expfmt.NewTextParser(model.UTF8Validation)
metrics, err := parser.TextToMetricFamilies(resp.Body)
if err != nil {
return nil, trace.Wrap(err)
}
return metrics, nil
}
// GetRawMetrics returns unprocessed prometheus metrics from the /metrics endpoint.
func (c *Client) GetRawMetrics(ctx context.Context) (io.ReadCloser, error) {
resp, err := c.do(ctx, http.MethodGet, url.URL{Path: "/metrics"}, nil)
if err != nil {
return nil, trace.Wrap(err)
}
return resp.Body, nil
}
func (c *Client) do(ctx context.Context, method string, u url.URL, body []byte) (*http.Response, error) {
u.Scheme = "http"
u.Host = "debug"
var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewBuffer(body)
}
req, err := http.NewRequestWithContext(ctx, method, u.String(), bodyReader)
if err != nil {
return nil, trace.Wrap(err)
}
resp, err := c.clt.Do(req)
if err != nil {
return nil, trace.Wrap(trace.ConvertSystemError(err))
}
return resp, nil
}