mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-19 19:19:04 +02:00
pdh: fall back to localized names of v2 counter sets (#2709)
Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
6b82e5a05f
commit
39277da8e5
@@ -103,6 +103,11 @@ exercised a live Windows role.
|
|||||||
instance occurrences within a sample; their suffixes are not stable identities.
|
instance occurrences within a sample; their suffixes are not stable identities.
|
||||||
Do not remove every name ending in `_Total`: legitimate process and database
|
Do not remove every name ending in `_Total`: legitimate process and database
|
||||||
names can use that suffix. Use the shared aggregate-instance policy.
|
names can use that suffix. Use the shared aggregate-instance policy.
|
||||||
|
- PDH collectors add counters by English path. On a Windows installation
|
||||||
|
without en-US resources, the 009 name table holds localized names for v2
|
||||||
|
(manifest-based) counter sets such as `SMB Server Shares` or `HTTP Service`;
|
||||||
|
`internal/pdh/localized.go` then maps the English names through the v2
|
||||||
|
registration API. Classic (v1) sets such as `Processor` are not affected.
|
||||||
- Keep `Build`, `Collect` and `Close` serialized for each collector instance.
|
- Keep `Build`, `Collect` and `Close` serialized for each collector instance.
|
||||||
Filtered `WithCollectors` views share execution state and do not own shutdown;
|
Filtered `WithCollectors` views share execution state and do not own shutdown;
|
||||||
the original `Collection` owns collectors and MI resources. A timed-out call
|
the original `Collection` owns collectors and MI resources. A timed-out call
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ func newCollector[T any](logger *slog.Logger, query *Query, resultType CounterTy
|
|||||||
var counterHandle pdhCounterHandle
|
var counterHandle pdhCounterHandle
|
||||||
|
|
||||||
//nolint:nestif
|
//nolint:nestif
|
||||||
if ret := AddEnglishCounter(handle, counterPath, 0, &counterHandle); ret != ErrorSuccess {
|
if ret := addEnglishCounter(handle, object, instance, counterName, &counterHandle); ret != ErrorSuccess {
|
||||||
if ret == CstatusNoCounter {
|
if ret == CstatusNoCounter {
|
||||||
if f.minOSBuild != "" {
|
if f.minOSBuild != "" {
|
||||||
if minOSBuild, err := strconv.Atoi(f.minOSBuild); err == nil {
|
if minOSBuild, err := strconv.Atoi(f.minOSBuild); err == nil {
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
// 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 (
|
||||||
|
"encoding/binary"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PdhAddEnglishCounterW resolves English names through the 009 name table.
|
||||||
|
// For v2 (manifest-based) counter sets, Windows fills that table from the
|
||||||
|
// provider's MUI resources. Without en-US resources, e.g. on a German-only
|
||||||
|
// installation, it holds the localized names, so the English path of a v2
|
||||||
|
// counter set fails with PDH_CSTATUS_NO_OBJECT. The functions below map the
|
||||||
|
// English names to the localized ones through the v2 registration API, which
|
||||||
|
// reads the names of each language from the provider itself.
|
||||||
|
|
||||||
|
const (
|
||||||
|
perfRegCounterSetNameString = 3 // PERF_REG_COUNTERSET_NAME_STRING
|
||||||
|
perfRegCounterNameStrings = 5 // PERF_REG_COUNTER_NAME_STRINGS
|
||||||
|
|
||||||
|
langIDEnglish = 0x0409
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:gochecknoglobals
|
||||||
|
var (
|
||||||
|
libAdvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||||
|
libKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
|
||||||
|
perfEnumerateCounterSet = libAdvapi32.NewProc("PerfEnumerateCounterSet")
|
||||||
|
perfQueryCounterSetRegistrationInfo = libAdvapi32.NewProc("PerfQueryCounterSetRegistrationInfo")
|
||||||
|
getUserDefaultUILanguage = libKernel32.NewProc("GetUserDefaultUILanguage")
|
||||||
|
getSystemDefaultUILanguage = libKernel32.NewProc("GetSystemDefaultUILanguage")
|
||||||
|
|
||||||
|
// englishCounterSets maps the lower-case English name of every v2 counter
|
||||||
|
// set to its GUID. It is built on first use, because only hosts whose 009
|
||||||
|
// table lacks a v2 counter set need it.
|
||||||
|
englishCounterSets = sync.OnceValue(loadEnglishCounterSets)
|
||||||
|
|
||||||
|
localizedNamesMu sync.Mutex
|
||||||
|
// localizedNames caches the name mappings by lower-case English object name.
|
||||||
|
// A nil entry means the object has no localized names to fall back to.
|
||||||
|
localizedNames = map[string][]localizedCounterSet{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// localizedCounterSet holds the names of a v2 counter set in one UI language.
|
||||||
|
type localizedCounterSet struct {
|
||||||
|
object string
|
||||||
|
// counters maps lower-case English counter names to localized names.
|
||||||
|
counters map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// addEnglishCounter adds the counter by its English path. If the object or
|
||||||
|
// counter is not found, it tries the localized paths of a v2 counter set.
|
||||||
|
func addEnglishCounter(handle pdhQueryHandle, object, instance, counterName string, counterHandle *pdhCounterHandle) uint32 {
|
||||||
|
ret := AddEnglishCounter(handle, formatCounterPath(object, instance, counterName), 0, counterHandle)
|
||||||
|
if ret != CstatusNoObject && ret != CstatusNoCounter {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range localizedCounterPaths(object, instance, counterName) {
|
||||||
|
if AddCounter(handle, path, 0, counterHandle) == ErrorSuccess {
|
||||||
|
return ErrorSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// localizedCounterPaths returns the counter path in each UI language of the
|
||||||
|
// process and the system whose names differ from the English ones. It
|
||||||
|
// returns nil if object is not a v2 counter set or has no such translation.
|
||||||
|
func localizedCounterPaths(object, instance, counterName string) []string {
|
||||||
|
sets := localizedCounterSets(object)
|
||||||
|
|
||||||
|
paths := make([]string, 0, len(sets))
|
||||||
|
|
||||||
|
for _, set := range sets {
|
||||||
|
counter, ok := set.counters[strings.ToLower(counterName)]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path := formatCounterPath(set.object, instance, counter)
|
||||||
|
if !slices.Contains(paths, path) {
|
||||||
|
paths = append(paths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func localizedCounterSets(object string) []localizedCounterSet {
|
||||||
|
key := strings.ToLower(object)
|
||||||
|
|
||||||
|
localizedNamesMu.Lock()
|
||||||
|
defer localizedNamesMu.Unlock()
|
||||||
|
|
||||||
|
if sets, ok := localizedNames[key]; ok {
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
var sets []localizedCounterSet
|
||||||
|
|
||||||
|
if guid, ok := englishCounterSets()[key]; ok {
|
||||||
|
english := counterNames(&guid, langIDEnglish)
|
||||||
|
|
||||||
|
for _, langID := range uiLanguages() {
|
||||||
|
if isEnglish(langID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := counterSetName(&guid, langID)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
localized := counterNames(&guid, langID)
|
||||||
|
counters := make(map[string]string, len(english))
|
||||||
|
|
||||||
|
for id, englishName := range english {
|
||||||
|
if localizedName, ok := localized[id]; ok {
|
||||||
|
counters[strings.ToLower(englishName)] = localizedName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sets = append(sets, localizedCounterSet{object: name, counters: counters})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localizedNames[key] = sets
|
||||||
|
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
// isEnglish reports whether langID is an English language, e.g. en-US or en-GB,
|
||||||
|
// whose names match the English ones.
|
||||||
|
func isEnglish(langID uint32) bool {
|
||||||
|
return langID&0x3FF == langIDEnglish&0x3FF
|
||||||
|
}
|
||||||
|
|
||||||
|
// uiLanguages returns the UI languages whose names PDH may expect: the one of
|
||||||
|
// the process' user and the one of the system.
|
||||||
|
func uiLanguages() []uint32 {
|
||||||
|
langIDs := make([]uint32, 0, 2)
|
||||||
|
|
||||||
|
for _, proc := range []*windows.LazyProc{getUserDefaultUILanguage, getSystemDefaultUILanguage} {
|
||||||
|
if proc.Find() != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, _ := proc.Call()
|
||||||
|
if langID := uint32(uint16(ret)); langID != 0 && !slices.Contains(langIDs, langID) {
|
||||||
|
langIDs = append(langIDs, langID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return langIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEnglishCounterSets() map[string]windows.GUID {
|
||||||
|
sets := map[string]windows.GUID{}
|
||||||
|
|
||||||
|
if perfEnumerateCounterSet.Find() != nil || perfQueryCounterSetRegistrationInfo.Find() != nil {
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
var guids []windows.GUID
|
||||||
|
|
||||||
|
for size := uint32(256); ; {
|
||||||
|
guids = make([]windows.GUID, size)
|
||||||
|
|
||||||
|
var actual uint32
|
||||||
|
|
||||||
|
ret, _, _ := perfEnumerateCounterSet.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&guids[0])),
|
||||||
|
uintptr(size),
|
||||||
|
uintptr(unsafe.Pointer(&actual)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == uintptr(windows.ERROR_NOT_ENOUGH_MEMORY) && actual > size {
|
||||||
|
size = actual
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret != uintptr(windows.ERROR_SUCCESS) {
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
guids = guids[:min(actual, size)]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, guid := range guids {
|
||||||
|
if name, ok := counterSetName(&guid, langIDEnglish); ok {
|
||||||
|
sets[strings.ToLower(name)] = guid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryRegistrationInfo calls PerfQueryCounterSetRegistrationInfo and grows the
|
||||||
|
// buffer until the result fits.
|
||||||
|
func queryRegistrationInfo(guid *windows.GUID, requestCode, langID uint32) ([]byte, bool) {
|
||||||
|
buf := make([]byte, 512)
|
||||||
|
|
||||||
|
for {
|
||||||
|
var actual uint32
|
||||||
|
|
||||||
|
ret, _, _ := perfQueryCounterSetRegistrationInfo.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(guid)),
|
||||||
|
uintptr(requestCode),
|
||||||
|
uintptr(langID),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(len(buf)),
|
||||||
|
uintptr(unsafe.Pointer(&actual)),
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ret == uintptr(windows.ERROR_NOT_ENOUGH_MEMORY) && int(actual) > len(buf):
|
||||||
|
buf = make([]byte, actual)
|
||||||
|
case ret == uintptr(windows.ERROR_SUCCESS) && int(actual) <= len(buf):
|
||||||
|
return buf[:actual], true
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func counterSetName(guid *windows.GUID, langID uint32) (string, bool) {
|
||||||
|
buf, ok := queryRegistrationInfo(guid, perfRegCounterSetNameString, langID)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
name := utf16String(buf)
|
||||||
|
|
||||||
|
return name, name != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// counterNames returns the counter names of a counter set by counter ID.
|
||||||
|
// The result is a PERF_STRING_BUFFER_HEADER block: the header, one
|
||||||
|
// PERF_STRING_COUNTER_HEADER per counter, then the strings they point to.
|
||||||
|
func counterNames(guid *windows.GUID, langID uint32) map[uint32]string {
|
||||||
|
buf, ok := queryRegistrationInfo(guid, perfRegCounterNameStrings, langID)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseCounterNames(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCounterNames(buf []byte) map[uint32]string {
|
||||||
|
if len(buf) < 8 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
count := binary.LittleEndian.Uint32(buf[4:8])
|
||||||
|
names := make(map[uint32]string, count)
|
||||||
|
|
||||||
|
for i := range count {
|
||||||
|
header := 8 + 8*int(i)
|
||||||
|
if header+8 > len(buf) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
id := binary.LittleEndian.Uint32(buf[header:])
|
||||||
|
offset := binary.LittleEndian.Uint32(buf[header+4:])
|
||||||
|
|
||||||
|
if offset == 0xFFFFFFFF || int(offset) >= len(buf) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if name := utf16String(buf[offset:]); name != "" {
|
||||||
|
names[id] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// utf16String decodes a NUL-terminated UTF-16LE string.
|
||||||
|
func utf16String(buf []byte) string {
|
||||||
|
chars := make([]uint16, 0, len(buf)/2)
|
||||||
|
|
||||||
|
for i := 0; i+1 < len(buf); i += 2 {
|
||||||
|
c := binary.LittleEndian.Uint16(buf[i:])
|
||||||
|
if c == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
chars = append(chars, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return windows.UTF16ToString(chars)
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// 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 (
|
||||||
|
"encoding/binary"
|
||||||
|
"testing"
|
||||||
|
"unicode/utf16"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUTF16String(t *testing.T) {
|
||||||
|
buf := utf16Bytes("SMB-Serverfreigaben")
|
||||||
|
buf = append(buf, 'x', 0)
|
||||||
|
|
||||||
|
require.Equal(t, "SMB-Serverfreigaben", utf16String(buf))
|
||||||
|
require.Empty(t, utf16String(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnglishCounterSets checks the English names of a v2 counter set that
|
||||||
|
// every supported Windows version registers. Classic sets like Processor
|
||||||
|
// Information are not v2 counter sets.
|
||||||
|
func TestEnglishCounterSets(t *testing.T) {
|
||||||
|
guid, ok := englishCounterSets()["smb client shares"]
|
||||||
|
require.True(t, ok, "SMB Client Shares counter set not found")
|
||||||
|
|
||||||
|
names := counterNames(&guid, langIDEnglish)
|
||||||
|
require.Contains(t, mapValues(names), "Avg. sec/Read")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalizedCounterPaths checks that an English path maps to the counter
|
||||||
|
// set's names in the UI language. On an English system, there is nothing to map.
|
||||||
|
func TestLocalizedCounterPaths(t *testing.T) {
|
||||||
|
paths := localizedCounterPaths("SMB Client Shares", "*", "Avg. sec/Read")
|
||||||
|
|
||||||
|
for _, langID := range uiLanguages() {
|
||||||
|
if isEnglish(langID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEmpty(t, paths, "no localized path for UI language %#x", langID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
var query pdhQueryHandle
|
||||||
|
|
||||||
|
require.Equal(t, uint32(ErrorSuccess), OpenQuery(0, 0, &query))
|
||||||
|
|
||||||
|
var counter pdhCounterHandle
|
||||||
|
|
||||||
|
ret := AddCounter(query, path, 0, &counter)
|
||||||
|
CloseQuery(query)
|
||||||
|
|
||||||
|
require.Equal(t, uint32(ErrorSuccess), ret, "add localized counter %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapValues(m map[uint32]string) []string {
|
||||||
|
values := make([]string, 0, len(m))
|
||||||
|
for _, v := range m {
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func utf16Bytes(s string) []byte {
|
||||||
|
chars := utf16.Encode([]rune(s))
|
||||||
|
buf := make([]byte, 0, 2*len(chars)+2)
|
||||||
|
|
||||||
|
for _, c := range chars {
|
||||||
|
buf = binary.LittleEndian.AppendUint16(buf, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return binary.LittleEndian.AppendUint16(buf, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCounterNamesBuffer(t *testing.T) {
|
||||||
|
first := utf16Bytes("Anzahl derzeit geöffneter Dateien")
|
||||||
|
second := utf16Bytes("Anzahl von Strukturverbindungen")
|
||||||
|
|
||||||
|
// PERF_STRING_BUFFER_HEADER, three PERF_STRING_COUNTER_HEADERs, strings.
|
||||||
|
const headers = 8 + 3*8
|
||||||
|
|
||||||
|
buf := binary.LittleEndian.AppendUint32(nil, uint32(headers+len(first)+len(second)))
|
||||||
|
buf = binary.LittleEndian.AppendUint32(buf, 3)
|
||||||
|
buf = binary.LittleEndian.AppendUint32(buf, 1)
|
||||||
|
buf = binary.LittleEndian.AppendUint32(buf, headers)
|
||||||
|
buf = binary.LittleEndian.AppendUint32(buf, 2)
|
||||||
|
buf = binary.LittleEndian.AppendUint32(buf, uint32(headers+len(first)))
|
||||||
|
buf = binary.LittleEndian.AppendUint32(buf, 3)
|
||||||
|
buf = binary.LittleEndian.AppendUint32(buf, 0xFFFFFFFF)
|
||||||
|
buf = append(buf, first...)
|
||||||
|
buf = append(buf, second...)
|
||||||
|
|
||||||
|
require.Equal(t, map[uint32]string{
|
||||||
|
1: "Anzahl derzeit geöffneter Dateien",
|
||||||
|
2: "Anzahl von Strukturverbindungen",
|
||||||
|
}, parseCounterNames(buf))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user