mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-22 04:29:03 +02:00
performancecounter: fix explicit instance lists and startup panics on invalid config (#2511)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0e45202032
commit
7b20daa9ac
@@ -186,6 +186,9 @@ This key is optional.
|
|||||||
|
|
||||||
Labels is a map of key-value pairs that will be added as labels to the metric.
|
Labels is a map of key-value pairs that will be added as labels to the metric.
|
||||||
|
|
||||||
|
Counters of the same object that share a metric name must differ in their labels, like `state` in the schema example above.
|
||||||
|
Otherwise, they would produce identical series and the collector fails to start.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -151,51 +152,50 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
names = append(names, object.Name)
|
names = append(names, object.Name)
|
||||||
counters := make([]string, 0, len(object.Counters))
|
counters := make([]Counter, 0, len(object.Counters))
|
||||||
fields := make([]reflect.StructField, 0, len(object.Counters)+2)
|
fields := make([]reflect.StructField, 0, len(object.Counters)+2)
|
||||||
|
object.fieldIndex = make(map[string]int, len(object.Counters))
|
||||||
|
|
||||||
for j, counter := range object.Counters {
|
for j, counter := range object.Counters {
|
||||||
if counter.Metric == "" {
|
if counter.Metric == "" {
|
||||||
c.config.Objects[i].Counters[j].Metric = sanitizeMetricName(
|
counter.Metric = sanitizeMetricName(
|
||||||
fmt.Sprintf("%s_%s_%s_%s", types.Namespace, Name, object.Object, counter.Name),
|
fmt.Sprintf("%s_%s_%s_%s", types.Namespace, Name, object.Object, counter.Name),
|
||||||
)
|
)
|
||||||
|
c.config.Objects[i].Counters[j].Metric = counter.Metric
|
||||||
}
|
}
|
||||||
|
|
||||||
if counter.Name == "" {
|
if counter.Name == "" {
|
||||||
errs = append(errs, errors.New("counter name is required"))
|
errs = append(errs, fmt.Errorf("object %s: counter name is required", object.Name))
|
||||||
c.config.Objects = slices.Delete(c.config.Objects, i, 1)
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(counters, counter.Name) {
|
if _, ok := object.fieldIndex[counter.Name]; ok {
|
||||||
errs = append(errs, fmt.Errorf("counter name %s is duplicated", counter.Name))
|
errs = append(errs, fmt.Errorf("object %s: counter name %s is duplicated", object.Name, counter.Name))
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
counters = append(counters, counter.Name)
|
if k := slices.IndexFunc(counters, func(other Counter) bool {
|
||||||
|
return other.Metric == counter.Metric && maps.Equal(other.Labels, counter.Labels)
|
||||||
field, err := func(name string) (_ reflect.StructField, err error) {
|
}); k != -1 {
|
||||||
defer func() {
|
errs = append(errs, fmt.Errorf("object %s: counters %s and %s produce identical series %s, set a different metric name or labels",
|
||||||
if r := recover(); r != nil {
|
object.Name, counters[k].Name, counter.Name, counter.Metric,
|
||||||
err = fmt.Errorf("failed to create field for %s: %v", name, r)
|
))
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return reflect.StructField{
|
|
||||||
Name: strings.ToUpper(sanitizeMetricName(name)),
|
|
||||||
Type: reflect.TypeFor[float64](),
|
|
||||||
Tag: reflect.StructTag(fmt.Sprintf(`perfdata:"%s"`, name)),
|
|
||||||
}, nil
|
|
||||||
}(counter.Name)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fields = append(fields, field)
|
counters = append(counters, counter)
|
||||||
|
|
||||||
|
// Counter names are free text and may produce invalid or colliding Go identifiers,
|
||||||
|
// so the struct fields get synthetic names and are looked up by index instead.
|
||||||
|
object.fieldIndex[counter.Name] = len(fields)
|
||||||
|
fields = append(fields, reflect.StructField{
|
||||||
|
Name: "F" + strconv.Itoa(len(fields)),
|
||||||
|
Type: reflect.TypeFor[float64](),
|
||||||
|
Tag: reflect.StructTag(`perfdata:` + strconv.Quote(counter.Name)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if object.Instances != nil {
|
if object.Instances != nil {
|
||||||
@@ -300,13 +300,14 @@ func (c *Collector) collectObject(ch chan<- prometheus.Metric, perfDataObject Ob
|
|||||||
for _, counter := range perfDataObject.Counters {
|
for _, counter := range perfDataObject.Counters {
|
||||||
val := reflect.ValueOf(sliceValue).Index(i)
|
val := reflect.ValueOf(sliceValue).Index(i)
|
||||||
|
|
||||||
field := val.FieldByName(strings.ToUpper(sanitizeMetricName(counter.Name)))
|
fieldIndex, ok := perfDataObject.fieldIndex[counter.Name]
|
||||||
if !field.IsValid() {
|
if !ok {
|
||||||
errs = append(errs, fmt.Errorf("%s not found in collected data", counter.Name))
|
errs = append(errs, fmt.Errorf("%s not found in collected data", counter.Name))
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
field := val.Field(fieldIndex)
|
||||||
if field.Kind() != reflect.Float64 {
|
if field.Kind() != reflect.Float64 {
|
||||||
errs = append(errs, fmt.Errorf("failed to cast %s to float64", counter.Name))
|
errs = append(errs, fmt.Errorf("failed to cast %s to float64", counter.Name))
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ import (
|
|||||||
"github.com/alecthomas/kingpin/v2"
|
"github.com/alecthomas/kingpin/v2"
|
||||||
"github.com/prometheus-community/windows_exporter/internal/collector/performancecounter"
|
"github.com/prometheus-community/windows_exporter/internal/collector/performancecounter"
|
||||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkCollector(b *testing.B) {
|
func BenchmarkCollector(b *testing.B) {
|
||||||
perfDataObjects := `[{"object":"Processor Information","instances":["*"],"instance_label":"core","counters":[{"name":"% Processor Time","metric":"windows_performancecounter_processor_information_processor_time","labels":{"state":"active"}},{"name":"% Idle Time","metric":"windows_performancecounter_processor_information_processor_time","labels":{"state":"idle"}}]},{"object":"Memory","counters":[{"name":"Cache Faults/sec","type":"counter"}]}]`
|
perfDataObjects := `[{"name":"processor_information","object":"Processor Information","instances":["*"],"instance_label":"core","counters":[{"name":"% Processor Time","metric":"windows_performancecounter_processor_information_processor_time","labels":{"state":"active"}},{"name":"% Idle Time","metric":"windows_performancecounter_processor_information_processor_time","labels":{"state":"idle"}}]},{"name":"memory","object":"Memory","counters":[{"name":"Cache Faults/sec","type":"counter"}]}]`
|
||||||
|
|
||||||
testutils.FuncBenchmarkCollector(b, performancecounter.Name, performancecounter.NewWithFlags, func(app *kingpin.Application) {
|
testutils.FuncBenchmarkCollector(b, performancecounter.Name, performancecounter.NewWithFlags, func(app *kingpin.Application) {
|
||||||
app.GetFlag("collector.perfdata.objects").StringVar(&perfDataObjects)
|
// The objects are only read by the application action, which runs on parse.
|
||||||
|
_, err := app.Parse([]string{"--collector.performancecounter.objects", perfDataObjects})
|
||||||
|
require.NoError(b, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,30 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
|
|||||||
counters: []performancecounter.Counter{{Name: "Available Bytes", Type: "gauge"}, {Name: "Available Bytes", Type: "gauge"}},
|
counters: []performancecounter.Counter{{Name: "Available Bytes", Type: "gauge"}, {Name: "Available Bytes", Type: "gauge"}},
|
||||||
expectedMetrics: nil,
|
expectedMetrics: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// PDH counter names are case-insensitive, but both names sanitize to the same Go identifier.
|
||||||
|
name: "counter names with the same sanitized name",
|
||||||
|
object: "Memory",
|
||||||
|
counterType: pdh.CounterTypeRaw,
|
||||||
|
instances: nil,
|
||||||
|
buildErr: "",
|
||||||
|
counters: []performancecounter.Counter{
|
||||||
|
{Name: "Available Bytes", Type: "gauge", Metric: "windows_performancecounter_memory_available_bytes"},
|
||||||
|
{Name: "available bytes", Type: "gauge", Metric: "windows_performancecounter_memory_available_bytes_lowercase"},
|
||||||
|
},
|
||||||
|
expectedMetrics: regexp.MustCompile(`^# HELP windows_performancecounter_collector_duration_seconds windows_exporter: Duration of an performancecounter child collection.
|
||||||
|
# TYPE windows_performancecounter_collector_duration_seconds gauge
|
||||||
|
windows_performancecounter_collector_duration_seconds\{collector="counter names with the same sanitized name"} [0-9.e+-]+
|
||||||
|
# HELP windows_performancecounter_collector_success windows_exporter: Whether a performancecounter child collector was successful.
|
||||||
|
# TYPE windows_performancecounter_collector_success gauge
|
||||||
|
windows_performancecounter_collector_success\{collector="counter names with the same sanitized name"} 1
|
||||||
|
# HELP windows_performancecounter_memory_available_bytes windows_exporter: custom Performance Counter metric
|
||||||
|
# TYPE windows_performancecounter_memory_available_bytes gauge
|
||||||
|
windows_performancecounter_memory_available_bytes [0-9.e+-]+
|
||||||
|
# HELP windows_performancecounter_memory_available_bytes_lowercase windows_exporter: custom Performance Counter metric
|
||||||
|
# TYPE windows_performancecounter_memory_available_bytes_lowercase gauge
|
||||||
|
windows_performancecounter_memory_available_bytes_lowercase [0-9.e+-]+`),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "counter with spaces and brackets",
|
name: "counter with spaces and brackets",
|
||||||
object: "invalid",
|
object: "invalid",
|
||||||
@@ -216,6 +240,101 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuildInvalidConfig verifies that invalid configurations are reported as
|
||||||
|
// errors. Build runs in a goroutine without recover during exporter startup,
|
||||||
|
// so a panic here would crash the exporter.
|
||||||
|
func TestBuildInvalidConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Build writes default metric names into the config, so every case needs its own objects.
|
||||||
|
availableBytes := func() performancecounter.Object {
|
||||||
|
return performancecounter.Object{
|
||||||
|
Name: "available_bytes",
|
||||||
|
Object: "Memory",
|
||||||
|
Counters: []performancecounter.Counter{{Name: "Available Bytes"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
committedBytes := func() performancecounter.Object {
|
||||||
|
return performancecounter.Object{
|
||||||
|
Name: "committed_bytes",
|
||||||
|
Object: "Memory",
|
||||||
|
Counters: []performancecounter.Counter{{Name: "Committed Bytes"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
objects []performancecounter.Object
|
||||||
|
buildErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty counter name in first object",
|
||||||
|
objects: []performancecounter.Object{
|
||||||
|
{Name: "memory", Object: "Memory", Counters: []performancecounter.Counter{{Name: ""}, {Name: "Available Bytes"}}},
|
||||||
|
committedBytes(),
|
||||||
|
},
|
||||||
|
buildErr: "object memory: counter name is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty counter name in third object",
|
||||||
|
objects: []performancecounter.Object{
|
||||||
|
availableBytes(),
|
||||||
|
committedBytes(),
|
||||||
|
{Name: "memory", Object: "Memory", Counters: []performancecounter.Counter{{Name: ""}}},
|
||||||
|
},
|
||||||
|
buildErr: "object memory: counter name is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counter name starting with a digit",
|
||||||
|
objects: []performancecounter.Object{
|
||||||
|
{Name: "memory", Object: "Memory", Counters: []performancecounter.Counter{{Name: "1 Available Bytes"}}},
|
||||||
|
},
|
||||||
|
buildErr: pdh.NewPdhError(pdh.CstatusNoCounter).Error(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counter names with the same default metric name",
|
||||||
|
objects: []performancecounter.Object{
|
||||||
|
{Name: "memory", Object: "Memory", Counters: []performancecounter.Counter{{Name: "Available Bytes"}, {Name: "available bytes"}}},
|
||||||
|
},
|
||||||
|
buildErr: "object memory: counters Available Bytes and available bytes produce identical series windows_performancecounter_memory_available_bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counters with the same metric name",
|
||||||
|
objects: []performancecounter.Object{
|
||||||
|
{Name: "memory", Object: "Memory", Counters: []performancecounter.Counter{
|
||||||
|
{Name: "Available Bytes", Metric: "windows_memory_bytes"},
|
||||||
|
{Name: "Committed Bytes", Metric: "windows_memory_bytes"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
buildErr: "object memory: counters Available Bytes and Committed Bytes produce identical series windows_memory_bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counters with the same metric name and labels",
|
||||||
|
objects: []performancecounter.Object{
|
||||||
|
{Name: "processor", Object: "Processor Information", Instances: pdh.InstancesAll, Counters: []performancecounter.Counter{
|
||||||
|
{Name: "% Processor Time", Metric: "windows_processor_time", Labels: map[string]string{"state": "active"}},
|
||||||
|
{Name: "% Idle Time", Metric: "windows_processor_time", Labels: map[string]string{"state": "idle"}},
|
||||||
|
{Name: "% User Time", Metric: "windows_processor_time", Labels: map[string]string{"state": "active"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
buildErr: "object processor: counters % Processor Time and % User Time produce identical series windows_processor_time",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
c := performancecounter.New(&performancecounter.Config{Objects: tc.objects})
|
||||||
|
|
||||||
|
t.Cleanup(func() { require.NoError(t, c.Close()) })
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
require.NotPanics(t, func() { err = c.Build(slog.New(slog.DiscardHandler), nil) })
|
||||||
|
require.ErrorContains(t, err, tc.buildErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCollectorClose(t *testing.T) {
|
func TestCollectorClose(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ type Object struct {
|
|||||||
|
|
||||||
collector *pdh.Collector
|
collector *pdh.Collector
|
||||||
perfDataObject any
|
perfDataObject any
|
||||||
|
// fieldIndex maps a counter name to its field index in the element type of perfDataObject.
|
||||||
|
fieldIndex map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Counter struct {
|
type Counter struct {
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ func BenchmarkCollector(b *testing.B) {
|
|||||||
keys := `[{"name":"windows_nt","key":"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","values":[{"name":"CurrentMajorVersionNumber"}]}]`
|
keys := `[{"name":"windows_nt","key":"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","values":[{"name":"CurrentMajorVersionNumber"}]}]`
|
||||||
|
|
||||||
testutils.FuncBenchmarkCollector(b, registry.Name, registry.NewWithFlags, func(app *kingpin.Application) {
|
testutils.FuncBenchmarkCollector(b, registry.Name, registry.NewWithFlags, func(app *kingpin.Application) {
|
||||||
app.GetFlag("collector.registry.keys").StringVar(&keys)
|
// The keys are only read by the application action, which runs on parse.
|
||||||
|
_, err := app.Parse([]string{"--collector.registry.keys", keys})
|
||||||
|
require.NoError(b, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-20
@@ -334,7 +334,7 @@ func (c *Collector) collectWorkerRaw() {
|
|||||||
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
|
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
|
||||||
|
|
||||||
indexMap := map[string]int{}
|
indexMap := map[string]int{}
|
||||||
stringMap := map[*uint16]string{}
|
nameCache := map[string]string{}
|
||||||
|
|
||||||
for _, counter := range c.counters {
|
for _, counter := range c.counters {
|
||||||
for _, instance := range counter.Instances {
|
for _, instance := range counter.Instances {
|
||||||
@@ -365,11 +365,6 @@ func (c *Collector) collectWorkerRaw() {
|
|||||||
|
|
||||||
items = unsafe.Slice((*RawCounterItem)(unsafe.Pointer(&buf[0])), itemCount)
|
items = unsafe.Slice((*RawCounterItem)(unsafe.Pointer(&buf[0])), itemCount)
|
||||||
|
|
||||||
var (
|
|
||||||
instanceName string
|
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if item.RawValue.CStatus != CstatusValidData && item.RawValue.CStatus != CstatusNewData {
|
if item.RawValue.CStatus != CstatusValidData && item.RawValue.CStatus != CstatusNewData {
|
||||||
c.logger.Debug("skipping counter item with invalid data status",
|
c.logger.Debug("skipping counter item with invalid data status",
|
||||||
@@ -381,10 +376,7 @@ func (c *Collector) collectWorkerRaw() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if instanceName, ok = stringMap[item.SzName]; !ok {
|
instanceName := decodeInstanceName(nameCache, item.SzName)
|
||||||
instanceName = windows.UTF16PtrToString(item.SzName)
|
|
||||||
stringMap[item.SzName] = instanceName
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(instanceName, InstanceTotal) && !c.totalCounterRequested {
|
if strings.HasSuffix(instanceName, InstanceTotal) && !c.totalCounterRequested {
|
||||||
continue
|
continue
|
||||||
@@ -501,7 +493,7 @@ func (c *Collector) collectWorkerFormatted() {
|
|||||||
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
|
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
|
||||||
|
|
||||||
indexMap := map[string]int{}
|
indexMap := map[string]int{}
|
||||||
stringMap := map[*uint16]string{}
|
nameCache := map[string]string{}
|
||||||
|
|
||||||
for _, counter := range c.counters {
|
for _, counter := range c.counters {
|
||||||
for _, instance := range counter.Instances {
|
for _, instance := range counter.Instances {
|
||||||
@@ -532,20 +524,12 @@ func (c *Collector) collectWorkerFormatted() {
|
|||||||
|
|
||||||
items = unsafe.Slice((*FmtCounterValueItemDouble)(unsafe.Pointer(&buf[0])), itemCount)
|
items = unsafe.Slice((*FmtCounterValueItemDouble)(unsafe.Pointer(&buf[0])), itemCount)
|
||||||
|
|
||||||
var (
|
|
||||||
instanceName string
|
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if item.FmtValue.CStatus != CstatusValidData && item.FmtValue.CStatus != CstatusNewData {
|
if item.FmtValue.CStatus != CstatusValidData && item.FmtValue.CStatus != CstatusNewData {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if instanceName, ok = stringMap[item.SzName]; !ok {
|
instanceName := decodeInstanceName(nameCache, item.SzName)
|
||||||
instanceName = windows.UTF16PtrToString(item.SzName)
|
|
||||||
stringMap[item.SzName] = instanceName
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(instanceName, InstanceTotal) && !c.totalCounterRequested {
|
if strings.HasSuffix(instanceName, InstanceTotal) && !c.totalCounterRequested {
|
||||||
continue
|
continue
|
||||||
@@ -631,6 +615,33 @@ func formatCounterPath(object, instance, counterName string) string {
|
|||||||
return counterPath
|
return counterPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeInstanceName returns the instance name p points to.
|
||||||
|
// Decoded names are cached by their raw UTF-16 content and not by p, because p points
|
||||||
|
// into the item buffer, which is overwritten by every Get*CounterArray call. With explicit
|
||||||
|
// instances, every call writes its single name at the same address.
|
||||||
|
func decodeInstanceName(cache map[string]string, p *uint16) string {
|
||||||
|
if p == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
for *(*uint16)(unsafe.Add(unsafe.Pointer(p), n*2)) != 0 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := unsafe.Slice((*byte)(unsafe.Pointer(p)), n*2)
|
||||||
|
|
||||||
|
// The compiler does not allocate for the string conversion in a map lookup.
|
||||||
|
if name, ok := cache[string(raw)]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
name := windows.UTF16ToString(unsafe.Slice(p, n))
|
||||||
|
cache[string(raw)] = name
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
func isKnownCounterDataError(err error) bool {
|
func isKnownCounterDataError(err error) bool {
|
||||||
var pdhErr *Error
|
var pdhErr *Error
|
||||||
|
|
||||||
|
|||||||
@@ -71,3 +71,67 @@ func TestCollector(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type processorInformation struct {
|
||||||
|
Name string
|
||||||
|
ProcessorTime float64 `perfdata:"% Processor Time"`
|
||||||
|
IdleTime float64 `perfdata:"% Idle Time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCollectorExplicitInstances verifies that a collector built with an explicit
|
||||||
|
// list of instances returns one row per requested instance. Each instance has its
|
||||||
|
// own counter handle, and PDH writes every single-item result into the same buffer,
|
||||||
|
// so the instance names must not be resolved through a cache keyed by buffer address.
|
||||||
|
func TestCollectorExplicitInstances(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logger := slog.New(slog.DiscardHandler)
|
||||||
|
|
||||||
|
discovery, err := pdh.NewCollector[processorInformation](logger, pdh.CounterTypeRaw, "Processor Information", pdh.InstancesAll)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(discovery.Close)
|
||||||
|
|
||||||
|
var all []processorInformation
|
||||||
|
|
||||||
|
require.NoError(t, discovery.Collect(&all))
|
||||||
|
|
||||||
|
instances := make([]string, 0, 3)
|
||||||
|
|
||||||
|
for _, row := range all {
|
||||||
|
if len(instances) == cap(instances) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
instances = append(instances, row.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instances) < 2 {
|
||||||
|
t.Skipf("need at least 2 Processor Information instances, got %v", instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, counterType := range []pdh.CounterType{pdh.CounterTypeRaw, pdh.CounterTypeFormatted} {
|
||||||
|
t.Run(string(counterType), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
collector, err := pdh.NewCollector[processorInformation](logger, counterType, "Processor Information", instances)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(collector.Close)
|
||||||
|
|
||||||
|
// Formatted rate counters need two samples before they report a value.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
var data []processorInformation
|
||||||
|
|
||||||
|
require.NoError(t, collector.Collect(&data))
|
||||||
|
|
||||||
|
names := make([]string, 0, len(data))
|
||||||
|
for _, row := range data {
|
||||||
|
names = append(names, row.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.ElementsMatch(t, instances, names)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user