Skip to content

Commit 1e24d7b

Browse files
jkroepkemwimpelberg28renovate[bot]
authored
dns: add enhanced metrics (#1999) (#2040)
Co-authored-by: Matthew Wimpelberg <[email protected]> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1 parent 109f537 commit 1e24d7b

File tree

4 files changed

+197
-13
lines changed

4 files changed

+197
-13
lines changed

docs/collector.dns.md

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
The dns collector exposes metrics about the DNS server
44

55
|||
6-
-|-
7-
Metric name prefix | `dns`
8-
Classes | [`Win32_PerfRawData_DNS_DNS`](https://technet.microsoft.com/en-us/library/cc977686.aspx)
9-
Enabled by default? | No
6+
-|-|-
7+
Metric name prefix | `dns` |
8+
Classes | [`Win32_PerfRawData_DNS_DNS`](https://technet.microsoft.com/en-us/library/cc977686.aspx) |
9+
Enabled by default | Yes |
10+
Metric name prefix (error stats) | `windows_dns` |
11+
Classes | [`MicrosoftDNS_Statistic`](https://learn.microsoft.com/en-us/windows/win32/dns/dns-wmi-provider-overview) |
12+
Enabled by default (error stats)? | Yes |
1013

1114
## Flags
1215

13-
None
16+
Name | Description
17+
-----|------------
18+
`collector.dns.enabled` | Comma-separated list of collectors to use. Available collectors: `metrics`, `error_stats`. Defaults to all collectors if not specified.
1419

1520
## Metrics
1621

@@ -38,12 +43,56 @@ Name | Description | Type | Labels
3843
`windows_dns_wins_queries_total` | _Not yet documented_ | counter | `direction`
3944
`windows_dns_wins_responses_total` | _Not yet documented_ | counter | `direction`
4045
`windows_dns_unmatched_responses_total` | _Not yet documented_ | counter | None
46+
`windows_dns_error_stats_total` | DNS error statistics from MicrosoftDNS_Statistic | counter | `name`, `collection_name`, `dns_server`
47+
48+
### Sub-collectors
49+
50+
The DNS collector is split into two sub-collectors:
51+
52+
1. `metrics` - Collects standard DNS performance metrics using PDH (Performance Data Helper)
53+
2. `wmi_stats` - Collects DNS error statistics from the MicrosoftDNS_Statistic WMI class
54+
55+
By default, both sub-collectors are enabled. You can enable specific sub-collectors using the `collector.dns.enabled` flag.
56+
57+
### Example Usage
58+
59+
To enable only DNS error statistics collection:
60+
```powershell
61+
windows_exporter.exe --collector.dns.enabled=wmi_stats
62+
```
63+
64+
To enable only standard DNS metrics:
65+
```powershell
66+
windows_exporter.exe --collector.dns.enabled=metrics
67+
```
68+
69+
To enable both (default behavior):
70+
```powershell
71+
windows_exporter.exe --collector.dns.enabled=metrics,wmi_stats
72+
```
4173

4274
### Example metric
43-
_This collector does not yet have explained examples, we would appreciate your help adding them!_
75+
```
76+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="BadKey"} 0
77+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="BadSig"} 0
78+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="BadTime"} 0
79+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="FormError"} 0
80+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="Max"} 0
81+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="NoError"} 0
82+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="NotAuth"} 0
83+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="NotImpl"} 0
84+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="NotZone"} 0
85+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="NxDomain"} 0
86+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="NxRRSet"} 0
87+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="Refused"} 0
88+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="ServFail"} 0
89+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="UnknownError"} 0
90+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="YxDomain"} 0
91+
windows_dns_wmi_stats_total{collection_name="Error Stats",dns_server="EC2AMAZ-5NNM8M1",name="YxRRSet"} 0
92+
```
4493

4594
## Useful queries
4695
_This collector does not yet have any useful queries added, we would appreciate your help adding them!_
4796

4897
## Alerting examples
49-
_This collector does not yet have alerting examples, we would appreciate your help adding them!_
98+
_This collector does not yet have alerting examples, we would appreciate your help adding them!_

internal/collector/dns/dns.go

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
package dns
1717

1818
import (
19+
"errors"
1920
"fmt"
2021
"log/slog"
22+
"slices"
23+
"strings"
2124

2225
"github.com/alecthomas/kingpin/v2"
2326
"github.com/prometheus-community/windows_exporter/internal/mi"
@@ -26,12 +29,23 @@ import (
2629
"github.com/prometheus/client_golang/prometheus"
2730
)
2831

29-
const Name = "dns"
32+
const (
33+
Name = "dns"
34+
subCollectorMetrics = "metrics"
35+
subCollectorWMIStats = "wmi_stats"
36+
)
3037

31-
type Config struct{}
38+
type Config struct {
39+
CollectorsEnabled []string `yaml:"collectors_enabled"`
40+
}
3241

3342
//nolint:gochecknoglobals
34-
var ConfigDefaults = Config{}
43+
var ConfigDefaults = Config{
44+
CollectorsEnabled: []string{
45+
subCollectorMetrics,
46+
subCollectorWMIStats,
47+
},
48+
}
3549

3650
// A Collector is a Prometheus Collector for WMI Win32_PerfRawData_DNS_DNS metrics.
3751
type Collector struct {
@@ -40,6 +54,9 @@ type Collector struct {
4054
perfDataCollector *pdh.Collector
4155
perfDataObject []perfDataCounterValues
4256

57+
miSession *mi.Session
58+
miQuery mi.Query
59+
4360
dynamicUpdatesFailures *prometheus.Desc
4461
dynamicUpdatesQueued *prometheus.Desc
4562
dynamicUpdatesReceived *prometheus.Desc
@@ -62,22 +79,45 @@ type Collector struct {
6279
zoneTransferResponsesReceived *prometheus.Desc
6380
zoneTransferSuccessReceived *prometheus.Desc
6481
zoneTransferSuccessSent *prometheus.Desc
82+
dnsWMIStats *prometheus.Desc
6583
}
6684

6785
func New(config *Config) *Collector {
6886
if config == nil {
6987
config = &ConfigDefaults
7088
}
7189

90+
if config.CollectorsEnabled == nil {
91+
config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled
92+
}
93+
7294
c := &Collector{
7395
config: *config,
7496
}
7597

7698
return c
7799
}
78100

79-
func NewWithFlags(_ *kingpin.Application) *Collector {
80-
return &Collector{}
101+
func NewWithFlags(app *kingpin.Application) *Collector {
102+
c := &Collector{
103+
config: ConfigDefaults,
104+
}
105+
c.config.CollectorsEnabled = make([]string, 0)
106+
107+
var collectorsEnabled string
108+
109+
app.Flag(
110+
"collector.dns.enabled",
111+
"Comma-separated list of collectors to use. Defaults to all, if not specified.",
112+
).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled)
113+
114+
app.Action(func(*kingpin.ParseContext) error {
115+
c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",")
116+
117+
return nil
118+
})
119+
120+
return c
81121
}
82122

83123
func (c *Collector) GetName() string {
@@ -90,7 +130,31 @@ func (c *Collector) Close() error {
90130
return nil
91131
}
92132

93-
func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error {
133+
func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
134+
for _, collector := range c.config.CollectorsEnabled {
135+
if !slices.Contains([]string{subCollectorMetrics, subCollectorWMIStats}, collector) {
136+
return fmt.Errorf("unknown sub collector: %s. Possible values: %s", collector,
137+
strings.Join([]string{subCollectorMetrics, subCollectorWMIStats}, ", "),
138+
)
139+
}
140+
}
141+
142+
if slices.Contains(c.config.CollectorsEnabled, subCollectorMetrics) {
143+
if err := c.buildMetricsCollector(); err != nil {
144+
return err
145+
}
146+
}
147+
148+
if slices.Contains(c.config.CollectorsEnabled, subCollectorWMIStats) {
149+
if err := c.buildErrorStatsCollector(miSession); err != nil {
150+
return err
151+
}
152+
}
153+
154+
return nil
155+
}
156+
157+
func (c *Collector) buildMetricsCollector() error {
94158
c.zoneTransferRequestsReceived = prometheus.NewDesc(
95159
prometheus.BuildFQName(types.Namespace, Name, "zone_transfer_requests_received_total"),
96160
"Number of zone transfer requests (AXFR/IXFR) received by the master DNS server",
@@ -224,6 +288,13 @@ func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error {
224288
nil,
225289
)
226290

291+
c.dnsWMIStats = prometheus.NewDesc(
292+
prometheus.BuildFQName(types.Namespace, Name, "wmi_stats_total"),
293+
"DNS WMI statistics from MicrosoftDNS_Statistic",
294+
[]string{"name", "collection_name", "dns_server"},
295+
nil,
296+
)
297+
227298
var err error
228299

229300
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](pdh.CounterTypeRaw, "DNS", pdh.InstancesAll)
@@ -234,9 +305,43 @@ func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error {
234305
return nil
235306
}
236307

308+
func (c *Collector) buildErrorStatsCollector(miSession *mi.Session) error {
309+
if miSession == nil {
310+
return errors.New("miSession is nil")
311+
}
312+
313+
query, err := mi.NewQuery("SELECT Name, CollectionName, Value, DnsServerName FROM MicrosoftDNS_Statistic WHERE CollectionName = 'Error Stats'")
314+
if err != nil {
315+
return fmt.Errorf("failed to create query: %w", err)
316+
}
317+
318+
c.miSession = miSession
319+
c.miQuery = query
320+
321+
return nil
322+
}
323+
237324
// Collect sends the metric values for each metric
238325
// to the provided prometheus Metric channel.
239326
func (c *Collector) Collect(ch chan<- prometheus.Metric) error {
327+
errs := make([]error, 0)
328+
329+
if slices.Contains(c.config.CollectorsEnabled, subCollectorMetrics) {
330+
if err := c.collectMetrics(ch); err != nil {
331+
errs = append(errs, fmt.Errorf("failed collecting metrics: %w", err))
332+
}
333+
}
334+
335+
if slices.Contains(c.config.CollectorsEnabled, subCollectorWMIStats) {
336+
if err := c.collectErrorStats(ch); err != nil {
337+
errs = append(errs, fmt.Errorf("failed collecting WMI statistics: %w", err))
338+
}
339+
}
340+
341+
return errors.Join(errs...)
342+
}
343+
344+
func (c *Collector) collectMetrics(ch chan<- prometheus.Metric) error {
240345
err := c.perfDataCollector.Collect(&c.perfDataObject)
241346
if err != nil {
242347
return fmt.Errorf("failed to collect DNS metrics: %w", err)
@@ -493,3 +598,24 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) error {
493598

494599
return nil
495600
}
601+
602+
func (c *Collector) collectErrorStats(ch chan<- prometheus.Metric) error {
603+
var stats []Statistic
604+
if err := c.miSession.Query(&stats, mi.NamespaceRootMicrosoftDNS, c.miQuery); err != nil {
605+
return fmt.Errorf("failed to query DNS statistics: %w", err)
606+
}
607+
608+
// Collect DNS error statistics
609+
for _, stat := range stats {
610+
ch <- prometheus.MustNewConstMetric(
611+
c.dnsWMIStats,
612+
prometheus.CounterValue,
613+
float64(stat.Value),
614+
stat.Name,
615+
stat.CollectionName,
616+
stat.DnsServerName,
617+
)
618+
}
619+
620+
return nil
621+
}

internal/collector/dns/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,11 @@ type perfDataCounterValues struct {
105105
_ float64 `perfdata:"Zone Transfer SOA Request Sent"`
106106
_ float64 `perfdata:"Zone Transfer Success"`
107107
}
108+
109+
// Statistic represents the structure for DNS error statistics
110+
type Statistic struct {
111+
Name string `mi:"Name"`
112+
CollectionName string `mi:"CollectionName"`
113+
Value uint64 `mi:"Value"`
114+
DnsServerName string `mi:"DnsServerName"`
115+
}

internal/mi/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var (
5151
NamespaceRootWindowsFSRM = utils.Must(NewNamespace("root/microsoft/windows/fsrm"))
5252
NamespaceRootWebAdministration = utils.Must(NewNamespace("root/WebAdministration"))
5353
NamespaceRootMSCluster = utils.Must(NewNamespace("root/MSCluster"))
54+
NamespaceRootMicrosoftDNS = utils.Must(NewNamespace("root/MicrosoftDNS"))
5455
)
5556

5657
type Query *uint16

0 commit comments

Comments
 (0)