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 6 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 table information.
/// </summary>
internal interface ITcpTableInfo
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Gets the last known snapshot of TCP/IP v4 state info on the system.
/// </summary>
/// <returns>An instance of <see cref="TcpStateInfo"/>.</returns>
TcpStateInfo GetIpV4CachingSnapshot();

/// <summary>
/// Gets the last known snapshot of TCP/IP v6 state info on the system.
/// </summary>
/// <returns>An instance of <see cref="TcpStateInfo"/>.</returns>
TcpStateInfo GetIpV6CachingSnapshot();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ internal interface IFileSystem
/// <summary>
/// Checks for file existence.
/// </summary>
/// <returns> True/False.</returns>
/// <returns><see langword="true"/> or <see langword="false"/>.</returns>
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
bool Exists(FileInfo fileInfo);

/// <summary>
/// Get directory names on the filesystem based on the provided pattern.
/// </summary>
/// <returns>string.</returns>
/// <returns>IReadOnlyCollection.</returns>
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
IReadOnlyCollection<string> GetDirectoryNames(string directory, string pattern);

/// <summary>
/// Reads content from the file.
/// </summary>
/// <returns>
/// Chars written.
/// Number of chars written.
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
/// </returns>
int Read(FileInfo file, int length, Span<char> destination);

Expand All @@ -42,4 +42,12 @@ internal interface IFileSystem
/// Reads first line from the file.
/// </summary>
void ReadFirstLine(FileInfo file, BufferWriter<char> destination);

/// <summary>
/// Reads all content from a file by line.
/// </summary>
/// <returns>
/// IEnumerable.
/// </returns>
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
IEnumerable<ReadOnlyMemory<char>> ReadAllByLines(FileInfo file, BufferWriter<char> destination);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 ITcpTableInfo _tcpTableInfo;

public LinuxNetworkMetrics(IMeterFactory meterFactory, ITcpTableInfo tcpTableInfo)
{
_tcpTableInfo = tcpTableInfo;

#pragma warning disable CA2000 // Dispose objects before losing scope
// We don't dispose the meter because IMeterFactory handles that
// An issue on analyzer side: https://github.com/dotnet/roslyn-analyzers/issues/6912
// Related documentation: https://github.com/dotnet/docs/pull/37170
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
var meter = meterFactory.Create(nameof(ResourceMonitoring));
#pragma warning restore CA2000 // Dispose objects before losing scope

var tcpTag = new KeyValuePair<string, object?>("network.transport", "tcp");
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
var commonTags = new TagList
{
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:
var tcpVersionFourTag = new KeyValuePair<string, object?>("network.type", "ipv4");
var tcpVersionSixTag = new KeyValuePair<string, object?>("network.type", "ipv6");

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

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

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

return measurements;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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.ComponentModel;
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 _tcp4 and parses it to extract information about the TCP/IP state info on the system.
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
/// </remarks>
public TcpStateInfo GetTcpIPv4StateInfo() => GetTcpStateInfo(_tcp);

/// <remarks>
/// Reads the contents of a file located at _tcp6 and parses it to extract information about the TCP/IP state info on the system.
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
/// </remarks>
public TcpStateInfo GetTcpIPv6StateInfo() => GetTcpStateInfo(_tcp6);

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

/// <remarks>
/// The method is used to read Span data and calculate the TCP state info.
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
/// 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;
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];

// on .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.
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
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.
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
_ = 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

// until this API proposal is implemented https://github.com/dotnet/runtime/issues/61397
// we have to allocate & throw away memory using ToString():
switch ((LinuxTcpState)Convert.ToInt32(tcpConnectionState.ToString(), Base16))
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
{
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 new InvalidEnumArgumentException($"Cannot find status: {tcpConnectionState} in LinuxTcpState");
evgenyfedorov2 marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

var 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