Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
be94825
Draft
amadeuszl Jul 11, 2025
bb36737
Draft
amadeuszl Jul 30, 2025
6024134
Merge branch 'main' into users/alechniak/kuberenetes-metadata
amadeuszl Aug 26, 2025
785a2f0
Add ResourceQuotasProvider
amadeuszl Aug 26, 2025
95e1a3c
Fix public API surface, remove Quotas provider component
amadeuszl Aug 28, 2025
cfc4861
Switch layering of the abstractions to support Kubernetes metadata
amadeuszl Sep 11, 2025
27fd482
Merge branch 'main' into users/alechniak/kuberenetes-metadata
amadeuszl Sep 11, 2025
632636f
Fix namespace
amadeuszl Sep 11, 2025
4faae5b
Remove ClusterMetadata
amadeuszl Sep 11, 2025
d49ab90
Add Linux changes
amadeuszl Sep 12, 2025
727159a
Add abstractions project
amadeuszl Sep 30, 2025
02517e4
Merge branch 'dotnet:main' into users/alechniak/kuberenetes-metadata
amadeuszl Sep 30, 2025
8057769
Merge branch 'users/alechniak/kuberenetes-metadata' of https://github…
amadeuszl Sep 30, 2025
ced4716
Add changes after API Review
amadeuszl Oct 22, 2025
66bf3ca
Add changes after API Review pt.2
amadeuszl Nov 5, 2025
67c3553
Merge branch 'main' into users/alechniak/kuberenetes-metadata
amadeuszl Nov 5, 2025
87b81cf
Fixes
amadeuszl Nov 5, 2025
cd8bf74
Add reading memory min and low for cgroupsv2
amadeuszl Nov 5, 2025
296f896
Add unit tests
amadeuszl Nov 6, 2025
01f2f4d
Fix typos
amadeuszl Nov 10, 2025
fb94614
Address PR comments and fix lint issues
amadeuszl Nov 11, 2025
d9158c0
Fix flaky test
amadeuszl Nov 11, 2025
9e4eb2b
Merge branch 'main' into users/alechniak/kuberenetes-metadata
amadeuszl Nov 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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;

internal class KubernetesMetadata
{
/// <summary>
/// Gets or sets the resource memory limit the container is allowed to use in bytes.
/// </summary>
public ulong LimitsMemory { get; set; }

/// <summary>
/// Gets or sets the resource CPU limit the container is allowed to use in milicores.
/// </summary>
public ulong LimitsCpu { get; set; }

/// <summary>
/// Gets or sets the resource memory request the container is allowed to use in bytes.
/// </summary>
public ulong RequestsMemory { get; set; }

/// <summary>
/// Gets or sets the resource CPU request the container is allowed to use in milicores.
/// </summary>
public ulong RequestsCpu { get; set; }

private string _environmentVariablePrefix;

public KubernetesMetadata(string environmentVariablePrefix)
{
_environmentVariablePrefix = environmentVariablePrefix;
}

/// <summary>
/// Fills the object with data loaded from environment variables.
/// </summary>
/// <returns>Self</returns>
public KubernetesMetadata Build()
{
LimitsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_MEMORY");
LimitsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_CPU");
RequestsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_MEMORY");
RequestsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_CPU");

return this;
}

private static ulong GetEnvironmentVariableAsUInt64(string variableName)
{
var value = Environment.GetEnvironmentVariable(variableName);
if (string.IsNullOrWhiteSpace(value))
{
return 0;
}

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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes;

internal class KubernetesResourceQuotaProvider : ResourceQuotaProvider
{
private const double MillicoresPerCore = 1000.0;
private KubernetesMetadata _kubernetesMetadata;

public KubernetesResourceQuotaProvider(KubernetesMetadata kubernetesMetadata)
{
_ = Throw.IfNull(kubernetesMetadata);
_kubernetesMetadata = kubernetesMetadata;
}

public override ResourceQuota GetResourceQuota()
{
ResourceQuota quota = new()
{
BaselineCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.RequestsCpu),
MaxCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.LimitsCpu),
BaselineMemoryInBytes = _kubernetesMetadata.RequestsMemory,
MaxMemoryInBytes = _kubernetesMetadata.LimitsMemory,
};

if (quota.BaselineCpuInCores <= 0.0)
{
quota.BaselineCpuInCores = quota.MaxCpuInCores;
}

if (quota.BaselineMemoryInBytes == 0)
{
quota.BaselineMemoryInBytes = quota.MaxMemoryInBytes;
}

return quota;
}

private static double ConvertMillicoreToCpuUnit(ulong millicores)
{
return millicores / MillicoresPerCore;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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;

/// <summary>
/// Lets you configure and register Kubernetes resource monitoring components.
/// </summary>
public static class KubernetesResourceQuotaServiceCollectionExtensions
{
/// <summary>
/// Configures and adds an Kubernetes resource monitoring components to a service collection alltoghter with necessary basic resource monitoring components.
/// </summary>
/// <param name="services">The dependency injection container to add the Kubernetes resource monitoring to.</param>
/// <param name="environmentVariablePrefix">Optional value of prefix used to read environment variables in the container.</param>
/// <returns>The value of <paramref name="services" />.</returns>
/// <remarks>
/// <para>
/// If you have configured your Kubernetes container with Downward API to add environment variable <c>MYCLUSTER_LIMITS_CPU</c> with CPU limits,
/// then you should pass <c>MYCLUSTER_</c> to <paramref name="environmentVariablePrefix"/> parameter. Environment variables will be read during DI Container resolution.
/// </para>
/// <para>
/// <strong>Important:</strong> Do not call <see cref="ResourceMonitoringServiceCollectionExtensions.AddResourceMonitoring(IServiceCollection)"/>
/// if you are using this method, as it already includes all necessary resource monitoring components and registers a Kubernetes-specific
/// <see cref="ResourceQuotaProvider"/> implementation. Calling both methods may result in conflicting service registrations.
/// </para>
/// </remarks>
public static IServiceCollection AddKubernetesResourceMonitoring(
this IServiceCollection services,
string? environmentVariablePrefix = default)
{
services.TryAddSingleton<KubernetesMetadata>(serviceProvider =>
{
var metadata = new KubernetesMetadata(environmentVariablePrefix ?? string.Empty);
return metadata.Build();
});
services.TryAddSingleton<ResourceQuotaProvider, KubernetesResourceQuotaProvider>();

_ = services.AddResourceMonitoring();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes</RootNamespace>
<Description>Provides Kubernetes data for measurements of processor and memory usage.</Description>
<Workstream>ResourceMonitoring</Workstream>
<NoWarn Condition="'$(TargetFramework)' == 'net462'">$(NoWarn);CS0436</NoWarn>
<EnablePackageValidation>false</EnablePackageValidation>
</PropertyGroup>

<PropertyGroup>
<InjectSharedInstruments>true</InjectSharedInstruments>
</PropertyGroup>

<PropertyGroup>
<Stage>normal</Stage>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be dev stage initially. After one release (= one month) we can change it to normal. And in this case we don't have to bother with EnablePackageValidation because it is not enabled for dev packages:

<PackageValidationBaselineVersion Condition="'$(Stage)' == 'normal' and '$(PackageValidationBaselineVersion)' == ''">9.10.0</PackageValidationBaselineVersion>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me verify that. I would prefer to release it already in December as normal.

<MinCodeCoverage>99</MinCodeCoverage>
<MinMutationScore>90</MinMutationScore>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<Compile Remove="Linux\**\*.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.Diagnostics.ResourceMonitoring\Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleToDynamicProxyGenAssembly2 Include="*" />
<InternalsVisibleToTest Include="$(AssemblyName).Tests" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes" Version="[CURRENTVERSION]" />
</ItemGroup>
```


## Feedback & Contributing

We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions).
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,10 @@ internal interface ILinuxUtilizationParser
/// </summary>
/// <returns>The number of CPU periods.</returns>
long GetCgroupPeriodsIntervalInMicroSecondsV2();

/// <summary>
/// For CgroupV2 only. Reads the file /sys/fs/cgroup/memory.min, if 0 reads the file /sys/fs/cgroup/memory.low.
/// </summary>
/// <returns>memory.</returns>
ulong GetMinMemoryInBytes();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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 : ResourceQuotaProvider
{
private readonly ILinuxUtilizationParser _parser;
private bool _useLinuxCalculationV2;

public LinuxResourceQuotaProvider(ILinuxUtilizationParser parser, IOptions<ResourceMonitoringOptions> options)
{
_parser = parser;
_useLinuxCalculationV2 = options.Value.UseLinuxCalculationV2;
}

public override ResourceQuota GetResourceQuota()
{
var resourceQuota = new ResourceQuota();
if (_useLinuxCalculationV2)
{
resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitV2();
resourceQuota.BaselineCpuInCores = _parser.GetCgroupRequestCpuV2();
}
else
{
resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitedCpus();
resourceQuota.BaselineCpuInCores = _parser.GetCgroupRequestCpu();
}

resourceQuota.MaxMemoryInBytes = _parser.GetAvailableMemoryInBytes();
resourceQuota.BaselineMemoryInBytes = _parser.GetMinMemoryInBytes();

if (resourceQuota.BaselineMemoryInBytes == 0)
{
resourceQuota.BaselineMemoryInBytes = resourceQuota.MaxMemoryInBytes;
}

return resourceQuota;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,12 @@ static void ThrowException(ReadOnlySpan<char> content) =>
$"Could not parse '{_cpuSetCpus}'. Expected comma-separated list of integers, with dashes (\"-\") based ranges (\"0\", \"2-6,12\") but got '{new string(content)}'.");
}

/// <summary>
/// In CgroupV1 there's no equivalent to memory.min or memory.low files.
/// </summary>
/// <returns>0.</returns>
public ulong GetMinMemoryInBytes() => 0;

/// <remarks>
/// The input must contain only number. If there is something more than whitespace before the number, it will return failure (-1).
/// </remarks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal sealed class LinuxUtilizationParserCgroupV2 : ILinuxUtilizationParser
private const string CpuStat = "cpu.stat"; // File containing CPU usage in nanoseconds.
private const string CpuLimit = "cpu.max"; // File with amount of CPU time available to the group along with the accounting period in microseconds.
private const string CpuRequest = "cpu.weight"; // CPU weights, also known as shares in cgroup v1, is used for resource allocation.
private const string MemoryMin = "memory.min"; // File contains min memory, set for QoS by K8s
private const string MemoryLow = "memory.low"; // File contains min memory, set if there was memory reservation for container
private static readonly ObjectPool<BufferWriter<char>> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool<char>();

/// <remarks>
Expand Down Expand Up @@ -513,6 +515,50 @@ static void ThrowException(ReadOnlySpan<char> content) =>
$"Could not parse '{_cpuSetCpus}'. Expected comma-separated list of integers, with dashes (\"-\") based ranges (\"0\", \"2-6,12\") but got '{new string(content)}'.");
}

public ulong GetMinMemoryInBytes()
{
FileInfo memoryMinFile = new(GetCgroupPath(MemoryMin));
if (_fileSystem.Exists(memoryMinFile))
{
using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
_fileSystem.ReadAll(memoryMinFile, bufferWriter.Buffer);

ReadOnlySpan<char> memoryMinBuffer = bufferWriter.Buffer.WrittenSpan;

_ = GetNextNumber(memoryMinBuffer, out long memoryMin);

if (memoryMin == -1)
{
Throw.InvalidOperationException($"Could not parse '{memoryMinFile}' content. Expected to find memory minimum in bytes but got '{new string(memoryMinBuffer)}' instead.");
}

if (memoryMin != 0)
{
return (ulong)memoryMin;
}
}

FileInfo memoryLowFile = new(GetCgroupPath(MemoryLow));
if (_fileSystem.Exists(memoryLowFile))
{
using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
_fileSystem.ReadAll(memoryLowFile, bufferWriter.Buffer);

ReadOnlySpan<char> memoryLowBuffer = bufferWriter.Buffer.WrittenSpan;

_ = GetNextNumber(memoryLowBuffer, out long memoryLow);

if (memoryLow == -1)
{
Throw.InvalidOperationException($"Could not parse '{memoryLowFile}' content. Expected to find memory low in bytes but got '{new string(memoryLowBuffer)}' instead.");
}

return (ulong)memoryLow;
}

return 0;
}

private static (long cpuUsageNanoseconds, long nrPeriods) ParseCpuUsageFromFile(IFileSystem fileSystem, FileInfo cpuUsageFile)
{
// The values we are interested in start with these prefixes
Expand Down
Loading
Loading