Files
windows_exporter/internal/httphandler/httphandler.go
T
2026-10-25 02:00:08 +02:00

300 lines
9.8 KiB
Go

// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package httphandler
import (
"context"
"errors"
"fmt"
"log/slog"
"math"
"net/http"
"regexp"
"strconv"
"time"
"github.com/prometheus-community/windows_exporter/pkg/collector"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/collectors/version"
"github.com/prometheus/client_golang/prometheus/promhttp"
dto "github.com/prometheus/client_model/go"
)
// Interface guard.
var _ http.Handler = (*MetricsHTTPHandler)(nil)
const (
// defaultScrapeTimeout applies if the request has no valid X-Prometheus-Scrape-Timeout-Seconds header.
defaultScrapeTimeout = 10 * time.Second
// minScrapeTimeout is the smallest time given to the collectors.
minScrapeTimeout = 10 * time.Millisecond
// MaxScrapeTimeout is the largest time given to the collectors, including the time a scrape
// waits for an earlier one. The HTTP server's write timeout must exceed it, so that a scrape
// that uses its full budget can still write its response.
MaxScrapeTimeout = 5 * time.Minute
)
type MetricsHTTPHandler struct {
metricCollectors *collector.Collection
// exporterMetricsRegistry is a separate registry for the metrics about
// the exporter itself.
exporterMetricsRegistry *prometheus.Registry
// registries holds idle registries for scrapes of all collectors. Since
// prometheus/client_golang#2170 (not in v1.25.0), a registry sizes the buffers of a
// Gather by the number of metrics of its previous Gather, so reusing it saves growing
// them on every scrape. Filtered scrapes use a new registry each.
registries chan *scrapeRegistry
logger *slog.Logger
timeoutMargin time.Duration
}
// maxIdleRegistries is the number of idle registries kept for reuse. More are only needed
// while scrapes overlap.
const maxIdleRegistries = 4
// scrapeRegistry is a registry that serves one scrape at a time.
type scrapeRegistry struct {
reg *prometheus.Registry
collector scrapeCollector
}
// scrapeCollector collects the metrics of the scrape that currently uses its registry.
type scrapeCollector struct {
handler *collector.Handler
}
func (c *scrapeCollector) Describe(_ chan<- *prometheus.Desc) {}
func (c *scrapeCollector) Collect(ch chan<- prometheus.Metric) {
c.handler.Collect(ch)
}
type Options struct {
DisableExporterMetrics bool
TimeoutMargin float64
}
func New(logger *slog.Logger, metricCollectors *collector.Collection, options *Options) *MetricsHTTPHandler {
if options == nil {
options = &Options{
DisableExporterMetrics: false,
TimeoutMargin: 0.5,
}
}
handler := &MetricsHTTPHandler{
metricCollectors: metricCollectors,
registries: make(chan *scrapeRegistry, maxIdleRegistries),
logger: logger,
timeoutMargin: toDuration(options.TimeoutMargin),
}
if !options.DisableExporterMetrics {
handler.exporterMetricsRegistry = prometheus.NewRegistry()
handler.exporterMetricsRegistry.MustRegister(
collectors.NewBuildInfoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
collectors.NewGoCollector(
// Goroutine states, goroutines created, runtime threads and scheduler latency
// help to spot goroutine leaks and CPU starvation of the exporter.
collectors.WithGoCollectorRuntimeMetrics(collectors.GoRuntimeMetricsRule{
Matcher: regexp.MustCompile(`^/sched/(goroutines(-created)?|goroutines/[a-z-]+|threads/total|latencies):`),
}),
),
)
}
return handler
}
func (c *MetricsHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger := c.logger.With(
slog.String("remote", r.RemoteAddr),
)
// The scrape's deadline starts now, so time spent waiting for an earlier scrape counts against it.
scrapeTimeout := c.getScrapeTimeout(logger, r)
requestedCollectors := r.URL.Query()["collect[]"]
collectionHandler, err := c.metricCollectors.NewHandlerWithContext(r.Context(), scrapeTimeout, logger, requestedCollectors)
if err != nil {
err = fmt.Errorf("couldn't create collector handler: %w", err)
} else if len(requestedCollectors) == 0 {
sr := c.getRegistry()
defer c.putRegistry(sr)
sr.collector.handler = collectionHandler
c.newHTTPHandler(logger, sr.reg).ServeHTTP(w, r)
return
}
var handler http.Handler
if err == nil {
handler, err = c.handlerFactory(logger, collectionHandler)
}
if err != nil {
logger.WarnContext(r.Context(), "Couldn't create filtered metrics handler",
slog.Any("err", err),
)
w.WriteHeader(http.StatusBadRequest)
_, _ = fmt.Fprintf(w, "Couldn't create filtered metrics handler: %s", err)
return
}
handler.ServeHTTP(w, r)
}
// getScrapeTimeout returns the time the collectors get for this request.
// It is the client's X-Prometheus-Scrape-Timeout-Seconds minus the timeout margin,
// but the margin takes at most half of the client's timeout.
// The result is between minScrapeTimeout and MaxScrapeTimeout.
func (c *MetricsHTTPHandler) getScrapeTimeout(logger *slog.Logger, r *http.Request) time.Duration {
timeout := defaultScrapeTimeout
if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" {
seconds, err := strconv.ParseFloat(v, 64)
switch {
case math.IsInf(seconds, 1):
// Includes values too large for a float64, which ParseFloat reports as an error.
timeout = MaxScrapeTimeout
case err != nil, math.IsNaN(seconds), seconds <= 0:
logger.LogAttrs(r.Context(), slog.LevelWarn, "invalid X-Prometheus-Scrape-Timeout-Seconds header, using the default timeout",
slog.String("value", v),
slog.Duration("timeout", defaultScrapeTimeout),
)
default:
timeout = toDuration(seconds)
}
}
timeout = max(timeout-c.timeoutMargin, timeout/2)
return min(max(timeout, minScrapeTimeout), MaxScrapeTimeout)
}
// toDuration converts seconds to a duration between 0 and MaxScrapeTimeout.
// NaN and negative values become 0.
func toDuration(seconds float64) time.Duration {
if math.IsNaN(seconds) || seconds <= 0 {
return 0
}
if seconds >= MaxScrapeTimeout.Seconds() {
return MaxScrapeTimeout
}
return time.Duration(seconds * float64(time.Second))
}
// getRegistry returns an idle registry for a scrape of all collectors, or a new one.
func (c *MetricsHTTPHandler) getRegistry() *scrapeRegistry {
select {
case sr := <-c.registries:
return sr
default:
}
sr := &scrapeRegistry{reg: prometheus.NewRegistry()}
sr.reg.MustRegister(version.NewCollector("windows_exporter"), &sr.collector)
return sr
}
// putRegistry keeps a registry whose scrape is done for reuse.
func (c *MetricsHTTPHandler) putRegistry(sr *scrapeRegistry) {
sr.collector.handler = nil
select {
case c.registries <- sr:
default:
}
}
// handlerFactory returns the handler of a filtered scrape, which uses a new registry.
func (c *MetricsHTTPHandler) handlerFactory(logger *slog.Logger, collectionHandler *collector.Handler) (http.Handler, error) {
reg := prometheus.NewRegistry()
reg.MustRegister(version.NewCollector("windows_exporter"))
if err := reg.Register(collectionHandler); err != nil {
return nil, fmt.Errorf("couldn't register Prometheus collector: %w", err)
}
return c.newHTTPHandler(logger, reg), nil
}
// newHTTPHandler returns the handler that serves the metrics of reg and of the exporter itself.
func (c *MetricsHTTPHandler) newHTTPHandler(logger *slog.Logger, reg *prometheus.Registry) http.Handler {
// Scrapes are serialized by the collection, so the handler doesn't limit requests in flight.
opts := promhttp.HandlerOpts{
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
ErrorHandling: promhttp.ContinueOnError,
EnableOpenMetrics: true,
ProcessStartTime: c.metricCollectors.GetStartTime(),
}
if c.exporterMetricsRegistry == nil {
return canceledScrapeHandler(logger, reg, opts)
}
// Note that we have to use h.exporterMetricsRegistry here to
// use the same promhttp metrics for all expositions.
opts.Registry = c.exporterMetricsRegistry
return promhttp.InstrumentMetricHandler(
c.exporterMetricsRegistry,
canceledScrapeHandler(logger, mergedGatherer{first: c.exporterMetricsRegistry, second: reg}, opts),
)
}
// canceledScrapeHandler serves the metrics of gatherer like [promhttp.HandlerFor], but fails the
// response with 503 Service Unavailable if the request was canceled while it gathered, for example
// because the exporter shuts down. The collectors return early then, so the response would report
// collectors as timed out or failed and miss their series. A failed scrape records up=0 instead.
// A scrape that ends at its deadline is still served, with the timed-out collectors marked.
func canceledScrapeHandler(logger *slog.Logger, gatherer prometheus.Gatherer, opts promhttp.HandlerOpts) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mfs, err := gatherer.Gather()
if ctx := r.Context(); errors.Is(ctx.Err(), context.Canceled) {
logger.DebugContext(ctx, "scrape canceled, metrics not served",
slog.Any("cause", context.Cause(ctx)),
)
http.Error(w, "Scrape canceled: "+context.Cause(ctx).Error(), http.StatusServiceUnavailable)
return
}
gathered := prometheus.GathererFunc(func() ([]*dto.MetricFamily, error) {
return mfs, err
})
promhttp.HandlerFor(gathered, opts).ServeHTTP(w, r)
})
}