Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support OpenMetrics #1772

Merged
merged 6 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .run/all.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<configuration default="false" name="all" type="GoApplicationRunConfiguration" factoryName="Go Application" folderName="run">
<module name="windows_exporter" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="--web.listen-address=127.0.0.1:9182 --log.level=debug --collectors.enabled=ad,adcs,adfs,cache,container,cpu,cpu_info,cs,dfsr,dhcp,diskdrive,dns,exchange,filetime,fsrmquota,hyperv,iis,license,logical_disk,logon,memory,mscluster,msmq,mssql,net,netframework,nps,os,pagefile,perfdata,physical_disk,printer,process,remote_fx,scheduled_task,service,smb,smbclient,smtp,system,tcp,terminal_services,textfile,thermalzone,time,udp,update,vmware" />
<parameters value="--web.listen-address=127.0.0.1:9182 --log.level=info --collectors.enabled=ad,adcs,adfs,cache,container,cpu,cpu_info,cs,dfsr,dhcp,diskdrive,dns,exchange,filetime,fsrmquota,hyperv,iis,license,logical_disk,logon,memory,mscluster,msmq,mssql,net,netframework,nps,os,pagefile,perfdata,physical_disk,printer,process,remote_fx,scheduled_task,service,smb,smbclient,smtp,system,tcp,terminal_services,thermalzone,time,udp,update,vmware --debug.enabled" />
<sudo value="true" />
<kind value="PACKAGE" />
<package value="github.com/prometheus-community/windows_exporter/cmd/windows_exporter" />
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ Windows Server 2012 and 2012R2 are supported as best-effort only, but not guaran

The prometheus metrics will be exposed on [localhost:9182](http://localhost:9182)

### HTTP Endpoints

windows_exporter provides the following HTTP endpoints:

* `/metrics`: Exposes metrics in the [Prometheus text format](https://prometheus.io/docs/instrumenting/exposition_formats/).
* `/health`: Returns 200 OK when the exporter is running.
* `/debug/pprof/`: Exposes the [pprof](https://golang.org/pkg/net/http/pprof/) endpoints. Only, if `--debug.enabled` is set.

## Examples

### Enable only service collector and specify a custom query
Expand Down
5 changes: 0 additions & 5 deletions cmd/windows_exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,6 @@ func run() int {
"web.disable-exporter-metrics",
"Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).",
).Bool()
maxRequests = app.Flag(
"telemetry.max-requests",
"Maximum number of concurrent requests. 0 to disable.",
).Default("5").Int()
enabledCollectors = app.Flag(
"collectors.enabled",
"Comma-separated list of collectors to use. Use '[defaults]' as a placeholder for all the collectors enabled by default.").
Expand Down Expand Up @@ -220,7 +216,6 @@ func run() int {
mux.Handle("GET "+*metricsPath, httphandler.New(logger, collectors, &httphandler.Options{
DisableExporterMetrics: *disableExporterMetrics,
TimeoutMargin: *timeoutMargin,
MaxRequests: *maxRequests,
}))

if *debugEnabled {
Expand Down
36 changes: 13 additions & 23 deletions internal/httphandler/httphandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,23 @@ type MetricsHTTPHandler struct {
type Options struct {
DisableExporterMetrics bool
TimeoutMargin float64
MaxRequests int
}

func New(logger *slog.Logger, metricCollectors *collector.MetricCollectors, options *Options) *MetricsHTTPHandler {
if options == nil {
options = &Options{
DisableExporterMetrics: false,
TimeoutMargin: 0.5,
MaxRequests: 5,
}
}

handler := &MetricsHTTPHandler{
metricCollectors: metricCollectors,
logger: logger,
options: *options,
concurrencyCh: make(chan struct{}, options.MaxRequests),

// We are expose metrics directly from the memory region of the Win32 API. We should not allow more than one request at a time.
concurrencyCh: make(chan struct{}, 1),
}

if !options.DisableExporterMetrics {
Expand Down Expand Up @@ -131,21 +131,11 @@ func (c *MetricsHTTPHandler) handlerFactory(logger *slog.Logger, scrapeTimeout t
if len(requestedCollectors) == 0 {
metricCollectors = c.metricCollectors
} else {
filteredCollectors := make(collector.Map)

for _, name := range requestedCollectors {
metricCollector, ok := c.metricCollectors.Collectors[name]
if !ok {
return nil, fmt.Errorf("couldn't find collector %s", name)
}

filteredCollectors[name] = metricCollector
}
var err error

metricCollectors = &collector.MetricCollectors{
Collectors: filteredCollectors,
MISession: c.metricCollectors.MISession,
PerfCounterQuery: c.metricCollectors.PerfCounterQuery,
metricCollectors, err = c.metricCollectors.CloneWithCollectors(requestedCollectors)
if err != nil {
return nil, fmt.Errorf("couldn't clone metric collectors: %w", err)
}
}

Expand All @@ -162,8 +152,10 @@ func (c *MetricsHTTPHandler) handlerFactory(logger *slog.Logger, scrapeTimeout t
promhttp.HandlerOpts{
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
ErrorHandling: promhttp.ContinueOnError,
MaxRequestsInFlight: c.options.MaxRequests,
MaxRequestsInFlight: 1,
Registry: c.exporterMetricsRegistry,
EnableOpenMetrics: true,
ProcessStartTime: c.metricCollectors.GetStartTime(),
},
)

Expand All @@ -178,7 +170,9 @@ func (c *MetricsHTTPHandler) handlerFactory(logger *slog.Logger, scrapeTimeout t
promhttp.HandlerOpts{
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
ErrorHandling: promhttp.ContinueOnError,
MaxRequestsInFlight: c.options.MaxRequests,
MaxRequestsInFlight: 1,
EnableOpenMetrics: true,
ProcessStartTime: c.metricCollectors.GetStartTime(),
},
)
}
Expand All @@ -187,10 +181,6 @@ func (c *MetricsHTTPHandler) handlerFactory(logger *slog.Logger, scrapeTimeout t
}

func (c *MetricsHTTPHandler) withConcurrencyLimit(next http.HandlerFunc) http.HandlerFunc {
if c.options.MaxRequests <= 0 {
return next
}

return func(w http.ResponseWriter, r *http.Request) {
select {
case c.concurrencyCh <- struct{}{}:
Expand Down
32 changes: 19 additions & 13 deletions internal/perfdata/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ func (c *Collector) Collect() (CounterValues, error) {
}

func (c *Collector) collectRoutine() {
var (
itemCount uint32
bytesNeeded uint32
)

buf := make([]byte, 1)

for range c.collectCh {
if ret := PdhCollectQueryData(c.handle); ret != ErrorSuccess {
c.counterValuesCh <- nil
Expand All @@ -207,25 +214,24 @@ func (c *Collector) collectRoutine() {
for _, counter := range c.counters {
for _, instance := range counter.Instances {
// Get the info with the current buffer size
var itemCount uint32

// Get the info with the current buffer size
bufLen := uint32(0)
bytesNeeded = uint32(cap(buf))

ret := PdhGetRawCounterArray(instance, &bufLen, &itemCount, nil)
if ret != PdhMoreData {
return nil, fmt.Errorf("PdhGetRawCounterArray: %w", NewPdhError(ret))
}
for {
ret := PdhGetRawCounterArray(instance, &bytesNeeded, &itemCount, &buf[0])

buf := make([]byte, bufLen)
if ret == ErrorSuccess {
break
}

ret = PdhGetRawCounterArray(instance, &bufLen, &itemCount, &buf[0])
if ret != ErrorSuccess {
if err := NewPdhError(ret); !isKnownCounterDataError(err) {
if err := NewPdhError(ret); ret != PdhMoreData && !isKnownCounterDataError(err) {
return nil, fmt.Errorf("PdhGetRawCounterArray: %w", err)
}

continue
if bytesNeeded <= uint32(cap(buf)) {
return nil, fmt.Errorf("PdhGetRawCounterArray reports buffer too small (%d), but buffer is large enough (%d): %w", uint32(cap(buf)), bytesNeeded, NewPdhError(ret))
}

buf = make([]byte, bytesNeeded)
}

items := unsafe.Slice((*PdhRawCounterItem)(unsafe.Pointer(&buf[0])), itemCount)
Expand Down
2 changes: 2 additions & 0 deletions internal/perfdata/collector_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ func BenchmarkTestCollector(b *testing.B) {
}

performanceData.Close()

b.ReportAllocs()
}
53 changes: 38 additions & 15 deletions pkg/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import (
"errors"
"fmt"
"log/slog"
"maps"
"slices"
"sync"
stdtime "time"

"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/collector/ad"
Expand Down Expand Up @@ -145,21 +147,21 @@ func NewWithConfig(config Config) *MetricCollectors {
// New To be called by the external libraries for collector initialization.
func New(collectors Map) *MetricCollectors {
return &MetricCollectors{
Collectors: collectors,
collectors: collectors,
}
}

// Enable removes all collectors that not enabledCollectors.
func (c *MetricCollectors) Enable(enabledCollectors []string) error {
for _, name := range enabledCollectors {
if _, ok := c.Collectors[name]; !ok {
if _, ok := c.collectors[name]; !ok {
return fmt.Errorf("unknown collector %s", name)
}
}

for name := range c.Collectors {
for name := range c.collectors {
if !slices.Contains(enabledCollectors, name) {
delete(c.Collectors, name)
delete(c.collectors, name)
}
}

Expand All @@ -168,22 +170,24 @@ func (c *MetricCollectors) Enable(enabledCollectors []string) error {

// Build To be called by the exporter for collector initialization.
func (c *MetricCollectors) Build(logger *slog.Logger) error {
c.startTime = stdtime.Now()

err := c.initMI()
if err != nil {
return fmt.Errorf("error from initialize MI: %w", err)
}

wg := sync.WaitGroup{}
wg.Add(len(c.Collectors))
wg.Add(len(c.collectors))

errCh := make(chan error, len(c.Collectors))
errs := make([]error, 0, len(c.Collectors))
errCh := make(chan error, len(c.collectors))
errs := make([]error, 0, len(c.collectors))

for _, collector := range c.Collectors {
for _, collector := range c.collectors {
go func() {
defer wg.Done()

if err = collector.Build(logger, c.MISession); err != nil {
if err = collector.Build(logger, c.miSession); err != nil {
errCh <- fmt.Errorf("error build collector %s: %w", collector.GetName(), err)
}
}()
Expand All @@ -202,20 +206,20 @@ func (c *MetricCollectors) Build(logger *slog.Logger) error {

// Close To be called by the exporter for collector cleanup.
func (c *MetricCollectors) Close() error {
errs := make([]error, 0, len(c.Collectors))
errs := make([]error, 0, len(c.collectors))

for _, collector := range c.Collectors {
for _, collector := range c.collectors {
if err := collector.Close(); err != nil {
errs = append(errs, fmt.Errorf("error from close collector %s: %w", collector.GetName(), err))
}
}

app, err := c.MISession.GetApplication()
app, err := c.miSession.GetApplication()
if err != nil && !errors.Is(err, mi.ErrNotInitialized) {
errs = append(errs, fmt.Errorf("error from get MI application: %w", err))
}

if err := c.MISession.Close(); err != nil && !errors.Is(err, mi.ErrNotInitialized) {
if err := c.miSession.Close(); err != nil && !errors.Is(err, mi.ErrNotInitialized) {
errs = append(errs, fmt.Errorf("error from close MI session: %w", err))
}

Expand All @@ -226,7 +230,7 @@ func (c *MetricCollectors) Close() error {
return errors.Join(errs...)
}

// Close To be called by the exporter for collector cleanup.
// initMI To be called by the exporter for collector initialization.
func (c *MetricCollectors) initMI() error {
app, err := mi.Application_Initialize()
if err != nil {
Expand All @@ -242,10 +246,29 @@ func (c *MetricCollectors) initMI() error {
return fmt.Errorf("error from set locale: %w", err)
}

c.MISession, err = app.NewSession(destinationOptions)
c.miSession, err = app.NewSession(destinationOptions)
if err != nil {
return fmt.Errorf("error from create NewSession: %w", err)
}

return nil
}

// CloneWithCollectors To be called by the exporter for collector initialization.
func (c *MetricCollectors) CloneWithCollectors(collectors []string) (*MetricCollectors, error) {
metricCollectors := &MetricCollectors{
collectors: maps.Clone(c.collectors),
miSession: c.miSession,
startTime: c.startTime,
}

if err := metricCollectors.Enable(collectors); err != nil {
return nil, err
}

return metricCollectors, nil
}

func (c *MetricCollectors) GetStartTime() stdtime.Time {
return c.startTime
}
8 changes: 4 additions & 4 deletions pkg/collector/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/perfdata"
types "github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
)

Expand Down Expand Up @@ -103,15 +103,15 @@ func (p *Prometheus) Collect(ch chan<- prometheus.Metric) {

// WaitGroup to wait for all collectors to finish
wg := sync.WaitGroup{}
wg.Add(len(p.metricCollectors.Collectors))
wg.Add(len(p.metricCollectors.collectors))

// Using a channel to collect the status of each collector
// A channel is safe to use concurrently while a map is not
collectorStatusCh := make(chan collectorStatus, len(p.metricCollectors.Collectors))
collectorStatusCh := make(chan collectorStatus, len(p.metricCollectors.collectors))

// Execute all collectors concurrently
// timeout handling is done in the execute function
for name, metricsCollector := range p.metricCollectors.Collectors {
for name, metricsCollector := range p.metricCollectors.collectors {
go func(name string, metricsCollector Collector) {
defer wg.Done()

Expand Down
7 changes: 4 additions & 3 deletions pkg/collector/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package collector

import (
"log/slog"
"time"

"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
Expand All @@ -26,9 +27,9 @@ import (
const DefaultCollectors = "cpu,cs,memory,logical_disk,physical_disk,net,os,service,system"

type MetricCollectors struct {
Collectors Map
MISession *mi.Session
PerfCounterQuery string
collectors Map
miSession *mi.Session
startTime time.Time
}

type (
Expand Down