Files
teleport/lib/client/debug/debug.go
T
Gabriel Coradoandrosstimothy 55e2ea204f Implement debug service and its CLI commands (#39865)
* feat: debug service

* fix(service): lint and test assertion

* refactor: code review suggestions

* chore(service): update change log level log message

* fix(service): wrong error assertion

* fix(service): unable to set trace log level

* chore(utils): rename SupportedLogLevelsString

* refactor(teleport): change `seconds` default value

* fix(teleport): remove trailing line on profiles file

* chore(teleport): add profile examples

* refactor(service): use marshal/unmarshal log level functions

* fix(teleport): fix tarball file timestamps

* refactor: move debug service + constants to a separate package

* test(debug): move request to a helper

* chore(debug): remove leftover file

* chore(log): typo

* refactor: move package from lib/debug to lib/srv/debug

* refactor: code review suggestions

* fix(service): socket file consistency

* test(common): use one temporary directory per socket test

* chore(tool): update debug service connection error message

* refactor(debug): use enhanced serve mux

* chore(teleport): fix usage examples

* Apply suggestions from code review

Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com>

* refactor: code review suggestions

* refactor(debug): move marshal/unmarshal text log level to debug

* refactor: code review suggestions

* refactor: move debug service client to lib/client/debug/

* refactor: code review changes

* feat(debug): progress on collect profiles

* refactor: code review suggestions

* chore(gomod): update

* fix(debug): set log level validations

* refactor(debug): move client interface

* feat(debug): add limit reader for collect profiles

* refactor(debug): remove progress bar

* Revert "chore(gomod): update"

This reverts commit 8dbebbf69f.

* refactor: remove debug service endpoints constants

* test(service): fix wrong constant

* refactor(debug): remove collect profiles limit reader

* chore(debug): make client struct public

* refactor(debug): change collect time to 0

* test(service): deflake test

* test(service): add missing auth config

* test(debug): fix missing assertion

---------

Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com>
2024-05-07 19:41:55 +00:00

161 lines
4.1 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"
"io"
"net"
"net/http"
"net/url"
"strconv"
"github.com/gravitational/trace"
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
}
// NewClient generates a new debug service client.
func NewClient(socketPath string) *Client {
return &Client{
clt: &http.Client{
Timeout: apidefaults.DefaultIOTimeout,
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", 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
}
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
}