Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add network metrics to Resource Monitoring for Linux #5367

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions eng/MSBuild/Shared.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@
<ItemGroup Condition="'$(InjectSharedBufferWriterPool)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\BufferWriterPool\*.cs" LinkBase="Shared\Pools" />
</ItemGroup>

<ItemGroup Condition="'$(InjectStringSplitExtensions)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\StringSplit\*.cs" LinkBase="Shared\StringSplit" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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;

/// <summary>
/// An interface for getting TCP/IP state information.
/// </summary>
internal interface ITcpStateInfoProvider
{
/// <summary>
/// Gets the last known TCP/IP v4 state of the system.
/// </summary>
/// <returns>An instance of <see cref="TcpStateInfo"/>.</returns>
TcpStateInfo GetpIpV4TcpStateInfo();

/// <summary>
/// Gets the last known TCP/IP v6 state of the system.
/// </summary>
/// <returns>An instance of <see cref="TcpStateInfo"/>.</returns>
TcpStateInfo GetpIpV6TcpStateInfo();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,38 @@ internal interface IFileSystem
/// <summary>
/// Checks for file existence.
/// </summary>
/// <returns> True/False.</returns>
/// <returns><see langword="true"/> if file exists; otherwise, <see langword="false"/>.</returns>
bool Exists(FileInfo fileInfo);

/// <summary>
/// Get directory names on the filesystem based on the provided pattern.
/// Get directory names on the filesystem based on the specified pattern.
/// </summary>
/// <returns>string.</returns>
/// <returns> A read-only collection of the paths of directories that match the specified pattern, or an empty read-only collection if no directories are found.</returns>
IReadOnlyCollection<string> GetDirectoryNames(string directory, string pattern);

/// <summary>
/// Reads content from the file.
/// Reads content of the given length from a file and writes the data in the destination buffer.
/// </summary>
/// <returns>
/// Chars written.
/// The total number of bytes read into the destination buffer.
/// </returns>
int Read(FileInfo file, int length, Span<char> destination);

/// <summary>
/// Read all content from a file.
/// Read all content from a file and writes the data in the destination buffer.
/// </summary>
void ReadAll(FileInfo file, BufferWriter<char> destination);

/// <summary>
/// Reads first line from the file.
/// Reads first line from the file and writes the data in the destination buffer.
/// </summary>
void ReadFirstLine(FileInfo file, BufferWriter<char> destination);

/// <summary>
/// Reads all content from a file line by line.
/// </summary>
/// <returns>
/// The enumerable that represents all the lines of the file.
/// </returns>
IEnumerable<ReadOnlyMemory<char>> ReadAllByLines(FileInfo file, BufferWriter<char> destination);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;

namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network;

internal sealed class LinuxNetworkMetrics
{
private readonly ITcpStateInfoProvider _tcpStateInfoProvider;

public LinuxNetworkMetrics(IMeterFactory meterFactory, ITcpStateInfoProvider tcpStateInfoProvider)
{
_tcpStateInfoProvider = tcpStateInfoProvider;

#pragma warning disable CA2000 // Dispose objects before losing scope
// We don't dispose the meter because IMeterFactory handles that
// Is's a false-positive, see: https://github.com/dotnet/roslyn-analyzers/issues/6912
// Related documentation: https://github.com/dotnet/docs/pull/37170.
var meter = meterFactory.Create(nameof(ResourceMonitoring));
#pragma warning restore CA2000 // Dispose objects before losing scope

KeyValuePair<string, object?> tcpTag = new("network.transport", "tcp");
TagList commonTags = new() { tcpTag };

// The metric is aligned with
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md#metric-systemnetworkconnections
_ = meter.CreateObservableUpDownCounter(
ResourceUtilizationInstruments.SystemNetworkConnections,
GetMeasurements,
unit: "{connection}",
description: null,
tags: commonTags);
}

private IEnumerable<Measurement<long>> GetMeasurements()
{
const string NetworkStateKey = "system.network.state";

// These are covered in https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-metrics.md#attributes:
KeyValuePair<string, object?> tcpVersionFourTag = new("network.type", "ipv4");
KeyValuePair<string, object?> tcpVersionSixTag = new("network.type", "ipv6");

List<Measurement<long>> measurements = new(24);

// IPv4:
TcpStateInfo stateV4 = _tcpStateInfoProvider.GetpIpV4TcpStateInfo();
measurements.Add(new Measurement<long>(stateV4.ClosedCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "close") }));
measurements.Add(new Measurement<long>(stateV4.ListenCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "listen") }));
measurements.Add(new Measurement<long>(stateV4.SynSentCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "syn_sent") }));
measurements.Add(new Measurement<long>(stateV4.SynRcvdCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "syn_recv") }));
measurements.Add(new Measurement<long>(stateV4.EstabCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "established") }));
measurements.Add(new Measurement<long>(stateV4.FinWait1Count, new TagList { tcpVersionFourTag, new(NetworkStateKey, "fin_wait_1") }));
measurements.Add(new Measurement<long>(stateV4.FinWait2Count, new TagList { tcpVersionFourTag, new(NetworkStateKey, "fin_wait_2") }));
measurements.Add(new Measurement<long>(stateV4.CloseWaitCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "close_wait") }));
measurements.Add(new Measurement<long>(stateV4.ClosingCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "closing") }));
measurements.Add(new Measurement<long>(stateV4.LastAckCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "last_ack") }));
measurements.Add(new Measurement<long>(stateV4.TimeWaitCount, new TagList { tcpVersionFourTag, new(NetworkStateKey, "time_wait") }));

// IPv6:
TcpStateInfo stateV6 = _tcpStateInfoProvider.GetpIpV6TcpStateInfo();
measurements.Add(new Measurement<long>(stateV6.ClosedCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "close") }));
measurements.Add(new Measurement<long>(stateV6.ListenCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "listen") }));
measurements.Add(new Measurement<long>(stateV6.SynSentCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "syn_sent") }));
measurements.Add(new Measurement<long>(stateV6.SynRcvdCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "syn_recv") }));
measurements.Add(new Measurement<long>(stateV6.EstabCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "established") }));
measurements.Add(new Measurement<long>(stateV6.FinWait1Count, new TagList { tcpVersionSixTag, new(NetworkStateKey, "fin_wait_1") }));
measurements.Add(new Measurement<long>(stateV6.FinWait2Count, new TagList { tcpVersionSixTag, new(NetworkStateKey, "fin_wait_2") }));
measurements.Add(new Measurement<long>(stateV6.CloseWaitCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "close_wait") }));
measurements.Add(new Measurement<long>(stateV6.ClosingCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "closing") }));
measurements.Add(new Measurement<long>(stateV6.LastAckCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "last_ack") }));
measurements.Add(new Measurement<long>(stateV6.TimeWaitCount, new TagList { tcpVersionSixTag, new(NetworkStateKey, "time_wait") }));

return measurements;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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.IO;
using Microsoft.Extensions.ObjectPool;
#if !NET8_0_OR_GREATER
using Microsoft.Shared.StringSplit;
#endif
using Microsoft.Shared.Diagnostics;
using Microsoft.Shared.Pools;

namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network;

internal sealed class LinuxNetworkUtilizationParser
{
private static readonly ObjectPool<BufferWriter<char>> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool<char>();

/// <remarks>
/// File that provide information about currently active TCP_IPv4 connections.
/// </remarks>
private static readonly FileInfo _tcp = new("/proc/net/tcp");

/// <remarks>
/// File that provide information about currently active TCP_IPv6 connections.
/// </remarks>
private static readonly FileInfo _tcp6 = new("/proc/net/tcp6");

private readonly IFileSystem _fileSystem;

/// <remarks>
/// Reads the contents of a file located at <see cref="_tcp"/> and parses it to extract information about the TCP/IP state info of the system.
/// </remarks>
public TcpStateInfo GetTcpIPv4StateInfo() => GetTcpStateInfo(_tcp);

/// <remarks>
/// Reads the contents of a file located at <see cref="_tcp6"/> and parses it to extract information about the TCP/IP state info of the system.
/// </remarks>
public TcpStateInfo GetTcpIPv6StateInfo() => GetTcpStateInfo(_tcp6);

public LinuxNetworkUtilizationParser(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}

/// <remarks>
/// Parses the contents of the <paramref name="buffer"/> and updates the <paramref name="tcpStateInfo"/> with the parsed information.
/// For the data format expected in the <paramref name="buffer"/>, refer <see href="https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt">proc net tcp</see>.
/// </remarks>
private static void UpdateTcpStateInfo(ReadOnlySpan<char> buffer, TcpStateInfo tcpStateInfo)
{
const int Base16 = 16;

// The buffer contains one line from /proc/net/tcp(6) file, e.g.:
// 0: 030011AC:8AF2 C1B17822:01BB 01 00000000:00000000 02:000000D1 00000000 472 0 2481276 2 00000000c62511cb 28 4 26 10 -1
// The line may contain leading spaces, so we have to trim those.
// tcpConnectionState is in the 4th column - i.e., "01".
ReadOnlySpan<char> line = buffer.TrimStart();
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved

#if NET8_0_OR_GREATER
const int Target = 5;
Span<Range> range = stackalloc Range[Target];

// In .NET 8+, if capacity of destination range array is less than number of ranges found by ReadOnlySpan<T>.Split(),
// the last range in the array will get all the remaining elements of the ReadOnlySpan.
// Therefore, we request 5 ranges instead of 4, and then range[Target - 2] will have the range we need without the remaining elements.
int numRanges = line.Split(range, ' ', StringSplitOptions.RemoveEmptyEntries);
#else
const int Target = 4;
Span<StringRange> range = stackalloc StringRange[Target];

// In our StringRange API, if capacity of destination range array is less than number of ranges found by ReadOnlySpan<T>.TrySplit(),
// the last range in the array will get the last range as expected, and all remaining elements will be ignored.
// Hence range[Target - 1] will have the last range as we need.
_ = line.TrySplit(" ", range, out int numRanges, StringComparison.OrdinalIgnoreCase, StringSplitOptions.RemoveEmptyEntries);
#endif
if (numRanges < Target)
{
Throw.InvalidOperationException($"Could not split contents. We expected every line to contain more than {Target - 1} elements, but it has only {numRanges} elements.");
}

#if NET8_0_OR_GREATER
ReadOnlySpan<char> tcpConnectionState = line.Slice(range[Target - 2].Start.Value, range[Target - 2].End.Value - range[Target - 2].Start.Value);
#else
ReadOnlySpan<char> tcpConnectionState = line.Slice(range[Target - 1].Index, range[Target - 1].Count);
#endif

// At this point, tcpConnectionState contains one of TCP connection states in hexadecimal format, e.g., "01",
// which we now need to convert to the LinuxTcpState enum.
// Note: until this API proposal is implemented https://github.com/dotnet/runtime/issues/61397
// we have to allocate and throw away memory using .ToString().
var state = (LinuxTcpState)Convert.ToInt32(tcpConnectionState.ToString(), Base16);
switch (state)
{
case LinuxTcpState.ESTABLISHED:
tcpStateInfo.EstabCount++;
break;
case LinuxTcpState.SYN_SENT:
tcpStateInfo.SynSentCount++;
break;
case LinuxTcpState.SYN_RECV:
tcpStateInfo.SynRcvdCount++;
break;
case LinuxTcpState.FIN_WAIT1:
tcpStateInfo.FinWait1Count++;
break;
case LinuxTcpState.FIN_WAIT2:
tcpStateInfo.FinWait2Count++;
break;
case LinuxTcpState.TIME_WAIT:
tcpStateInfo.TimeWaitCount++;
break;
case LinuxTcpState.CLOSE:
tcpStateInfo.ClosedCount++;
break;
case LinuxTcpState.CLOSE_WAIT:
tcpStateInfo.CloseWaitCount++;
break;
case LinuxTcpState.LAST_ACK:
tcpStateInfo.LastAckCount++;
break;
case LinuxTcpState.LISTEN:
tcpStateInfo.ListenCount++;
break;
case LinuxTcpState.CLOSING:
tcpStateInfo.ClosingCount++;
break;
default:
Throw.IfOutOfRange(state);
break;
}
}

/// <remarks>
/// Reads the contents of a file and parses it to extract information about the TCP/IP state info of the system.
/// </remarks>
private TcpStateInfo GetTcpStateInfo(FileInfo file)
{
// The value we are interested in starts with this "sl".
const string Sl = "sl";
TcpStateInfo tcpStateInfo = new();
using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
using IEnumerator<ReadOnlyMemory<char>> enumerableLines = _fileSystem.ReadAllByLines(file, bufferWriter.Buffer).GetEnumerator();
if (!enumerableLines.MoveNext())
{
Throw.InvalidOperationException($"Could not parse '{file}'. File was empty.");
}

ReadOnlySpan<char> firstLine = enumerableLines.Current.TrimStart().Span;
if (!firstLine.StartsWith(Sl, StringComparison.Ordinal))
{
Throw.InvalidOperationException($"Could not parse '{file}'. We expected first line of the file to start with '{Sl}' but it was '{firstLine.ToString()}' instead.");
}

while (enumerableLines.MoveNext())
{
UpdateTcpStateInfo(enumerableLines.Current.Span, tcpStateInfo);
}

return tcpStateInfo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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.Linux.Network;

/// <summary>
/// Enumerates all possible TCP states on Linux.
/// </summary>
internal enum LinuxTcpState
{
/// <summary>The TCP connection was established.</summary>
ESTABLISHED = 1,

/// <summary>The TCP connection has sent a SYN packet.</summary>
SYN_SENT = 2,

/// <summary>The TCP connection has received a SYN packet.</summary>
SYN_RECV = 3,

/// <summary>The TCP connection is waiting for a FIN packet.</summary>
FIN_WAIT1 = 4,

/// <summary>The TCP connection is waiting for a FIN packet.</summary>
FIN_WAIT2 = 5,

/// <summary>The TCP connection is in the time wait state.</summary>
TIME_WAIT = 6,

/// <summary>The TCP connection is closed.</summary>
CLOSE = 7,

/// <summary>The TCP connection is in the close wait state.</summary>
CLOSE_WAIT = 8,

/// <summary>The TCP connection is in the last ACK state.</summary>
LAST_ACK = 9,

/// <summary>The TCP connection is in the listen state.</summary>
LISTEN = 10,

/// <summary>The TCP connection is closing.</summary>
CLOSING = 11
}
Loading