File: Windows\Disk\WindowsDiskMetrics.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Diagnostics.ResourceMonitoring\Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj (Microsoft.Extensions.Diagnostics.ResourceMonitoring)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Shared.Instruments;
 
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Disk;
 
[SupportedOSPlatform("windows")]
internal sealed class WindowsDiskMetrics
{
    private const string DeviceKey = "system.device";
    private const string DirectionKey = "disk.io.direction";
 
    private static readonly KeyValuePair<string, object?> _directionReadTag = new(DirectionKey, "read");
    private static readonly KeyValuePair<string, object?> _directionWriteTag = new(DirectionKey, "write");
    private readonly ILogger<WindowsDiskMetrics> _logger;
    private readonly Dictionary<string, WindowsDiskIoRatePerfCounter> _diskIoRateCounters = new();
    private WindowsDiskIoTimePerfCounter? _diskIoTimePerfCounter;
 
    public WindowsDiskMetrics(
        ILogger<WindowsDiskMetrics>? logger,
        IMeterFactory meterFactory,
        IPerformanceCounterFactory performanceCounterFactory,
        TimeProvider timeProvider,
        IOptions<ResourceMonitoringOptions> options)
    {
        _logger = logger ?? NullLogger<WindowsDiskMetrics>.Instance;
        if (!options.Value.EnableDiskIoMetrics)
        {
            return;
        }
 
#pragma warning disable CA2000 // Dispose objects before losing scope
        // We don't dispose the meter because IMeterFactory handles that
        // It's a false-positive, see: https://github.com/dotnet/roslyn-analyzers/issues/6912.
        // Related documentation: https://github.com/dotnet/docs/pull/37170
        Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName);
#pragma warning restore CA2000 // Dispose objects before losing scope
 
        InitializeDiskCounters(performanceCounterFactory, timeProvider);
 
        // The metric is aligned with
        // https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio
        _ = meter.CreateObservableCounter(
            ResourceUtilizationInstruments.SystemDiskIo,
            GetDiskIoMeasurements,
            unit: "By",
            description: "Disk bytes transferred");
 
        // The metric is aligned with
        // https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskoperations
        _ = meter.CreateObservableCounter(
            ResourceUtilizationInstruments.SystemDiskOperations,
            GetDiskOperationMeasurements,
            unit: "{operation}",
            description: "Disk operations");
 
        // The metric is aligned with
        // https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemdiskio_time
        _ = meter.CreateObservableCounter(
            ResourceUtilizationInstruments.SystemDiskIoTime,
            GetDiskIoTimeMeasurements,
            unit: "s",
            description: "Time disk spent activated");
    }
 
    private void InitializeDiskCounters(IPerformanceCounterFactory performanceCounterFactory, TimeProvider timeProvider)
    {
        const string DiskCategoryName = "LogicalDisk";
        string[] instanceNames = performanceCounterFactory.GetCategoryInstances(DiskCategoryName)
            .Where(instanceName => !instanceName.Equals("_Total", StringComparison.OrdinalIgnoreCase))
            .ToArray();
        if (instanceNames.Length == 0)
        {
            return;
        }
 
        // Initialize disk performance counters for "system.disk.io_time" metric
        try
        {
            var ioTimePerfCounter = new WindowsDiskIoTimePerfCounter(
                performanceCounterFactory,
                timeProvider,
                DiskCategoryName,
                WindowsDiskPerfCounterNames.DiskIdleTimeCounter,
                instanceNames);
            ioTimePerfCounter.InitializeDiskCounters();
            _diskIoTimePerfCounter = ioTimePerfCounter;
        }
#pragma warning disable CA1031
        catch (Exception ex)
#pragma warning restore CA1031
        {
            Log.DiskIoPerfCounterException(_logger, WindowsDiskPerfCounterNames.DiskIdleTimeCounter, ex.Message);
        }
 
        // Initialize disk performance counters for "system.disk.io" and "system.disk.operations" metrics
        List<string> diskIoRatePerfCounterNames =
        [
            WindowsDiskPerfCounterNames.DiskWriteBytesCounter,
            WindowsDiskPerfCounterNames.DiskReadBytesCounter,
            WindowsDiskPerfCounterNames.DiskWritesCounter,
            WindowsDiskPerfCounterNames.DiskReadsCounter,
        ];
        foreach (string counterName in diskIoRatePerfCounterNames)
        {
            try
            {
                var ratePerfCounter = new WindowsDiskIoRatePerfCounter(
                    performanceCounterFactory,
                    timeProvider,
                    DiskCategoryName,
                    counterName,
                    instanceNames);
                ratePerfCounter.InitializeDiskCounters();
                _diskIoRateCounters.Add(counterName, ratePerfCounter);
            }
#pragma warning disable CA1031
            catch (Exception ex)
#pragma warning restore CA1031
            {
                Log.DiskIoPerfCounterException(_logger, counterName, ex.Message);
            }
        }
    }
 
    private IEnumerable<Measurement<long>> GetDiskIoMeasurements()
    {
        List<Measurement<long>> measurements = [];
 
        if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskWriteBytesCounter, out WindowsDiskIoRatePerfCounter? perSecondWriteCounter))
        {
            perSecondWriteCounter.UpdateDiskCounters();
            foreach (KeyValuePair<string, long> pair in perSecondWriteCounter.TotalCountDict)
            {
                measurements.Add(new Measurement<long>(pair.Value, new TagList { _directionWriteTag, new(DeviceKey, pair.Key) }));
            }
        }
 
        if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskReadBytesCounter, out WindowsDiskIoRatePerfCounter? perSecondReadCounter))
        {
            perSecondReadCounter.UpdateDiskCounters();
            foreach (KeyValuePair<string, long> pair in perSecondReadCounter.TotalCountDict)
            {
                measurements.Add(new Measurement<long>(pair.Value, new TagList { _directionReadTag, new(DeviceKey, pair.Key) }));
            }
        }
 
        return measurements;
    }
 
    private IEnumerable<Measurement<long>> GetDiskOperationMeasurements()
    {
        List<Measurement<long>> measurements = [];
 
        if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskWritesCounter, out WindowsDiskIoRatePerfCounter? writeCounter))
        {
            writeCounter.UpdateDiskCounters();
            foreach (KeyValuePair<string, long> pair in writeCounter.TotalCountDict)
            {
                measurements.Add(new Measurement<long>(pair.Value, new TagList { _directionWriteTag, new(DeviceKey, pair.Key) }));
            }
        }
 
        if (_diskIoRateCounters.TryGetValue(WindowsDiskPerfCounterNames.DiskReadsCounter, out WindowsDiskIoRatePerfCounter? readCounter))
        {
            readCounter.UpdateDiskCounters();
            foreach (KeyValuePair<string, long> pair in readCounter.TotalCountDict)
            {
                measurements.Add(new Measurement<long>(pair.Value, new TagList { _directionReadTag, new(DeviceKey, pair.Key) }));
            }
        }
 
        return measurements;
    }
 
    private IEnumerable<Measurement<double>> GetDiskIoTimeMeasurements()
    {
        List<Measurement<double>> measurements = [];
        if (_diskIoTimePerfCounter != null)
        {
            _diskIoTimePerfCounter.UpdateDiskCounters();
            foreach (KeyValuePair<string, double> pair in _diskIoTimePerfCounter.TotalSeconds)
            {
                measurements.Add(new Measurement<double>(pair.Value, new TagList { new(DeviceKey, pair.Key) }));
            }
        }
 
        return measurements;
    }
}