-
Notifications
You must be signed in to change notification settings - Fork 762
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add network metrics to Resource Monitoring for Linux (#5367)
- Loading branch information
1 parent
5fc7e2b
commit 8fa03a9
Showing
52 changed files
with
1,640 additions
and
192 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ITcpStateInfoProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
.../Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxNetworkMetrics.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
163 changes: 163 additions & 0 deletions
163
....Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxNetworkUtilizationParser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
...raries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Network/LinuxTcpState.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.