diff --git a/lib/client/debug/debug.go b/lib/client/debug/debug.go index a8b51a5fb2ef8..8beaf99a71d8b 100644 --- a/lib/client/debug/debug.go +++ b/lib/client/debug/debug.go @@ -201,6 +201,15 @@ func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, 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" diff --git a/tool/teleport/common/debug.go b/tool/teleport/common/debug.go index 801e920e1982e..bcb907098db1b 100644 --- a/tool/teleport/common/debug.go +++ b/tool/teleport/common/debug.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "io" + "os" "slices" "strings" "time" @@ -46,6 +47,8 @@ type DebugClient interface { CollectProfile(context.Context, string, int) ([]byte, error) // GetReadiness checks if the instance is ready to serve requests. GetReadiness(context.Context) (debugclient.Readiness, error) + // GetRawMetrics fetches the unprocessed Prometheus metrics. + GetRawMetrics(context.Context) (io.ReadCloser, error) SocketPath() string } @@ -196,6 +199,26 @@ func readyz(ctx context.Context, clt DebugClient) error { return nil } +// onMetrics fetches the current Prometheus metrics. +func onMetrics(ctx context.Context, configPath string) error { + clt, dataDir, err := newDebugClient(configPath) + if err != nil { + return trace.Wrap(err) + } + + metrics, err := clt.GetRawMetrics(ctx) + if err != nil { + return convertToReadableErr(err, dataDir, clt.SocketPath()) + } + defer metrics.Close() + + if _, err := io.Copy(os.Stdout, metrics); err != nil { + return trace.Wrap(err) + } + + return nil +} + // newDebugClient initializes the debug client based on the Teleport // configuration. It also returns the data dir and socket path used. func newDebugClient(configPath string) (DebugClient, string, error) { diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 7208c7c1efd5b..c651d2812bf04 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -618,6 +618,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con collectProfilesCmd.Arg("PROFILES", fmt.Sprintf("Comma-separated profile names to be exported. Supported profiles: %s. Default: %s", strings.Join(slices.Collect(maps.Keys(debugclient.SupportedProfiles)), ","), strings.Join(defaultCollectProfiles, ","))).StringVar(&ccf.Profiles) collectProfilesCmd.Flag("seconds", "For CPU and trace profiles, profile for the given duration (if set to 0, it returns a profile snapshot). For other profiles, return a delta profile. Default: 0").Short('s').Default("0").IntVar(&ccf.ProfileSeconds) readyzCmd := debugCmd.Command("readyz", "Checks if the instance is ready to serve requests.") + metricsCmd := debugCmd.Command("metrics", "Fetches the cluster's Prometheus metrics.") selinuxCmd := app.Command("selinux-ssh", "Commands related to SSH SELinux module.").Hidden() selinuxCmd.Flag("config", fmt.Sprintf("Path to a configuration file [%v].", defaults.ConfigFilePath)).Short('c').ExistingFileVar(&ccf.ConfigFile) @@ -813,6 +814,8 @@ Examples: err = onCollectProfiles(ccf.ConfigFile, ccf.Profiles, ccf.ProfileSeconds) case readyzCmd.FullCommand(): err = onReadyz(ctx, ccf.ConfigFile) + case metricsCmd.FullCommand(): + err = onMetrics(ctx, ccf.ConfigFile) case moduleSourceCmd.FullCommand(): if runtime.GOOS != "linux" { break