metric/system/diskio/diskstat_windows_helper.go (144 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 // +build windows package diskio import ( "fmt" "strings" "syscall" "unsafe" "github.com/shirou/gopsutil/v4/disk" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" "github.com/elastic/elastic-agent-libs/logp" ) const ( errorSuccess syscall.Errno = 0 // ioctlDiskPerformance is used to enable performance counters that provide disk performance information. ioctlDiskPerformance = 0x70020 ) var ( modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procGetLogicalDriveStringsW = modkernel32.NewProc("GetLogicalDriveStringsW") procGetDriveTypeW = modkernel32.NewProc("GetDriveTypeW") ) type logicalDrive struct { Name string UNCPath string } // ioCounters gets the diskio counters and maps them to the list of counterstat objects. func ioCounters(names ...string) (map[string]disk.IOCountersStat, error) { if err := enablePerformanceCounters(); err != nil { return nil, err } logicalDisks, err := getLogicalDriveStrings() if err != nil || len(logicalDisks) == 0 { return nil, err } ret := make(map[string]disk.IOCountersStat, len(logicalDisks)) for _, drive := range logicalDisks { // not get _Total or Harddrive if len(drive.Name) > 3 { continue } // filter by included devices if len(names) > 0 && !containsDrive(names, drive.Name) { continue } var counter diskPerformance err = ioCounter(drive.UNCPath, &counter) if err != nil { logp.Err("Could not return any performance counter values for %s .Error: %v", drive.UNCPath, err) continue } ret[drive.Name] = disk.IOCountersStat{ Name: drive.Name, ReadCount: uint64(counter.ReadCount), WriteCount: uint64(counter.WriteCount), ReadBytes: uint64(counter.BytesRead), WriteBytes: uint64(counter.BytesWritten), // Ticks (which is equal to 100 nanoseconds) will be converted to milliseconds for consistency reasons for both ReadTime and WriteTime (https://docs.microsoft.com/en-us/dotnet/api/system.timespan.ticks?redirectedfrom=MSDN&view=netframework-4.8#remarks) ReadTime: uint64(counter.ReadTime / 10000), WriteTime: uint64(counter.WriteTime / 10000), } } return ret, nil } // ioCounter calls syscall func CreateFile to generate a handler then executes the DeviceIoControl func in order to retrieve the metrics. func ioCounter(path string, diskPerformance *diskPerformance) error { utfPath, err := syscall.UTF16PtrFromString(path) if err != nil { return err } hFile, err := syscall.CreateFile(utfPath, syscall.GENERIC_READ|syscall.GENERIC_WRITE, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) if err != nil { return err } defer func() { _ = syscall.CloseHandle(hFile) }() var diskPerformanceSize uint32 return syscall.DeviceIoControl(hFile, ioctlDiskPerformance, nil, 0, (*byte)(unsafe.Pointer(diskPerformance)), uint32(unsafe.Sizeof(*diskPerformance)), &diskPerformanceSize, nil) } // enablePerformanceCounters will enable performance counters by adding the EnableCounterForIoctl registry key func enablePerformanceCounters() error { key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\partmgr", registry.READ|registry.WRITE) // closing handler for the registry key. If the key is not one of the predefined registry keys (which is the case here), a call the RegCloseKey function should be executed after using the handle. defer func() { if key != 0 { clErr := key.Close() if clErr != nil { logp.L().Named("diskio").Errorf("cannot close handler for HKLM:SYSTEM\\CurrentControlSet\\Services\\Partmgr\\EnableCounterForIoctl key in the registry: %s", clErr) } } }() if err != nil { return fmt.Errorf("cannot open new key in the registry in order to enable the performance counters: %w", err) } val, _, err := key.GetIntegerValue("EnableCounterForIoctl") if val != 1 || err != nil { if err = key.SetDWordValue("EnableCounterForIoctl", 1); err != nil { return fmt.Errorf("cannot create HKLM:SYSTEM\\CurrentControlSet\\Services\\Partmgr\\EnableCounterForIoctl key in the registry in order to enable the performance counters: %w", err) } logp.L().Named("diskio").Info("The registry key EnableCounterForIoctl at HKLM:SYSTEM\\CurrentControlSet\\Services\\Partmgr has been created in order to enable the performance counters") } return nil } // getLogicalDriveStrings calls the syscall GetLogicalDriveStrings in order to get the list of logical drives func getLogicalDriveStrings() ([]logicalDrive, error) { lpBuffer := make([]byte, 254) r1, _, e1 := syscall.SyscallN(procGetLogicalDriveStringsW.Addr(), uintptr(len(lpBuffer)), uintptr(unsafe.Pointer(&lpBuffer[0]))) if r1 == 0 { err := e1 if e1 != errorSuccess { err = syscall.EINVAL } return nil, err } logicalDrives := make([]logicalDrive, 0, len(lpBuffer)) for _, v := range lpBuffer { if v >= 65 && v <= 90 { s := string(v) if s == "A" || s == "B" { continue } path := s + ":" drive := logicalDrive{path, `\\.\` + path} if isValidLogicalDrive(path) { logicalDrives = append(logicalDrives, drive) } } } return logicalDrives, nil } func containsDrive(devices []string, disk string) bool { for _, vv := range devices { if vv == disk { return true } } return false } // isValidLogicalDrive should filter CD-ROM type drives based on https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-getdrivetypew func isValidLogicalDrive(path string) bool { utfPath, err := syscall.UTF16PtrFromString(path + `\`) if err != nil { return false } ret, _, err := syscall.SyscallN(procGetDriveTypeW.Addr(), uintptr(unsafe.Pointer(utfPath))) //DRIVE_NO_ROOT_DIR = 1 DRIVE_CDROM = 5 DRIVE_UNKNOWN = 0 DRIVE_RAMDISK = 6 if ret == 1 || ret == 5 || ret == 0 || ret == 6 || err != errorSuccess { //nolint: errorlint // keep old behaviour return false } //check for ramdisk label as the drive type is fixed in this case volumeLabel, err := GetVolumeLabel(utfPath) if err != nil { return false } if strings.ToLower(volumeLabel) == "ramdisk" { return false } return true } // GetVolumeLabel function will retrieve the volume label func GetVolumeLabel(path *uint16) (string, error) { lpVolumeNameBuffer := make([]uint16, 256) lpVolumeSerialNumber := uint32(0) lpMaximumComponentLength := uint32(0) lpFileSystemFlags := uint32(0) lpFileSystemNameBuffer := make([]uint16, 256) err := windows.GetVolumeInformation( path, &lpVolumeNameBuffer[0], uint32(len(lpVolumeNameBuffer)), &lpVolumeSerialNumber, &lpMaximumComponentLength, &lpFileSystemFlags, &lpFileSystemNameBuffer[0], uint32(len(lpFileSystemNameBuffer))) if err != nil { return "", err } return syscall.UTF16ToString(lpVolumeNameBuffer), nil }