fix: Ignore duplicate IIS entries from Perflib

Perflib often exposes duplicate IIS entries, suffixed with '#' and a
number (I.E. iis_site_name#1).
These duplicate entries were causing the exporter to fail scraping due
to duplicate metrics.

Based on user feedback, the entry with the highest suffix
value is kept, with other duplicate entries discarded.

E.G. Given the following list of site names, "Site_B" would be
discarded, and "Site_B#2" would be kept and presented as "Site_B" in the
collector metrics.
[ "Site_A", "Site_B", "Site_C", "Site_B#2" ]

Signed-off-by: Ben Reedy <breed808@breed808.com>
This commit is contained in:
Ben Reedy 2023-02-05 08:57:54 +10:00
parent b431ff6ac3
commit 1d3af58305
No known key found for this signature in database
GPG Key ID: 235C15B6086C9D7E
2 changed files with 166 additions and 64 deletions

View File

@ -7,6 +7,8 @@ import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/log"
@ -977,14 +979,63 @@ type perflibWebService struct {
TotalUnlockRequests float64 `perflib:"Total Unlock Requests"`
}
// Fulfill the hasGetIISName interface
func (p perflibWebService) getIISName() string {
return p.Name
}
// Fulfill the hasGetIISName interface
func (p perflibAPP_POOL_WAS) getIISName() string {
return p.Name
}
// Fulfill the hasGetIISName interface
func (p perflibW3SVC_W3WP) getIISName() string {
return p.Name
}
// Fulfill the hasGetIISName interface
func (p perflibW3SVC_W3WP_IIS8) getIISName() string {
return p.Name
}
// Required as Golang doesn't allow access to struct fields in generic functions. That restriction may be removed in a future release.
type hasGetIISName interface {
getIISName() string
}
// Deduplicate IIS site names from various IIS perflib objects.
//
// E.G. Given the following list of site names, "Site_B" would be
// discarded, and "Site_B#2" would be kept and presented as "Site_B" in the
// collector metrics.
// [ "Site_A", "Site_B", "Site_C", "Site_B#2" ]
func dedupIISNames[V hasGetIISName](services []V) map[string]V {
// Ensure IIS entry with the highest suffix occurs last
sort.SliceStable(services, func(i, j int) bool {
return services[i].getIISName() < services[j].getIISName()
})
var webServiceDeDuplicated = make(map[string]V)
// Use map to deduplicate IIS entries
for _, entry := range services {
name := strings.Split(entry.getIISName(), "#")[0]
webServiceDeDuplicated[name] = entry
}
return webServiceDeDuplicated
}
func (c *IISCollector) collectWebService(ctx *ScrapeContext, ch chan<- prometheus.Metric) (*prometheus.Desc, error) {
var WebService []perflibWebService
if err := unmarshalObject(ctx.perfObjects["Web Service"], &WebService); err != nil {
var webService []perflibWebService
if err := unmarshalObject(ctx.perfObjects["Web Service"], &webService); err != nil {
return nil, err
}
for _, app := range WebService {
if app.Name == "_Total" || c.siteExcludePattern.MatchString(app.Name) || !c.siteIncludePattern.MatchString(app.Name) {
webServiceDeDuplicated := dedupIISNames(webService)
for name, app := range webServiceDeDuplicated {
if name == "_Total" || c.siteExcludePattern.MatchString(name) || !c.siteIncludePattern.MatchString(name) {
continue
}
@ -992,238 +1043,238 @@ func (c *IISCollector) collectWebService(ctx *ScrapeContext, ch chan<- prometheu
c.CurrentAnonymousUsers,
prometheus.GaugeValue,
app.CurrentAnonymousUsers,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.CurrentBlockedAsyncIORequests,
prometheus.GaugeValue,
app.CurrentBlockedAsyncIORequests,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.CurrentCGIRequests,
prometheus.GaugeValue,
app.CurrentCGIRequests,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.CurrentConnections,
prometheus.GaugeValue,
app.CurrentConnections,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.CurrentISAPIExtensionRequests,
prometheus.GaugeValue,
app.CurrentISAPIExtensionRequests,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.CurrentNonAnonymousUsers,
prometheus.GaugeValue,
app.CurrentNonAnonymousUsers,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.ServiceUptime,
prometheus.GaugeValue,
app.ServiceUptime,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalBytesReceived,
prometheus.CounterValue,
app.TotalBytesReceived,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalBytesSent,
prometheus.CounterValue,
app.TotalBytesSent,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalAnonymousUsers,
prometheus.CounterValue,
app.TotalAnonymousUsers,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalBlockedAsyncIORequests,
prometheus.CounterValue,
app.TotalBlockedAsyncIORequests,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalCGIRequests,
prometheus.CounterValue,
app.TotalCGIRequests,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalConnectionAttemptsAllInstances,
prometheus.CounterValue,
app.TotalConnectionAttemptsAllInstances,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalFilesReceived,
prometheus.CounterValue,
app.TotalFilesReceived,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalFilesSent,
prometheus.CounterValue,
app.TotalFilesSent,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalISAPIExtensionRequests,
prometheus.CounterValue,
app.TotalISAPIExtensionRequests,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalLockedErrors,
prometheus.CounterValue,
app.TotalLockedErrors,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalLogonAttempts,
prometheus.CounterValue,
app.TotalLogonAttempts,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalNonAnonymousUsers,
prometheus.CounterValue,
app.TotalNonAnonymousUsers,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalNotFoundErrors,
prometheus.CounterValue,
app.TotalNotFoundErrors,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalRejectedAsyncIORequests,
prometheus.CounterValue,
app.TotalRejectedAsyncIORequests,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalOtherRequests,
app.Name,
name,
"other",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalCopyRequests,
app.Name,
name,
"COPY",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalDeleteRequests,
app.Name,
name,
"DELETE",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalGetRequests,
app.Name,
name,
"GET",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalHeadRequests,
app.Name,
name,
"HEAD",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalLockRequests,
app.Name,
name,
"LOCK",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalMkcolRequests,
app.Name,
name,
"MKCOL",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalMoveRequests,
app.Name,
name,
"MOVE",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalOptionsRequests,
app.Name,
name,
"OPTIONS",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalPostRequests,
app.Name,
name,
"POST",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalPropfindRequests,
app.Name,
name,
"PROPFIND",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalProppatchRequests,
app.Name,
name,
"PROPPATCH",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalPutRequests,
app.Name,
name,
"PUT",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalSearchRequests,
app.Name,
name,
"SEARCH",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalTraceRequests,
app.Name,
name,
"TRACE",
)
ch <- prometheus.MustNewConstMetric(
c.TotalRequests,
prometheus.CounterValue,
app.TotalUnlockRequests,
app.Name,
name,
"UNLOCK",
)
}
@ -1267,10 +1318,12 @@ func (c *IISCollector) collectAPP_POOL_WAS(ctx *ScrapeContext, ch chan<- prometh
return nil, err
}
for _, app := range APP_POOL_WAS {
if app.Name == "_Total" ||
c.appExcludePattern.MatchString(app.Name) ||
!c.appIncludePattern.MatchString(app.Name) {
appPoolDeDuplicated := dedupIISNames(APP_POOL_WAS)
for name, app := range appPoolDeDuplicated {
if name == "_Total" ||
c.appExcludePattern.MatchString(name) ||
!c.appIncludePattern.MatchString(name) {
continue
}
@ -1283,7 +1336,7 @@ func (c *IISCollector) collectAPP_POOL_WAS(ctx *ScrapeContext, ch chan<- prometh
c.CurrentApplicationPoolState,
prometheus.GaugeValue,
isCurrentState,
app.Name,
name,
label,
)
}
@ -1292,73 +1345,73 @@ func (c *IISCollector) collectAPP_POOL_WAS(ctx *ScrapeContext, ch chan<- prometh
c.CurrentApplicationPoolUptime,
prometheus.GaugeValue,
app.CurrentApplicationPoolUptime,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.CurrentWorkerProcesses,
prometheus.GaugeValue,
app.CurrentWorkerProcesses,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.MaximumWorkerProcesses,
prometheus.GaugeValue,
app.MaximumWorkerProcesses,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.RecentWorkerProcessFailures,
prometheus.GaugeValue,
app.RecentWorkerProcessFailures,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TimeSinceLastWorkerProcessFailure,
prometheus.GaugeValue,
app.TimeSinceLastWorkerProcessFailure,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalApplicationPoolRecycles,
prometheus.CounterValue,
app.TotalApplicationPoolRecycles,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalApplicationPoolUptime,
prometheus.CounterValue,
app.TotalApplicationPoolUptime,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalWorkerProcessesCreated,
prometheus.CounterValue,
app.TotalWorkerProcessesCreated,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalWorkerProcessFailures,
prometheus.CounterValue,
app.TotalWorkerProcessFailures,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalWorkerProcessPingFailures,
prometheus.CounterValue,
app.TotalWorkerProcessPingFailures,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalWorkerProcessShutdownFailures,
prometheus.CounterValue,
app.TotalWorkerProcessShutdownFailures,
app.Name,
name,
)
ch <- prometheus.MustNewConstMetric(
c.TotalWorkerProcessStartupFailures,
prometheus.CounterValue,
app.TotalWorkerProcessStartupFailures,
app.Name,
name,
)
}
@ -1442,16 +1495,23 @@ func (c *IISCollector) collectW3SVC_W3WP(ctx *ScrapeContext, ch chan<- prometheu
return nil, err
}
for _, app := range W3SVC_W3WP {
w3svcW3WPDeduplicated := dedupIISNames(W3SVC_W3WP)
for w3Name, app := range w3svcW3WPDeduplicated {
// Extract the apppool name from the format <PID>_<NAME>
pid := workerProcessNameExtractor.ReplaceAllString(app.Name, "$1")
name := workerProcessNameExtractor.ReplaceAllString(app.Name, "$2")
pid := workerProcessNameExtractor.ReplaceAllString(w3Name, "$1")
name := workerProcessNameExtractor.ReplaceAllString(w3Name, "$2")
if name == "" || name == "_Total" ||
c.appExcludePattern.MatchString(name) ||
!c.appIncludePattern.MatchString(name) {
continue
}
// Duplicate instances are suffixed # with an index number. These should be ignored
if strings.Contains(app.Name, "#") {
continue
}
ch <- prometheus.MustNewConstMetric(
c.Threads,
prometheus.GaugeValue,
@ -1694,10 +1754,12 @@ func (c *IISCollector) collectW3SVC_W3WP(ctx *ScrapeContext, ch chan<- prometheu
return nil, err
}
for _, app := range W3SVC_W3WP_IIS8 {
w3svcW3WPIIS8Deduplicated := dedupIISNames(W3SVC_W3WP_IIS8)
for w3Name, app := range w3svcW3WPIIS8Deduplicated {
// Extract the apppool name from the format <PID>_<NAME>
pid := workerProcessNameExtractor.ReplaceAllString(app.Name, "$1")
name := workerProcessNameExtractor.ReplaceAllString(app.Name, "$2")
pid := workerProcessNameExtractor.ReplaceAllString(w3Name, "$1")
name := workerProcessNameExtractor.ReplaceAllString(w3Name, "$2")
if name == "" || name == "_Total" ||
c.appExcludePattern.MatchString(name) ||
!c.appIncludePattern.MatchString(name) {

View File

@ -1,9 +1,49 @@
package collector
import (
"reflect"
"testing"
)
func BenchmarkIISCollector(b *testing.B) {
benchmarkCollector(b, "iis", newIISCollector)
}
func TestIISDeduplication(t *testing.T) {
start := []perflibAPP_POOL_WAS{
{
Name: "foo",
Frequency_Object: 1,
},
{
Name: "foo1#999",
Frequency_Object: 2,
},
{
Name: "foo#2",
Frequency_Object: 3,
},
{
Name: "bar$2",
Frequency_Object: 4,
},
{
Name: "bar_2",
Frequency_Object: 5,
},
}
var expected = make(map[string]perflibAPP_POOL_WAS)
// Should be deduplicated from "foo#2"
expected["foo"] = perflibAPP_POOL_WAS{Name: "foo#2", Frequency_Object: 3}
// Map key should have suffix stripped, but struct name field should be unchanged
expected["foo1"] = perflibAPP_POOL_WAS{Name: "foo1#999", Frequency_Object: 2}
// Map key and Name should be identical, as there is no suffix starting with "#"
expected["bar$2"] = perflibAPP_POOL_WAS{Name: "bar$2", Frequency_Object: 4}
// Map key and Name should be identical, as there is no suffix starting with "#"
expected["bar_2"] = perflibAPP_POOL_WAS{Name: "bar_2", Frequency_Object: 5}
deduplicated := dedupIISNames(start)
if !reflect.DeepEqual(expected, deduplicated) {
t.Errorf("Flattened values do not match!\nExpected result: %+v\nActual result: %+v", expected, deduplicated)
}
}