Skip to content

Commit

Permalink
Using Win32 API instead WMI
Browse files Browse the repository at this point in the history
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
  • Loading branch information
jkroepke committed May 18, 2024
1 parent f2c14ae commit 639d393
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 79 deletions.
12 changes: 11 additions & 1 deletion docs/collector.logical_disk.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ If given, a disk needs to *not* match the exclude regexp in order for the corres

Name | Description | Type | Labels
-----|-------------|------|-------
`windows_logical_disk_info` | A metric with a constant '1' value labeled with logical disk information | gauge | `disk`,`partition`,`filesystem`,`serial_number`,`volume`,`volume_name`
`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_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`
Expand All @@ -37,6 +37,7 @@ Name | Description | Type | Labels
`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_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`

### 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.
Expand All @@ -48,6 +49,15 @@ Query the rate of write operations to a disk
rate(windows_logical_disk_read_bytes_total{instance="localhost", volume=~"C:"}[2m])
```

Logical Volume information
```
windows_logical_disk_info{disk_id="0",filesystem="",serial_number="",type="",volume="HarddiskVolume2",volume_name=""} 1
windows_logical_disk_info{disk_id="0",filesystem="",serial_number="",type="",volume="HarddiskVolume3",volume_name=""} 1
windows_logical_disk_info{disk_id="0",filesystem="NTFS",serial_number="668EEC37",type="fixed",volume="C:",volume_name="Windows"} 1
windows_logical_disk_info{disk_id="1",filesystem="NTFS",serial_number="50AE953B",type="fixed",volume="D:",volume_name="Temporary Storage"} 1
windows_logical_disk_info{disk_id="1",filesystem="ReFS",serial_number="C69B59AD",type="fixed",volume="G:",volume_name="Volume"} 1
```

## Useful queries
Calculate rate of total IOPS for disk
```
Expand Down
204 changes: 126 additions & 78 deletions pkg/collector/logical_disk/logical_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
package logical_disk

import (
"errors"
"encoding/binary"
"fmt"
"golang.org/x/sys/windows"
"regexp"
"strconv"
"strings"

"github.com/alecthomas/kingpin/v2"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus-community/windows_exporter/pkg/perflib"
"github.com/prometheus-community/windows_exporter/pkg/types"
"github.com/prometheus-community/windows_exporter/pkg/wmi"
"github.com/prometheus/client_golang/prometheus"
)

Expand All @@ -22,27 +23,8 @@ const (

FlagLogicalDiskVolumeExclude = "collector.logical_disk.volume-exclude"
FlagLogicalDiskVolumeInclude = "collector.logical_disk.volume-include"

win32LogicalDiskQuery = "SELECT VolumeName,DeviceID,FileSystem,VolumeSerialNumber FROM WIN32_LogicalDisk"
win32LogicalDiskToPartitionQuery = "SELECT Antecedent, Dependent FROM Win32_LogicalDiskToPartition"
)

var (
reDiskID = regexp.MustCompile(`Disk #([0-9]+), Partition #([0-9]+)`)
)

type Win32_LogicalDisk struct {
VolumeName string
VolumeSerialNumber string
DeviceID string
FileSystem string
}

type Win32_LogicalDiskToPartition struct {
Antecedent string
Dependent string
}

type Config struct {
VolumeInclude string `yaml:"volume_include"`
VolumeExclude string `yaml:"volume_exclude"`
Expand All @@ -61,6 +43,7 @@ type collector struct {
volumeExclude *string

Information *prometheus.Desc
ReadOnly *prometheus.Desc
RequestsQueued *prometheus.Desc
AvgReadQueue *prometheus.Desc
AvgWriteQueue *prometheus.Desc
Expand All @@ -82,6 +65,14 @@ type collector struct {
volumeExcludePattern *regexp.Regexp
}

type volumeInfo struct {
filesystem string
serialNumber string
label string
volumeType string
readonly float64
}

func New(logger log.Logger, config *Config) types.Collector {
if config == nil {
config = &ConfigDefaults
Expand Down Expand Up @@ -126,7 +117,13 @@ func (c *collector) Build() error {
c.Information = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "info"),
"A metric with a constant '1' value labeled with logical disk information",
[]string{"disk", "partition", "volume", "volume_name", "filesystem", "serial_number"},
[]string{"disk", "type", "volume", "volume_name", "filesystem", "serial_number"},
nil,
)
c.ReadOnly = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "readonly"),
"Whether the logical disk is read-only",
[]string{"volume"},
nil,
)
c.RequestsQueued = prometheus.NewDesc(
Expand Down Expand Up @@ -270,9 +267,6 @@ func (c *collector) Collect(ctx *types.ScrapeContext, ch chan<- prometheus.Metri
// - https://msdn.microsoft.com/en-us/library/ms803973.aspx - LogicalDisk object reference
type logicalDisk struct {
Name string
VolumeName string
DiskID string
PartID string
CurrentDiskQueueLength float64 `perflib:"Current Disk Queue Length"`
AvgDiskReadQueueLength float64 `perflib:"Avg. Disk Read Queue Length"`
AvgDiskWriteQueueLength float64 `perflib:"Avg. Disk Write Queue Length"`
Expand All @@ -292,31 +286,14 @@ type logicalDisk struct {
}

func (c *collector) collect(ctx *types.ScrapeContext, ch chan<- prometheus.Metric) error {
var dst_Win32_LogicalDisk []Win32_LogicalDisk
var dst_Win32_LogicalDiskToPartition []Win32_LogicalDiskToPartition

if err := wmi.Query(win32LogicalDiskQuery, &dst_Win32_LogicalDisk); err != nil {
return err
}
if len(dst_Win32_LogicalDisk) == 0 {
return errors.New("WMI query returned empty result set")
}
if err := wmi.Query(win32LogicalDiskToPartitionQuery, &dst_Win32_LogicalDiskToPartition); err != nil {
return err
}
if len(dst_Win32_LogicalDiskToPartition) == 0 {
return errors.New("WMI query returned empty result set")
}

var (
filesystem string
serialNumber string
volumeName string
diskID string
partID string
dst []logicalDisk
err error
diskID string
info volumeInfo
dst []logicalDisk
)
if err := perflib.UnmarshalObject(ctx.PerfObjects["LogicalDisk"], &dst, c.logger); err != nil {

if err = perflib.UnmarshalObject(ctx.PerfObjects["LogicalDisk"], &dst, c.logger); err != nil {
return err
}

Expand All @@ -327,46 +304,26 @@ func (c *collector) collect(ctx *types.ScrapeContext, ch chan<- prometheus.Metri
continue
}

filesystem = ""
serialNumber = ""
volumeName = ""
diskID = "-1"
partID = "-1"

for _, logicalDisk := range dst_Win32_LogicalDisk {
if logicalDisk.DeviceID == volume.Name {
filesystem = logicalDisk.FileSystem
serialNumber = logicalDisk.VolumeSerialNumber
volumeName = logicalDisk.VolumeName

break
}
diskID, err = getDiskIDByVolume(volume.Name)
if err != nil {
_ = level.Warn(c.logger).Log("msg", fmt.Sprintf("failed to get disk ID for %s", volume.Name), "err", err)

Check failure on line 309 in pkg/collector/logical_disk/logical_disk.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Sprintf can be replaced with string concatenation (perfsprint)

Check failure on line 309 in pkg/collector/logical_disk/logical_disk.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Sprintf can be replaced with string concatenation (perfsprint)
}

for _, logicalDisk := range dst_Win32_LogicalDiskToPartition {
if strings.HasSuffix(logicalDisk.Dependent, volume.Name+`"`) {
ret := reDiskID.FindStringSubmatch(logicalDisk.Antecedent)
if len(ret) == 3 {
diskID = ret[1]
partID = ret[2]
} else {
_ = level.Warn(c.logger).Log("msg", "failed to parse disk ID", "antecedent", logicalDisk.Antecedent)
}

break
}
info, err = getVolumeInfo(volume.Name)
if err != nil {
_ = level.Warn(c.logger).Log("msg", fmt.Sprintf("failed to get volume information for %s", volume.Name), "err", err)

Check failure on line 314 in pkg/collector/logical_disk/logical_disk.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Sprintf can be replaced with string concatenation (perfsprint)

Check failure on line 314 in pkg/collector/logical_disk/logical_disk.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Sprintf can be replaced with string concatenation (perfsprint)
}

ch <- prometheus.MustNewConstMetric(
c.Information,
prometheus.GaugeValue,
1,
diskID,
partID,
info.volumeType,
volume.Name,
volumeName,
filesystem,
serialNumber,
info.label,
info.filesystem,
info.serialNumber,
)

ch <- prometheus.MustNewConstMetric(
Expand Down Expand Up @@ -484,3 +441,94 @@ func (c *collector) collect(ctx *types.ScrapeContext, ch chan<- prometheus.Metri

return nil
}

func getDriveType(driveType uint32) string {
switch driveType {
case windows.DRIVE_UNKNOWN:
return "unknown"
case windows.DRIVE_NO_ROOT_DIR:
return "norootdir"
case windows.DRIVE_REMOVABLE:
return "removable"
case windows.DRIVE_FIXED:
return "fixed"
case windows.DRIVE_REMOTE:
return "remote"
case windows.DRIVE_CDROM:
return "cdrom"
case windows.DRIVE_RAMDISK:
return "ramdisk"
default:
return "unknown"
}
}

// getDiskIDByVolume returns the disk ID for a given volume.
func getDiskIDByVolume(rootDrive string) (string, error) {
// Open a volume handle to the Disk Root.
var err error
var f windows.Handle

// mode has to include FILE_SHARE permission to allow concurrent access to the disk.
mode := uint32(windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE)
f, err = windows.CreateFile(
windows.StringToUTF16Ptr(`\\.\`+rootDrive),
windows.GENERIC_READ, mode, nil, windows.OPEN_EXISTING, uint32(windows.FILE_ATTRIBUTE_READONLY), 0)

if err != nil {
return "", err
}

defer windows.Close(f)

controlCode := uint32(5636096) // IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS
volumeDiskExtents := make([]byte, 16*1024)

var bytesReturned uint32
err = windows.DeviceIoControl(f, controlCode, nil, 0, &volumeDiskExtents[0], uint32(len(volumeDiskExtents)), &bytesReturned, nil)
if err != nil {
return "", err
}

if uint(binary.LittleEndian.Uint32(volumeDiskExtents)) != 1 {
return "", fmt.Errorf("could not identify physical drive for %s", rootDrive)
}

diskId := strconv.FormatUint(uint64(binary.LittleEndian.Uint32(volumeDiskExtents[8:])), 10)

return diskId, nil
}

func getVolumeInfo(rootDrive string) (volumeInfo, error) {
if !strings.HasSuffix(rootDrive, ":") {
return volumeInfo{}, nil
}

volPath := windows.StringToUTF16Ptr(rootDrive + `\`)

volBufLabel := make([]uint16, windows.MAX_PATH+1)
volSerialNum := uint32(0)
fsFlags := uint32(0)
volBufType := make([]uint16, windows.MAX_PATH+1)

driveType := windows.GetDriveType(volPath)

err := windows.GetVolumeInformation(volPath, &volBufLabel[0], uint32(len(volBufLabel)),
&volSerialNum, nil, &fsFlags, &volBufType[0], uint32(len(volBufType)))

if err != nil {
if driveType != windows.DRIVE_CDROM && driveType != windows.DRIVE_REMOVABLE {
return volumeInfo{}, err
}

return volumeInfo{}, nil
}

return volumeInfo{
volumeType: getDriveType(driveType),
label: windows.UTF16PtrToString(&volBufLabel[0]),
filesystem: windows.UTF16PtrToString(&volBufType[0]),
serialNumber: fmt.Sprintf("%X", volSerialNum),
readonly: float64(fsFlags & windows.FILE_READ_ONLY_VOLUME),
}, nil
}

0 comments on commit 639d393

Please sign in to comment.