mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-18 10:39:05 +02:00
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
188 lines
6.1 KiB
Go
188 lines
6.1 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 pdh
|
|
|
|
import (
|
|
"log/slog"
|
|
"math"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
type rowSetValues struct {
|
|
Name string
|
|
A float64 `perfdata:"A"`
|
|
B float64 `perfdata:"B"`
|
|
BSecond float64 `perfdata:"B,secondvalue"`
|
|
Optional float64 `perfdata:"Optional"`
|
|
}
|
|
|
|
// newRowSetTestCollector returns a collector with the counters A and B, which
|
|
// have instances, and Optional, which was skipped at Build and has none.
|
|
func newRowSetTestCollector(partialRows bool) *Collector[rowSetValues] {
|
|
return &Collector[rowSetValues]{
|
|
partialRows: partialRows,
|
|
logger: slog.New(slog.DiscardHandler),
|
|
rows: rowAccessor[rowSetValues]{
|
|
newRow: func(instance string) rowSetValues {
|
|
return rowSetValues{Name: instance}
|
|
},
|
|
setValue: func(row *rowSetValues, field int, value float64) {
|
|
reflect.ValueOf(row).Elem().Field(field).SetFloat(value)
|
|
},
|
|
},
|
|
counters: []Counter{
|
|
{Name: "A", Instances: map[string]pdhCounterHandle{"*": 1}, FieldIndexValue: 1, FieldIndexSecondValue: -1},
|
|
{Name: "B", Instances: map[string]pdhCounterHandle{"*": 2}, FieldIndexValue: 2, FieldIndexSecondValue: 3},
|
|
{Name: "Optional", Instances: map[string]pdhCounterHandle{}, FieldIndexValue: 4, FieldIndexSecondValue: -1},
|
|
},
|
|
}
|
|
}
|
|
|
|
// fillRows adds the instance "complete" with valid values for A and B, and the
|
|
// instance "partial" with a valid value for A only, as if its item for B had an
|
|
// invalid status.
|
|
func fillRows(t *testing.T, c *Collector[rowSetValues]) []rowSetValues {
|
|
t.Helper()
|
|
|
|
var dst []rowSetValues
|
|
|
|
rows := rowSet[rowSetValues]{
|
|
c: c,
|
|
dst: &dst,
|
|
index: map[string]int{},
|
|
nameCache: map[string]string{},
|
|
}
|
|
|
|
for _, name := range []string{"complete", "partial"} {
|
|
row, ok := rows.row(&c.counters[0], windows.StringToUTF16Ptr(name), CstatusValidData)
|
|
require.True(t, ok)
|
|
require.True(t, c.setRawValue(&dst[row], &c.counters[0], RawCounter{FirstValue: 1}))
|
|
rows.setValid(row, 0)
|
|
}
|
|
|
|
_, ok := rows.row(&c.counters[1], windows.StringToUTF16Ptr("partial"), CstatusInvalidData)
|
|
require.False(t, ok)
|
|
|
|
row, ok := rows.row(&c.counters[1], windows.StringToUTF16Ptr("complete"), CstatusNewData)
|
|
require.True(t, ok)
|
|
require.True(t, c.setRawValue(&dst[row], &c.counters[1], RawCounter{FirstValue: 2, SecondValue: 3}))
|
|
rows.setValid(row, 1)
|
|
|
|
rows.finish()
|
|
|
|
return dst
|
|
}
|
|
|
|
func TestRowSetOmitsIncompleteInstances(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rows := fillRows(t, newRowSetTestCollector(false))
|
|
|
|
// "partial" has no valid value for B. Without partial rows, it must be left
|
|
// out instead of reporting B as zero. Optional has no instances, so it is
|
|
// not required.
|
|
require.Equal(t, []rowSetValues{{Name: "complete", A: 1, B: 2, BSecond: 3}}, rows)
|
|
}
|
|
|
|
func TestRowSetPartialRows(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rows := fillRows(t, newRowSetTestCollector(true))
|
|
require.Len(t, rows, 2)
|
|
|
|
require.Equal(t, rowSetValues{Name: "complete", A: 1, B: 2, BSecond: 3}, rows[0])
|
|
|
|
require.Equal(t, "partial", rows[1].Name)
|
|
require.InDelta(t, 1, rows[1].A, 0)
|
|
require.True(t, math.IsNaN(rows[1].B), "B: %v", rows[1].B)
|
|
require.True(t, math.IsNaN(rows[1].BSecond), "BSecond: %v", rows[1].BSecond)
|
|
require.Zero(t, rows[1].Optional)
|
|
}
|
|
|
|
func TestSetRawValueElapsedTime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newRowSetTestCollector(false)
|
|
|
|
var values rowSetValues
|
|
|
|
counter := &Counter{Type: PERF_ELAPSED_TIME, Frequency: 10_000_000, FieldIndexValue: 1, FieldIndexSecondValue: -1}
|
|
|
|
// 2.5 s at 10 MHz. Integer division truncated this to 2.
|
|
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 100_000_000, SecondValue: 125_000_000}))
|
|
require.InDelta(t, 2.5, values.A, 0)
|
|
|
|
// A zero frequency panicked with an integer divide by zero.
|
|
counter.Frequency = 0
|
|
require.False(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 1, SecondValue: 2}))
|
|
|
|
// A counter with only a second value field must not set field -1.
|
|
counter = &Counter{Type: PERF_ELAPSED_TIME, Frequency: 1, FieldIndexValue: -1, FieldIndexSecondValue: 3}
|
|
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 1, SecondValue: 2}))
|
|
}
|
|
|
|
func TestMetricTypeForCounterType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeRaw, PERF_COUNTER_RAWCOUNT))
|
|
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_COUNTER_BULK_COUNT))
|
|
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeRaw, 0xFFFFFFFF))
|
|
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeFormatted, PERF_COUNTER_BULK_COUNT))
|
|
}
|
|
|
|
type processThreads struct {
|
|
Name string
|
|
ThreadCount float64 `perfdata:"Thread Count"`
|
|
}
|
|
|
|
// TestCollectRecoversPanic checks that a panic in the worker goroutine is
|
|
// returned as an error instead of terminating the process.
|
|
func TestCollectRecoversPanic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c, err := NewCollector[processThreads](slog.New(slog.DiscardHandler), CounterTypeRaw, "Process", InstancesAll)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(c.Close)
|
|
|
|
var dst []processThreads
|
|
|
|
require.NoError(t, c.Collect(&dst))
|
|
|
|
// Point the counter at a field that does not exist, so that reflect panics.
|
|
c.mu.Lock()
|
|
c.counters[0].FieldIndexValue = 42
|
|
c.mu.Unlock()
|
|
|
|
err = c.Collect(&dst)
|
|
require.ErrorContains(t, err, "panic while collecting performance counters of Process")
|
|
|
|
// The worker survives and serves the next request.
|
|
c.mu.Lock()
|
|
c.counters[0].FieldIndexValue = 1
|
|
c.mu.Unlock()
|
|
|
|
require.NoError(t, c.Collect(&dst))
|
|
require.NotEmpty(t, dst)
|
|
}
|