From b7348862f3b8d0053e438471e843a2a135046a61 Mon Sep 17 00:00:00 2001 From: kkeirstead <85592574+kkeirstead@users.noreply.github.com> Date: Mon, 28 Nov 2022 05:21:29 -0800 Subject: [PATCH] [Dotnet Monitor] Add Metadata Support For `CounterPayload` (#3498) --- .../Counters/CounterPayload.cs | 15 ++++-- .../Counters/ICounterPayload.cs | 2 + .../Counters/TraceEventExtensions.cs | 47 ++++++++++++++++++- .../EventCounterTriggerTests.cs | 34 +++++++++++++- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs index 5f0d3d7b83..0108e722bc 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs @@ -3,14 +3,19 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections; using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.Collections.ObjectModel; namespace Microsoft.Diagnostics.Monitoring.EventPipe { internal class CounterPayload : ICounterPayload { +#if NETSTANDARD + private static readonly IReadOnlyDictionary Empty = new ReadOnlyDictionary(new Dictionary(0)); +#else + private static readonly IReadOnlyDictionary Empty = System.Collections.Immutable.ImmutableDictionary.Empty; +#endif + public CounterPayload(DateTime timestamp, string provider, string name, @@ -18,7 +23,8 @@ public CounterPayload(DateTime timestamp, string unit, double value, CounterType counterType, - float interval) + float interval, + Dictionary metadata) { Timestamp = timestamp; Name = name; @@ -28,6 +34,7 @@ public CounterPayload(DateTime timestamp, CounterType = counterType; Provider = provider; Interval = interval; + Metadata = metadata ?? Empty; } public string Namespace { get; } @@ -47,5 +54,7 @@ public CounterPayload(DateTime timestamp, public CounterType CounterType { get; } public string Provider { get; } + + public IReadOnlyDictionary Metadata { get; } } } \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs index d44ee181c4..b492a629bb 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs @@ -34,5 +34,7 @@ internal interface ICounterPayload DateTime Timestamp { get; } float Interval { get; } + + IReadOnlyDictionary Metadata { get; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs index 9c8c0fdbcf..f0ec5c6310 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs @@ -23,6 +23,8 @@ public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilte string series = payloadFields["Series"].ToString(); string counterName = payloadFields["Name"].ToString(); + Dictionary metadataDict = GetMetadata(payloadFields["Metadata"].ToString()); + //CONSIDER //Concurrent counter sessions do not each get a separate interval. Instead the payload //for _all_ the counters changes the Series to be the lowest specified interval, on a per provider basis. @@ -62,13 +64,56 @@ public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilte displayUnits, value, counterType, - intervalSec); + intervalSec, + metadataDict); return true; } return false; } + //The metadata payload is formatted as a string of comma separated key:value pairs. + //This limitation means that metadata values cannot include commas; otherwise, the + //metadata will be parsed incorrectly. If a value contains a comma, then all metadata + //is treated as invalid and excluded from the payload. + internal static Dictionary GetMetadata(string metadataPayload) + { + var metadataDict = new Dictionary(); + + ReadOnlySpan metadata = metadataPayload; + + while (!metadata.IsEmpty) + { + int commaIndex = metadata.IndexOf(','); + + ReadOnlySpan kvPair; + + if (commaIndex < 0) + { + kvPair = metadata; + metadata = default; + } + else + { + kvPair = metadata[..commaIndex]; + metadata = metadata.Slice(commaIndex + 1); + } + + int colonIndex = kvPair.IndexOf(':'); + if (colonIndex < 0) + { + metadataDict.Clear(); + break; + } + + string metadataKey = kvPair[..colonIndex].ToString(); + string metadataValue = kvPair.Slice(colonIndex + 1).ToString(); + metadataDict[metadataKey] = metadataValue; + } + + return metadataDict; + } + private static int GetInterval(string series) { const string comparison = "Interval="; diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs index ca1317cba6..551e53e4b2 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs @@ -484,8 +484,40 @@ public ICounterPayload CreateNext(double value) EventCounterConstants.CpuUsageUnits, value, CounterType.Metric, - actualInterval); + actualInterval, + null); } } + + /// + /// Validates that metadata from TraceEvent payloads is parsed correctly. + /// + [Fact] + public void ValidateMetadataParsing_Success() + { + const string key1 = "K1"; + const string value1 = "V1"; + const string key2 = "K2"; + const string value2 = "V:2"; + Dictionary metadataDict = TraceEventExtensions.GetMetadata($"{key1}:{value1},{key2}:{value2}"); + + Assert.Equal(2, metadataDict.Count); + Assert.Equal(value1, metadataDict[key1]); + Assert.Equal(value2, metadataDict[key2]); + } + + /// + /// Validates that metadata with an invalid format from TraceEvent payloads is handled correctly. + /// + [Theory] + [InlineData("K1:V,1")] + [InlineData("K,1:V")] + [InlineData("K1")] + public void ValidateMetadataParsing_Failure(string invalidMetadata) + { + Dictionary metadataDict = TraceEventExtensions.GetMetadata(invalidMetadata); + + Assert.Empty(metadataDict); + } } }