<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="6.0.2" />

MetricsEventSource

This EventSource is intended to let out-of-process tools (such as dotnet-counters) do ad-hoc monitoring for the new Instrument APIs. This source only supports one listener at a time. Each new listener will overwrite the configuration about which metrics are being collected and the time interval for the collection. In the future it would be nice to have support for multiple concurrent out-of-proc tools but EventSource's handling of filter arguments doesn't make that easy right now. Configuration - The EventSource accepts the following filter arguments: - SessionId - An arbitrary opaque string that will be sent back to the listener in many event payloads. If listener B reconfigures the EventSource while listener A is still running it is possible that each of them will observe some of the events that were generated using the other's requested configuration. Filtering on sessionId allows each listener to ignore those events. - RefreshInterval - The frequency in seconds for sending the metric time series data. The format is anything parsable using double.TryParse(). Any value less than AggregationManager.MinCollectionTimeSecs (currently 0.1 sec) is rounded up to the minimum. If not specified the default interval is 1 second. - Metrics - A semicolon separated list. Each item in the list is either the name of a Meter or 'meter_name\instrument_name'. For example "Foo;System.Runtime\gc-gen0-size" would include all instruments in the 'Foo' meter and the single 'gc-gen0-size' instrument in the 'System.Runtime' meter. - MaxTimeSeries - An integer that sets an upper bound on the number of time series this event source will track. Because instruments can have unbounded sets of tags even specifying a single metric could create unbounded load without this limit. - MaxHistograms - An integer that sets an upper bound on the number of histograms this event source will track. This allows setting a tighter bound on histograms than time series in general given that histograms use considerably more memory.
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.Globalization; using System.Runtime.Versioning; using System.Text; using System.Threading; namespace System.Diagnostics.Metrics { [EventSource(Name = "System.Diagnostics.Metrics")] internal sealed class MetricsEventSource : EventSource { public static class Keywords { public const EventKeywords Messages = (EventKeywords)1; public const EventKeywords TimeSeriesValues = (EventKeywords)2; public const EventKeywords InstrumentPublishing = (EventKeywords)4; } private sealed class CommandHandler { private AggregationManager _aggregationManager; private string _sessionId = ""; private static readonly char[] s_instrumentSeperators = new char[4] { '\r', '\n', ',', ';' }; public MetricsEventSource Parent { get; set; } public CommandHandler(MetricsEventSource parent) { Parent = parent; } public void OnEventCommand(EventCommandEventArgs command) { try { if (command.Command != 0 && command.Command != EventCommand.Disable && command.Command != EventCommand.Enable) goto IL_0096; if (_aggregationManager == null) goto IL_008b; if (command.Command != EventCommand.Enable && command.Command != 0) { _aggregationManager.Dispose(); _aggregationManager = null; Parent.Message("Previous session with id " + _sessionId + " is stopped"); goto IL_008b; } Parent.MultipleSessionsNotSupportedError(_sessionId); goto end_IL_000d; IL_0096: if ((command.Command == EventCommand.Update || command.Command == EventCommand.Enable) && command.Arguments != null) { if (command.Arguments.TryGetValue("SessionId", out string value)) { _sessionId = value; Parent.Message("SessionId argument received: " + _sessionId); } else { _sessionId = Guid.NewGuid().ToString(); Parent.Message("New session started. SessionId auto-generated: " + _sessionId); } double num = 1; double result = num; if (command.Arguments.TryGetValue("RefreshInterval", out string value2)) { Parent.Message("RefreshInterval argument received: " + value2); if (!double.TryParse(value2, out result)) { Parent.Message($"""{num}"""); result = num; } else if (result < 0.1) { Parent.Message($"""{0.1}"""); result = 0.1; } } else { Parent.Message($"""{num}"""); result = num; } int num2 = 1000; int result2; if (command.Arguments.TryGetValue("MaxTimeSeries", out string value3)) { Parent.Message("MaxTimeSeries argument received: " + value3); if (!int.TryParse(value3, out result2)) { Parent.Message($"""{num2}"); result2 = num2; } } else { Parent.Message($"""{num2}"); result2 = num2; } int num3 = 20; int result3; if (command.Arguments.TryGetValue("MaxHistograms", out string value4)) { Parent.Message("MaxHistograms argument received: " + value4); if (!int.TryParse(value4, out result3)) { Parent.Message($"""{num3}"); result3 = num3; } } else { Parent.Message($"""{num3}"); result3 = num3; } string sessionId = _sessionId; _aggregationManager = new AggregationManager(result2, result3, delegate(Instrument i, LabeledAggregationStatistics s) { TransmitMetricValue(i, s, sessionId); }, delegate(DateTime startIntervalTime, DateTime endIntervalTime) { Parent.CollectionStart(sessionId, startIntervalTime, endIntervalTime); }, delegate(DateTime startIntervalTime, DateTime endIntervalTime) { Parent.CollectionStop(sessionId, startIntervalTime, endIntervalTime); }, delegate(Instrument i) { Parent.BeginInstrumentReporting(sessionId, i.Meter.Name, i.Meter.Version, i.Name, i.GetType().Name, i.Unit, i.Description); }, delegate(Instrument i) { Parent.EndInstrumentReporting(sessionId, i.Meter.Name, i.Meter.Version, i.Name, i.GetType().Name, i.Unit, i.Description); }, delegate(Instrument i) { Parent.InstrumentPublished(sessionId, i.Meter.Name, i.Meter.Version, i.Name, i.GetType().Name, i.Unit, i.Description); }, delegate { Parent.InitialInstrumentEnumerationComplete(sessionId); }, delegate(Exception e) { Parent.Error(sessionId, e.ToString()); }, delegate { Parent.TimeSeriesLimitReached(sessionId); }, delegate { Parent.HistogramLimitReached(sessionId); }, delegate(Exception e) { Parent.ObservableInstrumentCallbackError(sessionId, e.ToString()); }); _aggregationManager.SetCollectionPeriod(TimeSpan.FromSeconds(result)); if (command.Arguments.TryGetValue("Metrics", out string value5)) { Parent.Message("Metrics argument received: " + value5); ParseSpecs(value5); } else Parent.Message("No Metrics argument received"); _aggregationManager.Start(); } goto end_IL_000d; IL_008b: _sessionId = ""; goto IL_0096; end_IL_000d:; } catch (Exception e2) when (LogError(e2)) { } } private bool LogError(Exception e) { Parent.Error(_sessionId, e.ToString()); return false; } [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] private void ParseSpecs(string metricsSpecs) { if (metricsSpecs != null) { string[] array = metricsSpecs.Split(s_instrumentSeperators, StringSplitOptions.RemoveEmptyEntries); string[] array2 = array; foreach (string text in array2) { if (!MetricSpec.TryParse(text, out MetricSpec spec)) Parent.Message("Failed to parse metric spec: " + text); else { Parent.Message($"""{spec}"); if (spec.InstrumentName != null) _aggregationManager.Include(spec.MeterName, spec.InstrumentName); else _aggregationManager.Include(spec.MeterName); } } } } private void TransmitMetricValue(Instrument instrument, LabeledAggregationStatistics stats, string sessionId) { RateStatistics rateStatistics = stats.AggregationStatistics as RateStatistics; double value; if (rateStatistics != null) { MetricsEventSource log = Log; string name = instrument.Meter.Name; string version = instrument.Meter.Version; string name2 = instrument.Name; string unit = instrument.Unit; string tags = FormatTags(stats.Labels); object rate; if (!rateStatistics.Delta.HasValue) rate = ""; else { value = rateStatistics.Delta.Value; rate = value.ToString(CultureInfo.InvariantCulture); } log.CounterRateValuePublished(sessionId, name, version, name2, unit, tags, (string)rate); } else { LastValueStatistics lastValueStatistics = stats.AggregationStatistics as LastValueStatistics; if (lastValueStatistics != null) { MetricsEventSource log2 = Log; string name3 = instrument.Meter.Name; string version2 = instrument.Meter.Version; string name4 = instrument.Name; string unit2 = instrument.Unit; string tags2 = FormatTags(stats.Labels); object lastValue; if (!lastValueStatistics.LastValue.HasValue) lastValue = ""; else { value = lastValueStatistics.LastValue.Value; lastValue = value.ToString(CultureInfo.InvariantCulture); } log2.GaugeValuePublished(sessionId, name3, version2, name4, unit2, tags2, (string)lastValue); } else { HistogramStatistics histogramStatistics = stats.AggregationStatistics as HistogramStatistics; if (histogramStatistics != null) Log.HistogramValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, FormatTags(stats.Labels), FormatQuantiles(histogramStatistics.Quantiles)); } } } private string FormatTags(KeyValuePair<string, string>[] labels) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < labels.Length; i++) { stringBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0}={1}", labels[i].Key, labels[i].Value); if (i != labels.Length - 1) stringBuilder.Append(','); } return stringBuilder.ToString(); } private string FormatQuantiles(QuantileValue[] quantiles) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < quantiles.Length; i++) { stringBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0}={1}", quantiles[i].Quantile, quantiles[i].Value); if (i != quantiles.Length - 1) stringBuilder.Append(';'); } return stringBuilder.ToString(); } } private class MetricSpec { private const char MeterInstrumentSeparator = '\\'; public string MeterName { get; set; } public string InstrumentName { get; set; } public MetricSpec(string meterName, string instrumentName) { MeterName = meterName; InstrumentName = instrumentName; } public static bool TryParse(string text, out MetricSpec spec) { int num = text.IndexOf('\\'); if (num == -1) { spec = new MetricSpec(text.Trim(), null); return true; } string meterName = text.Substring(0, num).Trim(); string instrumentName = text.Substring(num + 1).Trim(); spec = new MetricSpec(meterName, instrumentName); return true; } public override string ToString() { if (InstrumentName == null) return MeterName; return MeterName + "\\" + InstrumentName; } } public static readonly MetricsEventSource Log = new MetricsEventSource(); private CommandHandler _handler; private CommandHandler Handler { get { if (_handler == null) Interlocked.CompareExchange(ref _handler, new CommandHandler(this), null); return _handler; } } private MetricsEventSource() { } [Event(1, Keywords = (EventKeywords)1)] public void Message(string Message) { WriteEvent(1, Message); } [Event(2, Keywords = (EventKeywords)2)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void CollectionStart(string sessionId, DateTime intervalStartTime, DateTime intervalEndTime) { WriteEvent(2, new object[3] { sessionId, intervalStartTime, intervalEndTime }); } [Event(3, Keywords = (EventKeywords)2)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void CollectionStop(string sessionId, DateTime intervalStartTime, DateTime intervalEndTime) { WriteEvent(3, new object[3] { sessionId, intervalStartTime, intervalEndTime }); } [Event(4, Keywords = (EventKeywords)2)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void CounterRateValuePublished(string sessionId, string meterName, string meterVersion, string instrumentName, string unit, string tags, string rate) { WriteEvent(4, new object[7] { sessionId, meterName, meterVersion ?? "", instrumentName, unit ?? "", tags, rate }); } [Event(5, Keywords = (EventKeywords)2)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void GaugeValuePublished(string sessionId, string meterName, string meterVersion, string instrumentName, string unit, string tags, string lastValue) { WriteEvent(5, new object[7] { sessionId, meterName, meterVersion ?? "", instrumentName, unit ?? "", tags, lastValue }); } [Event(6, Keywords = (EventKeywords)2)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void HistogramValuePublished(string sessionId, string meterName, string meterVersion, string instrumentName, string unit, string tags, string quantiles) { WriteEvent(6, new object[7] { sessionId, meterName, meterVersion ?? "", instrumentName, unit ?? "", tags, quantiles }); } [Event(7, Keywords = (EventKeywords)2)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void BeginInstrumentReporting(string sessionId, string meterName, string meterVersion, string instrumentName, string instrumentType, string unit, string description) { WriteEvent(7, new object[7] { sessionId, meterName, meterVersion ?? "", instrumentName, instrumentType, unit ?? "", description ?? "" }); } [Event(8, Keywords = (EventKeywords)2)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void EndInstrumentReporting(string sessionId, string meterName, string meterVersion, string instrumentName, string instrumentType, string unit, string description) { WriteEvent(8, new object[7] { sessionId, meterName, meterVersion ?? "", instrumentName, instrumentType, unit ?? "", description ?? "" }); } [Event(9, Keywords = (EventKeywords)7)] public void Error(string sessionId, string errorMessage) { WriteEvent(9, sessionId, errorMessage); } [Event(10, Keywords = (EventKeywords)6)] public void InitialInstrumentEnumerationComplete(string sessionId) { WriteEvent(10, sessionId); } [Event(11, Keywords = (EventKeywords)4)] [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "This calls WriteEvent with all primitive arguments which is safe. Primitives are always serialized properly.")] public void InstrumentPublished(string sessionId, string meterName, string meterVersion, string instrumentName, string instrumentType, string unit, string description) { WriteEvent(11, new object[7] { sessionId, meterName, meterVersion ?? "", instrumentName, instrumentType, unit ?? "", description ?? "" }); } [Event(12, Keywords = (EventKeywords)2)] public void TimeSeriesLimitReached(string sessionId) { WriteEvent(12, sessionId); } [Event(13, Keywords = (EventKeywords)2)] public void HistogramLimitReached(string sessionId) { WriteEvent(13, sessionId); } [Event(14, Keywords = (EventKeywords)2)] public void ObservableInstrumentCallbackError(string sessionId, string errorMessage) { WriteEvent(14, sessionId, errorMessage); } [Event(15, Keywords = (EventKeywords)7)] public void MultipleSessionsNotSupportedError(string runningSessionId) { WriteEvent(15, runningSessionId); } [NonEvent] protected override void OnEventCommand(EventCommandEventArgs command) { lock (this) { Handler.OnEventCommand(command); } } } }