Files
windows_exporter/internal/collector/iis/iis.go
T
2026-10-09 11:22:01 +02:00

376 lines
9.9 KiB
Go

// 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 iis
import (
"errors"
"fmt"
"log/slog"
"regexp"
"slices"
"strconv"
"strings"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows/registry"
)
const Name = "iis"
type Config struct {
SiteInclude *regexp.Regexp `yaml:"site-include"`
SiteExclude *regexp.Regexp `yaml:"site-exclude"`
AppInclude *regexp.Regexp `yaml:"app-include"`
AppExclude *regexp.Regexp `yaml:"app-exclude"`
}
//nolint:gochecknoglobals
var ConfigDefaults = Config{
SiteInclude: types.RegExpAny,
SiteExclude: types.RegExpEmpty,
AppInclude: types.RegExpAny,
AppExclude: types.RegExpEmpty,
}
type Collector struct {
collectorWebService
collectorHttpServiceRequestQueues
collectorAppPoolWAS
collectorW3SVCW3WP
collectorWebServiceCache
config Config
iisVersion simpleVersion
logger *slog.Logger
info *prometheus.Desc
}
func New(config *Config) *Collector {
if config == nil {
config = &ConfigDefaults
}
if config.AppExclude == nil {
config.AppExclude = ConfigDefaults.AppExclude
}
if config.AppInclude == nil {
config.AppInclude = ConfigDefaults.AppInclude
}
if config.SiteExclude == nil {
config.SiteExclude = ConfigDefaults.SiteExclude
}
if config.SiteInclude == nil {
config.SiteInclude = ConfigDefaults.SiteInclude
}
c := &Collector{
config: *config,
}
return c
}
func NewWithFlags(app *kingpin.Application) *Collector {
c := &Collector{
config: ConfigDefaults,
}
var appExclude, appInclude, siteExclude, siteInclude string
app.Flag(
"collector.iis.app-exclude",
"Regexp of apps to exclude. App name must both match include and not match exclude to be included.",
).Default("").StringVar(&appExclude)
app.Flag(
"collector.iis.app-include",
"Regexp of apps to include. App name must both match include and not match exclude to be included.",
).Default(".+").StringVar(&appInclude)
app.Flag(
"collector.iis.site-exclude",
"Regexp of sites to exclude. Site name must both match include and not match exclude to be included.",
).Default("").StringVar(&siteExclude)
app.Flag(
"collector.iis.site-include",
"Regexp of sites to include. Site name must both match include and not match exclude to be included.",
).Default(".+").StringVar(&siteInclude)
app.Action(func(*kingpin.ParseContext) error {
var err error
c.config.AppExclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", appExclude))
if err != nil {
return fmt.Errorf("collector.iis.app-exclude: %w", err)
}
c.config.AppInclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", appInclude))
if err != nil {
return fmt.Errorf("collector.iis.app-include: %w", err)
}
c.config.SiteExclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", siteExclude))
if err != nil {
return fmt.Errorf("collector.iis.site-exclude: %w", err)
}
c.config.SiteInclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", siteInclude))
if err != nil {
return fmt.Errorf("collector.iis.site-include: %w", err)
}
return nil
})
return c
}
func (c *Collector) GetName() string {
return Name
}
func (c *Collector) Close() error {
c.perfDataCollectorWebService.Close()
c.perfDataCollectorHttpServiceRequestQueues.Close()
c.perfDataCollectorAppPoolWAS.Close()
if c.w3SVCW3WPPerfDataCollector != nil {
c.w3SVCW3WPPerfDataCollector.Close()
}
c.w3SVCW3WPPerfDataCollectorV8.Close()
if c.serviceCachePerfDataCollector != nil {
c.serviceCachePerfDataCollector.Close()
}
return nil
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
c.iisVersion = c.getIISVersion()
c.info = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "info"),
"ISS information",
[]string{},
prometheus.Labels{"version": fmt.Sprintf("%d.%d", c.iisVersion.major, c.iisVersion.minor)},
)
errs := make([]error, 0)
if err := c.buildWebService(); err != nil {
errs = append(errs, fmt.Errorf("failed to build Web Service collector: %w", err))
}
if err := c.buildHttpServiceRequestQueues(); err != nil {
errs = append(errs, fmt.Errorf("failed to build Http Service collector: %w", err))
}
if err := c.buildAppPoolWAS(); err != nil {
errs = append(errs, fmt.Errorf("failed to build APP_POOL_WAS collector: %w", err))
}
if err := c.buildW3SVCW3WP(); err != nil {
errs = append(errs, fmt.Errorf("failed to build W3SVC_W3WP collector: %w", err))
}
if err := c.buildWebServiceCache(); err != nil {
errs = append(errs, fmt.Errorf("failed to build Web Service Cache collector: %w", err))
}
return errors.Join(errs...)
}
type simpleVersion struct {
major uint64
minor uint64
}
func (c *Collector) getIISVersion() simpleVersion {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\InetStp\`, registry.QUERY_VALUE)
if err != nil {
c.logger.Warn("couldn't open registry to determine IIS version",
slog.Any("err", err),
)
return simpleVersion{}
}
defer func() {
err = k.Close()
if err != nil {
c.logger.Warn("Failed to close registry key",
slog.Any("err", err),
)
}
}()
major, _, err := k.GetIntegerValue("MajorVersion")
if err != nil {
c.logger.Warn("Couldn't open registry to determine IIS version",
slog.Any("err", err),
)
return simpleVersion{}
}
minor, _, err := k.GetIntegerValue("MinorVersion")
if err != nil {
c.logger.Warn("Couldn't open registry to determine IIS version",
slog.Any("err", err),
)
return simpleVersion{}
}
c.logger.Debug(fmt.Sprintf("Detected IIS %d.%d\n", major, minor))
return simpleVersion{
major: major,
minor: minor,
}
}
// Collect sends the metric values for each metric
// to the provided prometheus Metric channel.
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
ch <- prometheus.MustNewConstMetric(
c.info,
prometheus.GaugeValue,
1,
)
errs := make([]error, 0)
if err := c.collectWebService(ch); err != nil {
errs = append(errs, fmt.Errorf("failed to collect Web Service metrics: %w", err))
}
if err := c.collectHttpServiceRequestQueues(ch); err != nil {
errs = append(errs, fmt.Errorf("failed to collect Http Service Request Queues metrics: %w", err))
}
if err := c.collectAppPoolWAS(ch); err != nil {
errs = append(errs, fmt.Errorf("failed to collect APP_POOL_WAS metrics: %w", err))
}
if err := c.collectW3SVCW3WP(ch); err != nil {
errs = append(errs, fmt.Errorf("failed to collect W3SVC_W3WP metrics: %w", err))
}
if err := c.collectWebServiceCache(ch); err != nil {
errs = append(errs, fmt.Errorf("failed to collect Web Service Cache metrics: %w", err))
}
return errors.Join(errs...)
}
type collectorName interface {
GetName() string
}
// deduplicateIISNames deduplicate IIS site names from various IIS perflib objects.
//
// IIS perflib may report duplicate entries for the same site/app-pool when
// instances are recycled; the duplicate carries a numeric suffix separated by
// "#" (e.g. "Site_B#2"). In that case "Site_B" is discarded and "Site_B#2"
// is kept. Callers must use iisCounterBaseName to present it as "Site_B".
//
// Application Pool names may legitimately contain a "#" character, so only a
// trailing "#<digits>" pattern is treated as an IIS counter suffix.
//
// Example input: [ "Site_A", "Site_B", "Site_C", "Site_B#2" ]
// Example output: [ "Site_A", "Site_B#2", "Site_C" ].
func deduplicateIISNames[T collectorName](counterValues []T) []T {
type dedupeEntry struct {
resultIdx int // index into result slice
numericSuffix int // -1 for base (no suffix), ≥0 for "#N" variants
}
// Sort by base name for a deterministic output order.
slices.SortFunc(counterValues, func(a, b T) int {
return strings.Compare(iisCounterBaseName(a.GetName()), iisCounterBaseName(b.GetName()))
})
seen := make(map[string]dedupeEntry, len(counterValues))
result := make([]T, 0, len(counterValues))
for _, counterValue := range counterValues {
name := counterValue.GetName()
baseName := iisCounterBaseName(name)
// Determine the numeric suffix for this entry (-1 means "no suffix").
suffix := -1
if baseName != name {
// iisCounterBaseName guarantees the remainder after '#' is all digits.
if n, err := strconv.Atoi(name[len(baseName)+1:]); err == nil {
suffix = n
}
}
if e, ok := seen[baseName]; ok {
// Keep whichever entry carries the higher numeric suffix.
if suffix > e.numericSuffix {
result[e.resultIdx] = counterValue
seen[baseName] = dedupeEntry{e.resultIdx, suffix}
}
} else {
seen[baseName] = dedupeEntry{len(result), suffix}
result = append(result, counterValue)
}
}
return result
}
// iisCounterBaseName strips a trailing IIS perflib numeric suffix ("#<digits>")
// from name and returns the base name. If no such suffix is present, name is
// returned unchanged.
//
// Only a trailing "#" followed exclusively by ASCII digits is treated as a
// counter suffix, so "#" characters that are part of a legitimate application
// name (e.g. "App#Pool") are left intact.
func iisCounterBaseName(name string) string {
idx := strings.LastIndex(name, "#")
if idx < 0 || idx == len(name)-1 {
return name
}
for _, r := range name[idx+1:] {
if r < '0' || r > '9' {
return name
}
}
return name[:idx]
}