//go:build windows // +build windows /* ** Zabbix ** Copyright (C) 2001-2023 Zabbix SIA ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **/ package perfmon import ( "errors" "strconv" "sync" "time" "git.zabbix.com/ap/plugin-support/plugin" "zabbix.com/pkg/pdh" "zabbix.com/pkg/win32" ) const ( maxInactivityPeriod = time.Hour * 25 maxInterval = 60 * 15 langDefault = 0 langEnglish = 1 ) type perfCounterIndex struct { path string lang int } type perfCounter struct { lastAccess time.Time interval int handle win32.PDH_HCOUNTER history []*float64 head, tail historyIndex err error } // Plugin - type Plugin struct { plugin.Base mutex sync.Mutex counters map[perfCounterIndex]*perfCounter query win32.PDH_HQUERY collectError error } var impl Plugin = Plugin{ counters: make(map[perfCounterIndex]*perfCounter), } type historyIndex int func (h historyIndex) inc(interval int) historyIndex { h++ if int(h) == interval { h = 0 } return h } func (h historyIndex) dec(interval int) historyIndex { h-- if int(h) < 0 { h = historyIndex(interval - 1) } return h } func (h historyIndex) sub(value int, interval int) historyIndex { h -= historyIndex(value) for int(h) < 0 { h += historyIndex(interval) } return h } func (p *Plugin) Collect() (err error) { p.mutex.Lock() defer p.mutex.Unlock() if len(p.counters) == 0 { return } p.collectError = win32.PdhCollectQueryData(p.query) expireTime := time.Now().Add(-maxInactivityPeriod) for index, c := range p.counters { if c.lastAccess.Before(expireTime) || nil != p.collectError { if cerr := win32.PdhRemoveCounter(c.handle); cerr != nil { p.Debugf("error while removing counter '%s': %s", index.path, cerr) } delete(p.counters, index) continue } c.history[c.tail], c.err = win32.PdhGetFormattedCounterValueDouble(c.handle) if c.tail = c.tail.inc(c.interval); c.tail == c.head { c.head = c.head.inc(c.interval) } } return p.collectError } func (p *Plugin) Period() int { return 1 } // addCounter adds new performance counter to query. The plugin mutex must be locked. func (p *Plugin) addCounter(index perfCounterIndex, interval int64) (err error) { var handle win32.PDH_HCOUNTER if index.lang == langEnglish { handle, err = win32.PdhAddEnglishCounter(p.query, index.path, 0) } else { handle, err = win32.PdhAddCounter(p.query, index.path, 0) } if err != nil { return } // extend the interval buffer by 1 to reserve space so tail/head doesn't overlap // when the buffer is full interval++ p.counters[index] = &perfCounter{ lastAccess: time.Now(), history: make([]*float64, interval), interval: int(interval), handle: handle, } return } func (c *perfCounter) getHistory(interval int) (value interface{}, err error) { c.lastAccess = time.Now() if c.err != nil { return nil, c.err } // extend history buffer if necessary if c.interval < interval+1 { h := make([]*float64, interval+1) copy(h, c.history) c.history = h c.interval = interval + 1 } totalnum := int(c.tail - c.head) if totalnum < 0 { totalnum += c.interval } if totalnum == 0 { // not enough samples collected return } if interval == 1 { if pvalue := c.history[c.tail.dec(c.interval)]; pvalue != nil { return *pvalue, nil } return nil, nil } if totalnum < interval { interval = totalnum } start := c.tail.sub(interval, c.interval) var total, num float64 for index := start; index != c.tail; index = index.inc(c.interval) { if pvalue := c.history[index]; pvalue != nil { total += *c.history[index] num++ } } if num != 0 { return total / num, nil } return nil, nil } // Export - func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) { var lang int switch key { case "perf_counter": lang = langDefault case "perf_counter_en": lang = langEnglish default: return nil, errors.New("Unsupported metric.") } if ctx == nil { return nil, errors.New("This item is available only in daemon mode.") } if len(params) > 2 { return nil, errors.New("Too many parameters.") } if len(params) == 0 || params[0] == "" { return nil, errors.New("Invalid first parameter.") } var interval int64 if len(params) == 1 || params[1] == "" { interval = 1 } else { if interval, err = strconv.ParseInt(params[1], 10, 32); err != nil { return nil, errors.New("Invalid second parameter.") } if interval < 1 || interval > maxInterval { return nil, errors.New("Interval out of range.") } } if path, tmperr := pdh.ConvertPath(params[0]); tmperr != nil { p.Debugf("cannot convert performance counter path: %s", tmperr) return nil, errors.New("Invalid performance counter path.") } else { p.mutex.Lock() defer p.mutex.Unlock() if p.query == 0 { if p.query, err = win32.PdhOpenQuery(nil, 0); err != nil { return } } index := perfCounterIndex{path, lang} if counter, ok := p.counters[index]; ok { if p.collectError != nil { return nil, p.collectError } return counter.getHistory(int(interval)) } else { if err = p.addCounter(index, interval); err != nil { return nil, err } return nil, p.collectError } } } func (p *Plugin) Start() { } func (p *Plugin) Stop() { } func init() { plugin.RegisterMetrics(&impl, "WindowsPerfMon", "perf_counter", "Value of any Windows performance counter.", "perf_counter_en", "Value of any Windows performance counter in English.", ) }