logical_disk: add bitlocker status sub-collector (#2077)

This commit is contained in:
Jan-Otto Kröpke
2025-06-16 12:48:23 +02:00
committed by GitHub
parent 3e8693f1e3
commit 34cfda306b
13 changed files with 623 additions and 283 deletions

100
README.md
View File

@@ -1,4 +1,4 @@
config.file# windows_exporter # windows_exporter
[![CI](https://github.com/prometheus-community/windows_exporter/actions/workflows/release.yml/badge.svg)](https://github.com/prometheus-community/windows_exporter) [![CI](https://github.com/prometheus-community/windows_exporter/actions/workflows/release.yml/badge.svg)](https://github.com/prometheus-community/windows_exporter)
[![Linting](https://github.com/prometheus-community/windows_exporter/actions/workflows/lint.yml/badge.svg)](https://github.com/prometheus-community/windows_exporter) [![Linting](https://github.com/prometheus-community/windows_exporter/actions/workflows/lint.yml/badge.svg)](https://github.com/prometheus-community/windows_exporter)
@@ -12,55 +12,55 @@ A Prometheus exporter for Windows machines.
## Collectors ## Collectors
Name | Description | Enabled by default | Name | Description | Enabled by default |
------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------- |------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
[ad](docs/collector.ad.md) | Active Directory Domain Services | | [ad](docs/collector.ad.md) | Active Directory Domain Services | |
[adcs](docs/collector.adcs.md) | Active Directory Certificate Services | | [adcs](docs/collector.adcs.md) | Active Directory Certificate Services | |
[adfs](docs/collector.adfs.md) | Active Directory Federation Services | | [adfs](docs/collector.adfs.md) | Active Directory Federation Services | |
[cache](docs/collector.cache.md) | Cache metrics | | [cache](docs/collector.cache.md) | Cache metrics | |
[cpu](docs/collector.cpu.md) | CPU usage | ✓ | [cpu](docs/collector.cpu.md) | CPU usage | ✓ |
[cpu_info](docs/collector.cpu_info.md) | CPU Information | | [cpu_info](docs/collector.cpu_info.md) | CPU Information | |
[cs](docs/collector.cs.md) | "Computer System" metrics (system properties, num cpus/total memory) | | [cs](docs/collector.cs.md) | "Computer System" metrics (system properties, num cpus/total memory) | |
[container](docs/collector.container.md) | Container metrics | | [container](docs/collector.container.md) | Container metrics | |
[diskdrive](docs/collector.diskdrive.md) | Diskdrive metrics | | [diskdrive](docs/collector.diskdrive.md) | Diskdrive metrics | |
[dfsr](docs/collector.dfsr.md) | DFSR metrics | | [dfsr](docs/collector.dfsr.md) | DFSR metrics | |
[dhcp](docs/collector.dhcp.md) | DHCP Server | | [dhcp](docs/collector.dhcp.md) | DHCP Server | |
[dns](docs/collector.dns.md) | DNS Server | | [dns](docs/collector.dns.md) | DNS Server | |
[exchange](docs/collector.exchange.md) | Exchange metrics | | [exchange](docs/collector.exchange.md) | Exchange metrics | |
[filetime](docs/collector.filetime.md) | FileTime metrics | | [filetime](docs/collector.filetime.md) | FileTime metrics | |
[fsrmquota](docs/collector.fsrmquota.md) | Microsoft File Server Resource Manager (FSRM) Quotas collector | | [fsrmquota](docs/collector.fsrmquota.md) | Microsoft File Server Resource Manager (FSRM) Quotas collector | |
[gpu](docs/collector.gpu.md) | GPU metrics | | [gpu](docs/collector.gpu.md) | GPU metrics | |
[hyperv](docs/collector.hyperv.md) | Hyper-V hosts | | [hyperv](docs/collector.hyperv.md) | Hyper-V hosts | |
[iis](docs/collector.iis.md) | IIS sites and applications | | [iis](docs/collector.iis.md) | IIS sites and applications | |
[license](docs/collector.license.md) | Windows license status | | [license](docs/collector.license.md) | Windows license status | |
[logical_disk](docs/collector.logical_disk.md) | Logical disks, disk I/O | ✓ | [logical_disk](docs/collector.logical_disk.md) | Logical disks, disk I/O | ✓ |
[memory](docs/collector.memory.md) | Memory usage metrics | ✓ | [memory](docs/collector.memory.md) | Memory usage metrics | ✓ |
[mscluster](docs/collector.mscluster.md) | MSCluster metrics | | [mscluster](docs/collector.mscluster.md) | MSCluster metrics | |
[msmq](docs/collector.msmq.md) | MSMQ queues | | [msmq](docs/collector.msmq.md) | MSMQ queues | |
[mssql](docs/collector.mssql.md) | [SQL Server Performance Objects](https://docs.microsoft.com/en-us/sql/relational-databases/performance-monitor/use-sql-server-objects#SQLServerPOs) metrics | | [mssql](docs/collector.mssql.md) | [SQL Server Performance Objects](https://docs.microsoft.com/en-us/sql/relational-databases/performance-monitor/use-sql-server-objects#SQLServerPOs) metrics | |
[netframework](docs/collector.netframework.md) | .NET Framework metrics | | [netframework](docs/collector.netframework.md) | .NET Framework metrics | |
[net](docs/collector.net.md) | Network interface I/O | ✓ | [net](docs/collector.net.md) | Network interface I/O | ✓ |
[os](docs/collector.os.md) | OS metrics (memory, processes, users) | ✓ | [os](docs/collector.os.md) | OS metrics (memory, processes, users) | ✓ |
[pagefile](docs/collector.pagefile.md) | pagefile metrics | | [pagefile](docs/collector.pagefile.md) | pagefile metrics | |
[performancecounter](docs/collector.performancecounter.md) | Custom performance counter metrics | | [performancecounter](docs/collector.performancecounter.md) | Custom performance counter metrics | |
[physical_disk](docs/collector.physical_disk.md) | physical disk metrics | ✓ | [physical_disk](docs/collector.physical_disk.md) | physical disk metrics | ✓ |
[printer](docs/collector.printer.md) | Printer metrics | | [printer](docs/collector.printer.md) | Printer metrics | |
[process](docs/collector.process.md) | Per-process metrics | | [process](docs/collector.process.md) | Per-process metrics | |
[remote_fx](docs/collector.remote_fx.md) | RemoteFX protocol (RDP) metrics | | [remote_fx](docs/collector.remote_fx.md) | RemoteFX protocol (RDP) metrics | |
[scheduled_task](docs/collector.scheduled_task.md) | Scheduled Tasks metrics | | [scheduled_task](docs/collector.scheduled_task.md) | Scheduled Tasks metrics | |
[service](docs/collector.service.md) | Service state metrics | ✓ | [service](docs/collector.service.md) | Service state metrics | ✓ |
[smb](docs/collector.smb.md) | SMB Server | | [smb](docs/collector.smb.md) | SMB Server | |
[smbclient](docs/collector.smbclient.md) | SMB Client | | [smbclient](docs/collector.smbclient.md) | SMB Client | |
[smtp](docs/collector.smtp.md) | IIS SMTP Server | | [smtp](docs/collector.smtp.md) | IIS SMTP Server | |
[system](docs/collector.system.md) | System calls | ✓ | [system](docs/collector.system.md) | System calls | ✓ |
[tcp](docs/collector.tcp.md) | TCP connections | | [tcp](docs/collector.tcp.md) | TCP connections | |
[terminal_services](docs/collector.terminal_services.md) | Terminal services (RDS) | [terminal_services](docs/collector.terminal_services.md) | Terminal services (RDS) | |
[textfile](docs/collector.textfile.md) | Read prometheus metrics from a text file | | [textfile](docs/collector.textfile.md) | Read prometheus metrics from a text file | |
[thermalzone](docs/collector.thermalzone.md) | Thermal information | | [thermalzone](docs/collector.thermalzone.md) | Thermal information | |
[time](docs/collector.time.md) | Windows Time Service | | [time](docs/collector.time.md) | Windows Time Service | |
[udp](docs/collector.udp.md) | UDP connections | | [udp](docs/collector.udp.md) | UDP connections | |
[update](docs/collector.update.md) | Windows Update Service | | [update](docs/collector.update.md) | Windows Update Service | |
[vmware](docs/collector.vmware.md) | Performance counters installed by the Vmware Guest agent | | [vmware](docs/collector.vmware.md) | Performance counters installed by the Vmware Guest agent | |
See the linked documentation on each collector for more information on reported metrics, configuration settings and usage examples. See the linked documentation on each collector for more information on reported metrics, configuration settings and usage examples.

View File

@@ -2,12 +2,12 @@
The logical_disk collector exposes metrics about logical disks (in contrast to physical disks) The logical_disk collector exposes metrics about logical disks (in contrast to physical disks)
||| | | |
-|- |---------------------|------------------|
Metric name prefix | `logical_disk` | Metric name prefix | `logical_disk` |
Data source | Perflib | Data source | Performance Data |
Counters | `LogicalDisk` ([`Win32_PerfRawData_PerfDisk_LogicalDisk`](https://msdn.microsoft.com/en-us/windows/hardware/aa394307(v=vs.71))) | Counters | `LogicalDisk` |
Enabled by default? | Yes | Enabled by default? | Yes |
## Flags ## Flags
@@ -19,25 +19,30 @@ If given, a disk needs to match the include regexp in order for the correspondin
If given, a disk needs to *not* match the exclude regexp in order for the corresponding disk metrics to be reported If given, a disk needs to *not* match the exclude regexp in order for the corresponding disk metrics to be reported
### `--collector.logical_disk.enabled`
Comma-separated list of collectors to use. Available collectors: metrics, bitlocker_status. Defaults to metrics, if not specified.
## Metrics ## Metrics
Name | Description | Type | Labels | Name | Description | Type | Labels |
-----|-------------|------|------- |--------------------------------------------------|----------------------------------------------------------------------------------------------------|---------|-------------------------------------------------------------------|
`windows_logical_disk_info` | A metric with a constant '1' value labeled with logical disk information | gauge | `disk`,`filesystem`,`serial_number`,`volume`,`volume_name`,`type` | `windows_logical_disk_info` | A metric with a constant '1' value labeled with logical disk information | gauge | `disk`,`filesystem`,`serial_number`,`volume`,`volume_name`,`type` |
`windows_logical_disk_requests_queued` | Number of requests outstanding on the disk at the time the performance data is collected | gauge | `volume` | `windows_logical_disk_requests_queued` | Number of requests outstanding on the disk at the time the performance data is collected | gauge | `volume` |
`windows_logical_disk_avg_read_requests_queued` | Average number of read requests that were queued for the selected disk during the sample interval | gauge | `volume` | `windows_logical_disk_avg_read_requests_queued` | Average number of read requests that were queued for the selected disk during the sample interval | gauge | `volume` |
`windows_logical_disk_avg_write_requests_queued` | Average number of write requests that were queued for the selected disk during the sample interval | gauge | `volume` | `windows_logical_disk_avg_write_requests_queued` | Average number of write requests that were queued for the selected disk during the sample interval | gauge | `volume` |
`windows_logical_disk_read_bytes_total` | Rate at which bytes are transferred from the disk during read operations | counter | `volume` | `windows_logical_disk_read_bytes_total` | Rate at which bytes are transferred from the disk during read operations | counter | `volume` |
`windows_logical_disk_reads_total` | Rate of read operations on the disk | counter | `volume` | `windows_logical_disk_reads_total` | Rate of read operations on the disk | counter | `volume` |
`windows_logical_disk_write_bytes_total` | Rate at which bytes are transferred to the disk during write operations | counter | `volume` | `windows_logical_disk_write_bytes_total` | Rate at which bytes are transferred to the disk during write operations | counter | `volume` |
`windows_logical_disk_writes_total` | Rate of write operations on the disk | counter | `volume` | `windows_logical_disk_writes_total` | Rate of write operations on the disk | counter | `volume` |
`windows_logical_disk_read_seconds_total` | Seconds the disk was busy servicing read requests | counter | `volume` | `windows_logical_disk_read_seconds_total` | Seconds the disk was busy servicing read requests | counter | `volume` |
`windows_logical_disk_write_seconds_total` | Seconds the disk was busy servicing write requests | counter | `volume` | `windows_logical_disk_write_seconds_total` | Seconds the disk was busy servicing write requests | counter | `volume` |
`windows_logical_disk_free_bytes` | Unused space of the disk in bytes (not real time, updates every 10-15 min) | gauge | `volume` | `windows_logical_disk_free_bytes` | Unused space of the disk in bytes (not real time, updates every 10-15 min) | gauge | `volume` |
`windows_logical_disk_size_bytes` | Total size of the disk in bytes (not real time, updates every 10-15 min) | gauge | `volume` | `windows_logical_disk_size_bytes` | Total size of the disk in bytes (not real time, updates every 10-15 min) | gauge | `volume` |
`windows_logical_disk_idle_seconds_total` | Seconds the disk was idle (not servicing read/write requests) | counter | `volume` | `windows_logical_disk_idle_seconds_total` | Seconds the disk was idle (not servicing read/write requests) | counter | `volume` |
`windows_logical_disk_split_ios_total` | Number of I/Os to the disk split into multiple I/Os | counter | `volume` | `windows_logical_disk_split_ios_total` | Number of I/Os to the disk split into multiple I/Os | counter | `volume` |
`windows_logical_disk_readonly` | Whether the logical disk is read-only | gauge | `volume` | `windows_logical_disk_readonly` | Whether the logical disk is read-only | gauge | `volume` |
| `windows_logical_disk_bitlocker_status` | BitLocker status for the logical disk | gauge | `volume`,`status` |
### Warning about size metrics ### Warning about size metrics
The `free_bytes` and `size_bytes` metrics are not updated in real time and might have a delay of 10-15min. The `free_bytes` and `size_bytes` metrics are not updated in real time and might have a delay of 10-15min.

View File

@@ -29,7 +29,7 @@ import (
"unsafe" "unsafe"
"github.com/alecthomas/kingpin/v2" "github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/headers/guid" "github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/hcn" "github.com/prometheus-community/windows_exporter/internal/headers/hcn"
"github.com/prometheus-community/windows_exporter/internal/headers/hcs" "github.com/prometheus-community/windows_exporter/internal/headers/hcs"
"github.com/prometheus-community/windows_exporter/internal/headers/iphlpapi" "github.com/prometheus-community/windows_exporter/internal/headers/iphlpapi"
@@ -536,7 +536,7 @@ func (c *Collector) collectNetworkMetrics(ch chan<- prometheus.Metric) error {
continue continue
} }
var nicGUID *guid.GUID var nicGUID *ole.GUID
for _, allocator := range properties.Resources.Allocators { for _, allocator := range properties.Resources.Allocators {
if allocator.AdapterNetCfgInstanceId != nil { if allocator.AdapterNetCfgInstanceId != nil {

View File

@@ -18,16 +18,22 @@
package logical_disk package logical_disk
import ( import (
"context"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"regexp" "regexp"
"runtime"
"runtime/debug"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"github.com/alecthomas/kingpin/v2" "github.com/alecthomas/kingpin/v2"
"github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/propsys"
"github.com/prometheus-community/windows_exporter/internal/headers/shell32"
"github.com/prometheus-community/windows_exporter/internal/mi" "github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh" "github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/types" "github.com/prometheus-community/windows_exporter/internal/types"
@@ -35,15 +41,23 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
const Name = "logical_disk" const (
Name = "logical_disk"
subCollectorMetrics = "metrics"
subCollectorBitlocker = "bitlocker_status"
)
type Config struct { type Config struct {
VolumeInclude *regexp.Regexp `yaml:"volume-include"` CollectorsEnabled []string `yaml:"enabled"`
VolumeExclude *regexp.Regexp `yaml:"volume-exclude"` VolumeInclude *regexp.Regexp `yaml:"volume-include"`
VolumeExclude *regexp.Regexp `yaml:"volume-exclude"`
} }
//nolint:gochecknoglobals //nolint:gochecknoglobals
var ConfigDefaults = Config{ var ConfigDefaults = Config{
CollectorsEnabled: []string{
subCollectorMetrics,
},
VolumeInclude: types.RegExpAny, VolumeInclude: types.RegExpAny,
VolumeExclude: types.RegExpEmpty, VolumeExclude: types.RegExpEmpty,
} }
@@ -56,6 +70,14 @@ type Collector struct {
perfDataCollector *pdh.Collector perfDataCollector *pdh.Collector
perfDataObject []perfDataCounterValues perfDataObject []perfDataCounterValues
bitlockerReqCh chan string
bitlockerResCh chan struct {
err error
status int
}
ctxCancelFunc context.CancelFunc
avgReadQueue *prometheus.Desc avgReadQueue *prometheus.Desc
avgWriteQueue *prometheus.Desc avgWriteQueue *prometheus.Desc
freeSpace *prometheus.Desc freeSpace *prometheus.Desc
@@ -74,6 +96,8 @@ type Collector struct {
writeLatency *prometheus.Desc writeLatency *prometheus.Desc
writesTotal *prometheus.Desc writesTotal *prometheus.Desc
writeTime *prometheus.Desc writeTime *prometheus.Desc
bitlockerStatus *prometheus.Desc
} }
type volumeInfo struct { type volumeInfo struct {
@@ -109,8 +133,9 @@ func NewWithFlags(app *kingpin.Application) *Collector {
c := &Collector{ c := &Collector{
config: ConfigDefaults, config: ConfigDefaults,
} }
c.config.CollectorsEnabled = make([]string, 0)
var volumeExclude, volumeInclude string var collectorsEnabled, volumeExclude, volumeInclude string
app.Flag( app.Flag(
"collector.logical_disk.volume-exclude", "collector.logical_disk.volume-exclude",
@@ -122,7 +147,17 @@ func NewWithFlags(app *kingpin.Application) *Collector {
"Regexp of volumes to include. Volume name must both match include and not match exclude to be included.", "Regexp of volumes to include. Volume name must both match include and not match exclude to be included.",
).Default(".+").StringVar(&volumeInclude) ).Default(".+").StringVar(&volumeInclude)
app.Flag(
"collector.logical_disk.enabled",
fmt.Sprintf("Comma-separated list of collectors to use. Available collectors: %s, %s. Defaults to metrics, if not specified.",
subCollectorMetrics,
subCollectorBitlocker,
),
).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled)
app.Action(func(*kingpin.ParseContext) error { app.Action(func(*kingpin.ParseContext) error {
c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",")
var err error var err error
c.config.VolumeExclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", volumeExclude)) c.config.VolumeExclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", volumeExclude))
@@ -146,12 +181,24 @@ func (c *Collector) GetName() string {
} }
func (c *Collector) Close() error { func (c *Collector) Close() error {
if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) {
c.ctxCancelFunc()
}
return nil return nil
} }
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name)) c.logger = logger.With(slog.String("collector", Name))
for _, collector := range c.config.CollectorsEnabled {
if !slices.Contains([]string{subCollectorMetrics, subCollectorBitlocker}, collector) {
return fmt.Errorf("unknown sub collector: %s. Possible values: %s", collector,
strings.Join([]string{subCollectorMetrics, subCollectorBitlocker}, ", "),
)
}
}
c.information = prometheus.NewDesc( c.information = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "info"), prometheus.BuildFQName(types.Namespace, Name, "info"),
"A metric with a constant '1' value labeled with logical disk information", "A metric with a constant '1' value labeled with logical disk information",
@@ -276,6 +323,13 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
nil, nil,
) )
c.bitlockerStatus = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "bitlocker_status"),
"BitLocker status for the logical disk",
[]string{"volume", "status"},
nil,
)
var err error var err error
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](pdh.CounterTypeRaw, "LogicalDisk", pdh.InstancesAll) c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](pdh.CounterTypeRaw, "LogicalDisk", pdh.InstancesAll)
@@ -283,6 +337,25 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
return fmt.Errorf("failed to create LogicalDisk collector: %w", err) return fmt.Errorf("failed to create LogicalDisk collector: %w", err)
} }
if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) {
initErrCh := make(chan error)
c.bitlockerReqCh = make(chan string, 1)
c.bitlockerResCh = make(chan struct {
err error
status int
}, 1)
ctx, cancel := context.WithCancel(context.Background())
c.ctxCancelFunc = cancel
go c.workerBitlocker(ctx, initErrCh)
if err = <-initErrCh; err != nil {
return fmt.Errorf("failed to initialize BitLocker worker: %w", err)
}
}
return nil return nil
} }
@@ -325,117 +398,155 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) error {
info.serialNumber, info.serialNumber,
) )
ch <- prometheus.MustNewConstMetric( if slices.Contains(c.config.CollectorsEnabled, subCollectorMetrics) {
c.requestsQueued, ch <- prometheus.MustNewConstMetric(
prometheus.GaugeValue, c.requestsQueued,
data.CurrentDiskQueueLength, prometheus.GaugeValue,
data.Name, data.CurrentDiskQueueLength,
) data.Name,
)
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.avgReadQueue, c.avgReadQueue,
prometheus.GaugeValue, prometheus.GaugeValue,
data.AvgDiskReadQueueLength*pdh.TicksToSecondScaleFactor, data.AvgDiskReadQueueLength*pdh.TicksToSecondScaleFactor,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.avgWriteQueue, c.avgWriteQueue,
prometheus.GaugeValue, prometheus.GaugeValue,
data.AvgDiskWriteQueueLength*pdh.TicksToSecondScaleFactor, data.AvgDiskWriteQueueLength*pdh.TicksToSecondScaleFactor,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.readBytesTotal, c.readBytesTotal,
prometheus.CounterValue, prometheus.CounterValue,
data.DiskReadBytesPerSec, data.DiskReadBytesPerSec,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.readsTotal, c.readsTotal,
prometheus.CounterValue, prometheus.CounterValue,
data.DiskReadsPerSec, data.DiskReadsPerSec,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.writeBytesTotal, c.writeBytesTotal,
prometheus.CounterValue, prometheus.CounterValue,
data.DiskWriteBytesPerSec, data.DiskWriteBytesPerSec,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.writesTotal, c.writesTotal,
prometheus.CounterValue, prometheus.CounterValue,
data.DiskWritesPerSec, data.DiskWritesPerSec,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.readTime, c.readTime,
prometheus.CounterValue, prometheus.CounterValue,
data.PercentDiskReadTime, data.PercentDiskReadTime,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.writeTime, c.writeTime,
prometheus.CounterValue, prometheus.CounterValue,
data.PercentDiskWriteTime, data.PercentDiskWriteTime,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.freeSpace, c.freeSpace,
prometheus.GaugeValue, prometheus.GaugeValue,
data.FreeSpace*1024*1024, data.FreeSpace*1024*1024,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.totalSpace, c.totalSpace,
prometheus.GaugeValue, prometheus.GaugeValue,
data.PercentFreeSpace*1024*1024, data.PercentFreeSpace*1024*1024,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.idleTime, c.idleTime,
prometheus.CounterValue, prometheus.CounterValue,
data.PercentIdleTime, data.PercentIdleTime,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.splitIOs, c.splitIOs,
prometheus.CounterValue, prometheus.CounterValue,
data.SplitIOPerSec, data.SplitIOPerSec,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.readLatency, c.readLatency,
prometheus.CounterValue, prometheus.CounterValue,
data.AvgDiskSecPerRead*pdh.TicksToSecondScaleFactor, data.AvgDiskSecPerRead*pdh.TicksToSecondScaleFactor,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.writeLatency, c.writeLatency,
prometheus.CounterValue, prometheus.CounterValue,
data.AvgDiskSecPerWrite*pdh.TicksToSecondScaleFactor, data.AvgDiskSecPerWrite*pdh.TicksToSecondScaleFactor,
data.Name, data.Name,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.readWriteLatency, c.readWriteLatency,
prometheus.CounterValue, prometheus.CounterValue,
data.AvgDiskSecPerTransfer*pdh.TicksToSecondScaleFactor, data.AvgDiskSecPerTransfer*pdh.TicksToSecondScaleFactor,
data.Name, data.Name,
) )
}
if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) {
c.bitlockerReqCh <- data.Name
bitlockerStatus := <-c.bitlockerResCh
if bitlockerStatus.err != nil {
c.logger.Warn("failed to get BitLocker status for "+data.Name,
slog.Any("err", bitlockerStatus.err),
)
continue
}
if bitlockerStatus.status == -1 {
c.logger.Debug("BitLocker status for "+data.Name+" is unknown",
slog.Int("status", bitlockerStatus.status),
)
continue
}
for i, status := range []string{"disabled", "on", "off", "encrypting", "decrypting", "suspended", "locked", "unknown", "waiting_for_activation"} {
val := 0.0
if bitlockerStatus.status == i {
val = 1.0
}
ch <- prometheus.MustNewConstMetric(
c.bitlockerStatus,
prometheus.GaugeValue,
val,
data.Name,
status,
)
}
}
} }
return nil return nil
@@ -609,3 +720,133 @@ func getAllMountedVolumes() (map[string]string, error) {
volumes[strings.TrimSuffix(mountPoint, `\`)] = strings.TrimSuffix(windows.UTF16ToString(guidBuf), `\`) volumes[strings.TrimSuffix(mountPoint, `\`)] = strings.TrimSuffix(windows.UTF16ToString(guidBuf), `\`)
} }
} }
/*
++ References
| System.Volume. | Control Panel | manage-bde conversion | manage-bde | Get-BitlockerVolume | Get-BitlockerVolume |
| BitLockerProtection | | | protection | VolumeStatus | ProtectionStatus |
| ------------------- | -------------------------------- | ------------------------- | -------------- | ---------------------------- | ------------------- |
| 1 | BitLocker on | Used Space Only Encrypted | Protection On | FullyEncrypted | On |
| 1 | BitLocker on | Fully Encrypted | Protection On | FullyEncrypted | On |
| 1 | BitLocker on | Fully Encrypted | Protection On | FullyEncryptedWipeInProgress | On |
| 2 | BitLocker off | Fully Decrypted | Protection Off | FullyDecrypted | Off |
| 3 | BitLocker Encrypting | Encryption In Progress | Protection Off | EncryptionInProgress | Off |
| 3 | BitLocker Encryption Paused | Encryption Paused | Protection Off | EncryptionSuspended | Off |
| 4 | BitLocker Decrypting | Decryption in progress | Protection Off | DecyptionInProgress | Off |
| 4 | BitLocker Decryption Paused | Decryption Paused | Protection Off | DecryptionSuspended | Off |
| 5 | BitLocker suspended | Used Space Only Encrypted | Protection Off | FullyEncrypted | Off |
| 5 | BitLocker suspended | Fully Encrypted | Protection Off | FullyEncrypted | Off |
| 6 | BitLocker on (Locked) | Unknown | Unknown | $null | Unknown |
| 7 | | | | | |
| 8 | BitLocker waiting for activation | Used Space Only Encrypted | Protection Off | FullyEncrypted | Off |
--
*/
func (c *Collector) workerBitlocker(ctx context.Context, initErrCh chan<- error) {
defer func() {
if r := recover(); r != nil {
c.logger.Error("workerBitlocker panic",
slog.Any("panic", r),
slog.String("stack", string(debug.Stack())),
)
// Restart the workerBitlocker
initErrCh := make(chan error)
go c.workerBitlocker(ctx, initErrCh)
if err := <-initErrCh; err != nil {
c.logger.Error("workerBitlocker restart failed",
slog.Any("err", err),
)
}
}
}()
// The only way to run WMI queries in parallel while being thread-safe is to
// ensure the CoInitialize[Ex]() call is bound to its current OS thread.
// Otherwise, attempting to initialize and run parallel queries across
// goroutines will result in protected memory errors.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE); err != nil {
var oleCode *ole.OleError
if errors.As(err, &oleCode) && oleCode.Code() != ole.S_OK && oleCode.Code() != 0x00000001 {
initErrCh <- fmt.Errorf("CoInitializeEx: %w", err)
return
}
}
defer ole.CoUninitialize()
var pkey propsys.PROPERTYKEY
// The ideal solution to check the disk encryption (BitLocker) status is to
// use the WMI APIs (Win32_EncryptableVolume). However, only programs running
// with elevated priledges can access those APIs.
//
// Our alternative solution is based on the value of the undocumented (shell)
// property: "System.Volume.BitLockerProtection". That property is essentially
// an enum containing the current BitLocker status for a given volume. This
// approached was suggested here:
// https://stackoverflow.com/questions/41308245/detect-bitlocker-programmatically-from-c-sharp-without-admin/41310139
//
// Note that the link above doesn't give any explanation / meaning for the
// enum values, it simply says that 1, 3 or 5 means the disk is encrypted.
//
// I directly tested and validated this strategy on a Windows 10 machine.
// The values given in the BitLockerStatus enum contain the relevant values
// for the shell property. I also directly validated them.
if err := propsys.PSGetPropertyKeyFromName("System.Volume.BitLockerProtection", &pkey); err != nil {
initErrCh <- fmt.Errorf("PSGetPropertyKeyFromName failed: %w", err)
return
}
close(initErrCh)
for {
select {
case <-ctx.Done():
return
case path, ok := <-c.bitlockerReqCh:
if !ok {
return
}
if !strings.Contains(path, `:`) {
c.bitlockerResCh <- struct {
err error
status int
}{err: nil, status: -1}
continue
}
status, err := func(path string) (int, error) {
item, err := shell32.SHCreateItemFromParsingName(path)
if err != nil {
return -1, fmt.Errorf("SHCreateItemFromParsingName failed: %w", err)
}
defer item.Release()
var v ole.VARIANT
if err := item.GetProperty(&pkey, &v); err != nil {
return -1, fmt.Errorf("GetProperty failed: %w", err)
}
return int(v.Val), v.Clear()
}(path)
c.bitlockerResCh <- struct {
err error
status int
}{err: err, status: status}
}
}
}

View File

@@ -1,96 +0,0 @@
// 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 guid
import (
"fmt"
"strconv"
"strings"
"golang.org/x/sys/windows"
)
type GUID windows.GUID
// FromString parses a string containing a GUID and returns the GUID. The only
// format currently supported is the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
// format.
func FromString(s string) (GUID, error) {
if len(s) != 36 {
return GUID{}, fmt.Errorf("invalid GUID %q", s)
}
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return GUID{}, fmt.Errorf("invalid GUID %q", s)
}
var g GUID
data1, err := strconv.ParseUint(s[0:8], 16, 32)
if err != nil {
return GUID{}, fmt.Errorf("invalid GUID %q", s)
}
g.Data1 = uint32(data1)
data2, err := strconv.ParseUint(s[9:13], 16, 16)
if err != nil {
return GUID{}, fmt.Errorf("invalid GUID %q", s)
}
g.Data2 = uint16(data2)
data3, err := strconv.ParseUint(s[14:18], 16, 16)
if err != nil {
return GUID{}, fmt.Errorf("invalid GUID %q", s)
}
g.Data3 = uint16(data3)
for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} {
v, err := strconv.ParseUint(s[x:x+2], 16, 8)
if err != nil {
return GUID{}, fmt.Errorf("invalid GUID %q", s)
}
g.Data4[i] = uint8(v)
}
return g, nil
}
func (g *GUID) UnmarshalJSON(b []byte) error {
guid, err := FromString(strings.Trim(strings.Trim(string(b), `"`), `{}`))
if err != nil {
return err
}
*g = guid
return nil
}
func (g *GUID) String() string {
return fmt.Sprintf(
"%08x-%04x-%04x-%04x-%012x",
g.Data1,
g.Data2,
g.Data3,
g.Data4[:2],
g.Data4[2:])
}

View File

@@ -20,7 +20,7 @@ package hcn
import ( import (
"fmt" "fmt"
"github.com/prometheus-community/windows_exporter/internal/headers/guid" "github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/utils" "github.com/prometheus-community/windows_exporter/internal/utils"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@@ -30,7 +30,7 @@ var (
defaultQuery = utils.Must(windows.UTF16PtrFromString(`{"SchemaVersion":{"Major": 2,"Minor": 0},"Flags":"None"}`)) defaultQuery = utils.Must(windows.UTF16PtrFromString(`{"SchemaVersion":{"Major": 2,"Minor": 0},"Flags":"None"}`))
) )
func GetEndpointProperties(endpointID guid.GUID) (EndpointProperties, error) { func GetEndpointProperties(endpointID ole.GUID) (EndpointProperties, error) {
endpoint, err := OpenEndpoint(endpointID) endpoint, err := OpenEndpoint(endpointID)
if err != nil { if err != nil {
return EndpointProperties{}, fmt.Errorf("failed to open endpoint: %w", err) return EndpointProperties{}, fmt.Errorf("failed to open endpoint: %w", err)

View File

@@ -22,7 +22,7 @@ import (
"fmt" "fmt"
"unsafe" "unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/guid" "github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/hcs" "github.com/prometheus-community/windows_exporter/internal/headers/hcs"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@@ -40,7 +40,7 @@ var (
// EnumerateEndpoints enumerates the endpoints. // EnumerateEndpoints enumerates the endpoints.
// //
// https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcnenumerateendpoints // https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcnenumerateendpoints
func EnumerateEndpoints() ([]guid.GUID, error) { func EnumerateEndpoints() ([]ole.GUID, error) {
var ( var (
endpointsJSON *uint16 endpointsJSON *uint16
errorRecord *uint16 errorRecord *uint16
@@ -59,7 +59,7 @@ func EnumerateEndpoints() ([]guid.GUID, error) {
return nil, fmt.Errorf("HcnEnumerateEndpoints failed: HRESULT 0x%X: %w", r1, hcs.Win32FromHResult(r1)) return nil, fmt.Errorf("HcnEnumerateEndpoints failed: HRESULT 0x%X: %w", r1, hcs.Win32FromHResult(r1))
} }
var endpoints []guid.GUID var endpoints []ole.GUID
if err := json.Unmarshal([]byte(result), &endpoints); err != nil { if err := json.Unmarshal([]byte(result), &endpoints); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
@@ -71,7 +71,7 @@ func EnumerateEndpoints() ([]guid.GUID, error) {
// OpenEndpoint opens an endpoint. // OpenEndpoint opens an endpoint.
// //
// https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcnopenendpoint // https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcnopenendpoint
func OpenEndpoint(id guid.GUID) (Endpoint, error) { func OpenEndpoint(id ole.GUID) (Endpoint, error) {
var ( var (
endpoint Endpoint endpoint Endpoint
errorRecord *uint16 errorRecord *uint16

View File

@@ -18,7 +18,7 @@
package hcn package hcn
import ( import (
"github.com/prometheus-community/windows_exporter/internal/headers/guid" "github.com/go-ole/go-ole"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@@ -38,7 +38,7 @@ type EndpointPropertiesResources struct {
Allocators []EndpointPropertiesAllocators `json:"Allocators"` Allocators []EndpointPropertiesAllocators `json:"Allocators"`
} }
type EndpointPropertiesAllocators struct { type EndpointPropertiesAllocators struct {
AdapterNetCfgInstanceId *guid.GUID `json:"AdapterNetCfgInstanceId"` AdapterNetCfgInstanceId *ole.GUID `json:"AdapterNetCfgInstanceId"`
} }
type EndpointStats struct { type EndpointStats struct {

View File

@@ -22,7 +22,7 @@ import (
"fmt" "fmt"
"unsafe" "unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/guid" "github.com/go-ole/go-ole"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@@ -152,7 +152,7 @@ func GetIfEntry2Ex(row *MIB_IF_ROW2) error {
// locally unique identifier (LUID) for the interface. // locally unique identifier (LUID) for the interface.
// //
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-convertinterfaceguidtoluid // https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-convertinterfaceguidtoluid
func ConvertInterfaceGUIDToLUID(guid guid.GUID) (uint64, error) { func ConvertInterfaceGUIDToLUID(guid ole.GUID) (uint64, error) {
var luid uint64 var luid uint64
ret, _, _ := procConvertInterfaceGuidToLuid.Call( ret, _, _ := procConvertInterfaceGuidToLuid.Call(
uintptr(unsafe.Pointer(&guid)), uintptr(unsafe.Pointer(&guid)),

View File

@@ -21,7 +21,7 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"github.com/prometheus-community/windows_exporter/internal/headers/guid" "github.com/go-ole/go-ole"
) )
// MIB_TCPROW_OWNER_PID structure for IPv4. // MIB_TCPROW_OWNER_PID structure for IPv4.
@@ -120,7 +120,7 @@ const (
type MIB_IF_ROW2 struct { type MIB_IF_ROW2 struct {
InterfaceLuid uint64 InterfaceLuid uint64
InterfaceIndex uint32 InterfaceIndex uint32
InterfaceGuid guid.GUID InterfaceGuid ole.GUID
Alias [IF_MAX_STRING_SIZE + 1]uint16 Alias [IF_MAX_STRING_SIZE + 1]uint16
Description [IF_MAX_STRING_SIZE + 1]uint16 Description [IF_MAX_STRING_SIZE + 1]uint16
PhysicalAddressLength uint32 PhysicalAddressLength uint32

View File

@@ -0,0 +1,55 @@
// 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 propsys
import (
"fmt"
"unsafe"
"github.com/go-ole/go-ole"
"golang.org/x/sys/windows"
)
//nolint:gochecknoglobals
var (
modPropsys = windows.NewLazySystemDLL("propsys.dll")
procPSGetPropertyKeyFromName = modPropsys.NewProc("PSGetPropertyKeyFromName")
)
type PROPERTYKEY struct {
Fmtid ole.GUID
Pid uint32
}
func PSGetPropertyKeyFromName(name string, key *PROPERTYKEY) error {
namePtr, err := windows.UTF16PtrFromString(name)
if err != nil {
return fmt.Errorf("failed to convert name to UTF16: %w", err)
}
hr, _, err := procPSGetPropertyKeyFromName.Call(
uintptr(unsafe.Pointer(namePtr)),
uintptr(unsafe.Pointer(key)),
)
if hr != 0 {
return fmt.Errorf("PSGetPropertyKeyFromName failed: %w", err)
}
return nil
}

View File

@@ -0,0 +1,84 @@
// 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 shell32
import (
"errors"
"fmt"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/propsys"
"golang.org/x/sys/windows"
)
//nolint:gochecknoglobals
var (
modShell32 = windows.NewLazySystemDLL("shell32.dll")
procSHCreateItemFromParsingName = modShell32.NewProc("SHCreateItemFromParsingName")
iidIShellItem2 = ole.NewGUID("{7E9FB0D3-919F-4307-AB2E-9B1860310C93}")
)
func SHCreateItemFromParsingName(path string) (*IShellItem2, error) {
ptrPath, err := windows.UTF16PtrFromString(path)
if err != nil {
return nil, fmt.Errorf("failed to convert path to UTF16: %w", err)
}
var result *IShellItem2
hr, _, err := procSHCreateItemFromParsingName.Call(
uintptr(unsafe.Pointer(ptrPath)),
0,
uintptr(unsafe.Pointer(iidIShellItem2)),
uintptr(unsafe.Pointer(&result)),
)
if hr != 0 {
return nil, fmt.Errorf("syscall failed: %w", err)
}
if result == nil {
return nil, errors.New("SHCreateItemFromParsingName returned nil")
}
return result, nil
}
func (item *IShellItem2) GetProperty(key *propsys.PROPERTYKEY, v *ole.VARIANT) error {
hr, _, err := syscall.SyscallN(
item.lpVtbl.GetProperty,
uintptr(unsafe.Pointer(item)),
uintptr(unsafe.Pointer(key)),
uintptr(unsafe.Pointer(v)),
)
if hr != 0 {
return fmt.Errorf("GetProperty failed: %w", err)
}
return nil
}
func (item *IShellItem2) Release() {
_, _, _ = syscall.SyscallN(
item.lpVtbl.Release,
uintptr(unsafe.Pointer(item)),
)
}

View File

@@ -0,0 +1,51 @@
// 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 shell32
type IShellItem2 struct {
lpVtbl *IShellItem2Vtbl
}
type IShellItem2Vtbl struct {
// IUnknown
QueryInterface uintptr
AddRef uintptr
Release uintptr
// IShellItem
BindToHandler uintptr
GetParent uintptr
GetDisplayName uintptr
GetAttributes uintptr
Compare uintptr
// IShellItem2
GetPropertyStore uintptr
GetPropertyStoreWithCreateObject uintptr
GetPropertyStoreForKeys uintptr
GetPropertyDescriptionList uintptr
Update uintptr
GetProperty uintptr
GetCLSID uintptr
GetFileTime uintptr
GetInt32 uintptr
GetString uintptr
GetUInt32 uintptr
GetUInt64 uintptr
GetBool uintptr
}