Skip to content

Commit

Permalink
Add network metrics to Resource Monitoring for Linux (#5367)
Browse files Browse the repository at this point in the history
  • Loading branch information
evgenyfedorov2 authored Aug 27, 2024
1 parent 5fc7e2b commit 8fa03a9
Show file tree
Hide file tree
Showing 52 changed files with 1,640 additions and 192 deletions.
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();

#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

0 comments on commit 8fa03a9

Please sign in to comment.