From be94825c8109f4487abfd68093f1a969d0043ff8 Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Fri, 11 Jul 2025 10:17:40 +0200 Subject: [PATCH 1/9] Draft --- .../KubernetesClusterMetadata.cs | 140 ++++++++++++++++++ .../KubernetesClusterMetadataExtensions.cs | 65 ++++++++ .../KubernetesClusterMetadataSource.cs | 94 ++++++++++++ .../KubernetesClusterMetadataValidator.cs | 11 ++ ...tensions.ClusterMetadata.Kubernetes.csproj | 29 ++++ ...ions.Diagnostics.ResourceMonitoring.csproj | 1 + .../WindowsContainerSnapshotProvider.cs | 39 +++-- 7 files changed, 366 insertions(+), 13 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs new file mode 100644 index 00000000000..f87a7c9b76a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; + +/// +/// Maintains metadata about Kubernetes cluster. +/// +public class KubernetesClusterMetadata +{ + /// + /// Gets or sets the name of the Kubernetes cluster. + /// + /// + /// Default value is an empty string. + /// + [Required] + public string ClusterName { get; set; } = string.Empty; + + /// + /// Gets or sets the name of a CronJob resource. + /// + /// + /// Default value is . + /// + public string? CronJob { get; set; } + + /// + /// Gets or sets the name of a DaemonSet resource. + /// + /// + /// Default value is . + /// + public string? DaemonSet { get; set; } + + /// + /// Gets or sets the name of a Deployment resource. + /// + /// + /// Default value is . + /// + public string? Deployment { get; set; } + + /// + /// Gets or sets the name of a Job resource. + /// + /// + /// Default value is . + /// + public string? Job { get; set; } + + /// + /// Gets or sets the name of a namespace where the service is deployed. + /// + /// + /// Default value is an empty string. + /// + [Required] + public string Namespace { get; set; } = string.Empty; + + /// + /// Gets or sets the name of a node where the service pod is running. + /// + /// + /// Default value is an empty string. + /// + [Required] + public string NodeName { get; set; } = string.Empty; + + /// + /// Gets or sets the name of a pod which is running the code. + /// + /// + /// Default value is an empty string. + /// + [Required] + public string PodName { get; set; } = string.Empty; + + /// + /// Gets or sets the name of a ReplicaSet resource. + /// + /// + /// Default value is . + /// + public string? ReplicaSet { get; set; } + + /// + /// Gets or sets the name of a StatefulSet resource. + /// + /// + /// Default value is . + /// + public string? StatefulSet { get; set; } + + /// + /// Gets or sets the name of an Azure cloud. + /// + /// + /// Default value is . + /// + public string? AzureCloud { get; set; } + + /// + /// Gets or sets the name of an Azure region. + /// + /// + /// Default value is . + /// + public string? AzureRegion { get; set; } + + /// + /// Gets or sets the name of an Azure geography. + /// + /// + /// Default value is . + /// + public string? AzureGeography { get; set; } + + /// + /// Gets or sets the resource memory limit the container is allowed to use. + /// + public ulong LimitsMemory { get; set; } + + /// + /// Gets or sets the resource CPU limit the container is allowed to use. + /// + public ulong LimitsCpu { get; set; } + + /// + /// Gets or sets the resource memory request the container is allowed to use. + /// + public ulong RequestsMemory { get; set; } + + /// + /// Gets or sets the resource CPU request the container is allowed to use. + /// + public ulong RequestsCpu { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs new file mode 100644 index 00000000000..76883b79397 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; + +/// +/// Extensions for Kubernetes metadata. +/// +public static class KubernetesClusterMetadataExtensions +{ + private const string DefaultSectionName = "clustermetadata:kubernetes"; + + /// + /// Registers configuration provider for Kubernetes metadata. + /// + /// The configuration builder. + /// Section name to save configuration into. Default set to "clustermetadata:kubernetes". + /// A prefix for environment variable names that have Kubernetes cluster information. + /// The input configuration builder for call chaining. + public static IConfigurationBuilder AddKubernetesClusterMetadata(this IConfigurationBuilder builder, string sectionName = DefaultSectionName, string environmentVariablePrefix = "") + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrWhitespace(sectionName); + _ = Throw.IfNull(environmentVariablePrefix); + + return builder.Add(new KubernetesClusterMetadataSource(sectionName, environmentVariablePrefix)); + } + + /// + /// Adds an instance of to the . + /// + /// The to add the services to. + /// The configuration section to bind. + /// The so that additional calls can be chained. + public static IServiceCollection AddKubernetesClusterMetadata(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + _ = services.AddOptionsWithValidateOnStart().Bind(section); + + return services; + } + + /// + /// Adds an instance of to the . + /// + /// The to add the services to. + /// The delegate to configure with. + /// The so that additional calls can be chained. + public static IServiceCollection AddKubernetesClusterMetadata(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services.AddOptionsWithValidateOnStart().Configure(configure); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs new file mode 100644 index 00000000000..cf17389db82 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; + +internal class KubernetesClusterMetadataSource : IConfigurationSource +{ + public string SectionName { get; } + private KubernetesClusterMetadata KubernetesClusterMetadata { get; } + private readonly string _environmentVariablePrefix; + + public KubernetesClusterMetadataSource(Func configure) + { + + } + + public KubernetesClusterMetadataSource(string sectionName, string environmentVariablePrefix = "") + { + SectionName = Throw.IfNullOrWhitespace(sectionName); + _environmentVariablePrefix = Throw.IfNull(environmentVariablePrefix); + + KubernetesClusterMetadata = InitializeKubernetesClusterMetadata(); + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) => new MemoryConfigurationProvider(new MemoryConfigurationSource()) + { + { $"{SectionName}:{nameof(KubernetesClusterMetadata.ClusterName)}", KubernetesClusterMetadata.ClusterName }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.CronJob)}", KubernetesClusterMetadata.CronJob }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.DaemonSet)}", KubernetesClusterMetadata.DaemonSet }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.Deployment)}", KubernetesClusterMetadata.Deployment }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.Job)}", KubernetesClusterMetadata.Job }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.Namespace)}", KubernetesClusterMetadata.Namespace }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.NodeName)}", KubernetesClusterMetadata.NodeName }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.PodName)}", KubernetesClusterMetadata.PodName }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.ReplicaSet)}", KubernetesClusterMetadata.ReplicaSet }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.StatefulSet)}", KubernetesClusterMetadata.StatefulSet }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.AzureCloud)}", KubernetesClusterMetadata.AzureCloud }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.AzureRegion)}", KubernetesClusterMetadata.AzureRegion }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.AzureGeography)}", KubernetesClusterMetadata.AzureGeography }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.LimitsMemory)}", KubernetesClusterMetadata.LimitsMemory.ToString(CultureInfo.InvariantCulture) }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.LimitsCpu)}", KubernetesClusterMetadata.LimitsCpu.ToString(CultureInfo.InvariantCulture) }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.RequestsMemory)}", KubernetesClusterMetadata.RequestsMemory.ToString(CultureInfo.InvariantCulture) }, + { $"{SectionName}:{nameof(KubernetesClusterMetadata.RequestsCpu)}", KubernetesClusterMetadata.RequestsCpu.ToString(CultureInfo.InvariantCulture) }, + }; + + private KubernetesClusterMetadata InitializeKubernetesClusterMetadata() + { + return new KubernetesClusterMetadata + { + ClusterName = GetEnvironmentVariableOrThrow("CLUSTER_NAME", "CLUSTERNAME"), + CronJob = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}CRONJOB_NAME") ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}CRONJOBNAME"), + DaemonSet = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DAEMONSET_NAME") + ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DAEMONSETNAME"), + Deployment = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DEPLOYMENT_NAME") + ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DEPLOYMENTNAME"), + Job = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}JOB_NAME") ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}JOBNAME"), + Namespace = GetEnvironmentVariableOrThrow("NAMESPACE", environmentVariableAltName: null), + NodeName = GetEnvironmentVariableOrThrow("NODE_NAME", "NODENAME"), + PodName = GetEnvironmentVariableOrThrow("POD_NAME", "PODNAME"), + ReplicaSet = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REPLICASET_NAME") + ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REPLICASETNAME"), + StatefulSet = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}STATEFULSET_NAME") + ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}STATEFULSETNAME"), + AzureCloud = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURE_CLOUD") + ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURECLOUD"), + AzureRegion = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURE_REGION") + ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZUREREGION"), + AzureGeography = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURE_GEOGRAPHY") + ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZUREGEOGRAPHY"), + + LimitsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_MEMORY"), CultureInfo.InvariantCulture), + LimitsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_CPU"), CultureInfo.InvariantCulture), + RequestsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_MEMORY"), CultureInfo.InvariantCulture), + RequestsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_CPU"), CultureInfo.InvariantCulture) + }; + } + + private string GetEnvironmentVariableOrThrow(string environmentVariableName, string? environmentVariableAltName) + { + var result = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}{environmentVariableName}"); + + if (result == null && environmentVariableAltName != null) + { + result = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}{environmentVariableAltName}"); + } + + return result ?? throw new InvalidOperationException($"Environment variable {_environmentVariablePrefix}{environmentVariableName} is not set."); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs new file mode 100644 index 00000000000..8ebe41d669f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; + +[OptionsValidator] +internal sealed partial class KubernetesClusterMetadataValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj new file mode 100644 index 00000000000..54c7f6b9918 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj @@ -0,0 +1,29 @@ + + + + Microsoft.Extensions.ClusterMetadata.Kubernetes + + $(NetCoreTargetFrameworks)$(ConditionalNet462) + enable + enable + true + $(NoWarn);CS0436 + + + + true + + + + dev + 100 + 80 + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj index 7cdb4b7de49..af2e6cd790a 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index ca6ceaff8bd..8657563db01 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Threading; +using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -53,9 +54,10 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider public WindowsContainerSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, - IOptions options) + IOptions options, + KubernetesClusterMetadata kubernetesClusterMetadataOptions) : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory, - static () => new JobHandleWrapper(), TimeProvider.System, options.Value) + static () => new JobHandleWrapper(), TimeProvider.System, options.Value, kubernetesClusterMetadataOptions) { } @@ -72,7 +74,8 @@ internal WindowsContainerSnapshotProvider( IMeterFactory meterFactory, Func createJobHandleObject, TimeProvider timeProvider, - ResourceMonitoringOptions options) + ResourceMonitoringOptions options, + KubernetesClusterMetadata kubernetesClusterMetadataOptions) { _logger = logger ?? NullLogger.Instance; _logger.RunningInsideJobObject(); @@ -89,16 +92,26 @@ internal WindowsContainerSnapshotProvider( using IJobHandle jobHandle = _createJobHandleObject(); - ulong memoryLimitLong = GetMemoryLimit(jobHandle); - _memoryLimit = memoryLimitLong; - _cpuLimit = GetCpuLimit(jobHandle, systemInfo); - - // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). - // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). - double cpuRequest = _cpuLimit; - ulong memoryRequest = memoryLimitLong; - Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); - _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); + if (kubernetesClusterMetadataOptions != null) + { + _cpuRequest = ConvertMillicoreToUnit(kubernetesClusterMetadataOptions.RequestsCpu); + _cpuLimit = ConvertMillicoreToUnit(kubernetesClusterMetadataOptions.LimitsCpu); + _memoryRequest = kubernetesClusterMetadataOptions.RequestsMemory; + _memoryLimit = kubernetesClusterMetadataOptions.LimitsMemory; + } + else + { + ulong memoryLimitLong = GetMemoryLimit(jobHandle); + _memoryLimit = memoryLimitLong; + _cpuLimit = GetCpuLimit(jobHandle, systemInfo); + + // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). + // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). + double cpuRequest = _cpuLimit; + ulong memoryRequest = memoryLimitLong; + Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); + _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); + } var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); _oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; From bb36737763779730b1cad0283ca686dd45f12e6e Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Wed, 30 Jul 2025 14:25:13 +0200 Subject: [PATCH 2/9] Draft --- .../KubernetesClusterMetadata.cs | 24 -- .../KubernetesClusterMetadataExtensions.cs | 1 - .../KubernetesClusterMetadataSource.cs | 5 - ...tensions.ClusterMetadata.Kubernetes.csproj | 9 +- ...Extensions.ClusterMetadata.Kubernetes.json | 0 .../KubernetesDataProvider.cs | 58 +++++ .../ResourceMonitoringOptions.Windows.cs | 8 + ...ceMonitoringServiceCollectionExtensions.cs | 2 + ...ernetesWindowsContainerSnapshotProvider.cs | 185 +++++++++++++++ ...UnifiedWindowsContainerSnapshotProvider.cs | 213 ++++++++++++++++++ .../WindowsContainerSnapshotProvider.cs | 39 ++-- .../ResourceUtilizationInstruments.cs | 8 + .../ResourceMonitoringBuilderTests.cs | 55 +++++ 13 files changed, 548 insertions(+), 59 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.json create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs index f87a7c9b76a..0cf3a2d0d33 100644 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs @@ -94,30 +94,6 @@ public class KubernetesClusterMetadata /// public string? StatefulSet { get; set; } - /// - /// Gets or sets the name of an Azure cloud. - /// - /// - /// Default value is . - /// - public string? AzureCloud { get; set; } - - /// - /// Gets or sets the name of an Azure region. - /// - /// - /// Default value is . - /// - public string? AzureRegion { get; set; } - - /// - /// Gets or sets the name of an Azure geography. - /// - /// - /// Default value is . - /// - public string? AzureGeography { get; set; } - /// /// Gets or sets the resource memory limit the container is allowed to use. /// diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs index 76883b79397..fe846dd8123 100644 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Shared.Diagnostics; diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs index cf17389db82..7c32ca04e5c 100644 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs @@ -14,11 +14,6 @@ internal class KubernetesClusterMetadataSource : IConfigurationSource private KubernetesClusterMetadata KubernetesClusterMetadata { get; } private readonly string _environmentVariablePrefix; - public KubernetesClusterMetadataSource(Func configure) - { - - } - public KubernetesClusterMetadataSource(string sectionName, string environmentVariablePrefix = "") { SectionName = Throw.IfNullOrWhitespace(sectionName); diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj index 54c7f6b9918..29975023508 100644 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj @@ -7,17 +7,20 @@ enable enable true + Telemetry $(NoWarn);CS0436 true + true - + dev - 100 - 80 + EXTEXP0015 + 99 + 90 diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.json b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs new file mode 100644 index 00000000000..6615b4afad1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs @@ -0,0 +1,58 @@ +// 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.Diagnostics; +using Microsoft.Extensions.ClusterMetadata.Kubernetes; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; + +/// +/// Provides resource data from Kubernetes metadata for Windows containers. +/// +internal sealed class KubernetesResourceDataProvider : IWindowsResourceDataProvider +{ + private readonly KubernetesClusterMetadata _kubernetesMetadata; + + public KubernetesResourceDataProvider(KubernetesClusterMetadata kubernetesMetadata) + { + _kubernetesMetadata = kubernetesMetadata; + } + + public long GetCurrentCpuTicks() + { + using var process = Process.GetCurrentProcess(); + return process.TotalProcessorTime.Ticks; + } + + public ulong GetCurrentMemoryUsage() + { + return (ulong)Environment.WorkingSet; + } + + public (long userTime, long kernelTime) GetCpuTimeBreakdown() + { + using var process = Process.GetCurrentProcess(); + return (process.UserProcessorTime.Ticks, process.PrivilegedProcessorTime.Ticks); + } + + public WindowsResourceLimits GetResourceLimits() + { + double cpuRequest = ConvertMillicoreToUnit(_kubernetesMetadata.RequestsCpu); + double cpuLimit = ConvertMillicoreToUnit(_kubernetesMetadata.LimitsCpu); + ulong memoryRequest = _kubernetesMetadata.RequestsMemory; + ulong memoryLimit = _kubernetesMetadata.LimitsMemory; + + return new WindowsResourceLimits( + CpuLimit: cpuLimit, + CpuRequest: cpuRequest, + MemoryLimit: memoryLimit, + MemoryRequest: memoryRequest, + HasRequests: true); + } + + private static double ConvertMillicoreToUnit(ulong millicores) + { + return millicores / 1000.0; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs index 9e8636506c7..d80822539b2 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs @@ -33,4 +33,12 @@ public partial class ResourceMonitoringOptions /// [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] public bool UseZeroToOneRangeForMetrics { get; set; } + + /// + /// Gets or sets a value indicating whether CPU and Memory utilization metrics will be calculated from Kubernetes container's limits and requests. + /// + /// + /// The default value is . + /// + public bool UseKubernetesLimitsAndRequests { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index c368e2bff91..5838aa2e4b9 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -6,6 +6,8 @@ using System.Runtime.Versioning; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +//using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + #if !NETFRAMEWORK using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs new file mode 100644 index 00000000000..78ffa3e6e7a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs @@ -0,0 +1,185 @@ +// 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.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.ClusterMetadata.Kubernetes; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Instruments; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +/// +/// Provides resource monitoring for containers running in Kubernetes environments using cluster metadata. +/// +internal sealed class KubernetesWindowsContainerSnapshotProvider : ISnapshotProvider +{ + private const double One = 1.0d; + private const double Hundred = 100.0d; + private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; + + private readonly object _cpuLocker = new(); + private readonly object _memoryLocker = new(); + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + private readonly KubernetesClusterMetadata _kubernetesMetadata; + private readonly TimeSpan _cpuRefreshInterval; + private readonly TimeSpan _memoryRefreshInterval; + private readonly double _metricValueMultiplier; + + private long _oldCpuUsageTicks; + private long _oldCpuTimeTicks; + private DateTimeOffset _refreshAfterCpu; + private DateTimeOffset _refreshAfterMemory; + private double _cpuPercentage = double.NaN; + private double _memoryPercentage; + + /// + /// Gets the static values of CPU and memory limitations defined by Kubernetes metadata. + /// + public SystemResources Resources { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The meter factory for creating metrics. + /// The resource monitoring options. + /// The Kubernetes cluster metadata containing resource limits and requests. + public KubernetesWindowsContainerSnapshotProvider( + ILogger? logger, + IMeterFactory meterFactory, + IOptions options, + KubernetesClusterMetadata kubernetesMetadata) + : this(logger, meterFactory, TimeProvider.System, options.Value, kubernetesMetadata) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The meter factory for creating metrics. + /// The time provider. + /// The resource monitoring options. + /// The Kubernetes cluster metadata containing resource limits and requests. + /// This constructor enables the mocking of dependencies for the purpose of Unit Testing only. + [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Dependencies for testing")] + internal KubernetesWindowsContainerSnapshotProvider( + ILogger? logger, + IMeterFactory meterFactory, + TimeProvider timeProvider, + ResourceMonitoringOptions options, + KubernetesClusterMetadata kubernetesMetadata) + { + _logger = logger ?? NullLogger.Instance; + _timeProvider = timeProvider; + _kubernetesMetadata = kubernetesMetadata; + + _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; + _cpuRefreshInterval = options.CpuConsumptionRefreshInterval; + _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; + + // Convert CPU from millicores to units (e.g., 1000m = 1.0 CPU) + double cpuRequest = ConvertMillicoreToUnit(kubernetesMetadata.RequestsCpu); + double cpuLimit = ConvertMillicoreToUnit(kubernetesMetadata.LimitsCpu); + + // Memory values are already in bytes + ulong memoryRequest = kubernetesMetadata.RequestsMemory; + ulong memoryLimit = kubernetesMetadata.LimitsMemory; + + Resources = new SystemResources(cpuRequest, cpuLimit, memoryRequest, memoryLimit); + + _logger.LogInformation("Kubernetes container resource limits initialized. CPU: {CpuLimit}, Memory: {MemoryLimit}MB, Pod: {PodName}", + cpuLimit, memoryLimit / (1024 * 1024), kubernetesMetadata.PodName); + + // Initialize tracking variables + _oldCpuUsageTicks = GetCurrentCpuTicks(); + _oldCpuTimeTicks = _timeProvider.GetUtcNow().Ticks; + _refreshAfterCpu = _timeProvider.GetUtcNow(); + _refreshAfterMemory = _timeProvider.GetUtcNow(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + // We don't dispose the meter because IMeterFactory handles that + Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); +#pragma warning restore CA2000 // Dispose objects before losing scope + + // Container based metrics: + _ = meter.CreateObservableCounter( + name: ResourceUtilizationInstruments.ContainerCpuTime, + observeValues: , + unit: "s", + description: "CPU time used by the Kubernetes container."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + observeValue: , + description: "CPU utilization percentage against Kubernetes CPU limits."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, + observeValue: , + description: "Memory utilization percentage against Kubernetes memory limits."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, + observeValue: , + description: "CPU utilization percentage against Kubernetes CPU requests."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, + observeValue: , + description: "Memory utilization percentage against Kubernetes memory requests."); + } + + /// + /// Get a snapshot of the resource utilization of the system. + /// + /// A snapshot containing current resource usage information. + public Snapshot GetSnapshot() + { + // Implementation will read from /proc/stat, /proc/meminfo, or cgroup files + // depending on the container runtime and cgroup version + throw new NotImplementedException("Snapshot implementation will read from system files to get current CPU and memory usage"); + } + + /// + /// Converts CPU value from millicores to CPU units. + /// + /// CPU value in millicores (e.g., 1000m = 1 CPU). + /// CPU value in units (e.g., 1.0 = 1 CPU). + private static double ConvertMillicoreToUnit(ulong millicores) + { + return millicores / 1000.0; + } + + + /// + /// Calculates CPU percentage utilization. + /// + /// CPU utilization percentage. + private double CpuPercentage() + { + } + + /// + /// Calculates memory percentage utilization. + /// + /// Memory utilization percentage. + private double MemoryPercentage() + { + + } + + /// + /// Gets CPU time measurements for telemetry. + /// + /// CPU time measurements broken down by mode. + private IEnumerable> GetCpuTime() + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs new file mode 100644 index 00000000000..0a1e381e53d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs @@ -0,0 +1,213 @@ +// 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.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.Instruments; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; + +/// +/// Unified Windows container snapshot provider that works with different data sources. +/// +internal sealed class UnifiedWindowsContainerSnapshotProvider : ISnapshotProvider +{ + private const double One = 1.0d; + private const double Hundred = 100.0d; + private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; + + private readonly object _cpuLocker = new(); + private readonly object _memoryLocker = new(); + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + private readonly IWindowsResourceDataProvider _resourceDataProvider; + private readonly WindowsResourceLimits _resourceLimits; + private readonly TimeSpan _cpuRefreshInterval; + private readonly TimeSpan _memoryRefreshInterval; + private readonly double _metricValueMultiplier; + + private long _oldCpuUsageTicks; + private long _oldCpuTimeTicks; + private DateTimeOffset _refreshAfterCpu; + private DateTimeOffset _refreshAfterMemory; + private double _cpuPercentage = double.NaN; + private double _memoryPercentage; + + public SystemResources Resources { get; } + + public UnifiedWindowsContainerSnapshotProvider( + ILogger? logger, + IMeterFactory meterFactory, + TimeProvider timeProvider, + IWindowsResourceDataProvider resourceDataProvider, + ResourceMonitoringOptions options) + { + _logger = logger ?? NullLogger.Instance; + _timeProvider = timeProvider; + _resourceDataProvider = resourceDataProvider; + _resourceLimits = resourceDataProvider.GetResourceLimits(); + + _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; + _cpuRefreshInterval = options.CpuConsumptionRefreshInterval; + _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; + + Resources = new SystemResources( + _resourceLimits.CpuRequest, + _resourceLimits.CpuLimit, + _resourceLimits.MemoryRequest, + _resourceLimits.MemoryLimit); + + _logger.LogInformation("Container resource limits initialized. CPU: {CpuLimit}, Memory: {MemoryLimit}MB, HasRequests: {HasRequests}", + _resourceLimits.CpuLimit, _resourceLimits.MemoryLimit / (1024 * 1024), _resourceLimits.HasRequests); + + // Initialize tracking variables + _oldCpuUsageTicks = _resourceDataProvider.GetCurrentCpuTicks(); + _oldCpuTimeTicks = _timeProvider.GetUtcNow().Ticks; + _refreshAfterCpu = _timeProvider.GetUtcNow(); + _refreshAfterMemory = _timeProvider.GetUtcNow(); + + RegisterMetrics(meterFactory); + } + + public Snapshot GetSnapshot() + { + var (userTime, kernelTime) = _resourceDataProvider.GetCpuTimeBreakdown(); + var memoryUsage = _resourceDataProvider.GetCurrentMemoryUsage(); + + return new Snapshot( + TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), + TimeSpan.FromTicks(kernelTime), + TimeSpan.FromTicks(userTime), + memoryUsage); + } + + private void RegisterMetrics(IMeterFactory meterFactory) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + var meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); +#pragma warning restore CA2000 // Dispose objects before losing scope + + // Always register these metrics + _ = meter.CreateObservableCounter( + name: ResourceUtilizationInstruments.ContainerCpuTime, + observeValues: GetCpuTime, + unit: "s", + description: "CPU time used by the container."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + observeValue: () => CpuPercentage(_resourceLimits.CpuLimit), + description: "CPU utilization percentage against container CPU limits."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, + observeValue: () => MemoryPercentage(_resourceLimits.MemoryLimit), + description: "Memory utilization percentage against container memory limits."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessCpuUtilization, + observeValue: () => CpuPercentage(_resourceLimits.CpuLimit), + description: "Process CPU utilization."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessMemoryUtilization, + observeValue: () => MemoryPercentage(_resourceLimits.MemoryLimit), + description: "Process memory utilization."); + + // Only register request metrics when we have meaningful request values + if (_resourceLimits.HasRequests) + { + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, + observeValue: () => CpuPercentage(_resourceLimits.CpuRequest), + description: "CPU utilization percentage against container CPU requests."); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, + observeValue: () => MemoryPercentage(_resourceLimits.MemoryRequest), + description: "Memory utilization percentage against container memory requests."); + } + } + + private double CpuPercentage(double cpuLimit) + { + var now = _timeProvider.GetUtcNow(); + + lock (_cpuLocker) + { + if (now < _refreshAfterCpu) + { + return _cpuPercentage; + } + } + + var currentCpuTicks = _resourceDataProvider.GetCurrentCpuTicks(); + + lock (_cpuLocker) + { + if (now >= _refreshAfterCpu) + { + var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks; + var timeTickDelta = (now.Ticks - _oldCpuTimeTicks) * cpuLimit; + + if (usageTickDelta > 0 && timeTickDelta > 0) + { + _cpuPercentage = Math.Min(_metricValueMultiplier, usageTickDelta / timeTickDelta * _metricValueMultiplier); + + _oldCpuUsageTicks = currentCpuTicks; + _oldCpuTimeTicks = now.Ticks; + _refreshAfterCpu = now.Add(_cpuRefreshInterval); + } + } + + return _cpuPercentage; + } + } + + private double MemoryPercentage(ulong memoryLimit) + { + var now = _timeProvider.GetUtcNow(); + + lock (_memoryLocker) + { + if (now < _refreshAfterMemory) + { + return _memoryPercentage; + } + } + + var memoryUsage = _resourceDataProvider.GetCurrentMemoryUsage(); + + lock (_memoryLocker) + { + if (now >= _refreshAfterMemory) + { + if (memoryLimit > 0) + { + _memoryPercentage = Math.Min(_metricValueMultiplier, memoryUsage / (double)memoryLimit * _metricValueMultiplier); + } + + _refreshAfterMemory = now.Add(_memoryRefreshInterval); + } + + _logger.LogDebug("Memory usage: {MemoryUsage}MB, Limit: {MemoryLimit}MB, Percentage: {MemoryPercentage}%", + memoryUsage / (1024 * 1024), memoryLimit / (1024 * 1024), _memoryPercentage); + + return _memoryPercentage; + } + } + + private IEnumerable> GetCpuTime() + { + var (userTime, kernelTime) = _resourceDataProvider.GetCpuTimeBreakdown(); + + yield return new Measurement(userTime / TicksPerSecondDouble, + [new KeyValuePair("cpu.mode", "user")]); + yield return new Measurement(kernelTime / TicksPerSecondDouble, + [new KeyValuePair("cpu.mode", "system")]); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 8657563db01..ca6ceaff8bd 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Threading; -using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -54,10 +53,9 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider public WindowsContainerSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, - IOptions options, - KubernetesClusterMetadata kubernetesClusterMetadataOptions) + IOptions options) : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory, - static () => new JobHandleWrapper(), TimeProvider.System, options.Value, kubernetesClusterMetadataOptions) + static () => new JobHandleWrapper(), TimeProvider.System, options.Value) { } @@ -74,8 +72,7 @@ internal WindowsContainerSnapshotProvider( IMeterFactory meterFactory, Func createJobHandleObject, TimeProvider timeProvider, - ResourceMonitoringOptions options, - KubernetesClusterMetadata kubernetesClusterMetadataOptions) + ResourceMonitoringOptions options) { _logger = logger ?? NullLogger.Instance; _logger.RunningInsideJobObject(); @@ -92,26 +89,16 @@ internal WindowsContainerSnapshotProvider( using IJobHandle jobHandle = _createJobHandleObject(); - if (kubernetesClusterMetadataOptions != null) - { - _cpuRequest = ConvertMillicoreToUnit(kubernetesClusterMetadataOptions.RequestsCpu); - _cpuLimit = ConvertMillicoreToUnit(kubernetesClusterMetadataOptions.LimitsCpu); - _memoryRequest = kubernetesClusterMetadataOptions.RequestsMemory; - _memoryLimit = kubernetesClusterMetadataOptions.LimitsMemory; - } - else - { - ulong memoryLimitLong = GetMemoryLimit(jobHandle); - _memoryLimit = memoryLimitLong; - _cpuLimit = GetCpuLimit(jobHandle, systemInfo); - - // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). - // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). - double cpuRequest = _cpuLimit; - ulong memoryRequest = memoryLimitLong; - Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); - _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); - } + ulong memoryLimitLong = GetMemoryLimit(jobHandle); + _memoryLimit = memoryLimitLong; + _cpuLimit = GetCpuLimit(jobHandle, systemInfo); + + // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). + // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). + double cpuRequest = _cpuLimit; + ulong memoryRequest = memoryLimitLong; + Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); + _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); _oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index 835d3099782..01529d44856 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -50,6 +50,14 @@ internal static class ResourceUtilizationInstruments /// public const string ContainerMemoryLimitUtilization = "container.memory.limit.utilization"; + /// + /// The name of an instrument to retrieve memory request consumption of all processes running inside a container or control group in range [0, 1]. + /// + /// + /// The type of an instrument is . + /// + public const string ContainerMemoryRequestUtilization = "container.memory.request.utilization"; + /// /// The name of an instrument to retrieve CPU consumption share of the running process in range [0, 1]. /// diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs index d2e3d5d8292..9b4fe5b943b 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs @@ -1,9 +1,12 @@ // 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.Diagnostics.Metrics; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; using Microsoft.TestUtilities; using Xunit; @@ -53,4 +56,56 @@ public void AddPublisher_CalledMultipleTimes_AddsMultiplePublishersToServiceColl Assert.IsAssignableFrom(publishersArray.First()); Assert.IsAssignableFrom(publishersArray.Last()); } + + [ConditionalFact] + public void AddResourceMonitoring_WithoutConfigureMonitor_CannotResolveSnapshotProviderRequiringOptions() + { + // Arrange - Try to register WindowsSnapshotProvider which requires IOptions + var services = new ServiceCollection() + .AddLogging() + .AddSingleton(sp => new FakeMeterFactory()) + .AddResourceMonitoring() + .AddSingleton(); // ← This will fail to resolve + + using var provider = services.BuildServiceProvider(); + + // Act & Assert - Should throw because IOptions is not registered + var exception = Assert.Throws(() => + provider.GetRequiredService()); + + Assert.Contains("IOptions", exception.Message); + } + + [ConditionalFact] + public void AddResourceMonitoring_WithManualOptionsConfiguration_AllowsSnapshotProviderResolution() + { + // Arrange - Manually register options to fix the issue + var services = new ServiceCollection() + .AddLogging() + .AddSingleton(sp => new FakeMeterFactory()) + .AddResourceMonitoring() + .Configure(options => { }) // ← Manual fix + .AddSingleton(); + + using var provider = services.BuildServiceProvider(); + + // Act & Assert - Should now work + var snapshotProvider = provider.GetRequiredService(); + + Assert.NotNull(snapshotProvider); + Assert.NotNull(snapshotProvider.Resources); + } + + internal sealed class FakeMeterFactory : IMeterFactory + { + public Meter Create(MeterOptions options) + { + return new Meter(options.Name, options.Version); + } + + public void Dispose() + { + // Nothing to dispose + } + } } From 785a2f067166b548ec5f1ea82486638b192573ad Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Tue, 26 Aug 2025 13:48:25 +0200 Subject: [PATCH 3/9] Add ResourceQuotasProvider --- .../KubernetesDataProvider.cs | 58 ----- .../KubernetesResourceQuotas.cs | 20 ++ .../KubernetesResourceQuotasProvider.cs | 61 +++++ .../Linux/LinuxUtilizationProvider.cs | 66 ++++-- ...ceMonitoringServiceCollectionExtensions.cs | 2 +- ...ernetesWindowsContainerSnapshotProvider.cs | 185 --------------- ...UnifiedWindowsContainerSnapshotProvider.cs | 213 ------------------ .../WindowsContainerSnapshotProvider.cs | 127 ++++------- .../ResourceUtilizationInstruments.cs | 8 + 9 files changed, 188 insertions(+), 552 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs deleted file mode 100644 index 6615b4afad1..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesDataProvider.cs +++ /dev/null @@ -1,58 +0,0 @@ -// 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.Diagnostics; -using Microsoft.Extensions.ClusterMetadata.Kubernetes; - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; - -/// -/// Provides resource data from Kubernetes metadata for Windows containers. -/// -internal sealed class KubernetesResourceDataProvider : IWindowsResourceDataProvider -{ - private readonly KubernetesClusterMetadata _kubernetesMetadata; - - public KubernetesResourceDataProvider(KubernetesClusterMetadata kubernetesMetadata) - { - _kubernetesMetadata = kubernetesMetadata; - } - - public long GetCurrentCpuTicks() - { - using var process = Process.GetCurrentProcess(); - return process.TotalProcessorTime.Ticks; - } - - public ulong GetCurrentMemoryUsage() - { - return (ulong)Environment.WorkingSet; - } - - public (long userTime, long kernelTime) GetCpuTimeBreakdown() - { - using var process = Process.GetCurrentProcess(); - return (process.UserProcessorTime.Ticks, process.PrivilegedProcessorTime.Ticks); - } - - public WindowsResourceLimits GetResourceLimits() - { - double cpuRequest = ConvertMillicoreToUnit(_kubernetesMetadata.RequestsCpu); - double cpuLimit = ConvertMillicoreToUnit(_kubernetesMetadata.LimitsCpu); - ulong memoryRequest = _kubernetesMetadata.RequestsMemory; - ulong memoryLimit = _kubernetesMetadata.LimitsMemory; - - return new WindowsResourceLimits( - CpuLimit: cpuLimit, - CpuRequest: cpuRequest, - MemoryLimit: memoryLimit, - MemoryRequest: memoryRequest, - HasRequests: true); - } - - private static double ConvertMillicoreToUnit(ulong millicores) - { - return millicores / 1000.0; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs new file mode 100644 index 00000000000..7a90a292456 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +internal class KubernetesResourceQuotas +{ + public double CpuLimit { get; set; } + public double CpuRequest { get; set; } + public ulong MemoryLimit { get; set; } + public ulong MemoryRequest { get; set; } + + public KubernetesResourceQuotas(double cpuLimit, double cpuRequest, ulong memoryLimit, ulong memoryRequest) + { + CpuLimit = cpuLimit; + CpuRequest = cpuRequest; + MemoryLimit = memoryLimit; + MemoryRequest = memoryRequest; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs new file mode 100644 index 00000000000..63f75fbf529 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ClusterMetadata.Kubernetes; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +internal sealed class KubernetesResourceQuotasProvider +{ + private readonly KubernetesClusterMetadata _kubernetesMetadata; + + public KubernetesResourceQuotasProvider(IOptions kubernetesMetadata) + { + _kubernetesMetadata = kubernetesMetadata.Value; + } + + public KubernetesResourceQuotas GetResourceLimits() + { + double cpuRequest = ConvertMillicoreToUnit(_kubernetesMetadata.RequestsCpu); + double cpuLimit = ConvertMillicoreToUnit(_kubernetesMetadata.LimitsCpu); + ulong memoryRequest = _kubernetesMetadata.RequestsMemory; + ulong memoryLimit = _kubernetesMetadata.LimitsMemory; + + if (cpuRequest <= 0) + { + Throw.InvalidOperationException($"REQUESTS_CPU detected value is {cpuRequest}, " + + $"environment variables may be misconfigured"); + } + + if (cpuLimit <= 0) + { + Throw.InvalidOperationException($"LIMITS_CPU detected value is {cpuLimit}, " + + $"environment variables may be misconfigured"); + } + + if (memoryRequest <= 0) + { + Throw.InvalidOperationException($"REQUESTS_MEMORY detected value is {memoryRequest}, " + + $"environment variables may be misconfigured"); + } + + if (memoryLimit <= 0) + { + Throw.InvalidOperationException($"LIMITS_MEMORY detected value is {memoryLimit}, " + + $"environment variables may be misconfigured"); + } + + return new KubernetesResourceQuotas( + cpuLimit, + cpuRequest, + memoryLimit, + memoryRequest); + } + + private static double ConvertMillicoreToUnit(ulong millicores) + { + return millicores / 1000.0; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 611b96f4f1d..d3819116896 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -22,7 +22,10 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private readonly object _memoryLocker = new(); private readonly ILogger _logger; private readonly ILinuxUtilizationParser _parser; - private readonly ulong _memoryLimit; + private readonly double _memoryLimit; + private readonly double _cpuLimit; + private ulong _memoryRequest; + private double _cpuRequest; private readonly long _cpuPeriodsInterval; private readonly TimeSpan _cpuRefreshInterval; private readonly TimeSpan _memoryRefreshInterval; @@ -43,8 +46,13 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private long _previousCgroupCpuPeriodCounter; public SystemResources Resources { get; } - public LinuxUtilizationProvider(IOptions options, ILinuxUtilizationParser parser, - IMeterFactory meterFactory, ILogger? logger = null, TimeProvider? timeProvider = null) + public LinuxUtilizationProvider( + IOptions options, + ILinuxUtilizationParser parser, + IMeterFactory meterFactory, + KubernetesResourceQuotasProvider quotasProvider, + ILogger? logger = null, + TimeProvider? timeProvider = null) { _parser = parser; _logger = logger ?? NullLogger.Instance; @@ -54,15 +62,29 @@ public LinuxUtilizationProvider(IOptions options, ILi _memoryRefreshInterval = options.Value.MemoryConsumptionRefreshInterval; _refreshAfterCpu = now; _refreshAfterMemory = now; - _memoryLimit = _parser.GetAvailableMemoryInBytes(); _previousHostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); _previousCgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); + if (options.Value.UseKubernetesLimitsAndRequests) + { + KubernetesResourceQuotas quotas = quotasProvider.GetResourceLimits(); + _memoryLimit = quotas.MemoryLimit; + _cpuLimit = quotas.CpuLimit; + _memoryRequest = quotas.MemoryRequest; + _cpuRequest = quotas.CpuRequest; + } + else + { + _memoryLimit = _parser.GetAvailableMemoryInBytes(); + _cpuLimit = _parser.GetCgroupLimitedCpus(); + _memoryRequest = _memoryLimit; // TO see if we can get it from cgroups + _cpuRequest = _parser.GetCgroupRequestCpu(); + + } + float hostCpus = _parser.GetHostCpuCount(); - float cpuLimit = _parser.GetCgroupLimitedCpus(); - float cpuRequest = _parser.GetCgroupRequestCpu(); - float scaleRelativeToCpuLimit = hostCpus / cpuLimit; - float scaleRelativeToCpuRequest = hostCpus / cpuRequest; + double scaleRelativeToCpuLimit = hostCpus / _cpuLimit; + double scaleRelativeToCpuRequest = hostCpus / _cpuRequest; _scaleRelativeToCpuRequestForTrackerApi = hostCpus; // the division by cpuRequest is performed later on in the ResourceUtilization class #pragma warning disable CA2000 // Dispose objects before losing scope @@ -74,8 +96,11 @@ public LinuxUtilizationProvider(IOptions options, ILi if (options.Value.UseLinuxCalculationV2) { - cpuLimit = _parser.GetCgroupLimitV2(); - cpuRequest = _parser.GetCgroupRequestCpuV2(); + if (!options.Value.UseKubernetesLimitsAndRequests) + { + _cpuLimit = _parser.GetCgroupLimitV2(); + _cpuRequest = _parser.GetCgroupRequestCpuV2(); + } // Get Cpu periods interval from cgroup _cpuPeriodsInterval = _parser.GetCgroupPeriodsIntervalInMicroSecondsV2(); @@ -83,12 +108,12 @@ public LinuxUtilizationProvider(IOptions options, ILi _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(_cpuLimit)), unit: "1"); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, - observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(cpuRequest)), + observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(_cpuRequest)), unit: "1"); _ = meter.CreateObservableGauge( @@ -119,6 +144,14 @@ public LinuxUtilizationProvider(IOptions options, ILi observeValues: () => GetMeasurementWithRetry(MemoryPercentage), unit: "1"); + if (options.Value.UseKubernetesLimitsAndRequests) + { + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, + observeValue: () => GetMeasurementWithRetry(MemoryPercantageForRequest), // todo implement + unit: "1"); + } + _ = meter.CreateObservableUpDownCounter( name: ResourceUtilizationInstruments.ContainerMemoryUsage, observeValues: () => GetMeasurementWithRetry(() => (long)MemoryUsage()), @@ -130,12 +163,9 @@ public LinuxUtilizationProvider(IOptions options, ILi observeValues: () => GetMeasurementWithRetry(MemoryPercentage), unit: "1"); - // cpuRequest is a CPU request (aka guaranteed number of CPU units) for pod, for host its 1 core - // cpuLimit is a CPU limit (aka max CPU units available) for a pod or for a host. - // _memoryLimit - Resource Memory Limit (in k8s terms) - // _memoryLimit - To keep the contract, this parameter will get the Host available memory - Resources = new SystemResources(cpuRequest, cpuLimit, _memoryLimit, _memoryLimit); - _logger.SystemResourcesInfo(cpuLimit, cpuRequest, _memoryLimit, _memoryLimit); + ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); + Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); + _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest); } public double CpuUtilizationV2() diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index 5838aa2e4b9..bb76ededb6a 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -6,7 +6,6 @@ using System.Runtime.Versioning; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; -//using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; #if !NETFRAMEWORK using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; @@ -77,6 +76,7 @@ private static IServiceCollection AddResourceMonitoringInternal( return services; } + _ = builder.Services.AddActivatedSingleton(); if (OperatingSystem.IsWindows()) { _ = builder.AddWindowsProvider(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs deleted file mode 100644 index 78ffa3e6e7a..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/KubernetesWindowsContainerSnapshotProvider.cs +++ /dev/null @@ -1,185 +0,0 @@ -// 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.CodeAnalysis; -using System.Diagnostics.Metrics; -using Microsoft.Extensions.ClusterMetadata.Kubernetes; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Shared.Instruments; - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; - -/// -/// Provides resource monitoring for containers running in Kubernetes environments using cluster metadata. -/// -internal sealed class KubernetesWindowsContainerSnapshotProvider : ISnapshotProvider -{ - private const double One = 1.0d; - private const double Hundred = 100.0d; - private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; - - private readonly object _cpuLocker = new(); - private readonly object _memoryLocker = new(); - private readonly TimeProvider _timeProvider; - private readonly ILogger _logger; - private readonly KubernetesClusterMetadata _kubernetesMetadata; - private readonly TimeSpan _cpuRefreshInterval; - private readonly TimeSpan _memoryRefreshInterval; - private readonly double _metricValueMultiplier; - - private long _oldCpuUsageTicks; - private long _oldCpuTimeTicks; - private DateTimeOffset _refreshAfterCpu; - private DateTimeOffset _refreshAfterMemory; - private double _cpuPercentage = double.NaN; - private double _memoryPercentage; - - /// - /// Gets the static values of CPU and memory limitations defined by Kubernetes metadata. - /// - public SystemResources Resources { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The meter factory for creating metrics. - /// The resource monitoring options. - /// The Kubernetes cluster metadata containing resource limits and requests. - public KubernetesWindowsContainerSnapshotProvider( - ILogger? logger, - IMeterFactory meterFactory, - IOptions options, - KubernetesClusterMetadata kubernetesMetadata) - : this(logger, meterFactory, TimeProvider.System, options.Value, kubernetesMetadata) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The meter factory for creating metrics. - /// The time provider. - /// The resource monitoring options. - /// The Kubernetes cluster metadata containing resource limits and requests. - /// This constructor enables the mocking of dependencies for the purpose of Unit Testing only. - [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Dependencies for testing")] - internal KubernetesWindowsContainerSnapshotProvider( - ILogger? logger, - IMeterFactory meterFactory, - TimeProvider timeProvider, - ResourceMonitoringOptions options, - KubernetesClusterMetadata kubernetesMetadata) - { - _logger = logger ?? NullLogger.Instance; - _timeProvider = timeProvider; - _kubernetesMetadata = kubernetesMetadata; - - _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; - _cpuRefreshInterval = options.CpuConsumptionRefreshInterval; - _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; - - // Convert CPU from millicores to units (e.g., 1000m = 1.0 CPU) - double cpuRequest = ConvertMillicoreToUnit(kubernetesMetadata.RequestsCpu); - double cpuLimit = ConvertMillicoreToUnit(kubernetesMetadata.LimitsCpu); - - // Memory values are already in bytes - ulong memoryRequest = kubernetesMetadata.RequestsMemory; - ulong memoryLimit = kubernetesMetadata.LimitsMemory; - - Resources = new SystemResources(cpuRequest, cpuLimit, memoryRequest, memoryLimit); - - _logger.LogInformation("Kubernetes container resource limits initialized. CPU: {CpuLimit}, Memory: {MemoryLimit}MB, Pod: {PodName}", - cpuLimit, memoryLimit / (1024 * 1024), kubernetesMetadata.PodName); - - // Initialize tracking variables - _oldCpuUsageTicks = GetCurrentCpuTicks(); - _oldCpuTimeTicks = _timeProvider.GetUtcNow().Ticks; - _refreshAfterCpu = _timeProvider.GetUtcNow(); - _refreshAfterMemory = _timeProvider.GetUtcNow(); - -#pragma warning disable CA2000 // Dispose objects before losing scope - // We don't dispose the meter because IMeterFactory handles that - Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); -#pragma warning restore CA2000 // Dispose objects before losing scope - - // Container based metrics: - _ = meter.CreateObservableCounter( - name: ResourceUtilizationInstruments.ContainerCpuTime, - observeValues: , - unit: "s", - description: "CPU time used by the Kubernetes container."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - observeValue: , - description: "CPU utilization percentage against Kubernetes CPU limits."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, - observeValue: , - description: "Memory utilization percentage against Kubernetes memory limits."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, - observeValue: , - description: "CPU utilization percentage against Kubernetes CPU requests."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, - observeValue: , - description: "Memory utilization percentage against Kubernetes memory requests."); - } - - /// - /// Get a snapshot of the resource utilization of the system. - /// - /// A snapshot containing current resource usage information. - public Snapshot GetSnapshot() - { - // Implementation will read from /proc/stat, /proc/meminfo, or cgroup files - // depending on the container runtime and cgroup version - throw new NotImplementedException("Snapshot implementation will read from system files to get current CPU and memory usage"); - } - - /// - /// Converts CPU value from millicores to CPU units. - /// - /// CPU value in millicores (e.g., 1000m = 1 CPU). - /// CPU value in units (e.g., 1.0 = 1 CPU). - private static double ConvertMillicoreToUnit(ulong millicores) - { - return millicores / 1000.0; - } - - - /// - /// Calculates CPU percentage utilization. - /// - /// CPU utilization percentage. - private double CpuPercentage() - { - } - - /// - /// Calculates memory percentage utilization. - /// - /// Memory utilization percentage. - private double MemoryPercentage() - { - - } - - /// - /// Gets CPU time measurements for telemetry. - /// - /// CPU time measurements broken down by mode. - private IEnumerable> GetCpuTime() - { - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs deleted file mode 100644 index 0a1e381e53d..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/UnifiedWindowsContainerSnapshotProvider.cs +++ /dev/null @@ -1,213 +0,0 @@ -// 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.CodeAnalysis; -using System.Diagnostics.Metrics; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Shared.Instruments; - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; - -/// -/// Unified Windows container snapshot provider that works with different data sources. -/// -internal sealed class UnifiedWindowsContainerSnapshotProvider : ISnapshotProvider -{ - private const double One = 1.0d; - private const double Hundred = 100.0d; - private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; - - private readonly object _cpuLocker = new(); - private readonly object _memoryLocker = new(); - private readonly TimeProvider _timeProvider; - private readonly ILogger _logger; - private readonly IWindowsResourceDataProvider _resourceDataProvider; - private readonly WindowsResourceLimits _resourceLimits; - private readonly TimeSpan _cpuRefreshInterval; - private readonly TimeSpan _memoryRefreshInterval; - private readonly double _metricValueMultiplier; - - private long _oldCpuUsageTicks; - private long _oldCpuTimeTicks; - private DateTimeOffset _refreshAfterCpu; - private DateTimeOffset _refreshAfterMemory; - private double _cpuPercentage = double.NaN; - private double _memoryPercentage; - - public SystemResources Resources { get; } - - public UnifiedWindowsContainerSnapshotProvider( - ILogger? logger, - IMeterFactory meterFactory, - TimeProvider timeProvider, - IWindowsResourceDataProvider resourceDataProvider, - ResourceMonitoringOptions options) - { - _logger = logger ?? NullLogger.Instance; - _timeProvider = timeProvider; - _resourceDataProvider = resourceDataProvider; - _resourceLimits = resourceDataProvider.GetResourceLimits(); - - _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; - _cpuRefreshInterval = options.CpuConsumptionRefreshInterval; - _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; - - Resources = new SystemResources( - _resourceLimits.CpuRequest, - _resourceLimits.CpuLimit, - _resourceLimits.MemoryRequest, - _resourceLimits.MemoryLimit); - - _logger.LogInformation("Container resource limits initialized. CPU: {CpuLimit}, Memory: {MemoryLimit}MB, HasRequests: {HasRequests}", - _resourceLimits.CpuLimit, _resourceLimits.MemoryLimit / (1024 * 1024), _resourceLimits.HasRequests); - - // Initialize tracking variables - _oldCpuUsageTicks = _resourceDataProvider.GetCurrentCpuTicks(); - _oldCpuTimeTicks = _timeProvider.GetUtcNow().Ticks; - _refreshAfterCpu = _timeProvider.GetUtcNow(); - _refreshAfterMemory = _timeProvider.GetUtcNow(); - - RegisterMetrics(meterFactory); - } - - public Snapshot GetSnapshot() - { - var (userTime, kernelTime) = _resourceDataProvider.GetCpuTimeBreakdown(); - var memoryUsage = _resourceDataProvider.GetCurrentMemoryUsage(); - - return new Snapshot( - TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), - TimeSpan.FromTicks(kernelTime), - TimeSpan.FromTicks(userTime), - memoryUsage); - } - - private void RegisterMetrics(IMeterFactory meterFactory) - { -#pragma warning disable CA2000 // Dispose objects before losing scope - var meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); -#pragma warning restore CA2000 // Dispose objects before losing scope - - // Always register these metrics - _ = meter.CreateObservableCounter( - name: ResourceUtilizationInstruments.ContainerCpuTime, - observeValues: GetCpuTime, - unit: "s", - description: "CPU time used by the container."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - observeValue: () => CpuPercentage(_resourceLimits.CpuLimit), - description: "CPU utilization percentage against container CPU limits."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, - observeValue: () => MemoryPercentage(_resourceLimits.MemoryLimit), - description: "Memory utilization percentage against container memory limits."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ProcessCpuUtilization, - observeValue: () => CpuPercentage(_resourceLimits.CpuLimit), - description: "Process CPU utilization."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ProcessMemoryUtilization, - observeValue: () => MemoryPercentage(_resourceLimits.MemoryLimit), - description: "Process memory utilization."); - - // Only register request metrics when we have meaningful request values - if (_resourceLimits.HasRequests) - { - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, - observeValue: () => CpuPercentage(_resourceLimits.CpuRequest), - description: "CPU utilization percentage against container CPU requests."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, - observeValue: () => MemoryPercentage(_resourceLimits.MemoryRequest), - description: "Memory utilization percentage against container memory requests."); - } - } - - private double CpuPercentage(double cpuLimit) - { - var now = _timeProvider.GetUtcNow(); - - lock (_cpuLocker) - { - if (now < _refreshAfterCpu) - { - return _cpuPercentage; - } - } - - var currentCpuTicks = _resourceDataProvider.GetCurrentCpuTicks(); - - lock (_cpuLocker) - { - if (now >= _refreshAfterCpu) - { - var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks; - var timeTickDelta = (now.Ticks - _oldCpuTimeTicks) * cpuLimit; - - if (usageTickDelta > 0 && timeTickDelta > 0) - { - _cpuPercentage = Math.Min(_metricValueMultiplier, usageTickDelta / timeTickDelta * _metricValueMultiplier); - - _oldCpuUsageTicks = currentCpuTicks; - _oldCpuTimeTicks = now.Ticks; - _refreshAfterCpu = now.Add(_cpuRefreshInterval); - } - } - - return _cpuPercentage; - } - } - - private double MemoryPercentage(ulong memoryLimit) - { - var now = _timeProvider.GetUtcNow(); - - lock (_memoryLocker) - { - if (now < _refreshAfterMemory) - { - return _memoryPercentage; - } - } - - var memoryUsage = _resourceDataProvider.GetCurrentMemoryUsage(); - - lock (_memoryLocker) - { - if (now >= _refreshAfterMemory) - { - if (memoryLimit > 0) - { - _memoryPercentage = Math.Min(_metricValueMultiplier, memoryUsage / (double)memoryLimit * _metricValueMultiplier); - } - - _refreshAfterMemory = now.Add(_memoryRefreshInterval); - } - - _logger.LogDebug("Memory usage: {MemoryUsage}MB, Limit: {MemoryLimit}MB, Percentage: {MemoryPercentage}%", - memoryUsage / (1024 * 1024), memoryLimit / (1024 * 1024), _memoryPercentage); - - return _memoryPercentage; - } - } - - private IEnumerable> GetCpuTime() - { - var (userTime, kernelTime) = _resourceDataProvider.GetCpuTimeBreakdown(); - - yield return new Measurement(userTime / TicksPerSecondDouble, - [new KeyValuePair("cpu.mode", "user")]); - yield return new Measurement(kernelTime / TicksPerSecondDouble, - [new KeyValuePair("cpu.mode", "system")]); - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index ce10cad0471..a5143747d06 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -29,12 +29,13 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); - private readonly object _processMemoryLocker = new(); private readonly TimeProvider _timeProvider; private readonly IProcessInfo _processInfo; private readonly ILogger _logger; private readonly double _memoryLimit; private readonly double _cpuLimit; + private ulong _memoryRequest; + private double _cpuRequest; private readonly TimeSpan _cpuRefreshInterval; private readonly TimeSpan _memoryRefreshInterval; private readonly double _metricValueMultiplier; @@ -43,10 +44,8 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private long _oldCpuTimeTicks; private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; - private DateTimeOffset _refreshAfterProcessMemory; private double _cpuPercentage = double.NaN; - private ulong _memoryUsage; - private double _processMemoryPercentage; + private double _memoryPercentage; public SystemResources Resources { get; } @@ -56,9 +55,10 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider public WindowsContainerSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, - IOptions options) + IOptions options, + KubernetesResourceQuotasProvider quotasProvider) : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory, - static () => new JobHandleWrapper(), TimeProvider.System, options.Value) + static () => new JobHandleWrapper(), TimeProvider.System, options.Value, quotasProvider) { } @@ -75,7 +75,8 @@ internal WindowsContainerSnapshotProvider( IMeterFactory meterFactory, Func createJobHandleObject, TimeProvider timeProvider, - ResourceMonitoringOptions options) + ResourceMonitoringOptions options, + KubernetesResourceQuotasProvider quotasProvider) { _logger = logger ?? NullLogger.Instance; _logger.RunningInsideJobObject(); @@ -92,16 +93,29 @@ internal WindowsContainerSnapshotProvider( using IJobHandle jobHandle = _createJobHandleObject(); - ulong memoryLimitLong = GetMemoryLimit(jobHandle); - _memoryLimit = memoryLimitLong; - _cpuLimit = GetCpuLimit(jobHandle, systemInfo); + if (options.UseKubernetesLimitsAndRequests) + { + KubernetesResourceQuotas quotas = quotasProvider.GetResourceLimits(); + _memoryLimit = quotas.MemoryLimit; + _cpuLimit = quotas.CpuLimit; + _memoryRequest = quotas.MemoryRequest; + _cpuRequest = quotas.CpuRequest; + } + else + { + ulong memoryLimitLong = GetMemoryLimit(jobHandle); + _memoryLimit = memoryLimitLong; + _cpuLimit = GetCpuLimit(jobHandle, systemInfo); + + // CPU request (aka guaranteed CPU units) is not supported on Windows in non K8s, so we set it to the same value as CPU limit (aka maximum CPU units). + // Memory request (aka guaranteed memory) is not supported on Windows in non K8s, so we set it to the same value as memory limit (aka maximum memory). + _cpuRequest = _cpuLimit; + _memoryRequest = memoryLimitLong; + } - // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). - // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). - double cpuRequest = _cpuLimit; - ulong memoryRequest = memoryLimitLong; - Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong); - _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest); + ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); + Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); + _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest); var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); _oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; @@ -110,7 +124,6 @@ internal WindowsContainerSnapshotProvider( _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; _refreshAfterCpu = _timeProvider.GetUtcNow(); _refreshAfterMemory = _timeProvider.GetUtcNow(); - _refreshAfterProcessMemory = _timeProvider.GetUtcNow(); #pragma warning disable CA2000 // Dispose objects before losing scope // We don't dispose the meter because IMeterFactory handles that @@ -120,34 +133,20 @@ internal WindowsContainerSnapshotProvider( #pragma warning restore CA2000 // Dispose objects before losing scope // Container based metrics: - _ = meter.CreateObservableCounter( - name: ResourceUtilizationInstruments.ContainerCpuTime, - observeValues: GetCpuTime, - unit: "s", - description: "CPU time used by the container."); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - observeValue: CpuPercentage); - - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, - observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryLimit * _metricValueMultiplier)); - - _ = meter.CreateObservableUpDownCounter( - name: ResourceUtilizationInstruments.ContainerMemoryUsage, - observeValue: () => (long)MemoryUsage(), - unit: "By", - description: "Memory usage of the container."); + _ = meter.CreateObservableCounter(name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, unit: "s", description: "CPU time used by the container."); + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: CpuPercentage); + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetMemoryUsage())); - // Process based metrics: - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ProcessCpuUtilization, - observeValue: CpuPercentage); + // Kubernetes enables metrics: + if (options.UseKubernetesLimitsAndRequests) + { + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValue: () => CpuPercentageForRequests()); // todo implement + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, observeValue: () => MemoryPercentaForRequests()); // todo implement + } - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ProcessMemoryUtilization, - observeValue: ProcessMemoryPercentage); + // Process based metrics: + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessCpuUtilization, observeValue: CpuPercentage); + _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessMemoryUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetCurrentProcessMemoryUsage())); } public Snapshot GetSnapshot() @@ -210,35 +209,7 @@ private ulong GetMemoryLimit(IJobHandle jobHandle) return memoryLimitInBytes; } - private double ProcessMemoryPercentage() - { - DateTimeOffset now = _timeProvider.GetUtcNow(); - - lock (_processMemoryLocker) - { - if (now < _refreshAfterProcessMemory) - { - return _processMemoryPercentage; - } - } - - ulong processMemoryUsage = _processInfo.GetCurrentProcessMemoryUsage(); - - lock (_processMemoryLocker) - { - if (now >= _refreshAfterProcessMemory) - { - _processMemoryPercentage = Math.Min(_metricValueMultiplier, processMemoryUsage / _memoryLimit * _metricValueMultiplier); - _refreshAfterProcessMemory = now.Add(_memoryRefreshInterval); - - _logger.ProcessMemoryPercentageData(processMemoryUsage, _memoryLimit, _processMemoryPercentage); - } - - return _processMemoryPercentage; - } - } - - private ulong MemoryUsage() + private double MemoryPercentage(Func getMemoryUsage) { DateTimeOffset now = _timeProvider.GetUtcNow(); @@ -246,22 +217,24 @@ private ulong MemoryUsage() { if (now < _refreshAfterMemory) { - return _memoryUsage; + return _memoryPercentage; } } - ulong memoryUsage = _processInfo.GetMemoryUsage(); + ulong memoryUsage = getMemoryUsage(); lock (_memoryLocker) { if (now >= _refreshAfterMemory) { - _memoryUsage = memoryUsage; + // Don't change calculation order, otherwise we loose some precision: + _memoryPercentage = Math.Min(_metricValueMultiplier, memoryUsage / _memoryLimit * _metricValueMultiplier); _refreshAfterMemory = now.Add(_memoryRefreshInterval); - _logger.ContainerMemoryUsageData(_memoryUsage, _memoryLimit); } - return _memoryUsage; + _logger.MemoryUsageData(memoryUsage, _memoryLimit, _memoryPercentage); + + return _memoryPercentage; } } diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index 01529d44856..88351385b21 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -50,6 +50,14 @@ internal static class ResourceUtilizationInstruments /// public const string ContainerMemoryLimitUtilization = "container.memory.limit.utilization"; + /// + /// The name of an instrument to retrieve memory usage measured in bytes of all processes running inside a container or control group. + /// + /// + /// The type of an instrument is . + /// + public const string ContainerMemoryUsage = "container.memory.usage"; + /// /// The name of an instrument to retrieve memory request consumption of all processes running inside a container or control group in range [0, 1]. /// From 95e1a3cc9c81712f61b817561f21562068d21ca8 Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Thu, 28 Aug 2025 12:03:04 +0200 Subject: [PATCH 4/9] Fix public API surface, remove Quotas provider component --- ...rMetadataConfigurationBuilderExtensions.cs | 31 ++++++++++ ...terMetadataServiceCollectionExtensions.cs} | 25 ++------ .../KubernetesResourceQuotas.cs | 20 ------ .../KubernetesResourceQuotasProvider.cs | 61 ------------------- .../Linux/LinuxUtilizationProvider.cs | 53 +++++++++++++--- ...ceMonitoringServiceCollectionExtensions.cs | 1 - .../WindowsContainerSnapshotProvider.cs | 55 ++++++++++++++--- 7 files changed, 124 insertions(+), 122 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs rename src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/{KubernetesClusterMetadataExtensions.cs => KubernetesClusterMetadataServiceCollectionExtensions.cs} (62%) delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs new file mode 100644 index 00000000000..c08995d68ad --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ClusterMetadata.Kubernetes; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Configuration; + +/// +/// extensions for Kubernetes metadata. +/// +public static class KubernetesClusterMetadataConfigurationBuilderExtensions +{ + private const string DefaultSectionName = "clustermetadata:kubernetes"; + + /// + /// Registers configuration provider for Kubernetes metadata. + /// + /// The configuration builder. + /// Section name to save configuration into. Default set to "clustermetadata:kubernetes". + /// A prefix for environment variable names that have Kubernetes cluster information. + /// The input configuration builder for call chaining. + public static IConfigurationBuilder AddKubernetesClusterMetadata(this IConfigurationBuilder builder, string sectionName = DefaultSectionName, string environmentVariablePrefix = "") + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrWhitespace(sectionName); + _ = Throw.IfNull(environmentVariablePrefix); + + return builder.Add(new KubernetesClusterMetadataSource(sectionName, environmentVariablePrefix)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs similarity index 62% rename from src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs rename to src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs index fe846dd8123..d60b91e83f4 100644 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs @@ -1,35 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; +namespace Microsoft.Extensions.DependencyInjection; /// -/// Extensions for Kubernetes metadata. +/// extensions for Kubernetes metadata. /// -public static class KubernetesClusterMetadataExtensions +public static class KubernetesClusterMetadataServiceCollectionExtensions { - private const string DefaultSectionName = "clustermetadata:kubernetes"; - - /// - /// Registers configuration provider for Kubernetes metadata. - /// - /// The configuration builder. - /// Section name to save configuration into. Default set to "clustermetadata:kubernetes". - /// A prefix for environment variable names that have Kubernetes cluster information. - /// The input configuration builder for call chaining. - public static IConfigurationBuilder AddKubernetesClusterMetadata(this IConfigurationBuilder builder, string sectionName = DefaultSectionName, string environmentVariablePrefix = "") - { - _ = Throw.IfNull(builder); - _ = Throw.IfNullOrWhitespace(sectionName); - _ = Throw.IfNull(environmentVariablePrefix); - - return builder.Add(new KubernetesClusterMetadataSource(sectionName, environmentVariablePrefix)); - } - /// /// Adds an instance of to the . /// diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs deleted file mode 100644 index 7a90a292456..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotas.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; - -internal class KubernetesResourceQuotas -{ - public double CpuLimit { get; set; } - public double CpuRequest { get; set; } - public ulong MemoryLimit { get; set; } - public ulong MemoryRequest { get; set; } - - public KubernetesResourceQuotas(double cpuLimit, double cpuRequest, ulong memoryLimit, ulong memoryRequest) - { - CpuLimit = cpuLimit; - CpuRequest = cpuRequest; - MemoryLimit = memoryLimit; - MemoryRequest = memoryRequest; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs deleted file mode 100644 index 63f75fbf529..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/KubernetesResourceQuotasProvider.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.ClusterMetadata.Kubernetes; -using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; - -internal sealed class KubernetesResourceQuotasProvider -{ - private readonly KubernetesClusterMetadata _kubernetesMetadata; - - public KubernetesResourceQuotasProvider(IOptions kubernetesMetadata) - { - _kubernetesMetadata = kubernetesMetadata.Value; - } - - public KubernetesResourceQuotas GetResourceLimits() - { - double cpuRequest = ConvertMillicoreToUnit(_kubernetesMetadata.RequestsCpu); - double cpuLimit = ConvertMillicoreToUnit(_kubernetesMetadata.LimitsCpu); - ulong memoryRequest = _kubernetesMetadata.RequestsMemory; - ulong memoryLimit = _kubernetesMetadata.LimitsMemory; - - if (cpuRequest <= 0) - { - Throw.InvalidOperationException($"REQUESTS_CPU detected value is {cpuRequest}, " + - $"environment variables may be misconfigured"); - } - - if (cpuLimit <= 0) - { - Throw.InvalidOperationException($"LIMITS_CPU detected value is {cpuLimit}, " + - $"environment variables may be misconfigured"); - } - - if (memoryRequest <= 0) - { - Throw.InvalidOperationException($"REQUESTS_MEMORY detected value is {memoryRequest}, " + - $"environment variables may be misconfigured"); - } - - if (memoryLimit <= 0) - { - Throw.InvalidOperationException($"LIMITS_MEMORY detected value is {memoryLimit}, " + - $"environment variables may be misconfigured"); - } - - return new KubernetesResourceQuotas( - cpuLimit, - cpuRequest, - memoryLimit, - memoryRequest); - } - - private static double ConvertMillicoreToUnit(ulong millicores) - { - return millicores / 1000.0; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index d3819116896..1990251296a 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -5,9 +5,11 @@ using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Threading; +using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Instruments; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; @@ -22,8 +24,8 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private readonly object _memoryLocker = new(); private readonly ILogger _logger; private readonly ILinuxUtilizationParser _parser; - private readonly double _memoryLimit; - private readonly double _cpuLimit; + private double _memoryLimit; + private double _cpuLimit; private ulong _memoryRequest; private double _cpuRequest; private readonly long _cpuPeriodsInterval; @@ -50,7 +52,7 @@ public LinuxUtilizationProvider( IOptions options, ILinuxUtilizationParser parser, IMeterFactory meterFactory, - KubernetesResourceQuotasProvider quotasProvider, + IOptions clusterMetadata, ILogger? logger = null, TimeProvider? timeProvider = null) { @@ -67,17 +69,13 @@ public LinuxUtilizationProvider( if (options.Value.UseKubernetesLimitsAndRequests) { - KubernetesResourceQuotas quotas = quotasProvider.GetResourceLimits(); - _memoryLimit = quotas.MemoryLimit; - _cpuLimit = quotas.CpuLimit; - _memoryRequest = quotas.MemoryRequest; - _cpuRequest = quotas.CpuRequest; + ExtractResourceQuotas(clusterMetadata.Value); } else { _memoryLimit = _parser.GetAvailableMemoryInBytes(); _cpuLimit = _parser.GetCgroupLimitedCpus(); - _memoryRequest = _memoryLimit; // TO see if we can get it from cgroups + _memoryRequest = _memoryLimit; // TODO: see if we can get it from cgroups _cpuRequest = _parser.GetCgroupRequestCpu(); } @@ -364,4 +362,41 @@ private IEnumerable> GetCpuTime() yield return new Measurement(userCpuTime, [new KeyValuePair("cpu.mode", "user")]); } } + + private void ExtractResourceQuotas(KubernetesClusterMetadata clusterMetadata) + { + _cpuRequest = ConvertMillicoreToUnit(clusterMetadata.RequestsCpu); + _cpuLimit = ConvertMillicoreToUnit(clusterMetadata.LimitsCpu); + _memoryRequest = clusterMetadata.RequestsMemory; + _memoryLimit = clusterMetadata.LimitsMemory; + + if (_cpuRequest <= 0) + { + Throw.InvalidOperationException($"REQUESTS_CPU detected value is {_cpuRequest}, " + + $"environment variables may be misconfigured"); + } + + if (_cpuLimit <= 0) + { + Throw.InvalidOperationException($"LIMITS_CPU detected value is {_cpuLimit}, " + + $"environment variables may be misconfigured"); + } + + if (_memoryRequest <= 0) + { + Throw.InvalidOperationException($"REQUESTS_MEMORY detected value is {_memoryRequest}, " + + $"environment variables may be misconfigured"); + } + + if (_memoryLimit <= 0) + { + Throw.InvalidOperationException($"LIMITS_MEMORY detected value is {_memoryLimit}, " + + $"environment variables may be misconfigured"); + } + } + + private static double ConvertMillicoreToUnit(ulong millicores) + { + return millicores / 1000.0; + } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index bb76ededb6a..19f89aa1455 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -76,7 +76,6 @@ private static IServiceCollection AddResourceMonitoringInternal( return services; } - _ = builder.Services.AddActivatedSingleton(); if (OperatingSystem.IsWindows()) { _ = builder.AddWindowsProvider(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index a5143747d06..5b633225d74 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -6,10 +6,12 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Threading; +using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Instruments; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; @@ -32,8 +34,8 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private readonly TimeProvider _timeProvider; private readonly IProcessInfo _processInfo; private readonly ILogger _logger; - private readonly double _memoryLimit; - private readonly double _cpuLimit; + private double _memoryLimit; + private double _cpuLimit; private ulong _memoryRequest; private double _cpuRequest; private readonly TimeSpan _cpuRefreshInterval; @@ -56,9 +58,9 @@ public WindowsContainerSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, IOptions options, - KubernetesResourceQuotasProvider quotasProvider) + IOptions clusterMetadata) : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory, - static () => new JobHandleWrapper(), TimeProvider.System, options.Value, quotasProvider) + static () => new JobHandleWrapper(), TimeProvider.System, options.Value, clusterMetadata.Value) { } @@ -76,7 +78,7 @@ internal WindowsContainerSnapshotProvider( Func createJobHandleObject, TimeProvider timeProvider, ResourceMonitoringOptions options, - KubernetesResourceQuotasProvider quotasProvider) + KubernetesClusterMetadata clusterMetadata) { _logger = logger ?? NullLogger.Instance; _logger.RunningInsideJobObject(); @@ -95,11 +97,7 @@ internal WindowsContainerSnapshotProvider( if (options.UseKubernetesLimitsAndRequests) { - KubernetesResourceQuotas quotas = quotasProvider.GetResourceLimits(); - _memoryLimit = quotas.MemoryLimit; - _cpuLimit = quotas.CpuLimit; - _memoryRequest = quotas.MemoryRequest; - _cpuRequest = quotas.CpuRequest; + ExtractResourceQuotas(clusterMetadata); } else { @@ -288,4 +286,41 @@ private double CpuPercentage() return _cpuPercentage; } } + + private void ExtractResourceQuotas(KubernetesClusterMetadata clusterMetadata) + { + _cpuRequest = ConvertMillicoreToUnit(clusterMetadata.RequestsCpu); + _cpuLimit = ConvertMillicoreToUnit(clusterMetadata.LimitsCpu); + _memoryRequest = clusterMetadata.RequestsMemory; + _memoryLimit = clusterMetadata.LimitsMemory; + + if (_cpuRequest <= 0) + { + Throw.InvalidOperationException($"REQUESTS_CPU detected value is {_cpuRequest}, " + + $"environment variables may be misconfigured"); + } + + if (_cpuLimit <= 0) + { + Throw.InvalidOperationException($"LIMITS_CPU detected value is {_cpuLimit}, " + + $"environment variables may be misconfigured"); + } + + if (_memoryRequest <= 0) + { + Throw.InvalidOperationException($"REQUESTS_MEMORY detected value is {_memoryRequest}, " + + $"environment variables may be misconfigured"); + } + + if (_memoryLimit <= 0) + { + Throw.InvalidOperationException($"LIMITS_MEMORY detected value is {_memoryLimit}, " + + $"environment variables may be misconfigured"); + } + } + + private static double ConvertMillicoreToUnit(ulong millicores) + { + return millicores / 1000.0; + } } From cfc4861dd47a687685476426a7e19260c199b2f4 Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Thu, 11 Sep 2025 16:47:22 +0200 Subject: [PATCH 5/9] Switch layering of the abstractions to support Kubernetes metadata --- ...sterMetadataServiceCollectionExtensions.cs | 1 + .../KubernetesMetadata.cs | 45 ++++++ .../KubernetesResourceQuotasProvider.cs | 31 +++++ ...ourceQuotasServiceCollectionsExtensions.cs | 37 +++++ ...stics.ResourceMonitoring.Kubernetes.csproj | 56 ++++++++ ...nostics.ResourceMonitoring.Kubernetes.json | 0 .../IResourceQuotasProvider.cs | 10 ++ .../LinuxResourceQuotasProvider.cs | 14 ++ .../ResourceMonitoringOptions.Windows.cs | 8 -- .../ResourceMonitoringOptions.cs | 2 + ...ceMonitoringServiceCollectionExtensions.cs | 1 + .../ResourceQuotas.cs | 27 ++++ .../WindowsContainerSnapshotProvider.cs | 68 ++------- .../WindowsContainerResourceQuotasProvider.cs | 13 ++ .../CpuPercentageCalculatorTests.cs | 130 ++++++++++++++++++ 15 files changed, 378 insertions(+), 65 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs index d60b91e83f4..557e60b7165 100644 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs new file mode 100644 index 00000000000..91b623093d8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs @@ -0,0 +1,45 @@ +// 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.Globalization; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +public class KubernetesMetadata +{ + /// + /// Gets or sets the resource memory limit the container is allowed to use. + /// + public ulong LimitsMemory { get; set; } + + /// + /// Gets or sets the resource CPU limit the container is allowed to use. + /// + public ulong LimitsCpu { get; set; } + + /// + /// Gets or sets the resource memory request the container is allowed to use. + /// + public ulong RequestsMemory { get; set; } + + /// + /// Gets or sets the resource CPU request the container is allowed to use. + /// + public ulong RequestsCpu { get; set; } + + private string _environmentVariablePrefix; + + public KubernetesMetadata(string environmentVariablePrefix) + { + _environmentVariablePrefix = environmentVariablePrefix; + } + + public void Build() + { + LimitsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_MEMORY"), CultureInfo.InvariantCulture); + LimitsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_CPU"), CultureInfo.InvariantCulture); + RequestsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_MEMORY"), CultureInfo.InvariantCulture); + RequestsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_CPU"), CultureInfo.InvariantCulture); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs new file mode 100644 index 00000000000..545aba44e7d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +internal class KubernetesResourceQuotasProvider : IResourceQuotasProvider +{ + private KubernetesMetadata _cosmicMetadata; + + public KubernetesResourceQuotasProvider(KubernetesMetadata cosmicMetadata) // we could inject ClusterMetadataHere in the future instead of that thingy + { + _cosmicMetadata = cosmicMetadata; + } + + public ResourceQuotas GetResourceQuotas() + { + // extract relevant cluster metadata + return new ResourceQuotas + { + RequestsCpu = ConvertMillicoreToUnit(_cosmicMetadata.RequestsCpu), + LimitsCpu = ConvertMillicoreToUnit(_cosmicMetadata.LimitsCpu), + RequestsMemory = _cosmicMetadata.RequestsMemory, + LimitsMemory = _cosmicMetadata.LimitsMemory, + }; + } + + private static double ConvertMillicoreToUnit(ulong millicores) + { + return millicores / 1000.0; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs new file mode 100644 index 00000000000..33cc7964966 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs @@ -0,0 +1,37 @@ +// 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 Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +/// +/// +/// +public static class KubernetesResourceQuotasServiceCollectionsExtensions +{ + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddKubernetesResourceQuotas( + this IServiceCollection services, + Action? configureOptions = null) + { + // Simple - just register before AddResourceMonitoring() is called + services.TryAddSingleton(); + + if (configureOptions != null) + { + _ = services.Configure(configureOptions); + } + + services.AddResourceMonitoring(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj new file mode 100644 index 00000000000..a4cff855de8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj @@ -0,0 +1,56 @@ + + + Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes + Measures processor and memory usage. + ResourceMonitoring + $(NoWarn);CS0436 + + + + true + true + true + true + true + true + true + true + true + true + true + true + + + + normal + 99 + 90 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs new file mode 100644 index 00000000000..c27bfdf7a63 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +public interface IResourceQuotasProvider +{ + public ResourceQuotas GetResourceQuotas(); +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs new file mode 100644 index 00000000000..74a4c72406a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +internal class LinuxResourceQuotasProvider : IResourceQuotasProvider + +{ + public ResourceQuotas GetResourceQuotas() + { + // bring logic from LinuxUtilizationProvider for limits and requests + } +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs index d80822539b2..9e8636506c7 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.Windows.cs @@ -33,12 +33,4 @@ public partial class ResourceMonitoringOptions /// [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] public bool UseZeroToOneRangeForMetrics { get; set; } - - /// - /// Gets or sets a value indicating whether CPU and Memory utilization metrics will be calculated from Kubernetes container's limits and requests. - /// - /// - /// The default value is . - /// - public bool UseKubernetesLimitsAndRequests { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs index eb890da291e..c2693864b4c 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs @@ -119,4 +119,6 @@ public partial class ResourceMonitoringOptions /// Previously EnableDiskIoMetrics. [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] public bool EnableSystemDiskIoMetrics { get; set; } + + public bool EmitRequestsMetrics { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index 19f89aa1455..c91704abb18 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -115,6 +115,7 @@ private static void PickWindowsSnapshotProvider(this ResourceMonitorBuilder buil if (JobObjectInfo.SafeJobHandle.IsProcessInJob()) { builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); } else { diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs new file mode 100644 index 00000000000..30aa8542e00 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +public class ResourceQuotas +{ + /// + /// Gets or sets the resource memory limit the container is allowed to use. + /// + public ulong LimitsMemory { get; set; } + + /// + /// Gets or sets the resource CPU limit the container is allowed to use. + /// + public ulong LimitsCpu { get; set; } + + /// + /// Gets or sets the resource memory request the container is allowed to use. + /// + public ulong RequestsMemory { get; set; } + + /// + /// Gets or sets the resource CPU request the container is allowed to use. + /// + public ulong RequestsCpu { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 5b633225d74..d595f0b250d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -6,12 +6,10 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using System.Threading; -using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Instruments; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; @@ -51,6 +49,8 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider public SystemResources Resources { get; } + private IResourceQuotasProvider _resourceQuotasProvider; + /// /// Initializes a new instance of the class. /// @@ -78,7 +78,7 @@ internal WindowsContainerSnapshotProvider( Func createJobHandleObject, TimeProvider timeProvider, ResourceMonitoringOptions options, - KubernetesClusterMetadata clusterMetadata) + IResourceQuotasProvider resourceQuotasProvider) { _logger = logger ?? NullLogger.Instance; _logger.RunningInsideJobObject(); @@ -95,21 +95,12 @@ internal WindowsContainerSnapshotProvider( using IJobHandle jobHandle = _createJobHandleObject(); - if (options.UseKubernetesLimitsAndRequests) - { - ExtractResourceQuotas(clusterMetadata); - } - else - { - ulong memoryLimitLong = GetMemoryLimit(jobHandle); - _memoryLimit = memoryLimitLong; - _cpuLimit = GetCpuLimit(jobHandle, systemInfo); - - // CPU request (aka guaranteed CPU units) is not supported on Windows in non K8s, so we set it to the same value as CPU limit (aka maximum CPU units). - // Memory request (aka guaranteed memory) is not supported on Windows in non K8s, so we set it to the same value as memory limit (aka maximum memory). - _cpuRequest = _cpuLimit; - _memoryRequest = memoryLimitLong; - } + _resourceQuotasProvider = resourceQuotasProvider; + var quotas = _resourceQuotasProvider.GetResourceQuotas(); + _memoryLimit = quotas.LimitsMemory; + _cpuLimit = quotas.LimitsCpu; + _cpuRequest = quotas.RequestsCpu; + _memoryRequest = quotas.RequestsMemory; ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); @@ -135,8 +126,8 @@ internal WindowsContainerSnapshotProvider( _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: CpuPercentage); _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetMemoryUsage())); - // Kubernetes enables metrics: - if (options.UseKubernetesLimitsAndRequests) + // Requests enabled metrics + if (options.EmitRequestsMetrics) { _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValue: () => CpuPercentageForRequests()); // todo implement _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, observeValue: () => MemoryPercentaForRequests()); // todo implement @@ -286,41 +277,4 @@ private double CpuPercentage() return _cpuPercentage; } } - - private void ExtractResourceQuotas(KubernetesClusterMetadata clusterMetadata) - { - _cpuRequest = ConvertMillicoreToUnit(clusterMetadata.RequestsCpu); - _cpuLimit = ConvertMillicoreToUnit(clusterMetadata.LimitsCpu); - _memoryRequest = clusterMetadata.RequestsMemory; - _memoryLimit = clusterMetadata.LimitsMemory; - - if (_cpuRequest <= 0) - { - Throw.InvalidOperationException($"REQUESTS_CPU detected value is {_cpuRequest}, " + - $"environment variables may be misconfigured"); - } - - if (_cpuLimit <= 0) - { - Throw.InvalidOperationException($"LIMITS_CPU detected value is {_cpuLimit}, " + - $"environment variables may be misconfigured"); - } - - if (_memoryRequest <= 0) - { - Throw.InvalidOperationException($"REQUESTS_MEMORY detected value is {_memoryRequest}, " + - $"environment variables may be misconfigured"); - } - - if (_memoryLimit <= 0) - { - Throw.InvalidOperationException($"LIMITS_MEMORY detected value is {_memoryLimit}, " + - $"environment variables may be misconfigured"); - } - } - - private static double ConvertMillicoreToUnit(ulong millicores) - { - return millicores / 1000.0; - } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs new file mode 100644 index 00000000000..7fea759a89a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +internal class WindowsContainerResourceQuotasProvider : IResourceQuotasProvider +{ + public ResourceQuotas GetResourceQuotas() + { + // bring logic from WindowsContainerSnapshotProvider for limits and requests + } +} + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs new file mode 100644 index 00000000000..ed7183cf256 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs @@ -0,0 +1,130 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Moq; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public class CpuPercentageCalculatorTests +{ + private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; // 10,000,000 + private const int MetricValueMultiplier = 100; + + public class TestCpuCalculator + { + private readonly double _cpuLimit; + private ulong _oldCpuUsageTicks; + private long _oldCpuTimeTicks; + + public TestCpuCalculator(double cpuLimit) + { + _cpuLimit = cpuLimit; + } + + public void SetInitialState(ulong initialCpuTicks, long initialTimeTicks) + { + _oldCpuUsageTicks = initialCpuTicks; + _oldCpuTimeTicks = initialTimeTicks; + } + + public double CalculateCpuPercentage(ulong currentCpuTicks, long currentTimeTicks) + { + var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks; + var timeTickDelta = (currentTimeTicks - _oldCpuTimeTicks) * _cpuLimit; + + if (usageTickDelta > 0 && timeTickDelta > 0) + { + return Math.Min(MetricValueMultiplier, usageTickDelta / timeTickDelta * MetricValueMultiplier); + } + return 0; + } + } + + [Theory] + [InlineData(0.3, 4, 1.2, "Current implementation - with core scaling")] + [InlineData(0.3, 1, 0.3, "Fixed implementation - without core scaling")] + public void CpuPercentageCalculation_WithDifferentLimits_ShowsScalingImpact( + double kubernetesLimit, int coreCount, double expectedCpuLimit, string testCase) + { + // Arrange - Simulate GetCpuLimit calculation + double cpuLimit = kubernetesLimit * coreCount; // Your current implementation + var calculator = new TestCpuCalculator(cpuLimit); + + // Initial state - container has been running and consumed some CPU + var initialTime = new DateTime(2025, 1, 1, 12, 0, 0).Ticks; + var initialCpuTicks = (ulong)(1.5 * TicksPerSecondDouble); // 1.5 seconds total CPU consumed + calculator.SetInitialState(initialCpuTicks, initialTime); + + // After 5 seconds, container consumed 0.2 more CPU seconds + var currentTime = initialTime + (5 * TimeSpan.TicksPerSecond); // 5 seconds later + var currentCpuTicks = initialCpuTicks + (ulong)(0.2 * TicksPerSecondDouble); // +0.2 CPU seconds + + // Act + var cpuPercentage = calculator.CalculateCpuPercentage(currentCpuTicks, currentTime); + + // Assert & Debug + var actualCpuUsagePercent = (0.2 / 5.0) * 100; // 4% of wall clock time + var expectedPercentOfLimit = (0.2 / kubernetesLimit) / 5.0 * 100; // % of allocated CPU budget + + Console.WriteLine($"\n=== {testCase} ==="); + Console.WriteLine($"Kubernetes CPU limit: {kubernetesLimit} cores"); + Console.WriteLine($"System cores: {coreCount}"); + Console.WriteLine($"Calculated _cpuLimit: {cpuLimit}"); + Console.WriteLine($"Actual CPU usage: 0.2 cores over 5 seconds = {actualCpuUsagePercent:F1}% of wall clock"); + Console.WriteLine($"Expected % of CPU budget: {expectedPercentOfLimit:F1}%"); + Console.WriteLine($"Your algorithm reports: {cpuPercentage:F1}%"); + Console.WriteLine($"Difference factor: {expectedPercentOfLimit / cpuPercentage:F1}x"); + + // Verify the scaling issue + if (testCase.Contains("Current implementation")) + { + Assert.True(cpuPercentage < 5, $"Current implementation should show low percentage due to scaling issue. Got: {cpuPercentage}%"); + } + else + { + Assert.InRange(cpuPercentage, 10, 15); // Should be around 13.3% with correct scaling + } + } + + [Fact] + public void CpuPercentage_RealisticKubernetesScenario_ShowsProblem() + { + // Arrange - Your exact scenario + // K8s: 0.3 CPU limit, 4-core node + double cpuLimit = 0.3 * 4; // 1.2 (current implementation) + var calculator = new TestCpuCalculator(cpuLimit); + + // Container running for a while, consumed 2 seconds of CPU total + var startTime = DateTime.UtcNow.AddMinutes(-5).Ticks; + calculator.SetInitialState((ulong)(2.0 * TicksPerSecondDouble), startTime); + + // 5 seconds later, consumed 0.15 more CPU seconds (moderate load) + var currentTime = startTime + (5 * TimeSpan.TicksPerSecond); + var currentCpuTicks = (ulong)((2.0 + 0.15) * TicksPerSecondDouble); + + // Act + var result = calculator.CalculateCpuPercentage(currentCpuTicks, currentTime); + + // Assert + Console.WriteLine($"\nRealistic Scenario:"); + Console.WriteLine($"Used 0.15 CPU cores over 5 seconds"); + Console.WriteLine($"That's {(0.15 / 5.0) * 100:F1}% of wall clock time"); + Console.WriteLine($"With 0.3 CPU limit, that's {(0.15 / 0.3 / 5.0) * 100:F1}% of CPU budget per second"); + Console.WriteLine($"Your algorithm reports: {result:F1}%"); + + // This demonstrates the problem - should be around 10%, but will be much lower + Assert.True(result < 5, "Should show deflated percentage due to core scaling"); + + // Shows what it should be without scaling + var correctLimit = 0.3; + var correctTimeTickDelta = (currentTime - startTime) * correctLimit; + var correctPercentage = ((currentCpuTicks - (ulong)(2.0 * TicksPerSecondDouble)) / correctTimeTickDelta) * 100; + Console.WriteLine($"With correct scaling (0.3 limit): {correctPercentage:F1}%"); + } +} From 632636f80fce712f9313d88e1bec32661b8229a6 Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Thu, 11 Sep 2025 16:55:29 +0200 Subject: [PATCH 6/9] Fix namespace --- .../KubernetesResourceQuotasServiceCollectionsExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs index 33cc7964966..d2fbe898f87 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; +namespace Microsoft.Extensions.DependencyInjection; /// /// From 4faae5bcee6afe179fe7f38b47fc90d4f5405875 Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Thu, 11 Sep 2025 16:58:18 +0200 Subject: [PATCH 7/9] Remove ClusterMetadata --- .../KubernetesClusterMetadata.cs | 116 ------------------ ...rMetadataConfigurationBuilderExtensions.cs | 31 ----- ...sterMetadataServiceCollectionExtensions.cs | 48 -------- .../KubernetesClusterMetadataSource.cs | 89 -------------- .../KubernetesClusterMetadataValidator.cs | 11 -- ...tensions.ClusterMetadata.Kubernetes.csproj | 32 ----- ...Extensions.ClusterMetadata.Kubernetes.json | 0 7 files changed, 327 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj delete mode 100644 src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.json diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs deleted file mode 100644 index 0cf3a2d0d33..00000000000 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadata.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; - -/// -/// Maintains metadata about Kubernetes cluster. -/// -public class KubernetesClusterMetadata -{ - /// - /// Gets or sets the name of the Kubernetes cluster. - /// - /// - /// Default value is an empty string. - /// - [Required] - public string ClusterName { get; set; } = string.Empty; - - /// - /// Gets or sets the name of a CronJob resource. - /// - /// - /// Default value is . - /// - public string? CronJob { get; set; } - - /// - /// Gets or sets the name of a DaemonSet resource. - /// - /// - /// Default value is . - /// - public string? DaemonSet { get; set; } - - /// - /// Gets or sets the name of a Deployment resource. - /// - /// - /// Default value is . - /// - public string? Deployment { get; set; } - - /// - /// Gets or sets the name of a Job resource. - /// - /// - /// Default value is . - /// - public string? Job { get; set; } - - /// - /// Gets or sets the name of a namespace where the service is deployed. - /// - /// - /// Default value is an empty string. - /// - [Required] - public string Namespace { get; set; } = string.Empty; - - /// - /// Gets or sets the name of a node where the service pod is running. - /// - /// - /// Default value is an empty string. - /// - [Required] - public string NodeName { get; set; } = string.Empty; - - /// - /// Gets or sets the name of a pod which is running the code. - /// - /// - /// Default value is an empty string. - /// - [Required] - public string PodName { get; set; } = string.Empty; - - /// - /// Gets or sets the name of a ReplicaSet resource. - /// - /// - /// Default value is . - /// - public string? ReplicaSet { get; set; } - - /// - /// Gets or sets the name of a StatefulSet resource. - /// - /// - /// Default value is . - /// - public string? StatefulSet { get; set; } - - /// - /// Gets or sets the resource memory limit the container is allowed to use. - /// - public ulong LimitsMemory { get; set; } - - /// - /// Gets or sets the resource CPU limit the container is allowed to use. - /// - public ulong LimitsCpu { get; set; } - - /// - /// Gets or sets the resource memory request the container is allowed to use. - /// - public ulong RequestsMemory { get; set; } - - /// - /// Gets or sets the resource CPU request the container is allowed to use. - /// - public ulong RequestsCpu { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs deleted file mode 100644 index c08995d68ad..00000000000 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataConfigurationBuilderExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.ClusterMetadata.Kubernetes; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Configuration; - -/// -/// extensions for Kubernetes metadata. -/// -public static class KubernetesClusterMetadataConfigurationBuilderExtensions -{ - private const string DefaultSectionName = "clustermetadata:kubernetes"; - - /// - /// Registers configuration provider for Kubernetes metadata. - /// - /// The configuration builder. - /// Section name to save configuration into. Default set to "clustermetadata:kubernetes". - /// A prefix for environment variable names that have Kubernetes cluster information. - /// The input configuration builder for call chaining. - public static IConfigurationBuilder AddKubernetesClusterMetadata(this IConfigurationBuilder builder, string sectionName = DefaultSectionName, string environmentVariablePrefix = "") - { - _ = Throw.IfNull(builder); - _ = Throw.IfNullOrWhitespace(sectionName); - _ = Throw.IfNull(environmentVariablePrefix); - - return builder.Add(new KubernetesClusterMetadataSource(sectionName, environmentVariablePrefix)); - } -} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs deleted file mode 100644 index 557e60b7165..00000000000 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataServiceCollectionExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.ClusterMetadata.Kubernetes; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// extensions for Kubernetes metadata. -/// -public static class KubernetesClusterMetadataServiceCollectionExtensions -{ - /// - /// Adds an instance of to the . - /// - /// The to add the services to. - /// The configuration section to bind. - /// The so that additional calls can be chained. - public static IServiceCollection AddKubernetesClusterMetadata(this IServiceCollection services, IConfigurationSection section) - { - _ = Throw.IfNull(services); - _ = Throw.IfNull(section); - - _ = services.AddOptionsWithValidateOnStart().Bind(section); - - return services; - } - - /// - /// Adds an instance of to the . - /// - /// The to add the services to. - /// The delegate to configure with. - /// The so that additional calls can be chained. - public static IServiceCollection AddKubernetesClusterMetadata(this IServiceCollection services, Action configure) - { - _ = Throw.IfNull(services); - _ = Throw.IfNull(configure); - - _ = services.AddOptionsWithValidateOnStart().Configure(configure); - - return services; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs deleted file mode 100644 index 7c32ca04e5c..00000000000 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataSource.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Globalization; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Memory; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; - -internal class KubernetesClusterMetadataSource : IConfigurationSource -{ - public string SectionName { get; } - private KubernetesClusterMetadata KubernetesClusterMetadata { get; } - private readonly string _environmentVariablePrefix; - - public KubernetesClusterMetadataSource(string sectionName, string environmentVariablePrefix = "") - { - SectionName = Throw.IfNullOrWhitespace(sectionName); - _environmentVariablePrefix = Throw.IfNull(environmentVariablePrefix); - - KubernetesClusterMetadata = InitializeKubernetesClusterMetadata(); - } - - public IConfigurationProvider Build(IConfigurationBuilder builder) => new MemoryConfigurationProvider(new MemoryConfigurationSource()) - { - { $"{SectionName}:{nameof(KubernetesClusterMetadata.ClusterName)}", KubernetesClusterMetadata.ClusterName }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.CronJob)}", KubernetesClusterMetadata.CronJob }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.DaemonSet)}", KubernetesClusterMetadata.DaemonSet }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.Deployment)}", KubernetesClusterMetadata.Deployment }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.Job)}", KubernetesClusterMetadata.Job }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.Namespace)}", KubernetesClusterMetadata.Namespace }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.NodeName)}", KubernetesClusterMetadata.NodeName }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.PodName)}", KubernetesClusterMetadata.PodName }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.ReplicaSet)}", KubernetesClusterMetadata.ReplicaSet }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.StatefulSet)}", KubernetesClusterMetadata.StatefulSet }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.AzureCloud)}", KubernetesClusterMetadata.AzureCloud }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.AzureRegion)}", KubernetesClusterMetadata.AzureRegion }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.AzureGeography)}", KubernetesClusterMetadata.AzureGeography }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.LimitsMemory)}", KubernetesClusterMetadata.LimitsMemory.ToString(CultureInfo.InvariantCulture) }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.LimitsCpu)}", KubernetesClusterMetadata.LimitsCpu.ToString(CultureInfo.InvariantCulture) }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.RequestsMemory)}", KubernetesClusterMetadata.RequestsMemory.ToString(CultureInfo.InvariantCulture) }, - { $"{SectionName}:{nameof(KubernetesClusterMetadata.RequestsCpu)}", KubernetesClusterMetadata.RequestsCpu.ToString(CultureInfo.InvariantCulture) }, - }; - - private KubernetesClusterMetadata InitializeKubernetesClusterMetadata() - { - return new KubernetesClusterMetadata - { - ClusterName = GetEnvironmentVariableOrThrow("CLUSTER_NAME", "CLUSTERNAME"), - CronJob = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}CRONJOB_NAME") ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}CRONJOBNAME"), - DaemonSet = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DAEMONSET_NAME") - ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DAEMONSETNAME"), - Deployment = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DEPLOYMENT_NAME") - ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}DEPLOYMENTNAME"), - Job = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}JOB_NAME") ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}JOBNAME"), - Namespace = GetEnvironmentVariableOrThrow("NAMESPACE", environmentVariableAltName: null), - NodeName = GetEnvironmentVariableOrThrow("NODE_NAME", "NODENAME"), - PodName = GetEnvironmentVariableOrThrow("POD_NAME", "PODNAME"), - ReplicaSet = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REPLICASET_NAME") - ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REPLICASETNAME"), - StatefulSet = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}STATEFULSET_NAME") - ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}STATEFULSETNAME"), - AzureCloud = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURE_CLOUD") - ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURECLOUD"), - AzureRegion = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURE_REGION") - ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZUREREGION"), - AzureGeography = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZURE_GEOGRAPHY") - ?? Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}AZUREGEOGRAPHY"), - - LimitsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_MEMORY"), CultureInfo.InvariantCulture), - LimitsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_CPU"), CultureInfo.InvariantCulture), - RequestsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_MEMORY"), CultureInfo.InvariantCulture), - RequestsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_CPU"), CultureInfo.InvariantCulture) - }; - } - - private string GetEnvironmentVariableOrThrow(string environmentVariableName, string? environmentVariableAltName) - { - var result = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}{environmentVariableName}"); - - if (result == null && environmentVariableAltName != null) - { - result = Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}{environmentVariableAltName}"); - } - - return result ?? throw new InvalidOperationException($"Environment variable {_environmentVariablePrefix}{environmentVariableName} is not set."); - } -} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs deleted file mode 100644 index 8ebe41d669f..00000000000 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/KubernetesClusterMetadataValidator.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ClusterMetadata.Kubernetes; - -[OptionsValidator] -internal sealed partial class KubernetesClusterMetadataValidator : IValidateOptions -{ -} diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj deleted file mode 100644 index 29975023508..00000000000 --- a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - Microsoft.Extensions.ClusterMetadata.Kubernetes - - $(NetCoreTargetFrameworks)$(ConditionalNet462) - enable - enable - true - Telemetry - $(NoWarn);CS0436 - - - - true - true - - - - dev - EXTEXP0015 - 99 - 90 - - - - - - - - - diff --git a/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.json b/src/Libraries/Microsoft.Extensions.ClusterMetadata.Kubernetes/Microsoft.Extensions.ClusterMetadata.Kubernetes.json deleted file mode 100644 index e69de29bb2d..00000000000 From d49ab90efb135732802a597d2b8a25d2623ba797 Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Fri, 12 Sep 2025 12:18:12 +0200 Subject: [PATCH 8/9] Add Linux changes --- .../Linux/LinuxUtilizationProvider.cs | 65 ++++--------------- ...ceMonitoringServiceCollectionExtensions.cs | 1 + .../WindowsContainerSnapshotProvider.cs | 4 +- 3 files changed, 14 insertions(+), 56 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index 1990251296a..b1e9630cc70 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -5,11 +5,9 @@ using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Threading; -using Microsoft.Extensions.ClusterMetadata.Kubernetes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Instruments; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; @@ -48,11 +46,13 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private long _previousCgroupCpuPeriodCounter; public SystemResources Resources { get; } + private IResourceQuotasProvider _resourceQuotasProvider; + public LinuxUtilizationProvider( IOptions options, ILinuxUtilizationParser parser, IMeterFactory meterFactory, - IOptions clusterMetadata, + IResourceQuotasProvider resourceQuotasProvider, ILogger? logger = null, TimeProvider? timeProvider = null) { @@ -67,18 +67,12 @@ public LinuxUtilizationProvider( _previousHostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); _previousCgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); - if (options.Value.UseKubernetesLimitsAndRequests) - { - ExtractResourceQuotas(clusterMetadata.Value); - } - else - { - _memoryLimit = _parser.GetAvailableMemoryInBytes(); - _cpuLimit = _parser.GetCgroupLimitedCpus(); - _memoryRequest = _memoryLimit; // TODO: see if we can get it from cgroups - _cpuRequest = _parser.GetCgroupRequestCpu(); - - } + _resourceQuotasProvider = resourceQuotasProvider; + var quotas = _resourceQuotasProvider.GetResourceQuotas(); + _memoryLimit = quotas.LimitsMemory; + _cpuLimit = quotas.LimitsCpu; + _cpuRequest = quotas.RequestsCpu; + _memoryRequest = quotas.RequestsMemory; float hostCpus = _parser.GetHostCpuCount(); double scaleRelativeToCpuLimit = hostCpus / _cpuLimit; @@ -94,7 +88,7 @@ public LinuxUtilizationProvider( if (options.Value.UseLinuxCalculationV2) { - if (!options.Value.UseKubernetesLimitsAndRequests) + if (!options.Value.EmitRequestsMetrics) { _cpuLimit = _parser.GetCgroupLimitV2(); _cpuRequest = _parser.GetCgroupRequestCpuV2(); @@ -142,7 +136,7 @@ public LinuxUtilizationProvider( observeValues: () => GetMeasurementWithRetry(MemoryPercentage), unit: "1"); - if (options.Value.UseKubernetesLimitsAndRequests) + if (options.Value.EmitRequestsMetrics) { _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, @@ -362,41 +356,4 @@ private IEnumerable> GetCpuTime() yield return new Measurement(userCpuTime, [new KeyValuePair("cpu.mode", "user")]); } } - - private void ExtractResourceQuotas(KubernetesClusterMetadata clusterMetadata) - { - _cpuRequest = ConvertMillicoreToUnit(clusterMetadata.RequestsCpu); - _cpuLimit = ConvertMillicoreToUnit(clusterMetadata.LimitsCpu); - _memoryRequest = clusterMetadata.RequestsMemory; - _memoryLimit = clusterMetadata.LimitsMemory; - - if (_cpuRequest <= 0) - { - Throw.InvalidOperationException($"REQUESTS_CPU detected value is {_cpuRequest}, " + - $"environment variables may be misconfigured"); - } - - if (_cpuLimit <= 0) - { - Throw.InvalidOperationException($"LIMITS_CPU detected value is {_cpuLimit}, " + - $"environment variables may be misconfigured"); - } - - if (_memoryRequest <= 0) - { - Throw.InvalidOperationException($"REQUESTS_MEMORY detected value is {_memoryRequest}, " + - $"environment variables may be misconfigured"); - } - - if (_memoryLimit <= 0) - { - Throw.InvalidOperationException($"LIMITS_MEMORY detected value is {_memoryLimit}, " + - $"environment variables may be misconfigured"); - } - } - - private static double ConvertMillicoreToUnit(ulong millicores) - { - return millicores / 1000.0; - } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index c91704abb18..c6a73ad639f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -129,6 +129,7 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild _ = Throw.IfNull(builder); builder.Services.TryAddActivatedSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(TimeProvider.System); builder.Services.TryAddSingleton(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index d595f0b250d..4cdb1d7c7af 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -58,9 +58,9 @@ public WindowsContainerSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, IOptions options, - IOptions clusterMetadata) + IResourceQuotasProvider resourceQuotasProvider) : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory, - static () => new JobHandleWrapper(), TimeProvider.System, options.Value, clusterMetadata.Value) + static () => new JobHandleWrapper(), TimeProvider.System, options.Value, resourceQuotasProvider) { } From 727159a3490ec5a44fc2c191d46feb771a7f3b5b Mon Sep 17 00:00:00 2001 From: amadeuszl Date: Tue, 30 Sep 2025 15:13:08 +0200 Subject: [PATCH 9/9] Add abstractions project --- .../IResourceQuotaProvider.cs | 25 +++ ...ics.ResourceMonitoring.Abstractions.csproj | 16 ++ ...stics.ResourceMonitoring.Abstractions.json | 0 .../ResourceQuota.cs} | 10 +- .../KubernetesMetadata.cs | 38 +++-- .../KubernetesResourceQuotaProvider.cs | 31 ++++ ...sourceQuotaServiceCollectionsExtensions.cs | 49 ++++++ .../KubernetesResourceQuotasProvider.cs | 31 ---- ...ourceQuotasServiceCollectionsExtensions.cs | 37 ----- ...stics.ResourceMonitoring.Kubernetes.csproj | 15 +- .../README.md | 24 +++ .../IResourceQuotasProvider.cs | 10 -- .../Linux/LinuxResourceQuotaProvider.cs | 39 +++++ .../Linux/LinuxUtilizationProvider.cs | 42 ++--- .../LinuxResourceQuotasProvider.cs | 14 -- ...ions.Diagnostics.ResourceMonitoring.csproj | 2 +- .../ResourceMonitoringOptions.cs | 2 - ...ceMonitoringServiceCollectionExtensions.cs | 4 +- .../WindowsContainerResourceQuotaProvider.cs | 96 +++++++++++ .../WindowsContainerSnapshotProvider.cs | 149 ++++++++---------- .../WindowsContainerResourceQuotasProvider.cs | 13 -- .../ResourceUtilizationInstruments.cs | 8 - ...eQuotasServiceCollectionExtensionsTests.cs | 30 ++++ ...ResourceMonitoring.Kubernetes.Tests.csproj | 17 ++ 24 files changed, 450 insertions(+), 252 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/IResourceQuotaProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions.json rename src/Libraries/{Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs => Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/ResourceQuota.cs} (67%) create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionsExtensions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/IResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/IResourceQuotaProvider.cs new file mode 100644 index 00000000000..960dbfd121e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/IResourceQuotaProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Provides resource quota information for resource monitoring purposes. +/// +/// +/// This interface defines a contract for retrieving resource quotas, which include +/// memory and CPU limits and requests that may be imposed by container orchestrators +/// or resource management systems. +/// +public interface IResourceQuotaProvider +{ + /// + /// Gets the current resource quota containing memory and CPU limits and requests.Returned is used in resource monitoring calculations. + /// + /// + /// A instance containing the current resource constraints + /// including memory limits, CPU limits, memory requests, and CPU requests. + /// + ResourceQuota GetResourceQuota(); +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions.csproj new file mode 100644 index 00000000000..065e0106e5f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions.csproj @@ -0,0 +1,16 @@ + + + + Microsoft.Extensions.Diagnostics.ResourceMonitoring + Measures processor and memory usage. + ResourceMonitoring + $(NoWarn);CS0436 + + + + normal + 99 + 90 + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions.json b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/ResourceQuota.cs similarity index 67% rename from src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs rename to src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/ResourceQuota.cs index 30aa8542e00..a2ebe85af45 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotas.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Abstractions/ResourceQuota.cs @@ -3,7 +3,11 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; -public class ResourceQuotas +/// +/// Represents resource quota information for a container, including CPU and memory limits and requests. +/// Limits define the maximum resources a container can use, while requests specify the minimum guaranteed resources. +/// +public class ResourceQuota { /// /// Gets or sets the resource memory limit the container is allowed to use. @@ -13,7 +17,7 @@ public class ResourceQuotas /// /// Gets or sets the resource CPU limit the container is allowed to use. /// - public ulong LimitsCpu { get; set; } + public double LimitsCpu { get; set; } /// /// Gets or sets the resource memory request the container is allowed to use. @@ -23,5 +27,5 @@ public class ResourceQuotas /// /// Gets or sets the resource CPU request the container is allowed to use. /// - public ulong RequestsCpu { get; set; } + public double RequestsCpu { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs index 91b623093d8..6d8ec54fdda 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs @@ -6,25 +6,25 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; -public class KubernetesMetadata +internal class KubernetesMetadata { /// - /// Gets or sets the resource memory limit the container is allowed to use. + /// Gets or sets the resource memory limit the container is allowed to use in bytes. /// public ulong LimitsMemory { get; set; } /// - /// Gets or sets the resource CPU limit the container is allowed to use. + /// Gets or sets the resource CPU limit the container is allowed to use in milicores. /// public ulong LimitsCpu { get; set; } /// - /// Gets or sets the resource memory request the container is allowed to use. + /// Gets or sets the resource memory request the container is allowed to use in bytes. /// public ulong RequestsMemory { get; set; } /// - /// Gets or sets the resource CPU request the container is allowed to use. + /// Gets or sets the resource CPU request the container is allowed to use in milicores. /// public ulong RequestsCpu { get; set; } @@ -35,11 +35,31 @@ public KubernetesMetadata(string environmentVariablePrefix) _environmentVariablePrefix = environmentVariablePrefix; } + /// + /// Fills the object with data loaded from environment variables. + /// public void Build() { - LimitsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_MEMORY"), CultureInfo.InvariantCulture); - LimitsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}LIMITS_CPU"), CultureInfo.InvariantCulture); - RequestsMemory = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_MEMORY"), CultureInfo.InvariantCulture); - RequestsCpu = Convert.ToUInt64(Environment.GetEnvironmentVariable($"{_environmentVariablePrefix}REQUESTS_CPU"), CultureInfo.InvariantCulture); + LimitsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_MEMORY"); + LimitsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_CPU"); + RequestsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_MEMORY"); + RequestsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_CPU"); + } + + private static ulong GetEnvironmentVariableAsUInt64(string variableName) + { + var value = Environment.GetEnvironmentVariable(variableName); + + if (string.IsNullOrEmpty(value)) + { + throw new InvalidOperationException($"Environment variable '{variableName}' is not set or is empty."); + } + + if (!ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong result)) + { + throw new InvalidOperationException($"Environment variable '{variableName}' contains invalid value '{value}'. Expected a non-negative integer."); + } + + return result; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs new file mode 100644 index 00000000000..7602d376359 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +internal class KubernetesResourceQuotaProvider : IResourceQuotaProvider +{ + private const double MillicoresPerCore = 1000.0; + private KubernetesMetadata _cosmicMetadata; + + public KubernetesResourceQuotaProvider(KubernetesMetadata cosmicMetadata) + { + _cosmicMetadata = cosmicMetadata; + } + + public ResourceQuota GetResourceQuota() + { + return new ResourceQuota + { + RequestsCpu = ConvertMillicoreToCpuUnit(_cosmicMetadata.RequestsCpu), + LimitsCpu = ConvertMillicoreToCpuUnit(_cosmicMetadata.LimitsCpu), + RequestsMemory = _cosmicMetadata.RequestsMemory, + LimitsMemory = _cosmicMetadata.LimitsMemory, + }; + } + + private static double ConvertMillicoreToCpuUnit(ulong millicores) + { + return millicores / MillicoresPerCore; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionsExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionsExtensions.cs new file mode 100644 index 00000000000..01f179f7505 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionsExtensions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Lets you configure and register Kubernetes resource monitoring components. +/// +public static class KubernetesResourceQuotaServiceCollectionsExtensions +{ + /// + /// Configures and adds an Kubernetes resource monitoring components to a service collection alltoghter with necessary basic resource monitoring components. + /// + /// The dependency injection container to add the Kubernetes resource monitoring to. + /// Value of prefix used to read environment varialbes in the container. + /// The value of . + /// + /// + /// If you have configured your Kubernetes container with Downward API to add environment variable MYCLUSTER_LIMITS_CPU with CPU limits, + /// then you should pass MYCLUSTER_ to parameter. Environment variables will be read during DI Container resolution. + /// + /// + /// Important: Do not call + /// if you are using this method, as it already includes all necessary resource monitoring components and registers a Kubernetes-specific + /// implementation. Calling both methods may result in conflicting service registrations. + /// + /// + public static IServiceCollection AddKubernetesResourceMonitoring( + this IServiceCollection services, + string environmentVariablePrefix = "") + { + services.TryAddSingleton(serviceProvider => + { + var metadata = new KubernetesMetadata(environmentVariablePrefix); + metadata.Build(); + + return metadata; + }); + services.TryAddSingleton(); + + _ = services.AddResourceMonitoring(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs deleted file mode 100644 index 545aba44e7d..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; - -internal class KubernetesResourceQuotasProvider : IResourceQuotasProvider -{ - private KubernetesMetadata _cosmicMetadata; - - public KubernetesResourceQuotasProvider(KubernetesMetadata cosmicMetadata) // we could inject ClusterMetadataHere in the future instead of that thingy - { - _cosmicMetadata = cosmicMetadata; - } - - public ResourceQuotas GetResourceQuotas() - { - // extract relevant cluster metadata - return new ResourceQuotas - { - RequestsCpu = ConvertMillicoreToUnit(_cosmicMetadata.RequestsCpu), - LimitsCpu = ConvertMillicoreToUnit(_cosmicMetadata.LimitsCpu), - RequestsMemory = _cosmicMetadata.RequestsMemory, - LimitsMemory = _cosmicMetadata.LimitsMemory, - }; - } - - private static double ConvertMillicoreToUnit(ulong millicores) - { - return millicores / 1000.0; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs deleted file mode 100644 index d2fbe898f87..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotasServiceCollectionsExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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 Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// -/// -public static class KubernetesResourceQuotasServiceCollectionsExtensions -{ - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddKubernetesResourceQuotas( - this IServiceCollection services, - Action? configureOptions = null) - { - // Simple - just register before AddResourceMonitoring() is called - services.TryAddSingleton(); - - if (configureOptions != null) - { - _ = services.Configure(configureOptions); - } - - services.AddResourceMonitoring(); - - return services; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj index a4cff855de8..bbe7604e186 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj @@ -7,17 +7,6 @@ - true - true - true - true - true - true - true - true - true - true - true true @@ -32,10 +21,8 @@ - - - + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md new file mode 100644 index 00000000000..1430f916d08 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md @@ -0,0 +1,24 @@ +# Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes + +Registers `ResourceQuota` implementation specific to Kubernetes. + +## Install the package + +From the command-line: + +```console +dotnet add package Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes +``` + +Or directly in the C# project file: + +```xml + + + +``` + + +## Feedback & Contributing + +We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions). diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs deleted file mode 100644 index c27bfdf7a63..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceQuotasProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; - -public interface IResourceQuotasProvider -{ - public ResourceQuotas GetResourceQuotas(); -} - diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs new file mode 100644 index 00000000000..09351235c6a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; + +internal class LinuxResourceQuotaProvider : IResourceQuotaProvider +{ + private readonly ILinuxUtilizationParser _parser; + private bool _useLinuxCalculationV2; + + public LinuxResourceQuotaProvider(ILinuxUtilizationParser parser, IOptions options) + { + _parser = parser; + _useLinuxCalculationV2 = options.Value.UseLinuxCalculationV2; + } + + public ResourceQuota GetResourceQuota() + { + var resourceQuota = new ResourceQuota(); + if (_useLinuxCalculationV2) + { + resourceQuota.LimitsCpu = _parser.GetCgroupLimitV2(); + resourceQuota.RequestsCpu = _parser.GetCgroupRequestCpuV2(); + } + else + { + resourceQuota.LimitsCpu = _parser.GetCgroupLimitedCpus(); + resourceQuota.RequestsCpu = _parser.GetCgroupRequestCpu(); + } + + resourceQuota.LimitsMemory = _parser.GetAvailableMemoryInBytes(); + resourceQuota.RequestsMemory = resourceQuota.LimitsMemory; + + return resourceQuota; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index b1e9630cc70..e945ac07714 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -22,10 +22,6 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private readonly object _memoryLocker = new(); private readonly ILogger _logger; private readonly ILinuxUtilizationParser _parser; - private double _memoryLimit; - private double _cpuLimit; - private ulong _memoryRequest; - private double _cpuRequest; private readonly long _cpuPeriodsInterval; private readonly TimeSpan _cpuRefreshInterval; private readonly TimeSpan _memoryRefreshInterval; @@ -36,6 +32,13 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private DateTimeOffset _lastFailure = DateTimeOffset.MinValue; private int _measurementsUnavailable; + private double _memoryLimit; + private double _cpuLimit; +#pragma warning disable S1450 // Private fields only used as local variables in methods should become local variables. This will be used once we bring relevant meters. + private ulong _memoryRequest; +#pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables + private double _cpuRequest; + private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; private double _cpuPercentage = double.NaN; @@ -46,13 +49,11 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider private long _previousCgroupCpuPeriodCounter; public SystemResources Resources { get; } - private IResourceQuotasProvider _resourceQuotasProvider; - public LinuxUtilizationProvider( IOptions options, ILinuxUtilizationParser parser, IMeterFactory meterFactory, - IResourceQuotasProvider resourceQuotasProvider, + IResourceQuotaProvider resourceQuotaProvider, ILogger? logger = null, TimeProvider? timeProvider = null) { @@ -67,12 +68,11 @@ public LinuxUtilizationProvider( _previousHostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); _previousCgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); - _resourceQuotasProvider = resourceQuotasProvider; - var quotas = _resourceQuotasProvider.GetResourceQuotas(); - _memoryLimit = quotas.LimitsMemory; - _cpuLimit = quotas.LimitsCpu; - _cpuRequest = quotas.RequestsCpu; - _memoryRequest = quotas.RequestsMemory; + var quota = resourceQuotaProvider.GetResourceQuota(); + _memoryLimit = quota.LimitsMemory; + _cpuLimit = quota.LimitsCpu; + _cpuRequest = quota.RequestsCpu; + _memoryRequest = quota.RequestsMemory; float hostCpus = _parser.GetHostCpuCount(); double scaleRelativeToCpuLimit = hostCpus / _cpuLimit; @@ -88,12 +88,6 @@ public LinuxUtilizationProvider( if (options.Value.UseLinuxCalculationV2) { - if (!options.Value.EmitRequestsMetrics) - { - _cpuLimit = _parser.GetCgroupLimitV2(); - _cpuRequest = _parser.GetCgroupRequestCpuV2(); - } - // Get Cpu periods interval from cgroup _cpuPeriodsInterval = _parser.GetCgroupPeriodsIntervalInMicroSecondsV2(); (_previousCgroupCpuTime, _previousCgroupCpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); @@ -136,14 +130,6 @@ public LinuxUtilizationProvider( observeValues: () => GetMeasurementWithRetry(MemoryPercentage), unit: "1"); - if (options.Value.EmitRequestsMetrics) - { - _ = meter.CreateObservableGauge( - name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, - observeValue: () => GetMeasurementWithRetry(MemoryPercantageForRequest), // todo implement - unit: "1"); - } - _ = meter.CreateObservableUpDownCounter( name: ResourceUtilizationInstruments.ContainerMemoryUsage, observeValues: () => GetMeasurementWithRetry(() => (long)MemoryUsage()), @@ -293,7 +279,7 @@ public Snapshot GetSnapshot() private double MemoryPercentage() { ulong memoryUsage = MemoryUsage(); - double memoryPercentage = Math.Min(One, (double)memoryUsage / _memoryLimit); + double memoryPercentage = Math.Min(One, memoryUsage / _memoryLimit); _logger.MemoryPercentageData(memoryUsage, _memoryLimit, memoryPercentage); return memoryPercentage; diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs deleted file mode 100644 index 74a4c72406a..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/LinuxResourceQuotasProvider.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; - -internal class LinuxResourceQuotasProvider : IResourceQuotasProvider - -{ - public ResourceQuotas GetResourceQuotas() - { - // bring logic from LinuxUtilizationProvider for limits and requests - } -} - diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj index af2e6cd790a..6bcbefad43f 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj @@ -32,7 +32,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs index c2693864b4c..eb890da291e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringOptions.cs @@ -119,6 +119,4 @@ public partial class ResourceMonitoringOptions /// Previously EnableDiskIoMetrics. [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] public bool EnableSystemDiskIoMetrics { get; set; } - - public bool EmitRequestsMetrics { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs index c6a73ad639f..453482e39e9 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs @@ -115,7 +115,7 @@ private static void PickWindowsSnapshotProvider(this ResourceMonitorBuilder buil if (JobObjectInfo.SafeJobHandle.IsProcessInJob()) { builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); } else { @@ -129,7 +129,7 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild _ = Throw.IfNull(builder); builder.Services.TryAddActivatedSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(TimeProvider.System); builder.Services.TryAddSingleton(); diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs new file mode 100644 index 00000000000..35dfa2db4ed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs @@ -0,0 +1,96 @@ +// 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.Threading; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; + +internal class WindowsContainerResourceQuotaProvider : IResourceQuotaProvider +{ + private readonly ISystemInfo _systemInfo; + private readonly Lazy _memoryStatus; + private Func _createJobHandleObject; + + public WindowsContainerResourceQuotaProvider() + : this(new MemoryInfo(), new SystemInfo(), static () => new JobHandleWrapper()) + { + } + + public WindowsContainerResourceQuotaProvider(IMemoryInfo memoryInfo, ISystemInfo systemInfo, Func createJobHandleObject) + { + _systemInfo = systemInfo; + _createJobHandleObject = createJobHandleObject; + + _memoryStatus = new Lazy( + memoryInfo.GetMemoryStatus, + LazyThreadSafetyMode.ExecutionAndPublication); + } + + public ResourceQuota GetResourceQuota() + { + // bring logic from WindowsContainerSnapshotProvider for limits and requests + using IJobHandle jobHandle = _createJobHandleObject(); + + var resourceQuota = new ResourceQuota + { + LimitsCpu = GetCpuLimit(jobHandle, _systemInfo), + LimitsMemory = GetMemoryLimit(jobHandle), + }; + + // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units). + // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory). + resourceQuota.RequestsCpu = resourceQuota.LimitsCpu; + resourceQuota.RequestsMemory = resourceQuota.LimitsMemory; + + return resourceQuota; + } + + private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo) + { + // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales + // the CPU units with the number of processors (cores) available in the system. + const double CpuCycles = 10_000U; + + var cpuLimit = jobHandle.GetJobCpuLimitInfo(); + double cpuRatio = 1.0; + if ((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0 && + (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0) + { + // The CpuRate is represented as number of cycles during scheduling interval, where + // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000, + // that means that the application (or container) is assigned 20% of the total CPU available. + // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU). + // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN + cpuRatio = cpuLimit.CpuRate / CpuCycles; + } + + SYSTEM_INFO systemInfoValue = systemInfo.GetSystemInfo(); + + // Multiply the cpu ratio by the number of processors to get you the portion + // of processors used from the system. + return cpuRatio * systemInfoValue.NumberOfProcessors; + } + + /// + /// Gets memory limit of the system. + /// + /// Memory limit allocated to the system in bytes. + private ulong GetMemoryLimit(IJobHandle jobHandle) + { + var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64(); + + if (memoryLimitInBytes <= 0) + { + MEMORYSTATUSEX memoryStatus = _memoryStatus.Value; + + // Technically, the unconstrained limit is memoryStatus.TotalPageFile. + // Leaving this at physical as it is more understandable to consumers. + memoryLimitInBytes = memoryStatus.TotalPhys; + } + + return memoryLimitInBytes; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 4cdb1d7c7af..a1ebf9c2c4e 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; -using System.Threading; using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -20,8 +19,6 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private const double Hundred = 100.0d; private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; - private readonly Lazy _memoryStatus; - /// /// This represents a factory method for creating the JobHandle. /// @@ -29,28 +26,32 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private readonly object _cpuLocker = new(); private readonly object _memoryLocker = new(); + private readonly object _processMemoryLocker = new(); private readonly TimeProvider _timeProvider; private readonly IProcessInfo _processInfo; private readonly ILogger _logger; + private readonly TimeSpan _cpuRefreshInterval; + private readonly TimeSpan _memoryRefreshInterval; + private readonly double _metricValueMultiplier; + private double _memoryLimit; private double _cpuLimit; +#pragma warning disable S1450 // Private fields only used as local variables in methods should become local variables. Those will be used once we bring relevant meters. private ulong _memoryRequest; private double _cpuRequest; - private readonly TimeSpan _cpuRefreshInterval; - private readonly TimeSpan _memoryRefreshInterval; - private readonly double _metricValueMultiplier; +#pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables private long _oldCpuUsageTicks; private long _oldCpuTimeTicks; private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; + private DateTimeOffset _refreshAfterProcessMemory; private double _cpuPercentage = double.NaN; - private double _memoryPercentage; + private double _processMemoryPercentage; + private ulong _memoryUsage; public SystemResources Resources { get; } - private IResourceQuotasProvider _resourceQuotasProvider; - /// /// Initializes a new instance of the class. /// @@ -58,9 +59,9 @@ public WindowsContainerSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, IOptions options, - IResourceQuotasProvider resourceQuotasProvider) - : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory, - static () => new JobHandleWrapper(), TimeProvider.System, options.Value, resourceQuotasProvider) + IResourceQuotaProvider resourceQuotaProvider) + : this(new ProcessInfo(), logger, meterFactory, + static () => new JobHandleWrapper(), TimeProvider.System, options.Value, resourceQuotaProvider) { } @@ -70,24 +71,19 @@ public WindowsContainerSnapshotProvider( /// This constructor enables the mocking of dependencies for the purpose of Unit Testing only. [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Dependencies for testing")] internal WindowsContainerSnapshotProvider( - IMemoryInfo memoryInfo, - ISystemInfo systemInfo, IProcessInfo processInfo, ILogger? logger, IMeterFactory meterFactory, Func createJobHandleObject, TimeProvider timeProvider, ResourceMonitoringOptions options, - IResourceQuotasProvider resourceQuotasProvider) + IResourceQuotaProvider resourceQuotaProvider) { _logger = logger ?? NullLogger.Instance; _logger.RunningInsideJobObject(); _metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred; - _memoryStatus = new Lazy( - memoryInfo.GetMemoryStatus, - LazyThreadSafetyMode.ExecutionAndPublication); _createJobHandleObject = createJobHandleObject; _processInfo = processInfo; @@ -95,12 +91,11 @@ internal WindowsContainerSnapshotProvider( using IJobHandle jobHandle = _createJobHandleObject(); - _resourceQuotasProvider = resourceQuotasProvider; - var quotas = _resourceQuotasProvider.GetResourceQuotas(); - _memoryLimit = quotas.LimitsMemory; - _cpuLimit = quotas.LimitsCpu; - _cpuRequest = quotas.RequestsCpu; - _memoryRequest = quotas.RequestsMemory; + var quota = resourceQuotaProvider.GetResourceQuota(); + _cpuLimit = quota.LimitsCpu; + _memoryLimit = quota.LimitsMemory; + _cpuRequest = quota.RequestsCpu; + _memoryRequest = quota.RequestsMemory; ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); @@ -113,6 +108,7 @@ internal WindowsContainerSnapshotProvider( _memoryRefreshInterval = options.MemoryConsumptionRefreshInterval; _refreshAfterCpu = _timeProvider.GetUtcNow(); _refreshAfterMemory = _timeProvider.GetUtcNow(); + _refreshAfterProcessMemory = _timeProvider.GetUtcNow(); #pragma warning disable CA2000 // Dispose objects before losing scope // We don't dispose the meter because IMeterFactory handles that @@ -121,21 +117,34 @@ internal WindowsContainerSnapshotProvider( Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName); #pragma warning restore CA2000 // Dispose objects before losing scope - // Container based metrics: - _ = meter.CreateObservableCounter(name: ResourceUtilizationInstruments.ContainerCpuTime, observeValues: GetCpuTime, unit: "s", description: "CPU time used by the container."); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, observeValue: CpuPercentage); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetMemoryUsage())); + _ = meter.CreateObservableCounter( + name: ResourceUtilizationInstruments.ContainerCpuTime, + observeValues: GetCpuTime, + unit: "s", + description: "CPU time used by the container."); - // Requests enabled metrics - if (options.EmitRequestsMetrics) - { - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, observeValue: () => CpuPercentageForRequests()); // todo implement - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, observeValue: () => MemoryPercentaForRequests()); // todo implement - } + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, + observeValue: CpuPercentage); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, + observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryLimit * _metricValueMultiplier)); + + _ = meter.CreateObservableUpDownCounter( + name: ResourceUtilizationInstruments.ContainerMemoryUsage, + observeValue: () => (long)MemoryUsage(), + unit: "By", + description: "Memory usage of the container."); // Process based metrics: - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessCpuUtilization, observeValue: CpuPercentage); - _ = meter.CreateObservableGauge(name: ResourceUtilizationInstruments.ProcessMemoryUtilization, observeValue: () => MemoryPercentage(() => _processInfo.GetCurrentProcessMemoryUsage())); + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessCpuUtilization, + observeValue: CpuPercentage); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ProcessMemoryUtilization, + observeValue: ProcessMemoryPercentage); } public Snapshot GetSnapshot() @@ -152,53 +161,35 @@ public Snapshot GetSnapshot() _processInfo.GetCurrentProcessMemoryUsage()); } - private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo) + private double ProcessMemoryPercentage() { - // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales - // the CPU units with the number of processors (cores) available in the system. - const double CpuCycles = 10_000U; - - var cpuLimit = jobHandle.GetJobCpuLimitInfo(); - double cpuRatio = 1.0; - if ((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0 && - (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0) + DateTimeOffset now = _timeProvider.GetUtcNow(); + + lock (_processMemoryLocker) { - // The CpuRate is represented as number of cycles during scheduling interval, where - // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000, - // that means that the application (or container) is assigned 20% of the total CPU available. - // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU). - // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN - cpuRatio = cpuLimit.CpuRate / CpuCycles; + if (now < _refreshAfterProcessMemory) + { + return _processMemoryPercentage; + } } - SYSTEM_INFO systemInfoValue = systemInfo.GetSystemInfo(); - - // Multiply the cpu ratio by the number of processors to get you the portion - // of processors used from the system. - return cpuRatio * systemInfoValue.NumberOfProcessors; - } - - /// - /// Gets memory limit of the system. - /// - /// Memory limit allocated to the system in bytes. - private ulong GetMemoryLimit(IJobHandle jobHandle) - { - var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64(); + ulong processMemoryUsage = _processInfo.GetCurrentProcessMemoryUsage(); - if (memoryLimitInBytes <= 0) + lock (_processMemoryLocker) { - MEMORYSTATUSEX memoryStatus = _memoryStatus.Value; + if (now >= _refreshAfterProcessMemory) + { + _processMemoryPercentage = Math.Min(_metricValueMultiplier, processMemoryUsage / _memoryLimit * _metricValueMultiplier); + _refreshAfterProcessMemory = now.Add(_memoryRefreshInterval); - // Technically, the unconstrained limit is memoryStatus.TotalPageFile. - // Leaving this at physical as it is more understandable to consumers. - memoryLimitInBytes = memoryStatus.TotalPhys; - } + _logger.ProcessMemoryPercentageData(processMemoryUsage, _memoryLimit, _processMemoryPercentage); + } - return memoryLimitInBytes; + return _processMemoryPercentage; + } } - private double MemoryPercentage(Func getMemoryUsage) + private ulong MemoryUsage() { DateTimeOffset now = _timeProvider.GetUtcNow(); @@ -206,24 +197,22 @@ private double MemoryPercentage(Func getMemoryUsage) { if (now < _refreshAfterMemory) { - return _memoryPercentage; + return _memoryUsage; } } - ulong memoryUsage = getMemoryUsage(); + ulong memoryUsage = _processInfo.GetMemoryUsage(); lock (_memoryLocker) { if (now >= _refreshAfterMemory) { - // Don't change calculation order, otherwise we loose some precision: - _memoryPercentage = Math.Min(_metricValueMultiplier, memoryUsage / _memoryLimit * _metricValueMultiplier); + _memoryUsage = memoryUsage; _refreshAfterMemory = now.Add(_memoryRefreshInterval); + _logger.ContainerMemoryUsageData(_memoryUsage, _memoryLimit); } - _logger.MemoryUsageData(memoryUsage, _memoryLimit, _memoryPercentage); - - return _memoryPercentage; + return _memoryUsage; } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs deleted file mode 100644 index 7fea759a89a..00000000000 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/WindowsContainerResourceQuotasProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; - -internal class WindowsContainerResourceQuotasProvider : IResourceQuotasProvider -{ - public ResourceQuotas GetResourceQuotas() - { - // bring logic from WindowsContainerSnapshotProvider for limits and requests - } -} - diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index 88351385b21..b1593edd396 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -58,14 +58,6 @@ internal static class ResourceUtilizationInstruments /// public const string ContainerMemoryUsage = "container.memory.usage"; - /// - /// The name of an instrument to retrieve memory request consumption of all processes running inside a container or control group in range [0, 1]. - /// - /// - /// The type of an instrument is . - /// - public const string ContainerMemoryRequestUtilization = "container.memory.request.utilization"; - /// /// The name of an instrument to retrieve CPU consumption share of the running process in range [0, 1]. /// diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000000..c0095e56cb8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests; + +public class KubernetesResourceQuotasServiceCollectionExtensionsTests +{ + [Fact] + public void AddKubernetesResourceMonitoring_WithoutConfiguration_RegistersServicesCorrectly() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddKubernetesResourceMonitoring(); + + // Assert + using var serviceProvider = services.BuildServiceProvider(); + + IResourceQuotaProvider? resourceQuotaProvider = serviceProvider.GetService(); + Assert.NotNull(resourceQuotaProvider); + Assert.IsType(resourceQuotaProvider); + Assert.NotNull(serviceProvider.GetService()); + Assert.NotNull(serviceProvider.GetService()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj new file mode 100644 index 00000000000..b2cb0677778 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj @@ -0,0 +1,17 @@ + + + + Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test + Unit tests for Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes. + + + + + + + + + + + +