helpers/windows/pdh/pdh_query_windows.go (323 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 ( "errors" "fmt" "regexp" "runtime" "strings" "syscall" "unsafe" "golang.org/x/sys/windows" ) var ( instanceNameRegexp = regexp.MustCompile(`(\(.+\))\\`) objectNameRegexp = regexp.MustCompile(`(?:^\\\\[^\\]+\\|^\\)([^\\]+)`) ) // Counter object will contain the handle and format of each performance counter. type Counter struct { handle PdhCounterHandle format PdhCounterFormat instanceName string } // Query contains the pdh. type Query struct { Handle PdhQueryHandle Counters map[string]*Counter } // CounterValue contains the performance counter values. type CounterValue struct { Instance string Measurement interface{} Err CounterValueError } // CounterValueError contains the performance counter error. type CounterValueError struct { Error error CStatus uint32 } // Open creates a new query. func (q *Query) Open() error { h, err := PdhOpenQuery("", 0) if err != nil { return err } q.Handle = h q.Counters = make(map[string]*Counter) return nil } // AddEnglishCounter adds the specified counter to the query. func (q *Query) AddEnglishCounter(counterPath string) (PdhCounterHandle, error) { h, err := PdhAddEnglishCounter(q.Handle, counterPath, 0) return h, err } // AddCounter adds the specified counter to the query. func (q *Query) AddCounter(counterPath string, instance string, format string, wildcard bool, english bool) error { if _, found := q.Counters[counterPath]; found { return nil } var err error var instanceName string // Extract the instance name from the counterPath. if instance == "" || wildcard { instanceName, err = MatchInstanceName(counterPath) if err != nil { return err } } else { instanceName = instance } var h PdhCounterHandle if english { h, err = PdhAddEnglishCounter(q.Handle, counterPath, 0) } else { h, err = PdhAddCounter(q.Handle, counterPath, 0) } if err != nil { return err } q.Counters[counterPath] = &Counter{ handle: h, instanceName: instanceName, format: getPDHFormat(format), } return nil } // GetCounterPaths func will check the computer or log file and return the counter paths that match the given counter path which contains wildcard characters. func (q *Query) GetCounterPaths(counterPath string) ([]string, error) { paths, err := q.ExpandWildCardPath(counterPath) if err == nil { return paths, err } //check if Windows installed language is not ENG, the ExpandWildCardPath will return either one of the errors below. if err == PDH_CSTATUS_NO_OBJECT || err == PDH_CSTATUS_NO_COUNTER { handle, err := q.AddEnglishCounter(counterPath) if err != nil { return nil, err } defer PdhRemoveCounter(handle) info, err := PdhGetCounterInfo(handle) if err != nil { return nil, err } path := UTF16PtrToString(info.SzFullPath) if path != counterPath { return q.ExpandWildCardPath(path) } } return nil, err } // RemoveUnusedCounters will remove all counter handles for the paths that are not found anymore func (q *Query) RemoveUnusedCounters(counters []string) error { // check if the expandwildcard func did expand th wildcard queries, if not, no counters will be removed for _, counter := range counters { if strings.Contains(counter, "*") { return nil } } unused := make(map[string]*Counter) for counterPath, counter := range q.Counters { if !matchCounter(counterPath, counters) { unused[counterPath] = counter } } if len(unused) == 0 { return nil } for counterPath, cnt := range unused { err := PdhRemoveCounter(cnt.handle) if err != nil { return err } delete(q.Counters, counterPath) } return nil } func matchCounter(counterPath string, counterList []string) bool { for _, cn := range counterList { if cn == counterPath { return true } } return false } // CollectData collects the value for all counters in the query. func (q *Query) CollectData() error { return PdhCollectQueryData(q.Handle) } // CollectData collects the value for all counters in the query. func (q *Query) CollectDataEx(interval uint32, event windows.Handle) error { return PdhCollectQueryDataEx(q.Handle, interval, event) } // GetFormattedCounterValues returns an array of formatted values for a query. func (q *Query) GetFormattedCounterValues() (map[string][]CounterValue, error) { if q.Counters == nil || len(q.Counters) == 0 { return nil, errors.New("no counter list found") } rtn := make(map[string][]CounterValue, len(q.Counters)) for path, counter := range q.Counters { rtn[path] = append(rtn[path], getCounterValue(counter)) } return rtn, nil } // GetCountersAndInstances returns a list of counters and instances for a given object func (q *Query) GetCountersAndInstances(objectName string) ([]string, []string, error) { counters, instances, err := PdhEnumObjectItems(objectName) if err != nil { return nil, nil, fmt.Errorf("Unable to retrieve counter and instance list for %s: %w", objectName, err) } if len(counters) == 0 && len(instances) == 0 { return nil, nil, fmt.Errorf("Unable to retrieve counter and instance list for %s", objectName) } return UTF16ToStringArray(counters), UTF16ToStringArray(instances), nil } func (q *Query) GetRawCounterValue(counterName string) (PdhRawCounter, error) { if _, ok := q.Counters[counterName]; !ok { return PdhRawCounter{}, fmt.Errorf("%s doesn't exist in the map; call AddCounter()", counterName) } c, err := PdhGetRawCounterValue(q.Counters[counterName].handle) if err != nil { return PdhRawCounter{}, err } return c, nil } func (q *Query) GetRawCounterArray(counterName string, filterTotal bool) (RawCounterArray, error) { if _, ok := q.Counters[counterName]; !ok { return nil, fmt.Errorf("%s doesn't exist in the map; call AddCounter()", counterName) } c, err := PdhGetRawCounterArray(q.Counters[counterName].handle, filterTotal) if err != nil { return nil, err } return c, nil } // ExpandWildCardPath examines local computer and returns those counter paths that match the given counter path which contains wildcard characters. func (q *Query) ExpandWildCardPath(wildCardPath string) ([]string, error) { if wildCardPath == "" { return nil, errors.New("no query path given") } utfPath, err := syscall.UTF16PtrFromString(wildCardPath) if err != nil { return nil, err } var expdPaths []uint16 // PdhExpandWildCardPath will not return the counter paths for windows 32 bit systems but PdhExpandCounterPath will. if runtime.GOARCH == "386" { if expdPaths, err = PdhExpandCounterPath(utfPath); err != nil { return nil, err } if expdPaths == nil { return nil, errors.New("no counter paths found") } return UTF16ToStringArray(expdPaths), nil } else { if expdPaths, err = PdhExpandWildCardPath(utfPath); err != nil { if err == PDH_MORE_DATA { if expdPaths, err = PdhExpandWildCardPath(utfPath); err != nil { return nil, err } } else { return nil, err } } paths := UTF16ToStringArray(expdPaths) // in several cases ExpandWildCardPath win32 api seems to return initial wildcard without any errors, adding some waiting time between the 2 ExpandWildCardPath api calls seems to be succesfull but that will delay data retrieval // A call is triggered again if len(paths) == 1 && strings.Contains(paths[0], "*") && paths[0] == wildCardPath { expdPaths, err = PdhExpandWildCardPath(utfPath) if err == nil { return paths, err } } else { return paths, err } } return nil, PdhErrno(syscall.ERROR_NOT_FOUND) } // Close closes the query and all of its counters. func (q *Query) Close() error { return PdhCloseQuery(q.Handle) } // matchInstanceName will check first for instance and then for any objects names. func MatchInstanceName(counterPath string) (string, error) { matches := instanceNameRegexp.FindStringSubmatch(counterPath) if len(matches) == 2 { return returnLastInstance(matches[1]), nil } matches = objectNameRegexp.FindStringSubmatch(counterPath) if len(matches) == 2 { return matches[1], nil } return "", errors.New("query doesn't contain an instance name. In this case you have to define 'instance_name'") } // returnLastInstance will return the content from the last parentheses, this covers cases as `\WF (System.Workflow) 4.0.0.0(*)\Workflows Created`. func returnLastInstance(match string) string { var openedParanth int var innerMatch string var matches []string runeMatch := []rune(match) for i := 0; i < len(runeMatch); i++ { char := string(runeMatch[i]) // check if string ends between parentheses if char == ")" { openedParanth -= 1 } if openedParanth > 0 { innerMatch += char } if openedParanth == 0 && innerMatch != "" { matches = append(matches, innerMatch) innerMatch = "" } // check if string starts between parentheses if char == "(" { openedParanth += 1 } } if len(matches) > 0 { return matches[len(matches)-1] } return match } // getCounterValue will retrieve the counter value based on the format applied in the config options func getCounterValue(counter *Counter) CounterValue { counterValue := CounterValue{Instance: counter.instanceName, Err: CounterValueError{CStatus: 0}} switch counter.format { case PdhFmtLong: _, value, err := PdhGetFormattedCounterValueLong(counter.handle) if err != nil { counterValue.Err.Error = err if value != nil { counterValue.Err.CStatus = value.CStatus } } else { counterValue.Measurement = value.Value } case PdhFmtLarge: _, value, err := PdhGetFormattedCounterValueLarge(counter.handle) if err != nil { counterValue.Err.Error = err if value != nil { counterValue.Err.CStatus = value.CStatus } } else { counterValue.Measurement = value.Value } case PdhFmtDouble: _, value, err := PdhGetFormattedCounterValueDouble(counter.handle) if err != nil { counterValue.Err.Error = err if value != nil { counterValue.Err.CStatus = value.CStatus } } else { counterValue.Measurement = value.Value } default: counterValue.Err.Error = fmt.Errorf("initialization failed: format '%#v' "+ "for instance '%s' is invalid (must be PdhFmtDouble, PdhFmtLarge or PdhFmtLong)", counter.format, counter.instanceName) } return counterValue } // getPDHFormat calculates data pdhformat. func getPDHFormat(format string) PdhCounterFormat { switch format { case "long": return PdhFmtLong case "large": return PdhFmtLarge default: return PdhFmtDouble } } // UTF16ToStringArray converts list of Windows API NULL terminated strings to Go string array. func UTF16ToStringArray(buf []uint16) []string { var strings []string nextLineStart := 0 stringLine := UTF16PtrToString(&buf[0]) for stringLine != "" { strings = append(strings, stringLine) nextLineStart += len([]rune(stringLine)) + 1 remainingBuf := buf[nextLineStart:] stringLine = UTF16PtrToString(&remainingBuf[0]) } return strings } // UTF16PtrToString converts Windows API LPTSTR (pointer to string) to Go string. func UTF16PtrToString(s *uint16) string { if s == nil { return "" } return syscall.UTF16ToString((*[1 << 29]uint16)(unsafe.Pointer(s))[0:]) }