From c173b9ee1007344045a3802fb333beff94ff95a9 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sat, 12 Jul 2025 09:33:25 -0500 Subject: [PATCH] Minimal work to make System.Diagnostics.Activity usable by the dotnet CLI application and codebase --- Directory.Packages.props | 1 + eng/Versions.props | 1 + .../Microsoft.DotNet.Cli.Utils/Activities.cs | 20 ++++ .../ForwardingAppImplementation.cs | 6 +- .../MSBuildForwardingAppWithoutLogging.cs | 8 +- .../Microsoft.DotNet.Cli.Utils.csproj | 1 + .../Commands/MSBuild/MSBuildForwardingApp.cs | 2 +- src/Cli/dotnet/Telemetry/Telemetry.cs | 108 +++++++++++++----- .../Telemetry/TelemetryCommonProperties.cs | 5 +- 9 files changed, 114 insertions(+), 38 deletions(-) create mode 100644 src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 104dedfd9eaf..abe2e9e23b4c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -108,6 +108,7 @@ + diff --git a/eng/Versions.props b/eng/Versions.props index 99e97cedaae8..1b71e21ebfc6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -119,6 +119,7 @@ 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs new file mode 100644 index 000000000000..9f4665572436 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.DotNet.Cli.Utils; + +/// +/// Contains helpers for working with Activities in the .NET CLI. +/// +public static class Activities +{ + + /// + /// The main entrypoint for creating Activities in the .NET CLI. + /// All activities created in the CLI should use this , to allow + /// consumers to easily filter and trace CLI activities. + /// + public static ActivitySource Source { get; } = new("dotnet-cli", Product.Version); +} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs index f6acb60ea0a1..1b2058d17744 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/ForwardingAppImplementation.cs @@ -19,7 +19,7 @@ internal class ForwardingAppImplementation private readonly string? _depsFile; private readonly string? _runtimeConfig; private readonly string? _additionalProbingPath; - private Dictionary _environmentVariables; + private Dictionary _environmentVariables; private readonly string[] _allArgs; @@ -29,7 +29,7 @@ public ForwardingAppImplementation( string? depsFile = null, string? runtimeConfig = null, string? additionalProbingPath = null, - Dictionary? environmentVariables = null) + Dictionary? environmentVariables = null) { _forwardApplicationPath = forwardApplicationPath; _argsToForward = argsToForward; @@ -86,7 +86,7 @@ public ProcessStartInfo GetProcessStartInfo() return processInfo; } - public ForwardingAppImplementation WithEnvironmentVariable(string name, string value) + public ForwardingAppImplementation WithEnvironmentVariable(string name, string? value) { _environmentVariables.Add(name, value); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs index 2e95365623a0..64e6fea0ccf9 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs @@ -39,7 +39,7 @@ public static string MSBuildVersion // True if, given current state of the class, MSBuild would be executed in its own process. public bool ExecuteMSBuildOutOfProc => _forwardingApp != null; - private readonly Dictionary _msbuildRequiredEnvironmentVariables = GetMSBuildRequiredEnvironmentVariables(); + private readonly Dictionary _msbuildRequiredEnvironmentVariables = GetMSBuildRequiredEnvironmentVariables(); private readonly List _msbuildRequiredParameters = [ "-maxcpucount", "--verbosity:m" ]; @@ -112,7 +112,7 @@ private static string EmitProperty(KeyValuePair property, string : $"--{label}:{property.Key}={property.Value}"; } - public void EnvironmentVariable(string name, string value) + public void EnvironmentVariable(string name, string? value) { if (_forwardingApp != null) { @@ -153,7 +153,7 @@ public int ExecuteInProc(string[] arguments) Dictionary savedEnvironmentVariables = []; try { - foreach (KeyValuePair kvp in _msbuildRequiredEnvironmentVariables) + foreach (KeyValuePair kvp in _msbuildRequiredEnvironmentVariables) { savedEnvironmentVariables[kvp.Key] = Environment.GetEnvironmentVariable(kvp.Key); Environment.SetEnvironmentVariable(kvp.Key, kvp.Value); @@ -218,7 +218,7 @@ private static string GetDotnetPath() return new Muxer().MuxerPath; } - internal static Dictionary GetMSBuildRequiredEnvironmentVariables() + internal static Dictionary GetMSBuildRequiredEnvironmentVariables() { return new() { diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj b/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj index 115f5dd125ec..3c0f616409d4 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj @@ -54,6 +54,7 @@ + diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs index f3ef9d93a44f..67c2a47f1051 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs @@ -59,7 +59,7 @@ public MSBuildForwardingApp(MSBuildArgs msBuildArgs, string? msbuildPath = null, public IEnumerable MSBuildArguments { get { return _forwardingAppWithoutLogging.GetAllArguments(); } } - public void EnvironmentVariable(string name, string value) + public void EnvironmentVariable(string name, string? value) { _forwardingAppWithoutLogging.EnvironmentVariable(name, value); } diff --git a/src/Cli/dotnet/Telemetry/Telemetry.cs b/src/Cli/dotnet/Telemetry/Telemetry.cs index 28851da921b1..f908d01e7fe0 100644 --- a/src/Cli/dotnet/Telemetry/Telemetry.cs +++ b/src/Cli/dotnet/Telemetry/Telemetry.cs @@ -1,8 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - +using System.Collections.Frozen; using System.Diagnostics; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; @@ -14,13 +13,13 @@ namespace Microsoft.DotNet.Cli.Telemetry; public class Telemetry : ITelemetry { - internal static string CurrentSessionId = null; + internal static string? CurrentSessionId = null; internal static bool DisabledForTests = false; private readonly int _senderCount; - private TelemetryClient _client = null; - private Dictionary _commonProperties = null; - private Dictionary _commonMeasurements = null; - private Task _trackEventTask = null; + private TelemetryClient? _client = null; + private FrozenDictionary? _commonProperties = null; + private FrozenDictionary? _commonMeasurements = null; + private Task? _trackEventTask = null; private const string ConnectionString = "InstrumentationKey=74cc1c9e-3e6e-4d05-b3fc-dde9101d0254"; @@ -28,13 +27,13 @@ public class Telemetry : ITelemetry public Telemetry() : this(null) { } - public Telemetry(IFirstTimeUseNoticeSentinel sentinel) : this(sentinel, null) { } + public Telemetry(IFirstTimeUseNoticeSentinel? sentinel) : this(sentinel, null) { } public Telemetry( - IFirstTimeUseNoticeSentinel sentinel, - string sessionId, + IFirstTimeUseNoticeSentinel? sentinel, + string? sessionId, bool blockThreadInitialization = false, - IEnvironmentProvider environmentProvider = null, + IEnvironmentProvider? environmentProvider = null, int senderCount = 3) { @@ -78,7 +77,7 @@ internal static void EnableForTests() DisabledForTests = false; } - private static bool PermissionExists(IFirstTimeUseNoticeSentinel sentinel) + private static bool PermissionExists(IFirstTimeUseNoticeSentinel? sentinel) { if (sentinel == null) { @@ -97,9 +96,17 @@ public void TrackEvent(string eventName, IDictionary properties, } //continue the task in different threads - _trackEventTask = _trackEventTask.ContinueWith( - x => TrackEventTask(eventName, properties, measurements) - ); + if (_trackEventTask == null) + { + _trackEventTask = Task.Run(() => TrackEventTask(eventName, properties, measurements)); + return; + } + else + { + _trackEventTask = _trackEventTask.ContinueWith( + x => TrackEventTask(eventName, properties, measurements) + ); + } } public void Flush() @@ -148,7 +155,7 @@ private void InitializeTelemetry() _client.Context.Device.OperatingSystem = CLIRuntimeEnvironment.OperatingSystem; _commonProperties = new TelemetryCommonProperties().GetTelemetryCommonProperties(); - _commonMeasurements = []; + _commonMeasurements = FrozenDictionary.Empty; } catch (Exception e) { @@ -170,12 +177,14 @@ private void TrackEventTask( try { - Dictionary eventProperties = GetEventProperties(properties); - Dictionary eventMeasurements = GetEventMeasures(measurements); + var eventProperties = GetEventProperties(properties); + var eventMeasurements = GetEventMeasures(measurements); + eventProperties ??= new Dictionary(); eventProperties.Add("event id", Guid.NewGuid().ToString()); _client.TrackEvent(PrependProducerNamespace(eventName), eventProperties, eventMeasurements); + Activity.Current?.AddEvent(CreateActivityEvent(eventName, eventProperties, eventMeasurements)); } catch (Exception e) { @@ -183,26 +192,69 @@ private void TrackEventTask( } } - private static string PrependProducerNamespace(string eventName) + private static ActivityEvent CreateActivityEvent( + string eventName, + IDictionary? properties, + IDictionary? measurements) { - return "dotnet/cli/" + eventName; + var tags = MakeTags(properties, measurements); + return new ActivityEvent( + PrependProducerNamespace(eventName), + tags: tags); } - private Dictionary GetEventMeasures(IDictionary measurements) + private static ActivityTagsCollection? MakeTags( + IDictionary? properties, + IDictionary? measurements) { - Dictionary eventMeasurements = new(_commonMeasurements); - if (measurements != null) + if (properties == null && measurements == null) { - foreach (KeyValuePair measurement in measurements) - { - eventMeasurements[measurement.Key] = measurement.Value; - } + return null; + } + else if (properties != null && measurements == null) + { + return [.. properties.Select(p => new KeyValuePair(p.Key, p.Value))]; + } + else if (properties == null && measurements != null) + { + return [.. measurements.Select(m => new KeyValuePair(m.Key, m.Value.ToString()))]; + } + else return [ .. properties!.Select(p => new KeyValuePair(p.Key, p.Value)), + .. measurements!.Select(m => new KeyValuePair(m.Key, m.Value.ToString())) ]; + } + + private static string PrependProducerNamespace(string eventName) => $"dotnet/cli/{eventName}"; + + private IDictionary? GetEventMeasures(IDictionary? measurements) + { + if (measurements is null) + { + return _commonMeasurements; + } + if (_commonMeasurements == null) + { + return measurements; + } + + IDictionary eventMeasurements = new Dictionary(_commonMeasurements); + foreach (KeyValuePair measurement in measurements) + { + eventMeasurements[measurement.Key] = measurement.Value; } return eventMeasurements; } - private Dictionary GetEventProperties(IDictionary properties) + private IDictionary? GetEventProperties(IDictionary? properties) { + if (properties is null) + { + return _commonProperties; + } + if (_commonProperties == null) + { + return properties; + } + var eventProperties = new Dictionary(_commonProperties); if (properties != null) { diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs index 88b2c2152100..9f58d488a12c 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Collections.Frozen; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment; @@ -52,7 +53,7 @@ internal class TelemetryCommonProperties( private const string MachineIdCacheKey = "MachineId"; private const string IsDockerContainerCacheKey = "IsDockerContainer"; - public Dictionary GetTelemetryCommonProperties() + public FrozenDictionary GetTelemetryCommonProperties() { return new Dictionary { @@ -82,7 +83,7 @@ public Dictionary GetTelemetryCommonProperties() {ProductType, ExternalTelemetryProperties.GetProductType()}, {LibcRelease, ExternalTelemetryProperties.GetLibcRelease()}, {LibcVersion, ExternalTelemetryProperties.GetLibcVersion()} - }; + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } private string GetMachineId()