mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
cmd: add --scrape.stdout one-shot mode (#2705)
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
This commit is contained in:
@@ -23,10 +23,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -93,6 +95,12 @@ func run(ctx context.Context, args []string) int {
|
||||
app := kingpin.New("windows_exporter", "A metrics collector for Windows.")
|
||||
|
||||
var (
|
||||
scrapeStdout = app.Flag(
|
||||
"scrape.stdout",
|
||||
"Collect metrics once and write them to stdout in Prometheus text format, then exit. "+
|
||||
"Logs are sent to stderr unless --log.file is set explicitly. "+
|
||||
"PDH rate counters need two samples; the first scrape after startup covers only a short interval.",
|
||||
).Default("false").Bool()
|
||||
configFile = app.Flag(
|
||||
"config.file",
|
||||
"YAML configuration file to use. Values set in this file will be overridden by CLI flags.",
|
||||
@@ -160,6 +168,13 @@ func run(ctx context.Context, args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// When scraping to stdout, redirect logs to stderr so they don't
|
||||
// mix with the metrics output. Only override if the user hasn't
|
||||
// explicitly set --log.file to something other than the default.
|
||||
if *scrapeStdout && logFile.String() == "stdout" {
|
||||
_ = logFile.Set("stderr")
|
||||
}
|
||||
|
||||
logger, err := log.New(logConfig)
|
||||
if err != nil {
|
||||
logStartupError(ctx, "failed to create logger", err)
|
||||
@@ -233,12 +248,18 @@ func run(ctx context.Context, args []string) int {
|
||||
|
||||
logger.InfoContext(ctx, "Enabled collectors: "+strings.Join(effectiveCollectors(enabledCollectorList, disabledCollectorList), ", "))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /health", httphandler.NewHealthHandler())
|
||||
mux.Handle("GET "+*metricsPath, httphandler.New(logger, collectors, &httphandler.Options{
|
||||
handlerOpts := &httphandler.Options{
|
||||
DisableExporterMetrics: *disableExporterMetrics,
|
||||
TimeoutMargin: *timeoutMargin,
|
||||
}))
|
||||
}
|
||||
|
||||
if *scrapeStdout {
|
||||
return scrapeToStdout(ctx, os.Stdout, logger, collectors, handlerOpts)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /health", httphandler.NewHealthHandler())
|
||||
mux.Handle("GET "+*metricsPath, httphandler.New(logger, collectors, handlerOpts))
|
||||
|
||||
if *debugEnabled {
|
||||
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
|
||||
@@ -405,6 +426,47 @@ func expandEnabledCollectors(enabled string) []string {
|
||||
return slices.Compact(strings.Split(expanded, ","))
|
||||
}
|
||||
|
||||
// scrapeToStdout performs a single scrape through the HTTP handler and writes
|
||||
// the response body to stdout. It reuses the same handler that serves /metrics
|
||||
// so the output is identical, including exporter self-instrumentation and
|
||||
// timeout margin.
|
||||
func scrapeToStdout(ctx context.Context, w io.Writer, logger *slog.Logger, collectors *collector.Collection, opts *httphandler.Options) int {
|
||||
handler := httphandler.New(logger, collectors, opts)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/metrics", nil)
|
||||
if err != nil {
|
||||
logger.LogAttrs(ctx, slog.LevelError, "couldn't create request",
|
||||
slog.Any("err", err),
|
||||
)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
result := rec.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
if result.StatusCode != http.StatusOK {
|
||||
logger.LogAttrs(ctx, slog.LevelError, "scrape failed",
|
||||
slog.Int("status", result.StatusCode),
|
||||
)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, result.Body); err != nil {
|
||||
logger.LogAttrs(ctx, slog.LevelError, "failed to write metrics to stdout",
|
||||
slog.Any("err", err),
|
||||
)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// setProcessMemoryLimit applies the CLI contract: zero disables the soft limit.
|
||||
func setProcessMemoryLimit(limit int64) error {
|
||||
if limit < 0 {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -31,6 +32,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/prometheus-community/windows_exporter/internal/httphandler"
|
||||
"github.com/prometheus-community/windows_exporter/pkg/collector"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
@@ -180,6 +184,34 @@ func waitUntilListening(tb testing.TB, network, address string) error {
|
||||
return fmt.Errorf("listener not listening: %w", err)
|
||||
}
|
||||
|
||||
func TestScrapeStdout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
|
||||
collectors := collector.NewWithFlags(kingpin.New("test", "test"))
|
||||
require.NoError(t, collectors.Enable([]string{"cpu"}))
|
||||
require.NoError(t, collectors.Build(t.Context(), logger))
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, collectors.Close())
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
exitCode := scrapeToStdout(t.Context(), &buf, logger, collectors, &httphandler.Options{})
|
||||
require.Equal(t, 0, exitCode)
|
||||
|
||||
output := buf.String()
|
||||
|
||||
// Must contain collector metrics.
|
||||
require.Contains(t, output, "windows_cpu_time_total")
|
||||
// Must contain exporter build info (proves the HTTP handler path is used).
|
||||
require.Contains(t, output, "windows_exporter_build_info")
|
||||
// Must contain collector success metadata.
|
||||
require.Contains(t, output, `windows_exporter_collector_success{collector="cpu"} 1`)
|
||||
}
|
||||
|
||||
func TestSetPriorityWindows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user