From 6bfc72d932482225d24436a2d59f19c587ae5b57 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 20 Oct 2025 18:43:35 +0200 Subject: [PATCH 01/47] telemetry spike --- eng/Packages.props | 2 +- .../OpenTelemetryActivities_Tests.cs | 195 ---------- .../Telemetry/OpenTelemetryManager_Tests.cs | 142 -------- .../BackEnd/BuildManager/BuildManager.cs | 48 +-- src/Build/Microsoft.Build.csproj | 1 - .../TelemetryInfra/ITelemetryForwarder.cs | 7 +- .../InternalTelemetryConsumingLogger.cs | 4 + .../TelemetryInfra/TelemetryDataUtils.cs | 339 ------------------ .../Microsoft.Build.Framework.csproj | 1 + .../Telemetry/BuildCheckTelemetry.cs | 3 - src/Framework/Telemetry/BuildTelemetry.cs | 137 +++---- .../Telemetry/IWorkerNodeTelemetryData.cs | 1 + src/Framework/Telemetry/TelemetryConstants.cs | 2 + src/Framework/Telemetry/TelemetryDataUtils.cs | 306 ++++++++++++++++ src/Framework/Telemetry/TelemetryItem.cs | 2 +- src/Framework/Telemetry/VSTelemetry.cs | 18 + .../Telemetry/VSTelemetryActivity.cs | 79 ++++ .../VSTelemetryActivityExtensions.cs | 103 ++++++ src/Framework/Telemetry/VSTelemetryManager.cs | 58 +++ src/MSBuild/XMake.cs | 14 +- 20 files changed, 648 insertions(+), 814 deletions(-) delete mode 100644 src/Build.UnitTests/Telemetry/OpenTelemetryActivities_Tests.cs delete mode 100644 src/Build.UnitTests/Telemetry/OpenTelemetryManager_Tests.cs delete mode 100644 src/Build/TelemetryInfra/TelemetryDataUtils.cs create mode 100644 src/Framework/Telemetry/TelemetryDataUtils.cs create mode 100644 src/Framework/Telemetry/VSTelemetry.cs create mode 100644 src/Framework/Telemetry/VSTelemetryActivity.cs create mode 100644 src/Framework/Telemetry/VSTelemetryActivityExtensions.cs create mode 100644 src/Framework/Telemetry/VSTelemetryManager.cs diff --git a/eng/Packages.props b/eng/Packages.props index 3aff737ba66..1b7cccf8001 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -42,7 +42,7 @@ - + diff --git a/src/Build.UnitTests/Telemetry/OpenTelemetryActivities_Tests.cs b/src/Build.UnitTests/Telemetry/OpenTelemetryActivities_Tests.cs deleted file mode 100644 index 7a567e79495..00000000000 --- a/src/Build.UnitTests/Telemetry/OpenTelemetryActivities_Tests.cs +++ /dev/null @@ -1,195 +0,0 @@ -// 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.Diagnostics; -using Microsoft.Build.Framework.Telemetry; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.Engine.UnitTests.Telemetry -{ - public class ActivityExtensionsTests - { - [Fact] - public void WithTag_ShouldSetUnhashedValue() - { - var activity = new Activity("TestActivity"); - activity.Start(); - - var telemetryItem = new TelemetryItem( - Name: "TestItem", - Value: "TestValue", - NeedsHashing: false); - - activity.WithTag(telemetryItem); - - var tagValue = activity.GetTagItem("VS.MSBuild.TestItem"); - tagValue.ShouldNotBeNull(); - tagValue.ShouldBe("TestValue"); - - activity.Dispose(); - } - - [Fact] - public void WithTag_ShouldSetHashedValue() - { - var activity = new Activity("TestActivity"); - var telemetryItem = new TelemetryItem( - Name: "TestItem", - Value: "SensitiveValue", - NeedsHashing: true); - - activity.WithTag(telemetryItem); - - var tagValue = activity.GetTagItem("VS.MSBuild.TestItem"); - tagValue.ShouldNotBeNull(); - tagValue.ShouldNotBe("SensitiveValue"); // Ensure it’s not the plain text - activity.Dispose(); - } - - [Fact] - public void WithTags_ShouldSetMultipleTags() - { - var activity = new Activity("TestActivity"); - var tags = new List - { - new("Item1", "Value1", false), - new("Item2", "Value2", true) // hashed - }; - - activity.WithTags(tags); - - var tagValue1 = activity.GetTagItem("VS.MSBuild.Item1"); - var tagValue2 = activity.GetTagItem("VS.MSBuild.Item2"); - - tagValue1.ShouldNotBeNull(); - tagValue1.ShouldBe("Value1"); - - tagValue2.ShouldNotBeNull(); - tagValue2.ShouldNotBe("Value2"); // hashed - - activity.Dispose(); - } - - [Fact] - public void WithTags_DataHolderShouldSetMultipleTags() - { - var activity = new Activity("TestActivity"); - var dataHolder = new MockTelemetryDataHolder(); // see below - - activity.WithTags(dataHolder); - - var tagValueA = activity.GetTagItem("VS.MSBuild.TagA"); - var tagValueB = activity.GetTagItem("VS.MSBuild.TagB"); - - tagValueA.ShouldNotBeNull(); - tagValueA.ShouldBe("ValueA"); - - tagValueB.ShouldNotBeNull(); - tagValueB.ShouldNotBe("ValueB"); // should be hashed - activity.Dispose(); - } - - [Fact] - public void WithStartTime_ShouldSetActivityStartTime() - { - var activity = new Activity("TestActivity"); - var now = DateTime.UtcNow; - - activity.WithStartTime(now); - - activity.StartTimeUtc.ShouldBe(now); - activity.Dispose(); - } - - [Fact] - public void WithStartTime_NullDateTime_ShouldNotSetStartTime() - { - var activity = new Activity("TestActivity"); - var originalStartTime = activity.StartTimeUtc; // should be default (min) if not started - - activity.WithStartTime(null); - - activity.StartTimeUtc.ShouldBe(originalStartTime); - - activity.Dispose(); - } - } - - /// - /// A simple mock for testing IActivityTelemetryDataHolder. - /// Returns two items: one hashed, one not hashed. - /// - internal sealed class MockTelemetryDataHolder : IActivityTelemetryDataHolder - { - public IList GetActivityProperties() - { - return new List - { - new("TagA", "ValueA", false), - new("TagB", "ValueB", true), - }; - } - } - - - public class MSBuildActivitySourceTests - { - [Fact] - public void StartActivity_ShouldPrefixNameCorrectly_WhenNoRemoteParent() - { - var source = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, 1.0); - using var listener = new ActivityListener - { - ShouldListenTo = activitySource => activitySource.Name == TelemetryConstants.DefaultActivitySourceNamespace, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - ActivitySource.AddActivityListener(listener); - - - var activity = source.StartActivity("Build"); - - activity.ShouldNotBeNull(); - activity?.DisplayName.ShouldBe("VS/MSBuild/Build"); - - activity?.Dispose(); - } - - [Fact] - public void StartActivity_ShouldUseParentId_WhenRemoteParentExists() - { - // Arrange - var parentActivity = new Activity("ParentActivity"); - parentActivity.SetParentId("|12345.abcde."); // Simulate some parent trace ID - parentActivity.AddTag("sampleTag", "sampleVal"); - parentActivity.Start(); - - var source = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, 1.0); - using var listener = new ActivityListener - { - ShouldListenTo = activitySource => activitySource.Name == TelemetryConstants.DefaultActivitySourceNamespace, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - ActivitySource.AddActivityListener(listener); - - // Act - var childActivity = source.StartActivity("ChildBuild"); - - // Assert - childActivity.ShouldNotBeNull(); - // If HasRemoteParent is true, the code uses `parentId: Activity.Current.ParentId`. - // However, by default .NET Activity doesn't automatically set HasRemoteParent = true - // unless you explicitly set it. If you have logic that sets it, you can test it here. - // For demonstration, we assume the ParentId is carried over if HasRemoteParent == true. - if (Activity.Current?.HasRemoteParent == true) - { - childActivity?.ParentId.ShouldBe("|12345.abcde."); - } - - parentActivity.Dispose(); - childActivity?.Dispose(); - } - } -} diff --git a/src/Build.UnitTests/Telemetry/OpenTelemetryManager_Tests.cs b/src/Build.UnitTests/Telemetry/OpenTelemetryManager_Tests.cs deleted file mode 100644 index 3faa3ab54a9..00000000000 --- a/src/Build.UnitTests/Telemetry/OpenTelemetryManager_Tests.cs +++ /dev/null @@ -1,142 +0,0 @@ -// 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.Linq; -using System.Reflection; -using Microsoft.Build.Execution; -using Microsoft.Build.Framework.Telemetry; -using Microsoft.Build.UnitTests; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.Engine.UnitTests.Telemetry -{ - // Putting the tests to a collection ensures tests run serially by default, that's needed to isolate the manager singleton state and env vars in some telemetry tests. - [Collection("OpenTelemetryManagerTests")] - public class OpenTelemetryManagerTests : IDisposable - { - - private const string TelemetryFxOptoutEnvVarName = "MSBUILD_TELEMETRY_OPTOUT"; - private const string DotnetOptOut = "DOTNET_CLI_TELEMETRY_OPTOUT"; - private const string TelemetrySampleRateOverrideEnvVarName = "MSBUILD_TELEMETRY_SAMPLE_RATE"; - private const string VS1714TelemetryOptInEnvVarName = "MSBUILD_TELEMETRY_OPTIN"; - - public OpenTelemetryManagerTests() - { - } - - public void Dispose() - { - ResetManagerState(); - } - - [Theory] - [InlineData(DotnetOptOut, "true")] - [InlineData(TelemetryFxOptoutEnvVarName, "true")] - [InlineData(DotnetOptOut, "1")] - [InlineData(TelemetryFxOptoutEnvVarName, "1")] - public void Initialize_ShouldSetStateToOptOut_WhenOptOutEnvVarIsTrue(string optoutVar, string value) - { - // Arrange - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(optoutVar, value); - - // Act - OpenTelemetryManager.Instance.Initialize(isStandalone: false); - - // Assert - OpenTelemetryManager.Instance.IsActive().ShouldBeFalse(); - } - -#if NETCOREAPP - [Fact] - public void Initialize_ShouldSetStateToUnsampled_WhenNoOverrideOnNetCore() - { - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, null); - environment.SetEnvironmentVariable(DotnetOptOut, null); - - OpenTelemetryManager.Instance.Initialize(isStandalone: false); - - // If no override on .NET, we expect no Active ActivitySource - OpenTelemetryManager.Instance.DefaultActivitySource.ShouldBeNull(); - } -#endif - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Initialize_ShouldSetSampleRateOverride_AndCreateActivitySource_WhenRandomBelowOverride(bool standalone) - { - // Arrange - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(VS1714TelemetryOptInEnvVarName, "1"); - environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, "1.0"); - environment.SetEnvironmentVariable(DotnetOptOut, null); - - // Act - OpenTelemetryManager.Instance.Initialize(isStandalone: standalone); - - // Assert - OpenTelemetryManager.Instance.IsActive().ShouldBeTrue(); - OpenTelemetryManager.Instance.DefaultActivitySource.ShouldNotBeNull(); - } - - [Fact] - public void Initialize_ShouldNoOp_WhenCalledMultipleTimes() - { - using TestEnvironment environment = TestEnvironment.Create(); - environment.SetEnvironmentVariable(DotnetOptOut, "true"); - OpenTelemetryManager.Instance.Initialize(isStandalone: true); - var state1 = OpenTelemetryManager.Instance.IsActive(); - - environment.SetEnvironmentVariable(DotnetOptOut, null); - OpenTelemetryManager.Instance.Initialize(isStandalone: true); - var state2 = OpenTelemetryManager.Instance.IsActive(); - - // Because the manager is already initialized, second call is a no-op - state1.ShouldBe(false); - state2.ShouldBe(false); - } - - [Fact] - public void TelemetryLoadFailureIsLoggedOnce() - { - OpenTelemetryManager.Instance.LoadFailureExceptionMessage = new System.IO.FileNotFoundException().ToString(); - using BuildManager bm = new BuildManager(); - var deferredMessages = new List(); - bm.BeginBuild(new BuildParameters(), deferredMessages); - deferredMessages.ShouldContain(x => x.Text.Contains("FileNotFound")); - bm.EndBuild(); - bm.BeginBuild(new BuildParameters()); - bm.EndBuild(); - - // should not add message twice - int count = deferredMessages.Count(x => x.Text.Contains("FileNotFound")); - count.ShouldBe(1); - } - - /* Helper methods */ - - /// - /// Resets the singleton manager to a known uninitialized state so each test is isolated. - /// - private void ResetManagerState() - { - var instance = OpenTelemetryManager.Instance; - - // 1. Reset the private _telemetryState field - var telemetryStateField = typeof(OpenTelemetryManager) - .GetField("_telemetryState", BindingFlags.NonPublic | BindingFlags.Instance); - telemetryStateField?.SetValue(instance, OpenTelemetryManager.TelemetryState.Uninitialized); - - // 2. Null out the DefaultActivitySource property - var defaultSourceProp = typeof(OpenTelemetryManager) - .GetProperty(nameof(OpenTelemetryManager.DefaultActivitySource), - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - defaultSourceProp?.SetValue(instance, null); - } - } -} diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index ff7f67df8c1..84625fcfd3d 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -13,7 +13,11 @@ using System.IO; using System.Linq; using System.Reflection; + +#if FEATURE_REPORTFILEACCESSES using System.Runtime.CompilerServices; +#endif + using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; @@ -459,8 +463,9 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) /// Thrown if a build is already in progress. public void BeginBuild(BuildParameters parameters) { - InitializeTelemetry(); - +#if NETFRAMEWORK + VSTelemetryManager telemetryManager = new VSTelemetryManager(isStandalone: false); +#endif if (_previousLowPriority != null) { if (parameters.LowPriority != _previousLowPriority) @@ -525,6 +530,7 @@ public void BeginBuild(BuildParameters parameters) _buildTelemetry = new() { StartAt = now, + IsStandaloneExecution = false, }; } @@ -585,7 +591,6 @@ public void BeginBuild(BuildParameters parameters) // Initialize components. _nodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.NodeManager) as INodeManager; - _buildParameters.IsTelemetryEnabled |= OpenTelemetryManager.Instance.IsActive(); var loggingService = InitializeLoggingService(); // Log deferred messages and response files @@ -739,25 +744,6 @@ void InitializeCaches() } } - private void InitializeTelemetry() - { - OpenTelemetryManager.Instance.Initialize(isStandalone: false); - string? failureMessage = OpenTelemetryManager.Instance.LoadFailureExceptionMessage; - if (_deferredBuildMessages != null && - failureMessage != null && - _deferredBuildMessages is ICollection deferredBuildMessagesCollection) - { - deferredBuildMessagesCollection.Add( - new DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "OpenTelemetryLoadFailed", - failureMessage), - MessageImportance.Low)); - - // clean up the message from OpenTelemetryManager to avoid double logging it - OpenTelemetryManager.Instance.LoadFailureExceptionMessage = null; - } - } #if FEATURE_REPORTFILEACCESSES /// @@ -1127,11 +1113,9 @@ public void EndBuild() _buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement; loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties()); - if (OpenTelemetryManager.Instance.IsActive()) - { - EndBuildTelemetry(); - } - +#if NETFRAMEWORK + EndBuildTelemetry(); +#endif // Clean telemetry to make it ready for next build submission. _buildTelemetry = null; } @@ -1179,20 +1163,18 @@ void SerializeCaches() } } - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads of System.Diagnostics.DiagnosticSource, TODO: when this is agreed to perf-wise enable instrumenting using activities anywhere... +#if NETFRAMEWORK + [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - OpenTelemetryManager.Instance.DefaultActivitySource? - .StartActivity("Build")? + VSTelemetryManager.StartActivity("Build")? .WithTags(_buildTelemetry) .WithTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, includeTargetDetails: false)) - .WithStartTime(_buildTelemetry!.InnerStartAt) .Dispose(); - OpenTelemetryManager.Instance.ForceFlush(); } - +#endif /// /// Convenience method. Submits a lone build request and blocks until results are available. /// diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 6b6cdec68f3..91b0180f5b8 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -182,7 +182,6 @@ - diff --git a/src/Build/TelemetryInfra/ITelemetryForwarder.cs b/src/Build/TelemetryInfra/ITelemetryForwarder.cs index 15d021bfb81..97735076593 100644 --- a/src/Build/TelemetryInfra/ITelemetryForwarder.cs +++ b/src/Build/TelemetryInfra/ITelemetryForwarder.cs @@ -14,7 +14,12 @@ internal interface ITelemetryForwarder { bool IsTelemetryCollected { get; } - void AddTask(string name, TimeSpan cumulativeExecutionTime, short executionsCount, long totalMemoryConsumed, bool isCustom, + void AddTask( + string name, + TimeSpan cumulativeExecutionTime, + short executionsCount, + long totalMemoryConsumed, + bool isCustom, bool isFromNugetCache); /// diff --git a/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs b/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs index b028dd4b7fa..d4a388d79ce 100644 --- a/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs +++ b/src/Build/TelemetryInfra/InternalTelemetryConsumingLogger.cs @@ -11,7 +11,9 @@ namespace Microsoft.Build.TelemetryInfra; internal sealed class InternalTelemetryConsumingLogger : ILogger { public LoggerVerbosity Verbosity { get; set; } + public string? Parameters { get; set; } + internal static event Action? TestOnly_InternalTelemetryAggregted; public void Initialize(IEventSource eventSource) @@ -70,12 +72,14 @@ private void FlushDataIntoConsoleIfRequested() { Console.WriteLine($"{task.Key} - {task.Value.TotalMemoryBytes / 1024.0:0.00}kB"); } + Console.WriteLine("=========================================="); Console.WriteLine("Tasks by Executions count:"); foreach (var task in _workerNodeTelemetryData.TasksExecutionData.OrderByDescending(t => t.Value.ExecutionsCount)) { Console.WriteLine($"{task.Key} - {task.Value.ExecutionsCount}"); } + Console.WriteLine("=========================================="); } diff --git a/src/Build/TelemetryInfra/TelemetryDataUtils.cs b/src/Build/TelemetryInfra/TelemetryDataUtils.cs deleted file mode 100644 index e2759bec030..00000000000 --- a/src/Build/TelemetryInfra/TelemetryDataUtils.cs +++ /dev/null @@ -1,339 +0,0 @@ -// 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.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Build.Framework.Telemetry -{ - internal static class TelemetryDataUtils - { - /// - /// Transforms collected telemetry data to format recognized by the telemetry infrastructure. - /// - /// Data about tasks and target forwarded from nodes. - /// Controls whether Task details should attached to the telemetry. - /// Controls whether Target details should be attached to the telemetry. - /// Node Telemetry data wrapped in a list of properties that can be attached as tags to a . - public static IActivityTelemetryDataHolder? AsActivityDataHolder(this IWorkerNodeTelemetryData? telemetryData, bool includeTasksDetails, bool includeTargetDetails) - { - if (telemetryData == null) - { - return null; - } - - List telemetryItems = new(4); - - if (includeTasksDetails) - { - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Tasks, - JsonSerializer.Serialize(telemetryData.TasksExecutionData, _serializerOptions), false)); - } - - if (includeTargetDetails) - { - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Targets, - JsonSerializer.Serialize(telemetryData.TargetsExecutionData, _serializerOptions), false)); - } - - TargetsSummaryConverter targetsSummary = new(); - targetsSummary.Process(telemetryData.TargetsExecutionData); - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TargetsSummary, - JsonSerializer.Serialize(targetsSummary, _serializerOptions), false)); - - TasksSummaryConverter tasksSummary = new(); - tasksSummary.Process(telemetryData.TasksExecutionData); - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TasksSummary, - JsonSerializer.Serialize(tasksSummary, _serializerOptions), false)); - - return new NodeTelemetry(telemetryItems); - } - - private static JsonSerializerOptions _serializerOptions = CreateSerializerOptions(); - - private static JsonSerializerOptions CreateSerializerOptions() - { - var opt = new JsonSerializerOptions - { - Converters = - { - new TargetsDetailsConverter(), - new TasksDetailsConverter(), - new TargetsSummaryConverter(), - new TasksSummaryConverter(), - }, - }; - - return opt; - } - - private class TargetsDetailsConverter : JsonConverter?> - { - public override Dictionary? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - Dictionary? value, - JsonSerializerOptions options) - { - if (value == null) - { - throw new NotSupportedException("TaskOrTargetTelemetryKey cannot be null in telemetry data"); - } - - // Following needed - as System.Text.Json doesn't support indexing dictionary by composite types - writer.WriteStartObject(); - - foreach (KeyValuePair valuePair in value) - { - string keyName = ShouldHashKey(valuePair.Key) ? - ActivityExtensions.GetHashed(valuePair.Key.Name) : - valuePair.Key.Name; - - writer.WriteStartObject(keyName); - writer.WriteBoolean("WasExecuted", valuePair.Value); - writer.WriteBoolean(nameof(valuePair.Key.IsCustom), valuePair.Key.IsCustom); - writer.WriteBoolean(nameof(valuePair.Key.IsNuget), valuePair.Key.IsNuget); - writer.WriteBoolean(nameof(valuePair.Key.IsMetaProj), valuePair.Key.IsMetaProj); - writer.WriteEndObject(); - } - - writer.WriteEndObject(); - } - - private bool ShouldHashKey(TaskOrTargetTelemetryKey key) => key.IsCustom || key.IsMetaProj; - } - - private class TasksDetailsConverter : JsonConverter?> - { - public override Dictionary? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - Dictionary? value, - JsonSerializerOptions options) - { - if (value == null) - { - throw new NotSupportedException("TaskOrTargetTelemetryKey cannot be null in telemetry data"); - } - - // Following needed - as System.Text.Json doesn't support indexing dictionary by composite types - writer.WriteStartObject(); - - foreach (KeyValuePair valuePair in value) - { - string keyName = valuePair.Key.IsCustom ? - ActivityExtensions.GetHashed(valuePair.Key.Name) : - valuePair.Key.Name; - writer.WriteStartObject(keyName); - writer.WriteNumber(nameof(valuePair.Value.CumulativeExecutionTime.TotalMilliseconds), valuePair.Value.CumulativeExecutionTime.TotalMilliseconds); - writer.WriteNumber(nameof(valuePair.Value.ExecutionsCount), valuePair.Value.ExecutionsCount); - writer.WriteNumber(nameof(valuePair.Value.TotalMemoryBytes), valuePair.Value.TotalMemoryBytes); - writer.WriteBoolean(nameof(valuePair.Key.IsCustom), valuePair.Key.IsCustom); - writer.WriteBoolean(nameof(valuePair.Key.IsNuget), valuePair.Key.IsNuget); - writer.WriteEndObject(); - } - - writer.WriteEndObject(); - } - } - - private class TargetsSummaryConverter : JsonConverter - { - /// - /// Processes target execution data to compile summary statistics for both built-in and custom targets. - /// - /// Dictionary containing target execution data keyed by task identifiers. - public void Process(Dictionary targetsExecutionData) - { - foreach (KeyValuePair targetPair in targetsExecutionData) - { - TaskOrTargetTelemetryKey key = targetPair.Key; - bool wasExecuted = targetPair.Value; - - // Update loaded targets statistics (all targets are loaded) - UpdateTargetStatistics(key, isExecuted: false); - - // Update executed targets statistics (only targets that were actually executed) - if (wasExecuted) - { - UpdateTargetStatistics(key, isExecuted: true); - } - } - } - - private void UpdateTargetStatistics(TaskOrTargetTelemetryKey key, bool isExecuted) - { - // Select the appropriate target info collections based on execution state - TargetInfo builtinTargetInfo = isExecuted ? ExecutedBuiltinTargetInfo : LoadedBuiltinTargetInfo; - TargetInfo customTargetInfo = isExecuted ? ExecutedCustomTargetInfo : LoadedCustomTargetInfo; - - // Update either custom or builtin target info based on target type - TargetInfo targetInfo = key.IsCustom ? customTargetInfo : builtinTargetInfo; - - targetInfo.Total++; - if (key.IsNuget) - { - targetInfo.FromNuget++; - } - if (key.IsMetaProj) - { - targetInfo.FromMetaproj++; - } - } - - private TargetInfo LoadedBuiltinTargetInfo { get; } = new(); - private TargetInfo LoadedCustomTargetInfo { get; } = new(); - private TargetInfo ExecutedBuiltinTargetInfo { get; } = new(); - private TargetInfo ExecutedCustomTargetInfo { get; } = new(); - - private class TargetInfo - { - public int Total { get; internal set; } - public int FromNuget { get; internal set; } - public int FromMetaproj { get; internal set; } - } - - public override TargetsSummaryConverter? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - TargetsSummaryConverter value, - JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteStartObject("Loaded"); - WriteStat(writer, value.LoadedBuiltinTargetInfo, value.LoadedCustomTargetInfo); - writer.WriteEndObject(); - writer.WriteStartObject("Executed"); - WriteStat(writer, value.ExecutedBuiltinTargetInfo, value.ExecutedCustomTargetInfo); - writer.WriteEndObject(); - writer.WriteEndObject(); - - void WriteStat(Utf8JsonWriter writer, TargetInfo builtinTargetsInfo, TargetInfo customTargetsInfo) - { - writer.WriteNumber(nameof(builtinTargetsInfo.Total), builtinTargetsInfo.Total + customTargetsInfo.Total); - WriteSingleStat(writer, builtinTargetsInfo, "Microsoft"); - WriteSingleStat(writer, customTargetsInfo, "Custom"); - } - - void WriteSingleStat(Utf8JsonWriter writer, TargetInfo targetInfo, string name) - { - if (targetInfo.Total > 0) - { - writer.WriteStartObject(name); - writer.WriteNumber(nameof(targetInfo.Total), targetInfo.Total); - writer.WriteNumber(nameof(targetInfo.FromNuget), targetInfo.FromNuget); - writer.WriteNumber(nameof(targetInfo.FromMetaproj), targetInfo.FromMetaproj); - writer.WriteEndObject(); - } - } - } - } - - private class TasksSummaryConverter : JsonConverter - { - /// - /// Processes task execution data to compile summary statistics for both built-in and custom tasks. - /// - /// Dictionary containing task execution data keyed by task identifiers. - public void Process(Dictionary tasksExecutionData) - { - foreach (KeyValuePair taskInfo in tasksExecutionData) - { - UpdateTaskStatistics(BuiltinTasksInfo, CustomTasksInfo, taskInfo.Key, taskInfo.Value); - } - } - - private void UpdateTaskStatistics( - TasksInfo builtinTaskInfo, - TasksInfo customTaskInfo, - TaskOrTargetTelemetryKey key, - TaskExecutionStats taskExecutionStats) - { - TasksInfo taskInfo = key.IsCustom ? customTaskInfo : builtinTaskInfo; - taskInfo.Total.Accumulate(taskExecutionStats); - - if (key.IsNuget) - { - taskInfo.FromNuget.Accumulate(taskExecutionStats); - } - } - - private TasksInfo BuiltinTasksInfo { get; } = new TasksInfo(); - - private TasksInfo CustomTasksInfo { get; } = new TasksInfo(); - - private class TasksInfo - { - public TaskExecutionStats Total { get; } = TaskExecutionStats.CreateEmpty(); - - public TaskExecutionStats FromNuget { get; } = TaskExecutionStats.CreateEmpty(); - } - - public override TasksSummaryConverter? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) => - throw new NotImplementedException("Reading is not supported"); - - public override void Write( - Utf8JsonWriter writer, - TasksSummaryConverter value, - JsonSerializerOptions options) - { - writer.WriteStartObject(); - WriteStat(writer, value.BuiltinTasksInfo, "Microsoft"); - WriteStat(writer, value.CustomTasksInfo, "Custom"); - writer.WriteEndObject(); - - void WriteStat(Utf8JsonWriter writer, TasksInfo tasksInfo, string name) - { - writer.WriteStartObject(name); - WriteSingleStat(writer, tasksInfo.Total, nameof(tasksInfo.Total)); - WriteSingleStat(writer, tasksInfo.FromNuget, nameof(tasksInfo.FromNuget)); - writer.WriteEndObject(); - } - - void WriteSingleStat(Utf8JsonWriter writer, TaskExecutionStats stats, string name) - { - if (stats.ExecutionsCount > 0) - { - writer.WriteStartObject(name); - writer.WriteNumber(nameof(stats.ExecutionsCount), stats.ExecutionsCount); - writer.WriteNumber(nameof(stats.CumulativeExecutionTime.TotalMilliseconds), stats.CumulativeExecutionTime.TotalMilliseconds); - writer.WriteNumber(nameof(stats.TotalMemoryBytes), stats.TotalMemoryBytes); - writer.WriteEndObject(); - } - } - } - } - - private class NodeTelemetry : IActivityTelemetryDataHolder - { - private readonly IList _items; - - public NodeTelemetry(IList items) => _items = items; - - public IList GetActivityProperties() - => _items; - } - } -} diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 2f972d7903e..3fb5bbc49cb 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Framework/Telemetry/BuildCheckTelemetry.cs b/src/Framework/Telemetry/BuildCheckTelemetry.cs index 3b8507203c1..8555a1b33e8 100644 --- a/src/Framework/Telemetry/BuildCheckTelemetry.cs +++ b/src/Framework/Telemetry/BuildCheckTelemetry.cs @@ -87,10 +87,7 @@ internal class BuildCheckTelemetry yield return (RuleStatsEventName, properties); } - // set for the new submission in case of build server _submissionId = Guid.NewGuid(); } } - - diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index c20c5817558..e8a2e63b9e8 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -32,6 +32,11 @@ internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder /// public DateTime? InnerStartAt { get; set; } + /// + /// True if MSBuild runs from command line. + /// + public bool? IsStandaloneExecution { get; set; } + /// /// Time at which build have finished. /// @@ -104,78 +109,42 @@ public override IDictionary GetProperties() { var properties = new Dictionary(); - // populate property values - if (BuildEngineDisplayVersion != null) - { - properties[nameof(BuildEngineDisplayVersion)] = BuildEngineDisplayVersion; - } - + AddIfNotNull(nameof(BuildEngineDisplayVersion), BuildEngineDisplayVersion); + AddIfNotNull(nameof(BuildEngineFrameworkName), BuildEngineFrameworkName); + AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); + AddIfNotNull(nameof(InitialMSBuildServerState), InitialMSBuildServerState); + AddIfNotNull(nameof(ProjectPath), ProjectPath); + AddIfNotNull(nameof(ServerFallbackReason), ServerFallbackReason); + AddIfNotNull(nameof(BuildTarget), BuildTarget); + AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion?.ToString()); + AddIfNotNull(nameof(BuildSuccess), BuildSuccess?.ToString()); + AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled?.ToString()); + AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled?.ToString()); + AddIfNotNull(nameof(SACEnabled), SACEnabled?.ToString()); + AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution?.ToString()); + + // Calculate durations if (StartAt.HasValue && FinishedAt.HasValue) { - properties[TelemetryConstants.BuildDurationPropertyName] = (FinishedAt.Value - StartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + properties[TelemetryConstants.BuildDurationPropertyName] = + (FinishedAt.Value - StartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); } if (InnerStartAt.HasValue && FinishedAt.HasValue) { - properties[TelemetryConstants.InnerBuildDurationPropertyName] = (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); - } - - if (BuildEngineFrameworkName != null) - { - properties[nameof(BuildEngineFrameworkName)] = BuildEngineFrameworkName; - } - - if (BuildEngineHost != null) - { - properties[nameof(BuildEngineHost)] = BuildEngineHost; - } - - if (InitialMSBuildServerState != null) - { - properties[nameof(InitialMSBuildServerState)] = InitialMSBuildServerState; + properties[TelemetryConstants.InnerBuildDurationPropertyName] = + (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); } - if (ProjectPath != null) - { - properties[nameof(ProjectPath)] = ProjectPath; - } - - if (ServerFallbackReason != null) - { - properties[nameof(ServerFallbackReason)] = ServerFallbackReason; - } - - if (BuildSuccess.HasValue) - { - properties[nameof(BuildSuccess)] = BuildSuccess.Value.ToString(CultureInfo.InvariantCulture); - } - - if (BuildTarget != null) - { - properties[nameof(BuildTarget)] = BuildTarget; - } - - if (BuildEngineVersion != null) - { - properties[nameof(BuildEngineVersion)] = BuildEngineVersion.ToString(); - } - - if (BuildCheckEnabled != null) - { - properties[nameof(BuildCheckEnabled)] = BuildCheckEnabled.Value.ToString(CultureInfo.InvariantCulture); - } - - if (MultiThreadedModeEnabled != null) - { - properties[nameof(MultiThreadedModeEnabled)] = MultiThreadedModeEnabled.Value.ToString(CultureInfo.InvariantCulture); - } + return properties; - if (SACEnabled != null) + void AddIfNotNull(string key, string? value) { - properties[nameof(SACEnabled)] = SACEnabled.Value.ToString(CultureInfo.InvariantCulture); + if (value != null) + { + properties[key] = value; + } } - - return properties; } /// @@ -196,42 +165,24 @@ public IList GetActivityProperties() telemetryItems.Add(new TelemetryItem(TelemetryConstants.InnerBuildDurationPropertyName, (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds, false)); } - if (BuildEngineHost != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildEngineHost), BuildEngineHost, false)); - } - - if (BuildSuccess.HasValue) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildSuccess), BuildSuccess, false)); - } - - if (BuildTarget != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildTarget), BuildTarget, true)); - } - - if (BuildEngineVersion != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildEngineVersion), BuildEngineVersion.ToString(), false)); - } - - if (BuildCheckEnabled != null) - { - telemetryItems.Add(new TelemetryItem(nameof(BuildCheckEnabled), BuildCheckEnabled, false)); - } + AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); + AddIfNotNull(nameof(BuildSuccess), BuildSuccess?.ToString()); + AddIfNotNull(nameof(BuildTarget), BuildTarget); + AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion?.ToString()); + AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled?.ToString()); + AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled?.ToString()); + AddIfNotNull(nameof(SACEnabled), SACEnabled?.ToString()); + AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution?.ToString()); - if (MultiThreadedModeEnabled != null) - { - telemetryItems.Add(new TelemetryItem(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled, false)); - } + return telemetryItems; - if (SACEnabled != null) + void AddIfNotNull(string key, string? value) { - telemetryItems.Add(new TelemetryItem(nameof(SACEnabled), SACEnabled, false)); + if (value != null) + { + telemetryItems.Add(new TelemetryItem(key, value, NeedsHashing: false)); + } } - - return telemetryItems; } } } diff --git a/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs b/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs index a0303e4a4e2..b4ca028d57d 100644 --- a/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs +++ b/src/Framework/Telemetry/IWorkerNodeTelemetryData.cs @@ -8,5 +8,6 @@ namespace Microsoft.Build.Framework.Telemetry; internal interface IWorkerNodeTelemetryData { Dictionary TasksExecutionData { get; } + Dictionary TargetsExecutionData { get; } } diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index dc51085f60c..461f122ba63 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -47,6 +47,8 @@ internal static class TelemetryConstants /// Name of the property for inner build duration. /// public const string InnerBuildDurationPropertyName = "InnerBuildDurationInMilliseconds"; + + public const string BuildEvent = nameof(BuildEvent); } internal static class NodeTelemetryTags diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs new file mode 100644 index 00000000000..470ddd86154 --- /dev/null +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -0,0 +1,306 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#if NETFRAMEWORK + +using System.Collections.Generic; +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.Build.Framework.Telemetry +{ + internal static class TelemetryDataUtils + { + /// + /// Transforms collected telemetry data to format recognized by the telemetry infrastructure. + /// + /// Data about tasks and target forwarded from nodes. + /// Controls whether Task details should attached to the telemetry. + /// Controls whether Target details should be attached to the telemetry. + /// Node Telemetry data wrapped in a list of properties that can be attached as tags to a . + public static IActivityTelemetryDataHolder? AsActivityDataHolder(this IWorkerNodeTelemetryData? telemetryData, bool includeTasksDetails, bool includeTargetDetails) + { + if (telemetryData == null) + { + return null; + } + + List telemetryItems = new(4); + + if (includeTasksDetails) + { + telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Tasks, ConvertTasksDetailsToPropertyBag(telemetryData.TasksExecutionData))); + } + + if (includeTargetDetails) + { + telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Targets, ConvertTargetsDetailsToPropertyBag(telemetryData.TargetsExecutionData))); + } + + TargetsSummaryConverter targetsSummary = new(); + targetsSummary.Process(telemetryData.TargetsExecutionData); + telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TargetsSummary, new TelemetryComplexProperty(ConvertTargetsSummaryToPropertyBag(targetsSummary)))); + + TasksSummaryConverter tasksSummary = new(); + tasksSummary.Process(telemetryData.TasksExecutionData); + telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TasksSummary, new TelemetryComplexProperty(ConvertTasksSummaryToPropertyBag(tasksSummary)))); + + return new NodeTelemetry(telemetryItems); + } + + /// + /// Converts targets details to a property bag (dictionary) for telemetry. + /// + private static Dictionary ConvertTargetsDetailsToPropertyBag( + Dictionary targetsDetails) + { + var result = new Dictionary(); + + foreach (KeyValuePair valuePair in targetsDetails) + { + string keyName = ShouldHashKey(valuePair.Key) ? + ActivityExtensions.GetHashed(valuePair.Key.Name) : + valuePair.Key.Name; + + result[keyName] = new Dictionary + { + ["WasExecuted"] = valuePair.Value, + ["IsCustom"] = valuePair.Key.IsCustom, + ["IsNuget"] = valuePair.Key.IsNuget, + ["IsMetaProj"] = valuePair.Key.IsMetaProj + }; + } + + return result; + + static bool ShouldHashKey(TaskOrTargetTelemetryKey key) => key.IsCustom || key.IsMetaProj; + } + + /// + /// Converts tasks details to a property bag (dictionary) for telemetry. + /// + private static Dictionary ConvertTasksDetailsToPropertyBag( + Dictionary tasksDetails) + { + var result = new Dictionary(); + + foreach (KeyValuePair valuePair in tasksDetails) + { + string keyName = valuePair.Key.IsCustom ? + ActivityExtensions.GetHashed(valuePair.Key.Name) : + valuePair.Key.Name; + + result[keyName] = new Dictionary + { + ["TotalMilliseconds"] = valuePair.Value.CumulativeExecutionTime.TotalMilliseconds, + ["ExecutionsCount"] = valuePair.Value.ExecutionsCount, + ["TotalMemoryBytes"] = valuePair.Value.TotalMemoryBytes, + ["IsCustom"] = valuePair.Key.IsCustom, + ["IsNuget"] = valuePair.Key.IsNuget + }; + } + + return result; + } + + /// + /// Converts targets summary to a property bag (dictionary) for telemetry. + /// + private static Dictionary ConvertTargetsSummaryToPropertyBag(TargetsSummaryConverter summary) + { + return new Dictionary + { + ["Loaded"] = CreateTargetStats( + summary.LoadedBuiltinTargetInfo, + summary.LoadedCustomTargetInfo), + ["Executed"] = CreateTargetStats( + summary.ExecutedBuiltinTargetInfo, + summary.ExecutedCustomTargetInfo) + }; + + static Dictionary CreateTargetStats( + TargetsSummaryConverter.TargetInfo builtinInfo, + TargetsSummaryConverter.TargetInfo customInfo) + { + var stats = new Dictionary + { + ["Total"] = builtinInfo.Total + customInfo.Total + }; + + if (builtinInfo.Total > 0) + { + stats["Microsoft"] = new Dictionary + { + ["Total"] = builtinInfo.Total, + ["FromNuget"] = builtinInfo.FromNuget, + ["FromMetaproj"] = builtinInfo.FromMetaproj + }; + } + + if (customInfo.Total > 0) + { + stats["Custom"] = new Dictionary + { + ["Total"] = customInfo.Total, + ["FromNuget"] = customInfo.FromNuget, + ["FromMetaproj"] = customInfo.FromMetaproj + }; + } + + return stats; + } + } + + /// + /// Converts tasks summary to a property bag (dictionary) for telemetry. + /// + private static Dictionary ConvertTasksSummaryToPropertyBag(TasksSummaryConverter summary) + { + var result = new Dictionary(); + + var microsoftDict = new Dictionary(); + AddStatsIfNotEmpty(microsoftDict, "Total", summary.BuiltinTasksInfo.Total); + AddStatsIfNotEmpty(microsoftDict, "FromNuget", summary.BuiltinTasksInfo.FromNuget); + if (microsoftDict.Count > 0) + { + result["Microsoft"] = microsoftDict; + } + + var customDict = new Dictionary(); + AddStatsIfNotEmpty(customDict, "Total", summary.CustomTasksInfo.Total); + AddStatsIfNotEmpty(customDict, "FromNuget", summary.CustomTasksInfo.FromNuget); + if (customDict.Count > 0) + { + result["Custom"] = customDict; + } + + return result; + + static void AddStatsIfNotEmpty(Dictionary parent, string key, TaskExecutionStats stats) + { + if (stats.ExecutionsCount > 0) + { + parent[key] = new Dictionary + { + ["ExecutionsCount"] = stats.ExecutionsCount, + ["TotalMilliseconds"] = stats.CumulativeExecutionTime.TotalMilliseconds, + ["TotalMemoryBytes"] = stats.TotalMemoryBytes + }; + } + } + } + + private class TargetsSummaryConverter + { + /// + /// Processes target execution data to compile summary statistics for both built-in and custom targets. + /// + /// Dictionary containing target execution data keyed by task identifiers. + public void Process(Dictionary targetsExecutionData) + { + foreach (KeyValuePair targetPair in targetsExecutionData) + { + TaskOrTargetTelemetryKey key = targetPair.Key; + bool wasExecuted = targetPair.Value; + + // Update loaded targets statistics (all targets are loaded) + UpdateTargetStatistics(key, isExecuted: false); + + // Update executed targets statistics (only targets that were actually executed) + if (wasExecuted) + { + UpdateTargetStatistics(key, isExecuted: true); + } + } + } + + private void UpdateTargetStatistics(TaskOrTargetTelemetryKey key, bool isExecuted) + { + // Select the appropriate target info collections based on execution state + TargetInfo builtinTargetInfo = isExecuted ? ExecutedBuiltinTargetInfo : LoadedBuiltinTargetInfo; + TargetInfo customTargetInfo = isExecuted ? ExecutedCustomTargetInfo : LoadedCustomTargetInfo; + + // Update either custom or builtin target info based on target type + TargetInfo targetInfo = key.IsCustom ? customTargetInfo : builtinTargetInfo; + + targetInfo.Total++; + if (key.IsNuget) + { + targetInfo.FromNuget++; + } + if (key.IsMetaProj) + { + targetInfo.FromMetaproj++; + } + } + + internal TargetInfo LoadedBuiltinTargetInfo { get; } = new(); + + internal TargetInfo LoadedCustomTargetInfo { get; } = new(); + + internal TargetInfo ExecutedBuiltinTargetInfo { get; } = new(); + + internal TargetInfo ExecutedCustomTargetInfo { get; } = new(); + + internal class TargetInfo + { + public int Total { get; internal set; } + + public int FromNuget { get; internal set; } + + public int FromMetaproj { get; internal set; } + } + } + + private class TasksSummaryConverter + { + /// + /// Processes task execution data to compile summary statistics for both built-in and custom tasks. + /// + /// Dictionary containing task execution data keyed by task identifiers. + public void Process(Dictionary tasksExecutionData) + { + foreach (KeyValuePair taskInfo in tasksExecutionData) + { + UpdateTaskStatistics(BuiltinTasksInfo, CustomTasksInfo, taskInfo.Key, taskInfo.Value); + } + } + + private void UpdateTaskStatistics( + TasksInfo builtinTaskInfo, + TasksInfo customTaskInfo, + TaskOrTargetTelemetryKey key, + TaskExecutionStats taskExecutionStats) + { + TasksInfo taskInfo = key.IsCustom ? customTaskInfo : builtinTaskInfo; + taskInfo.Total.Accumulate(taskExecutionStats); + + if (key.IsNuget) + { + taskInfo.FromNuget.Accumulate(taskExecutionStats); + } + } + + internal TasksInfo BuiltinTasksInfo { get; } = new TasksInfo(); + + internal TasksInfo CustomTasksInfo { get; } = new TasksInfo(); + + internal class TasksInfo + { + public TaskExecutionStats Total { get; } = TaskExecutionStats.CreateEmpty(); + + public TaskExecutionStats FromNuget { get; } = TaskExecutionStats.CreateEmpty(); + } + } + + private class NodeTelemetry : IActivityTelemetryDataHolder + { + private readonly IList _items; + + public NodeTelemetry(IList items) => _items = items; + + public IList GetActivityProperties() + => _items; + } + } +} + +#endif diff --git a/src/Framework/Telemetry/TelemetryItem.cs b/src/Framework/Telemetry/TelemetryItem.cs index f037d7ddbea..e6b39eab333 100644 --- a/src/Framework/Telemetry/TelemetryItem.cs +++ b/src/Framework/Telemetry/TelemetryItem.cs @@ -3,4 +3,4 @@ namespace Microsoft.Build.Framework.Telemetry; -internal record TelemetryItem(string Name, object Value, bool NeedsHashing); +internal record TelemetryItem(string Name, object Value, bool NeedsHashing = false); diff --git a/src/Framework/Telemetry/VSTelemetry.cs b/src/Framework/Telemetry/VSTelemetry.cs new file mode 100644 index 00000000000..fa05f8490a2 --- /dev/null +++ b/src/Framework/Telemetry/VSTelemetry.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Visual Studio telemetry session implementation using Microsoft.VisualStudio.Telemetry. + /// + internal sealed class VsTelemetrySession + { + + } +} + +#endif diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs new file mode 100644 index 00000000000..b819f54ca25 --- /dev/null +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK + +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.Build.Framework.Telemetry +{ + internal class VsTelemetryActivity + { + private readonly TelemetryScope _scope; + private TelemetryResult _result = TelemetryResult.Success; + private string? _resultSummary; + + private bool _disposed; + + public VsTelemetryActivity(TelemetryScope scope) + { + _scope = scope; + } + + public VsTelemetryActivity? AddTag(string key, string? value) + { + _scope.EndEvent.Properties[key] = value; + return this; + } + + public VsTelemetryActivity? SetTag(string key, object? value) + { + _scope.EndEvent.Properties[key] = value; + return this; + } + + public VsTelemetryActivity? SetStatus(ActivityStatusCode status, string? description = null) + { + // Map ActivityStatusCode to TelemetryResult + _result = status switch + { + ActivityStatusCode.Ok => TelemetryResult.Success, + ActivityStatusCode.Error => TelemetryResult.Failure, + _ => TelemetryResult.None, + }; + + _resultSummary = description; + + return this; + } + + public VsTelemetryActivity? AddEvent(ActivityEvent activityEvent) + { + // VS Telemetry doesn't have a direct equivalent to ActivityEvent + // We could create and immediately post a custom event if needed. + var telemetryEvent = new TelemetryEvent(activityEvent.Name); + foreach (KeyValuePair tag in activityEvent.Tags) + { + telemetryEvent.Properties[tag.Key] = tag.Value; + } + + TelemetryService.DefaultSession.PostEvent(telemetryEvent); + return this; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + // End the operation + _scope.End(_result, _resultSummary); + _disposed = true; + } + } +} +#endif diff --git a/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs b/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs new file mode 100644 index 00000000000..c3de8413d26 --- /dev/null +++ b/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK + +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Extension methods for . usage in VS OpenTelemetry. + /// + internal static class VSTelemetryActivityExtensions + { + /// + /// Add tags to the activity from a . + /// + public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, IActivityTelemetryDataHolder? dataHolder) + { + if (dataHolder != null) + { + activity.WithTags(dataHolder.GetActivityProperties()); + } + + return activity; + } + + /// + /// Add tags to the activity from a list of TelemetryItems. + /// + public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, IList tags) + { + foreach (var tag in tags) + { + activity.WithTag(tag); + } + + return activity; + } + + /// + /// Add a tag to the activity from a . + /// + public static VsTelemetryActivity WithTag(this VsTelemetryActivity activity, TelemetryItem item) + { + object value = item.NeedsHashing ? GetHashed(item.Value) : item.Value; + activity.SetTag($"{TelemetryConstants.PropertyPrefix}{item.Name}", value); + + return activity; + } + + /// + /// Depending on the platform, hash the value using an available mechanism. + /// + internal static string GetHashed(object value) + { + return Sha256Hasher.Hash(value.ToString() ?? ""); + } + + // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs + workaround for netstandard2.0 + private static class Sha256Hasher + { + /// + /// The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode) + /// + public static string Hash(string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); +#if NET + byte[] hash = SHA256.HashData(bytes); +#if NET9_0_OR_GREATER + return Convert.ToHexStringLower(hash); +#else + return Convert.ToHexString(hash).ToLowerInvariant(); +#endif + +#else + // Create the SHA256 object and compute the hash + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(bytes); + + // Convert the hash bytes to a lowercase hex string (manual loop approach) + var sb = new StringBuilder(hash.Length * 2); + foreach (byte b in hash) + { + sb.AppendFormat("{0:x2}", b); + } + + return sb.ToString(); + } +#endif + } + + public static string HashWithNormalizedCasing(string text) => Hash(text.ToUpperInvariant()); + } + } +} + +#endif diff --git a/src/Framework/Telemetry/VSTelemetryManager.cs b/src/Framework/Telemetry/VSTelemetryManager.cs new file mode 100644 index 00000000000..37648366e92 --- /dev/null +++ b/src/Framework/Telemetry/VSTelemetryManager.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK + +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.Build.Framework.Telemetry +{ + internal class VSTelemetryManager + { + private const string CollectorApiKey = "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255"; + + private static TelemetrySession? _telemetrySession; + + private static bool _disposed; + + public VSTelemetryManager(bool isStandalone) => Initialize(isStandalone); + + private void Initialize(bool isStandalone) + { + if (isStandalone) + { + _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); + TelemetryService.DefaultSession.IsOptedIn = true; + + // Start session, so we can start sending events + TelemetryService.DefaultSession.Start(); + + return; + } + + _telemetrySession = TelemetryService.DefaultSession; + } + + public static VsTelemetryActivity? StartActivity(string name) + { + string eventName = $"{TelemetryConstants.EventPrefix}{name}"; + TelemetryScope? operation = _telemetrySession.StartOperation(eventName); + + return operation != null ? new VsTelemetryActivity(operation) : null; + } + + public static void Dispose() + { + if (_disposed) + { + return; + } + + _telemetrySession?.Dispose(); + + _disposed = true; + } + } +} + +#endif diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 7febc492d7a..902deccc44d 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -248,9 +248,12 @@ string[] args DebuggerLaunchCheck(); // Initialize new build telemetry and record start of this build. - KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow }; - // Initialize OpenTelemetry infrastructure - OpenTelemetryManager.Instance.Initialize(isStandalone: true); + KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true}; + + // Initialize VSTelemetry +#if NETFRAMEWORK + VSTelemetryManager tm = new VSTelemetryManager(isStandalone: true); +#endif using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); @@ -298,12 +301,13 @@ string[] args { DumpCounters(false /* log to console */); } - OpenTelemetryManager.Instance.Shutdown(); +#if NETFRAMEWORK + VSTelemetryManager.Dispose(); +#endif return exitCode; } - /// /// Returns true if arguments allows or make sense to leverage msbuild server. /// From 0a43573eeb7deb43c0fcb1b93bf4b10b91ce42aa Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 21 Oct 2025 19:16:34 +0200 Subject: [PATCH 02/47] now it works for command line --- .../BackEnd/BuildManager/BuildManager.cs | 10 +- src/Framework/Telemetry/ActivityExtensions.cs | 111 ------- src/Framework/Telemetry/BuildTelemetry.cs | 40 +-- .../Telemetry/IActivityTelemetryDataHolder.cs | 8 +- .../Telemetry/MSBuildActivitySource.cs | 35 -- .../Telemetry/OpenTelemetryManager.cs | 6 - src/Framework/Telemetry/TelemetryDataUtils.cs | 308 ++++++++---------- src/Framework/Telemetry/TelemetryItem.cs | 39 ++- src/Framework/Telemetry/VSBuildTelemetry.cs | 54 +++ .../Telemetry/VSTelemetryActivity.cs | 7 + .../VSTelemetryActivityExtensions.cs | 25 +- src/Framework/Telemetry/VSTelemetryManager.cs | 9 +- 12 files changed, 262 insertions(+), 390 deletions(-) delete mode 100644 src/Framework/Telemetry/ActivityExtensions.cs delete mode 100644 src/Framework/Telemetry/MSBuildActivitySource.cs create mode 100644 src/Framework/Telemetry/VSBuildTelemetry.cs diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 84625fcfd3d..09ca96d290f 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -530,11 +530,11 @@ public void BeginBuild(BuildParameters parameters) _buildTelemetry = new() { StartAt = now, - IsStandaloneExecution = false, }; } _buildTelemetry.InnerStartAt = now; + _buildTelemetry.IsStandaloneExecution ??= false; if (BuildParameters.DumpOpportunisticInternStats) { @@ -1168,8 +1168,8 @@ void SerializeCaches() private void EndBuildTelemetry() { VSTelemetryManager.StartActivity("Build")? - .WithTags(_buildTelemetry) - .WithTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( + .WithTags("generalbuilddata", _buildTelemetry) + .WithTags("buildsinsights", _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, includeTargetDetails: false)) .Dispose(); @@ -3025,8 +3025,7 @@ private ILoggingService CreateLoggingService( loggerSwitchParameters: null, verbosity: LoggerVerbosity.Quiet); - _telemetryConsumingLogger = - new InternalTelemetryConsumingLogger(); + _telemetryConsumingLogger = new InternalTelemetryConsumingLogger(); ForwardingLoggerRecord[] forwardingLogger = { new ForwardingLoggerRecord(_telemetryConsumingLogger, forwardingLoggerDescription) }; @@ -3038,7 +3037,6 @@ private ILoggingService CreateLoggingService( loggingService.EnableTargetOutputLogging = true; } - try { if (loggers != null) diff --git a/src/Framework/Telemetry/ActivityExtensions.cs b/src/Framework/Telemetry/ActivityExtensions.cs deleted file mode 100644 index 9b4e05f7c02..00000000000 --- a/src/Framework/Telemetry/ActivityExtensions.cs +++ /dev/null @@ -1,111 +0,0 @@ -// 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.Diagnostics; -using System.Security.Cryptography; -using System.Text; - -namespace Microsoft.Build.Framework.Telemetry -{ - /// - /// Extension methods for . usage in VS OpenTelemetry. - /// - internal static class ActivityExtensions - { - /// - /// Add tags to the activity from a . - /// - public static Activity WithTags(this Activity activity, IActivityTelemetryDataHolder? dataHolder) - { - if (dataHolder != null) - { - activity.WithTags(dataHolder.GetActivityProperties()); - } - return activity; - } - - /// - /// Add tags to the activity from a list of TelemetryItems. - /// - public static Activity WithTags(this Activity activity, IList tags) - { - foreach (var tag in tags) - { - activity.WithTag(tag); - } - return activity; - } - /// - /// Add a tag to the activity from a . - /// - public static Activity WithTag(this Activity activity, TelemetryItem item) - { - object value = item.NeedsHashing ? GetHashed(item.Value) : item.Value; - activity.SetTag($"{TelemetryConstants.PropertyPrefix}{item.Name}", value); - return activity; - } - - /// - /// Set the start time of the activity. - /// - public static Activity WithStartTime(this Activity activity, DateTime? startTime) - { - if (startTime.HasValue) - { - activity.SetStartTime(startTime.Value); - } - return activity; - } - - /// - /// Depending on the platform, hash the value using an available mechanism. - /// - internal static string GetHashed(object value) - { - return Sha256Hasher.Hash(value.ToString() ?? ""); - } - - // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs + workaround for netstandard2.0 - private static class Sha256Hasher - { - /// - /// The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode) - /// - public static string Hash(string text) - { - byte[] bytes = Encoding.UTF8.GetBytes(text); -#if NET - byte[] hash = SHA256.HashData(bytes); -#if NET9_0_OR_GREATER - return Convert.ToHexStringLower(hash); -#else - return Convert.ToHexString(hash).ToLowerInvariant(); -#endif - -#else - // Create the SHA256 object and compute the hash - using (var sha256 = SHA256.Create()) - { - byte[] hash = sha256.ComputeHash(bytes); - - // Convert the hash bytes to a lowercase hex string (manual loop approach) - var sb = new StringBuilder(hash.Length * 2); - foreach (byte b in hash) - { - sb.AppendFormat("{0:x2}", b); - } - - return sb.ToString(); - } -#endif - } - - public static string HashWithNormalizedCasing(string text) - { - return Hash(text.ToUpperInvariant()); - } - } - } -} diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index e8a2e63b9e8..462413fc486 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -10,7 +10,7 @@ namespace Microsoft.Build.Framework.Telemetry /// /// Telemetry of build. /// - internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder + internal class BuildTelemetry : TelemetryBase { public override string EventName => "build"; @@ -146,43 +146,5 @@ void AddIfNotNull(string key, string? value) } } } - - /// - /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. - /// - /// - public IList GetActivityProperties() - { - List telemetryItems = new(8); - - if (StartAt.HasValue && FinishedAt.HasValue) - { - telemetryItems.Add(new TelemetryItem(TelemetryConstants.BuildDurationPropertyName, (FinishedAt.Value - StartAt.Value).TotalMilliseconds, false)); - } - - if (InnerStartAt.HasValue && FinishedAt.HasValue) - { - telemetryItems.Add(new TelemetryItem(TelemetryConstants.InnerBuildDurationPropertyName, (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds, false)); - } - - AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); - AddIfNotNull(nameof(BuildSuccess), BuildSuccess?.ToString()); - AddIfNotNull(nameof(BuildTarget), BuildTarget); - AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion?.ToString()); - AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled?.ToString()); - AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled?.ToString()); - AddIfNotNull(nameof(SACEnabled), SACEnabled?.ToString()); - AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution?.ToString()); - - return telemetryItems; - - void AddIfNotNull(string key, string? value) - { - if (value != null) - { - telemetryItems.Add(new TelemetryItem(key, value, NeedsHashing: false)); - } - } - } } } diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs index 9eeb0a7509f..5cfc1c828a6 100644 --- a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -1,8 +1,10 @@ // 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; +#if NETFRAMEWORK + using System.Diagnostics; +using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry; @@ -11,5 +13,7 @@ namespace Microsoft.Build.Framework.Telemetry; /// internal interface IActivityTelemetryDataHolder { - IList GetActivityProperties(); + TelemetryComplexProperty GetActivityProperties(); } + +#endif diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs deleted file mode 100644 index 33668c0926f..00000000000 --- a/src/Framework/Telemetry/MSBuildActivitySource.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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.Build.Framework.Telemetry -{ - /// - /// Wrapper class for ActivitySource with a method that wraps Activity name with VS OTel prefix. - /// - internal class MSBuildActivitySource - { - private readonly ActivitySource _source; - private readonly double _sampleRate; - - public MSBuildActivitySource(string name, double sampleRate) - { - _source = new ActivitySource(name); - _sampleRate = sampleRate; - } - /// - /// Prefixes activity with VS OpenTelemetry. - /// - /// Name of the telemetry event without prefix. - /// - public Activity? StartActivity(string name) - { - var activity = Activity.Current?.HasRemoteParent == true - ? _source.StartActivity($"{TelemetryConstants.EventPrefix}{name}", ActivityKind.Internal, parentId: Activity.Current.ParentId) - : _source.StartActivity($"{TelemetryConstants.EventPrefix}{name}"); - activity?.WithTag(new("SampleRate", _sampleRate, false)); - return activity; - } - } -} diff --git a/src/Framework/Telemetry/OpenTelemetryManager.cs b/src/Framework/Telemetry/OpenTelemetryManager.cs index 7ee7813bddf..b7e74c760ad 100644 --- a/src/Framework/Telemetry/OpenTelemetryManager.cs +++ b/src/Framework/Telemetry/OpenTelemetryManager.cs @@ -41,11 +41,6 @@ internal class OpenTelemetryManager public string? LoadFailureExceptionMessage { get; set; } - /// - /// Optional activity source for MSBuild or other telemetry usage. - /// - public MSBuildActivitySource? DefaultActivitySource { get; private set; } - private OpenTelemetryManager() { } @@ -116,7 +111,6 @@ public void Initialize(bool isStandalone) private void InitializeActivitySources() { _telemetryState = TelemetryState.TracerInitialized; - DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, _sampleRate); } #if NETFRAMEWORK diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index 470ddd86154..572d5199d52 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.VisualStudio.Telemetry; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; namespace Microsoft.Build.Framework.Telemetry { @@ -23,50 +24,41 @@ internal static class TelemetryDataUtils return null; } - List telemetryItems = new(4); - - if (includeTasksDetails) - { - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Tasks, ConvertTasksDetailsToPropertyBag(telemetryData.TasksExecutionData))); - } - - if (includeTargetDetails) - { - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.Targets, ConvertTargetsDetailsToPropertyBag(telemetryData.TargetsExecutionData))); - } - - TargetsSummaryConverter targetsSummary = new(); + var targetsSummary = new TargetsSummaryConverter(); targetsSummary.Process(telemetryData.TargetsExecutionData); - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TargetsSummary, new TelemetryComplexProperty(ConvertTargetsSummaryToPropertyBag(targetsSummary)))); - TasksSummaryConverter tasksSummary = new(); + var tasksSummary = new TasksSummaryConverter(); tasksSummary.Process(telemetryData.TasksExecutionData); - telemetryItems.Add(new TelemetryItem(NodeTelemetryTags.TasksSummary, new TelemetryComplexProperty(ConvertTasksSummaryToPropertyBag(tasksSummary)))); - return new NodeTelemetry(telemetryItems); + var buildInsights = new BuildInsights( + GetTasksDetails(telemetryData.TasksExecutionData), + GetTargetsDetails(telemetryData.TargetsExecutionData), + GetTargetsSummary(targetsSummary), + GetTasksSummary(tasksSummary) + ); + + return new NodeTelemetry(buildInsights); } /// - /// Converts targets details to a property bag (dictionary) for telemetry. + /// Converts targets details to a list of custom objects for telemetry. /// - private static Dictionary ConvertTargetsDetailsToPropertyBag( - Dictionary targetsDetails) + private static List GetTargetsDetails(Dictionary targetsDetails) { - var result = new Dictionary(); + var result = new List(); foreach (KeyValuePair valuePair in targetsDetails) { - string keyName = ShouldHashKey(valuePair.Key) ? - ActivityExtensions.GetHashed(valuePair.Key.Name) : + string targetName = ShouldHashKey(valuePair.Key) ? + VSTelemetryActivityExtensions.GetHashed(valuePair.Key.Name) : valuePair.Key.Name; - result[keyName] = new Dictionary - { - ["WasExecuted"] = valuePair.Value, - ["IsCustom"] = valuePair.Key.IsCustom, - ["IsNuget"] = valuePair.Key.IsNuget, - ["IsMetaProj"] = valuePair.Key.IsMetaProj - }; + result.Add(new TargetDetailInfo( + targetName, + valuePair.Value.ToString(), + valuePair.Key.IsCustom.ToString(), + valuePair.Key.IsNuget.ToString(), + valuePair.Key.IsMetaProj.ToString())); } return result; @@ -74,215 +66,182 @@ private static Dictionary ConvertTargetsDetailsToPropertyBag( static bool ShouldHashKey(TaskOrTargetTelemetryKey key) => key.IsCustom || key.IsMetaProj; } + internal record TargetDetailInfo(string Name, string WasExecuted, string IsCustom, string IsNuget, string IsMetaProj); + /// - /// Converts tasks details to a property bag (dictionary) for telemetry. + /// Converts tasks details to a list of custom objects for telemetry. /// - private static Dictionary ConvertTasksDetailsToPropertyBag( + private static List GetTasksDetails( Dictionary tasksDetails) { - var result = new Dictionary(); + var result = new List(); foreach (KeyValuePair valuePair in tasksDetails) { - string keyName = valuePair.Key.IsCustom ? - ActivityExtensions.GetHashed(valuePair.Key.Name) : + string taskName = valuePair.Key.IsCustom ? + VSTelemetryActivityExtensions.GetHashed(valuePair.Key.Name) : valuePair.Key.Name; - result[keyName] = new Dictionary - { - ["TotalMilliseconds"] = valuePair.Value.CumulativeExecutionTime.TotalMilliseconds, - ["ExecutionsCount"] = valuePair.Value.ExecutionsCount, - ["TotalMemoryBytes"] = valuePair.Value.TotalMemoryBytes, - ["IsCustom"] = valuePair.Key.IsCustom, - ["IsNuget"] = valuePair.Key.IsNuget - }; + result.Add(new TaskDetailInfo( + taskName, + valuePair.Value.CumulativeExecutionTime.TotalMilliseconds.ToString(), + valuePair.Value.ExecutionsCount.ToString(), + valuePair.Value.TotalMemoryBytes.ToString(), + valuePair.Key.IsCustom.ToString(), + valuePair.Key.IsNuget.ToString())); } return result; } + internal record TaskDetailInfo(string Name, string TotalMilliseconds, string ExecutionsCount, string TotalMemoryBytes, string IsCustom, string IsNuget); + /// - /// Converts targets summary to a property bag (dictionary) for telemetry. + /// Converts targets summary to a custom object for telemetry. /// - private static Dictionary ConvertTargetsSummaryToPropertyBag(TargetsSummaryConverter summary) + private static TargetsSummaryInfo GetTargetsSummary(TargetsSummaryConverter summary) { - return new Dictionary - { - ["Loaded"] = CreateTargetStats( - summary.LoadedBuiltinTargetInfo, - summary.LoadedCustomTargetInfo), - ["Executed"] = CreateTargetStats( - summary.ExecutedBuiltinTargetInfo, - summary.ExecutedCustomTargetInfo) - }; - - static Dictionary CreateTargetStats( + return new TargetsSummaryInfo( + CreateTargetStats(summary.LoadedBuiltinTargetInfo, summary.LoadedCustomTargetInfo), + CreateTargetStats(summary.ExecutedBuiltinTargetInfo, summary.ExecutedCustomTargetInfo)); + + static TargetStatsInfo CreateTargetStats( TargetsSummaryConverter.TargetInfo builtinInfo, TargetsSummaryConverter.TargetInfo customInfo) { - var stats = new Dictionary - { - ["Total"] = builtinInfo.Total + customInfo.Total - }; - - if (builtinInfo.Total > 0) - { - stats["Microsoft"] = new Dictionary - { - ["Total"] = builtinInfo.Total, - ["FromNuget"] = builtinInfo.FromNuget, - ["FromMetaproj"] = builtinInfo.FromMetaproj - }; - } + var microsoft = builtinInfo.Total > 0 + ? new TargetCategoryInfo(builtinInfo.Total.ToString(), builtinInfo.FromNuget.ToString(), builtinInfo.FromMetaproj.ToString()) + : null; - if (customInfo.Total > 0) - { - stats["Custom"] = new Dictionary - { - ["Total"] = customInfo.Total, - ["FromNuget"] = customInfo.FromNuget, - ["FromMetaproj"] = customInfo.FromMetaproj - }; - } + var custom = customInfo.Total > 0 + ? new TargetCategoryInfo(customInfo.Total.ToString(), customInfo.FromNuget.ToString(), customInfo.FromMetaproj.ToString()) + : null; - return stats; + return new TargetStatsInfo((builtinInfo.Total + customInfo.Total).ToString(), microsoft, custom); } } + internal record TargetsSummaryInfo(TargetStatsInfo Loaded, TargetStatsInfo Executed); + + internal record TargetStatsInfo(string Total, TargetCategoryInfo? Microsoft, TargetCategoryInfo? Custom); + + internal record TargetCategoryInfo(string Total, string FromNuget, string FromMetaproj); + /// - /// Converts tasks summary to a property bag (dictionary) for telemetry. + /// Converts tasks summary to a custom object for telemetry. /// - private static Dictionary ConvertTasksSummaryToPropertyBag(TasksSummaryConverter summary) + private static TasksSummaryInfo GetTasksSummary(TasksSummaryConverter summary) { - var result = new Dictionary(); + var microsoft = CreateTaskStats(summary.BuiltinTasksInfo.Total, summary.BuiltinTasksInfo.FromNuget); + var custom = CreateTaskStats(summary.CustomTasksInfo.Total, summary.CustomTasksInfo.FromNuget); - var microsoftDict = new Dictionary(); - AddStatsIfNotEmpty(microsoftDict, "Total", summary.BuiltinTasksInfo.Total); - AddStatsIfNotEmpty(microsoftDict, "FromNuget", summary.BuiltinTasksInfo.FromNuget); - if (microsoftDict.Count > 0) - { - result["Microsoft"] = microsoftDict; - } + return new TasksSummaryInfo(microsoft, custom); - var customDict = new Dictionary(); - AddStatsIfNotEmpty(customDict, "Total", summary.CustomTasksInfo.Total); - AddStatsIfNotEmpty(customDict, "FromNuget", summary.CustomTasksInfo.FromNuget); - if (customDict.Count > 0) + static TaskCategoryStats? CreateTaskStats(TaskExecutionStats total, TaskExecutionStats fromNuget) { - result["Custom"] = customDict; - } - - return result; - - static void AddStatsIfNotEmpty(Dictionary parent, string key, TaskExecutionStats stats) - { - if (stats.ExecutionsCount > 0) - { - parent[key] = new Dictionary - { - ["ExecutionsCount"] = stats.ExecutionsCount, - ["TotalMilliseconds"] = stats.CumulativeExecutionTime.TotalMilliseconds, - ["TotalMemoryBytes"] = stats.TotalMemoryBytes - }; - } + var totalStats = total.ExecutionsCount > 0 + ? new TaskStatsInfo( + total.ExecutionsCount.ToString(), + total.CumulativeExecutionTime.TotalMilliseconds.ToString(), + total.TotalMemoryBytes.ToString()) + : null; + + var nugetStats = fromNuget.ExecutionsCount > 0 + ? new TaskStatsInfo( + fromNuget.ExecutionsCount.ToString(), + fromNuget.CumulativeExecutionTime.TotalMilliseconds.ToString(), + fromNuget.TotalMemoryBytes.ToString()) + : null; + + return (totalStats != null || nugetStats != null) + ? new TaskCategoryStats(totalStats, nugetStats) + : null; } } private class TargetsSummaryConverter { + internal TargetInfo LoadedBuiltinTargetInfo { get; } = new(); + + internal TargetInfo LoadedCustomTargetInfo { get; } = new(); + + internal TargetInfo ExecutedBuiltinTargetInfo { get; } = new(); + + internal TargetInfo ExecutedCustomTargetInfo { get; } = new(); + /// /// Processes target execution data to compile summary statistics for both built-in and custom targets. /// - /// Dictionary containing target execution data keyed by task identifiers. public void Process(Dictionary targetsExecutionData) { - foreach (KeyValuePair targetPair in targetsExecutionData) + foreach (var kv in targetsExecutionData) { - TaskOrTargetTelemetryKey key = targetPair.Key; - bool wasExecuted = targetPair.Value; - - // Update loaded targets statistics (all targets are loaded) - UpdateTargetStatistics(key, isExecuted: false); + GetTargetInfo(kv.Key, isExecuted: false).Increment(kv.Key); - // Update executed targets statistics (only targets that were actually executed) - if (wasExecuted) + // Update executed targets statistics (only if executed) + if (kv.Value) { - UpdateTargetStatistics(key, isExecuted: true); + GetTargetInfo(kv.Key, isExecuted: true).Increment(kv.Key); } } } - private void UpdateTargetStatistics(TaskOrTargetTelemetryKey key, bool isExecuted) - { - // Select the appropriate target info collections based on execution state - TargetInfo builtinTargetInfo = isExecuted ? ExecutedBuiltinTargetInfo : LoadedBuiltinTargetInfo; - TargetInfo customTargetInfo = isExecuted ? ExecutedCustomTargetInfo : LoadedCustomTargetInfo; - - // Update either custom or builtin target info based on target type - TargetInfo targetInfo = key.IsCustom ? customTargetInfo : builtinTargetInfo; - - targetInfo.Total++; - if (key.IsNuget) + private TargetInfo GetTargetInfo(TaskOrTargetTelemetryKey key, bool isExecuted) => + (key.IsCustom, isExecuted) switch { - targetInfo.FromNuget++; - } - if (key.IsMetaProj) - { - targetInfo.FromMetaproj++; - } - } - - internal TargetInfo LoadedBuiltinTargetInfo { get; } = new(); - - internal TargetInfo LoadedCustomTargetInfo { get; } = new(); - - internal TargetInfo ExecutedBuiltinTargetInfo { get; } = new(); - - internal TargetInfo ExecutedCustomTargetInfo { get; } = new(); + (true, true) => ExecutedCustomTargetInfo, + (true, false) => LoadedCustomTargetInfo, + (false, true) => ExecutedBuiltinTargetInfo, + (false, false) => LoadedBuiltinTargetInfo + }; internal class TargetInfo { - public int Total { get; internal set; } + public int Total { get; private set; } - public int FromNuget { get; internal set; } + public int FromNuget { get; private set; } - public int FromMetaproj { get; internal set; } + public int FromMetaproj { get; private set; } + + internal void Increment(TaskOrTargetTelemetryKey key) + { + Total++; + if (key.IsNuget) + { + FromNuget++; + } + + if (key.IsMetaProj) + { + FromMetaproj++; + } + } } } private class TasksSummaryConverter { + internal TasksInfo BuiltinTasksInfo { get; } = new(); + + internal TasksInfo CustomTasksInfo { get; } = new(); + /// /// Processes task execution data to compile summary statistics for both built-in and custom tasks. /// - /// Dictionary containing task execution data keyed by task identifiers. public void Process(Dictionary tasksExecutionData) { - foreach (KeyValuePair taskInfo in tasksExecutionData) + foreach (KeyValuePair kv in tasksExecutionData) { - UpdateTaskStatistics(BuiltinTasksInfo, CustomTasksInfo, taskInfo.Key, taskInfo.Value); - } - } - - private void UpdateTaskStatistics( - TasksInfo builtinTaskInfo, - TasksInfo customTaskInfo, - TaskOrTargetTelemetryKey key, - TaskExecutionStats taskExecutionStats) - { - TasksInfo taskInfo = key.IsCustom ? customTaskInfo : builtinTaskInfo; - taskInfo.Total.Accumulate(taskExecutionStats); + var taskInfo = kv.Key.IsCustom ? CustomTasksInfo : BuiltinTasksInfo; + taskInfo.Total.Accumulate(kv.Value); - if (key.IsNuget) - { - taskInfo.FromNuget.Accumulate(taskExecutionStats); + if (kv.Key.IsNuget) + { + taskInfo.FromNuget.Accumulate(kv.Value); + } } } - internal TasksInfo BuiltinTasksInfo { get; } = new TasksInfo(); - - internal TasksInfo CustomTasksInfo { get; } = new TasksInfo(); - internal class TasksInfo { public TaskExecutionStats Total { get; } = TaskExecutionStats.CreateEmpty(); @@ -291,14 +250,9 @@ internal class TasksInfo } } - private class NodeTelemetry : IActivityTelemetryDataHolder + private sealed class NodeTelemetry(BuildInsights insights) : IActivityTelemetryDataHolder { - private readonly IList _items; - - public NodeTelemetry(IList items) => _items = items; - - public IList GetActivityProperties() - => _items; + TelemetryComplexProperty IActivityTelemetryDataHolder.GetActivityProperties() => new(insights); } } } diff --git a/src/Framework/Telemetry/TelemetryItem.cs b/src/Framework/Telemetry/TelemetryItem.cs index e6b39eab333..08d257bd78b 100644 --- a/src/Framework/Telemetry/TelemetryItem.cs +++ b/src/Framework/Telemetry/TelemetryItem.cs @@ -1,6 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if NETFRAMEWORK + +using System.Collections.Generic; +using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils; + namespace Microsoft.Build.Framework.Telemetry; -internal record TelemetryItem(string Name, object Value, bool NeedsHashing = false); +/// +/// Container for all build telemetry insights including tasks and targets details and summaries. +/// +internal sealed class BuildInsights +{ + public List Tasks { get; } + + public List Targets { get; } + + public TargetsSummaryInfo TargetsSummary { get; } + + public TasksSummaryInfo TasksSummary { get; } + + public BuildInsights( + List tasks, + List targets, + TargetsSummaryInfo targetsSummary, + TasksSummaryInfo tasksSummary) + { + Tasks = tasks; + Targets = targets; + TargetsSummary = targetsSummary; + TasksSummary = tasksSummary; + } + + internal record TasksSummaryInfo(TaskCategoryStats? Microsoft, TaskCategoryStats? Custom); + + internal record TaskCategoryStats(TaskStatsInfo? Total, TaskStatsInfo? FromNuget); + + internal record TaskStatsInfo(string ExecutionsCount, string TotalMilliseconds, string TotalMemoryBytes); +} + +#endif diff --git a/src/Framework/Telemetry/VSBuildTelemetry.cs b/src/Framework/Telemetry/VSBuildTelemetry.cs new file mode 100644 index 00000000000..81171333ccd --- /dev/null +++ b/src/Framework/Telemetry/VSBuildTelemetry.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK + +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.Build.Framework.Telemetry +{ + internal class VSBuildTelemetry : IActivityTelemetryDataHolder + { + private BuildTelemetry _buildTelemetry; + + public VSBuildTelemetry(BuildTelemetry buildTelemetry) => _buildTelemetry = buildTelemetry; + + /// + /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. + /// + public TelemetryComplexProperty GetActivityProperties() + { + var buildTelemetryData = new BuildTelemetryData( + _buildTelemetry.StartAt.HasValue && _buildTelemetry.FinishedAt.HasValue + ? (_buildTelemetry.FinishedAt.Value - _buildTelemetry.StartAt.Value).TotalMilliseconds.ToString() + : null, + _buildTelemetry.InnerStartAt.HasValue && _buildTelemetry.FinishedAt.HasValue + ? (_buildTelemetry.FinishedAt.Value - _buildTelemetry.InnerStartAt.Value).TotalMilliseconds.ToString() + : null, + _buildTelemetry.BuildEngineHost, + _buildTelemetry.BuildSuccess?.ToString(), + _buildTelemetry.BuildTarget, + _buildTelemetry.BuildEngineVersion?.ToString(), + _buildTelemetry.BuildCheckEnabled?.ToString(), + _buildTelemetry.MultiThreadedModeEnabled?.ToString(), + _buildTelemetry.SACEnabled?.ToString(), + _buildTelemetry.IsStandaloneExecution?.ToString()); + + return new TelemetryComplexProperty(buildTelemetryData); + } + + internal readonly record struct BuildTelemetryData( + string? BuildDuration, + string? InnerBuildDuration, + string? BuildEngineHost, + string? BuildSuccess, + string? BuildTarget, + string? BuildEngineVersion, + string? BuildCheckEnabled, + string? MultiThreadedModeEnabled, + string? SACEnabled, + string? IsStandaloneExecution); + } +} + +#endif diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index b819f54ca25..4b95e5e930f 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -22,6 +22,13 @@ public VsTelemetryActivity(TelemetryScope scope) _scope = scope; } + public VsTelemetryActivity? AddComplexProperty(string key, TelemetryComplexProperty complexProperty) + { + _scope.EndEvent.Properties[key] = complexProperty; + + return this; + } + public VsTelemetryActivity? AddTag(string key, string? value) { _scope.EndEvent.Properties[key] = value; diff --git a/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs b/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs index c3de8413d26..b11ac8e5b76 100644 --- a/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs +++ b/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Security.Cryptography; using System.Text; +using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry { @@ -14,28 +15,33 @@ namespace Microsoft.Build.Framework.Telemetry /// Extension methods for . usage in VS OpenTelemetry. /// internal static class VSTelemetryActivityExtensions - { + { + /// /// Add tags to the activity from a . /// - public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, IActivityTelemetryDataHolder? dataHolder) + public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, string key, IActivityTelemetryDataHolder? dataHolder) { if (dataHolder != null) { - activity.WithTags(dataHolder.GetActivityProperties()); + activity.WithTag(key, dataHolder.GetActivityProperties()); } return activity; } /// - /// Add tags to the activity from a list of TelemetryItems. + /// Add tags to the activity from a . /// - public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, IList tags) + public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, string key, BuildTelemetry? buildTelemetry) { - foreach (var tag in tags) + if (buildTelemetry != null) { - activity.WithTag(tag); + TelemetryComplexProperty dataHolder = new VSBuildTelemetry(buildTelemetry).GetActivityProperties(); + if (dataHolder != null) + { + activity.WithTag(key, dataHolder); + } } return activity; @@ -44,10 +50,9 @@ public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, IL /// /// Add a tag to the activity from a . /// - public static VsTelemetryActivity WithTag(this VsTelemetryActivity activity, TelemetryItem item) + public static VsTelemetryActivity WithTag(this VsTelemetryActivity activity, string key, TelemetryComplexProperty telemetryComplexProperty) { - object value = item.NeedsHashing ? GetHashed(item.Value) : item.Value; - activity.SetTag($"{TelemetryConstants.PropertyPrefix}{item.Name}", value); + activity.AddComplexProperty($"{TelemetryConstants.PropertyPrefix}{key}", telemetryComplexProperty); return activity; } diff --git a/src/Framework/Telemetry/VSTelemetryManager.cs b/src/Framework/Telemetry/VSTelemetryManager.cs index 37648366e92..c9a81984df7 100644 --- a/src/Framework/Telemetry/VSTelemetryManager.cs +++ b/src/Framework/Telemetry/VSTelemetryManager.cs @@ -9,7 +9,7 @@ namespace Microsoft.Build.Framework.Telemetry { internal class VSTelemetryManager { - private const string CollectorApiKey = "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255"; + private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; private static TelemetrySession? _telemetrySession; @@ -19,12 +19,15 @@ internal class VSTelemetryManager private void Initialize(bool isStandalone) { + if (_telemetrySession != null) + { + return; + } + if (isStandalone) { _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); TelemetryService.DefaultSession.IsOptedIn = true; - - // Start session, so we can start sending events TelemetryService.DefaultSession.Start(); return; From 2c987c98403ec8e28c99a11193ccccf1b368581c Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 27 Oct 2025 15:19:40 +0100 Subject: [PATCH 03/47] updates --- .../BackEnd/BuildManager/BuildManager.cs | 16 ++- src/Framework/Telemetry/BuildTelemetry.cs | 43 +++++++ .../Telemetry/IActivityTelemetryDataHolder.cs | 4 +- src/Framework/Telemetry/TelemetryDataUtils.cs | 68 +++++++++-- ...elemetryManager.cs => TelemetryManager.cs} | 8 +- src/Framework/Telemetry/VSBuildTelemetry.cs | 54 --------- .../Telemetry/VSTelemetryActivity.cs | 64 +++++++---- .../VSTelemetryActivityExtensions.cs | 108 ------------------ src/MSBuild/XMake.cs | 4 +- 9 files changed, 163 insertions(+), 206 deletions(-) rename src/Framework/Telemetry/{VSTelemetryManager.cs => TelemetryManager.cs} (85%) delete mode 100644 src/Framework/Telemetry/VSBuildTelemetry.cs delete mode 100644 src/Framework/Telemetry/VSTelemetryActivityExtensions.cs diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 09ca96d290f..98916b545e6 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -464,7 +464,7 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) public void BeginBuild(BuildParameters parameters) { #if NETFRAMEWORK - VSTelemetryManager telemetryManager = new VSTelemetryManager(isStandalone: false); + TelemetryManager.Initialize(isStandalone: false); #endif if (_previousLowPriority != null) { @@ -1167,12 +1167,16 @@ void SerializeCaches() [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - VSTelemetryManager.StartActivity("Build")? - .WithTags("generalbuilddata", _buildTelemetry) - .WithTags("buildsinsights", _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( + var activity = TelemetryManager.StartActivity("Build"); + + activity?.SetTag("generalbuilddata", _buildTelemetry?.GetActivityProperties()); + activity?.SetTag( + "buildsinsights", + _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, - includeTargetDetails: false)) - .Dispose(); + includeTargetDetails: false)); + + activity?.Dispose(); } #endif /// diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 462413fc486..5bb686bf350 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -11,6 +11,9 @@ namespace Microsoft.Build.Framework.Telemetry /// Telemetry of build. /// internal class BuildTelemetry : TelemetryBase +#if NETFRAMEWORK + , IActivityTelemetryDataHolder +#endif { public override string EventName => "build"; @@ -105,6 +108,46 @@ internal class BuildTelemetry : TelemetryBase /// public string? BuildEngineFrameworkName { get; set; } +#if NETFRAMEWORK + /// + /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. + /// + /// + public Dictionary GetActivityProperties() + { + Dictionary telemetryItems = new(8); + + if (StartAt.HasValue && FinishedAt.HasValue) + { + telemetryItems.Add(TelemetryConstants.BuildDurationPropertyName, (FinishedAt.Value - StartAt.Value).TotalMilliseconds); + } + + if (InnerStartAt.HasValue && FinishedAt.HasValue) + { + telemetryItems.Add(TelemetryConstants.InnerBuildDurationPropertyName, (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds); + } + + AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); + AddIfNotNull(nameof(BuildSuccess), BuildSuccess); + AddIfNotNull(nameof(BuildTarget), BuildTarget); + AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion); + AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled); + AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled); + AddIfNotNull(nameof(SACEnabled), SACEnabled); + AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution); + + return telemetryItems; + + void AddIfNotNull(string key, object? value) + { + if (value != null) + { + telemetryItems.Add(key, value); + } + } + } +#endif + public override IDictionary GetProperties() { var properties = new Dictionary(); diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs index 5cfc1c828a6..68c7d672f67 100644 --- a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -3,8 +3,8 @@ #if NETFRAMEWORK +using System.Collections.Generic; using System.Diagnostics; -using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry; @@ -13,7 +13,7 @@ namespace Microsoft.Build.Framework.Telemetry; /// internal interface IActivityTelemetryDataHolder { - TelemetryComplexProperty GetActivityProperties(); + Dictionary GetActivityProperties(); } #endif diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index 572d5199d52..f341d5e4cf2 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -3,6 +3,8 @@ #if NETFRAMEWORK using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; using Microsoft.VisualStudio.Telemetry; using static Microsoft.Build.Framework.Telemetry.BuildInsights; @@ -49,9 +51,7 @@ private static List GetTargetsDetails(Dictionary valuePair in targetsDetails) { - string targetName = ShouldHashKey(valuePair.Key) ? - VSTelemetryActivityExtensions.GetHashed(valuePair.Key.Name) : - valuePair.Key.Name; + string targetName = ShouldHashKey(valuePair.Key) ? GetHashed(valuePair.Key.Name) : valuePair.Key.Name; result.Add(new TargetDetailInfo( targetName, @@ -78,9 +78,7 @@ private static List GetTasksDetails( foreach (KeyValuePair valuePair in tasksDetails) { - string taskName = valuePair.Key.IsCustom ? - VSTelemetryActivityExtensions.GetHashed(valuePair.Key.Name) : - valuePair.Key.Name; + string taskName = valuePair.Key.IsCustom ? GetHashed(valuePair.Key.Name) : valuePair.Key.Name; result.Add(new TaskDetailInfo( taskName, @@ -94,6 +92,52 @@ private static List GetTasksDetails( return result; } + /// + /// Depending on the platform, hash the value using an available mechanism. + /// + internal static string GetHashed(object value) + { + return Sha256Hasher.Hash(value.ToString() ?? ""); + } + + // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs + workaround for netstandard2.0 + private static class Sha256Hasher + { + /// + /// The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode) + /// + public static string Hash(string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); +#if NET + byte[] hash = SHA256.HashData(bytes); +#if NET9_0_OR_GREATER + return Convert.ToHexStringLower(hash); +#else + return Convert.ToHexString(hash).ToLowerInvariant(); +#endif + +#else + // Create the SHA256 object and compute the hash + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(bytes); + + // Convert the hash bytes to a lowercase hex string (manual loop approach) + var sb = new StringBuilder(hash.Length * 2); + foreach (byte b in hash) + { + sb.AppendFormat("{0:x2}", b); + } + + return sb.ToString(); + } +#endif + } + + public static string HashWithNormalizedCasing(string text) => Hash(text.ToUpperInvariant()); + } + internal record TaskDetailInfo(string Name, string TotalMilliseconds, string ExecutionsCount, string TotalMemoryBytes, string IsCustom, string IsNuget); /// @@ -252,7 +296,17 @@ internal class TasksInfo private sealed class NodeTelemetry(BuildInsights insights) : IActivityTelemetryDataHolder { - TelemetryComplexProperty IActivityTelemetryDataHolder.GetActivityProperties() => new(insights); + Dictionary IActivityTelemetryDataHolder.GetActivityProperties() + { + Dictionary properties = new(); + + properties[nameof(BuildInsights.Tasks)] = insights.Tasks; + properties[nameof(BuildInsights.Targets)] = insights.Targets; + properties[nameof(BuildInsights.TargetsSummary)] = insights.TargetsSummary; + properties[nameof(BuildInsights.TasksSummary)] = insights.TasksSummary; + + return properties; + } } } } diff --git a/src/Framework/Telemetry/VSTelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs similarity index 85% rename from src/Framework/Telemetry/VSTelemetryManager.cs rename to src/Framework/Telemetry/TelemetryManager.cs index c9a81984df7..a2724e123d2 100644 --- a/src/Framework/Telemetry/VSTelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -7,7 +7,7 @@ namespace Microsoft.Build.Framework.Telemetry { - internal class VSTelemetryManager + internal static class TelemetryManager { private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; @@ -15,9 +15,7 @@ internal class VSTelemetryManager private static bool _disposed; - public VSTelemetryManager(bool isStandalone) => Initialize(isStandalone); - - private void Initialize(bool isStandalone) + public static void Initialize(bool isStandalone) { if (_telemetrySession != null) { @@ -36,7 +34,7 @@ private void Initialize(bool isStandalone) _telemetrySession = TelemetryService.DefaultSession; } - public static VsTelemetryActivity? StartActivity(string name) + public static IActivity? StartActivity(string name) { string eventName = $"{TelemetryConstants.EventPrefix}{name}"; TelemetryScope? operation = _telemetrySession.StartOperation(eventName); diff --git a/src/Framework/Telemetry/VSBuildTelemetry.cs b/src/Framework/Telemetry/VSBuildTelemetry.cs deleted file mode 100644 index 81171333ccd..00000000000 --- a/src/Framework/Telemetry/VSBuildTelemetry.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NETFRAMEWORK - -using Microsoft.VisualStudio.Telemetry; - -namespace Microsoft.Build.Framework.Telemetry -{ - internal class VSBuildTelemetry : IActivityTelemetryDataHolder - { - private BuildTelemetry _buildTelemetry; - - public VSBuildTelemetry(BuildTelemetry buildTelemetry) => _buildTelemetry = buildTelemetry; - - /// - /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. - /// - public TelemetryComplexProperty GetActivityProperties() - { - var buildTelemetryData = new BuildTelemetryData( - _buildTelemetry.StartAt.HasValue && _buildTelemetry.FinishedAt.HasValue - ? (_buildTelemetry.FinishedAt.Value - _buildTelemetry.StartAt.Value).TotalMilliseconds.ToString() - : null, - _buildTelemetry.InnerStartAt.HasValue && _buildTelemetry.FinishedAt.HasValue - ? (_buildTelemetry.FinishedAt.Value - _buildTelemetry.InnerStartAt.Value).TotalMilliseconds.ToString() - : null, - _buildTelemetry.BuildEngineHost, - _buildTelemetry.BuildSuccess?.ToString(), - _buildTelemetry.BuildTarget, - _buildTelemetry.BuildEngineVersion?.ToString(), - _buildTelemetry.BuildCheckEnabled?.ToString(), - _buildTelemetry.MultiThreadedModeEnabled?.ToString(), - _buildTelemetry.SACEnabled?.ToString(), - _buildTelemetry.IsStandaloneExecution?.ToString()); - - return new TelemetryComplexProperty(buildTelemetryData); - } - - internal readonly record struct BuildTelemetryData( - string? BuildDuration, - string? InnerBuildDuration, - string? BuildEngineHost, - string? BuildSuccess, - string? BuildTarget, - string? BuildEngineVersion, - string? BuildCheckEnabled, - string? MultiThreadedModeEnabled, - string? SACEnabled, - string? IsStandaloneExecution); - } -} - -#endif diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index 4b95e5e930f..bb712de4845 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -3,13 +3,15 @@ #if NETFRAMEWORK +using System; using System.Collections.Generic; using System.Diagnostics; +using System.Windows.Forms; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry { - internal class VsTelemetryActivity + internal class VsTelemetryActivity : IActivity { private readonly TelemetryScope _scope; private TelemetryResult _result = TelemetryResult.Success; @@ -17,31 +19,19 @@ internal class VsTelemetryActivity private bool _disposed; - public VsTelemetryActivity(TelemetryScope scope) - { - _scope = scope; - } - - public VsTelemetryActivity? AddComplexProperty(string key, TelemetryComplexProperty complexProperty) - { - _scope.EndEvent.Properties[key] = complexProperty; + public VsTelemetryActivity(TelemetryScope scope) => _scope = scope; - return this; - } - - public VsTelemetryActivity? AddTag(string key, string? value) + public IActivity? SetTag(string key, object? value) { - _scope.EndEvent.Properties[key] = value; - return this; - } + if (value != null) + { + _scope.EndEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{key}"] = new TelemetryComplexProperty(value); + } - public VsTelemetryActivity? SetTag(string key, object? value) - { - _scope.EndEvent.Properties[key] = value; return this; } - public VsTelemetryActivity? SetStatus(ActivityStatusCode status, string? description = null) + public IActivity? SetStatus(ActivityStatusCode status, string? description = null) { // Map ActivityStatusCode to TelemetryResult _result = status switch @@ -56,14 +46,14 @@ public VsTelemetryActivity(TelemetryScope scope) return this; } - public VsTelemetryActivity? AddEvent(ActivityEvent activityEvent) + public IActivity? AddEvent(ActivityEvent activityEvent) { // VS Telemetry doesn't have a direct equivalent to ActivityEvent // We could create and immediately post a custom event if needed. var telemetryEvent = new TelemetryEvent(activityEvent.Name); foreach (KeyValuePair tag in activityEvent.Tags) { - telemetryEvent.Properties[tag.Key] = tag.Value; + telemetryEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{tag.Key}"] = tag.Value; } TelemetryService.DefaultSession.PostEvent(telemetryEvent); @@ -83,4 +73,34 @@ public void Dispose() } } } + +/// +/// Represents an activity for telemetry tracking. +/// +internal interface IActivity : IDisposable +{ + /// + /// Sets a tag on the activity. + /// + /// The tag key. + /// The tag value. + /// The activity instance for method chaining. + IActivity? SetTag(string key, object? value); + + /// + /// Sets the status of the activity + /// + /// The status. + /// An optional description. + /// The activity instance for method chaining. + IActivity? SetStatus(ActivityStatusCode status, string? description = null); + + /// + /// Adds an event to the activity. + /// + /// The event to add. + /// The activity instance for method chaining. + IActivity? AddEvent(ActivityEvent activityEvent); +} + #endif diff --git a/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs b/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs deleted file mode 100644 index b11ac8e5b76..00000000000 --- a/src/Framework/Telemetry/VSTelemetryActivityExtensions.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NETFRAMEWORK - -using System.Collections.Generic; -using System.Diagnostics; -using System.Security.Cryptography; -using System.Text; -using Microsoft.VisualStudio.Telemetry; - -namespace Microsoft.Build.Framework.Telemetry -{ - /// - /// Extension methods for . usage in VS OpenTelemetry. - /// - internal static class VSTelemetryActivityExtensions - { - - /// - /// Add tags to the activity from a . - /// - public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, string key, IActivityTelemetryDataHolder? dataHolder) - { - if (dataHolder != null) - { - activity.WithTag(key, dataHolder.GetActivityProperties()); - } - - return activity; - } - - /// - /// Add tags to the activity from a . - /// - public static VsTelemetryActivity WithTags(this VsTelemetryActivity activity, string key, BuildTelemetry? buildTelemetry) - { - if (buildTelemetry != null) - { - TelemetryComplexProperty dataHolder = new VSBuildTelemetry(buildTelemetry).GetActivityProperties(); - if (dataHolder != null) - { - activity.WithTag(key, dataHolder); - } - } - - return activity; - } - - /// - /// Add a tag to the activity from a . - /// - public static VsTelemetryActivity WithTag(this VsTelemetryActivity activity, string key, TelemetryComplexProperty telemetryComplexProperty) - { - activity.AddComplexProperty($"{TelemetryConstants.PropertyPrefix}{key}", telemetryComplexProperty); - - return activity; - } - - /// - /// Depending on the platform, hash the value using an available mechanism. - /// - internal static string GetHashed(object value) - { - return Sha256Hasher.Hash(value.ToString() ?? ""); - } - - // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs + workaround for netstandard2.0 - private static class Sha256Hasher - { - /// - /// The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode) - /// - public static string Hash(string text) - { - byte[] bytes = Encoding.UTF8.GetBytes(text); -#if NET - byte[] hash = SHA256.HashData(bytes); -#if NET9_0_OR_GREATER - return Convert.ToHexStringLower(hash); -#else - return Convert.ToHexString(hash).ToLowerInvariant(); -#endif - -#else - // Create the SHA256 object and compute the hash - using (var sha256 = SHA256.Create()) - { - byte[] hash = sha256.ComputeHash(bytes); - - // Convert the hash bytes to a lowercase hex string (manual loop approach) - var sb = new StringBuilder(hash.Length * 2); - foreach (byte b in hash) - { - sb.AppendFormat("{0:x2}", b); - } - - return sb.ToString(); - } -#endif - } - - public static string HashWithNormalizedCasing(string text) => Hash(text.ToUpperInvariant()); - } - } -} - -#endif diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 902deccc44d..8462ca568ff 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -252,7 +252,7 @@ string[] args // Initialize VSTelemetry #if NETFRAMEWORK - VSTelemetryManager tm = new VSTelemetryManager(isStandalone: true); + TelemetryManager.Initialize(isStandalone: true); #endif using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); @@ -303,7 +303,7 @@ string[] args } #if NETFRAMEWORK - VSTelemetryManager.Dispose(); + TelemetryManager.Dispose(); #endif return exitCode; } From 2eb3f6ed25059e25392de1bb641fc0fea79ade8b Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 27 Oct 2025 15:25:06 +0100 Subject: [PATCH 04/47] cleanup --- src/Build/BackEnd/BuildManager/BuildManager.cs | 15 +++++++-------- src/Framework/Telemetry/VSTelemetryActivity.cs | 1 - 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 98916b545e6..b88857a3d51 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1167,14 +1167,13 @@ void SerializeCaches() [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - var activity = TelemetryManager.StartActivity("Build"); - - activity?.SetTag("generalbuilddata", _buildTelemetry?.GetActivityProperties()); - activity?.SetTag( - "buildsinsights", - _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( - includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, - includeTargetDetails: false)); + var activity = TelemetryManager.StartActivity("Build") + ?.SetTag("generalbuilddata", _buildTelemetry?.GetActivityProperties()) + ?.SetTag( + "buildsinsights", + _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( + includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, + includeTargetDetails: false)); activity?.Dispose(); } diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index bb712de4845..880e46b509f 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Windows.Forms; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry From d28c2fa51ee05021dd5f784bd4ad5b89cb6f16b9 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 27 Oct 2025 17:12:57 +0100 Subject: [PATCH 05/47] fix init --- src/Build/BackEnd/BuildManager/BuildManager.cs | 14 +++++++------- src/Framework/Telemetry/TelemetryManager.cs | 16 ++++++++++++---- src/MSBuild/XMake.cs | 4 ++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index b88857a3d51..5e87f67ed47 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -464,7 +464,7 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) public void BeginBuild(BuildParameters parameters) { #if NETFRAMEWORK - TelemetryManager.Initialize(isStandalone: false); + TelemetryManager.Instance.Initialize(isStandalone: false); #endif if (_previousLowPriority != null) { @@ -1167,15 +1167,15 @@ void SerializeCaches() [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - var activity = TelemetryManager.StartActivity("Build") - ?.SetTag("generalbuilddata", _buildTelemetry?.GetActivityProperties()) + using var activity = TelemetryManager.Instance.StartActivity("Build") + ?.SetTag("CheckSimpleParam", "test") + ?.SetTag("GeneralBuildData", _buildTelemetry?.GetActivityProperties()) ?.SetTag( - "buildsinsights", + "BuildInsights", _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, - includeTargetDetails: false)); - - activity?.Dispose(); + includeTargetDetails: false)) + ?.SetStatus(ActivityStatusCode.Ok); } #endif /// diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index a2724e123d2..e962108d3b6 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -7,7 +7,7 @@ namespace Microsoft.Build.Framework.Telemetry { - internal static class TelemetryManager + internal class TelemetryManager { private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; @@ -15,7 +15,15 @@ internal static class TelemetryManager private static bool _disposed; - public static void Initialize(bool isStandalone) + private static readonly TelemetryManager _instance = new TelemetryManager(); + + private TelemetryManager() + { + } + + public static TelemetryManager Instance => _instance; + + public void Initialize(bool isStandalone) { if (_telemetrySession != null) { @@ -34,7 +42,7 @@ public static void Initialize(bool isStandalone) _telemetrySession = TelemetryService.DefaultSession; } - public static IActivity? StartActivity(string name) + public IActivity? StartActivity(string name) { string eventName = $"{TelemetryConstants.EventPrefix}{name}"; TelemetryScope? operation = _telemetrySession.StartOperation(eventName); @@ -42,7 +50,7 @@ public static void Initialize(bool isStandalone) return operation != null ? new VsTelemetryActivity(operation) : null; } - public static void Dispose() + public void Dispose() { if (_disposed) { diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 8462ca568ff..a909cd4fafd 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -252,7 +252,7 @@ string[] args // Initialize VSTelemetry #if NETFRAMEWORK - TelemetryManager.Initialize(isStandalone: true); + TelemetryManager.Instance.Initialize(isStandalone: true); #endif using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); @@ -303,7 +303,7 @@ string[] args } #if NETFRAMEWORK - TelemetryManager.Dispose(); + TelemetryManager.Instance.Dispose(); #endif return exitCode; } From 7f655558e204c8a59eae8cdee8627ba3966d5058 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 27 Oct 2025 21:04:58 +0100 Subject: [PATCH 06/47] cleanup --- src/Build/BackEnd/BuildManager/BuildManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 5e87f67ed47..c59f0a54a82 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1168,10 +1168,9 @@ void SerializeCaches() private void EndBuildTelemetry() { using var activity = TelemetryManager.Instance.StartActivity("Build") - ?.SetTag("CheckSimpleParam", "test") ?.SetTag("GeneralBuildData", _buildTelemetry?.GetActivityProperties()) ?.SetTag( - "BuildInsights", + "BuildsInsights", _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, includeTargetDetails: false)) From 6a3a27a06bd5ee09c3378dec9119654f108d650b Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 29 Oct 2025 13:01:50 +0100 Subject: [PATCH 07/47] final cleanup --- .../Telemetry/Telemetry_Tests.cs | 325 ------------------ .../BackEnd/BuildManager/BuildManager.cs | 20 +- src/Framework/Telemetry/BuildTelemetry.cs | 1 - .../Telemetry/OpenTelemetryManager.cs | 276 --------------- src/Framework/Telemetry/TelemetryDataUtils.cs | 17 +- src/Framework/Telemetry/TelemetryManager.cs | 17 +- src/Framework/Telemetry/VSTelemetry.cs | 18 - .../Telemetry/VSTelemetryActivity.cs | 39 +-- 8 files changed, 46 insertions(+), 667 deletions(-) delete mode 100644 src/Build.UnitTests/Telemetry/Telemetry_Tests.cs delete mode 100644 src/Framework/Telemetry/OpenTelemetryManager.cs delete mode 100644 src/Framework/Telemetry/VSTelemetry.cs diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs deleted file mode 100644 index f03ee221094..00000000000 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ /dev/null @@ -1,325 +0,0 @@ -// 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.Diagnostics; -using System.Linq; -using System.Text.Json; -using Microsoft.Build.Execution; -using Microsoft.Build.Framework; -using Microsoft.Build.Framework.Telemetry; -using Microsoft.Build.TelemetryInfra; -using Microsoft.Build.UnitTests; -using Shouldly; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Build.Engine.UnitTests -{ - [Collection("OpenTelemetryManagerTests")] - public class Telemetry_Tests - { - private readonly ITestOutputHelper _output; - - public Telemetry_Tests(ITestOutputHelper output) - { - _output = output; - } - - private sealed class ProjectFinishedCapturingLogger : ILogger - { - private readonly List _projectFinishedEventArgs = []; - public LoggerVerbosity Verbosity { get; set; } - public string? Parameters { get; set; } - - public IReadOnlyList ProjectFinishedEventArgsReceived => - _projectFinishedEventArgs; - - public void Initialize(IEventSource eventSource) - { - eventSource.ProjectFinished += EventSource_ProjectFinished; - } - - private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) - { - _projectFinishedEventArgs.Add(e); - } - - public void Shutdown() - { } - } - - [Fact] - public void WorkerNodeTelemetryCollection_BasicTarget() - { - WorkerNodeTelemetryData? workerNodeTelemetryData = null; - InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; - - var testProject = """ - - - - - - - - - - """; - - MockLogger logger = new MockLogger(_output); - Helpers.BuildProjectContentUsingBuildManager(testProject, logger, - new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); - - workerNodeTelemetryData!.ShouldNotBeNull(); - var buildTargetKey = new TaskOrTargetTelemetryKey("Build", true, false); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(buildTargetKey); - workerNodeTelemetryData.TargetsExecutionData[buildTargetKey].ShouldBeTrue(); - workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(1); - - workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(2); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - - workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsCustom && !k.IsNuget); - workerNodeTelemetryData.TasksExecutionData.Values - .Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(2); - } - - [Fact] - public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() - { - WorkerNodeTelemetryData? workerNodeTelemetryData = null; - InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; - - var testProject = """ - - - - - - Log.LogMessage(MessageImportance.Low, "Hello, world!"); - - - - - - - - - Log.LogMessage(MessageImportance.High, "Hello, world!"); - - - - - - - - - - - - - - - - - - - - - - - """; - MockLogger logger = new MockLogger(_output); - Helpers.BuildProjectContentUsingBuildManager(testProject, logger, - new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); - - workerNodeTelemetryData!.ShouldNotBeNull(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("Build", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("Build", true, false)].ShouldBeTrue(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("BeforeBuild", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("BeforeBuild", true, false)].ShouldBeTrue(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("NotExecuted", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("NotExecuted", true, false)].ShouldBeFalse(); - workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(3); - - workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(3); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - - ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].ExecutionsCount).ShouldBe(2); - workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - - ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].ExecutionsCount).ShouldBe(0); - workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].CumulativeExecutionTime.ShouldBe(TimeSpan.Zero); - - workerNodeTelemetryData.TasksExecutionData.Values - .Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(3); - - workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); - } - -#if NET - // test in .net core with opentelemetry opted in to avoid sending it but enable listening to it - [Fact] - public void NodeTelemetryE2E() - { - using TestEnvironment env = TestEnvironment.Create(); - env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTIN", "1"); - env.SetEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE", "1.0"); - env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); - env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); - - // Reset the OpenTelemetryManager state to ensure clean test - ResetManagerState(); - - // track activities through an ActivityListener - var capturedActivities = new List(); - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.DefaultActivitySourceNamespace), - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, - ActivityStarted = capturedActivities.Add, - ActivityStopped = _ => { } - }; - ActivitySource.AddActivityListener(listener); - - var testProject = @" - - - - - - - - - - - - "; - - using var testEnv = TestEnvironment.Create(_output); - var projectFile = testEnv.CreateFile("test.proj", testProject).Path; - - // Set up loggers - var projectFinishedLogger = new ProjectFinishedCapturingLogger(); - var buildParameters = new BuildParameters - { - Loggers = new ILogger[] { projectFinishedLogger }, - IsTelemetryEnabled = true - }; - - // Act - using (var buildManager = new BuildManager()) - { - // Phase 1: Begin Build - This initializes telemetry infrastructure - buildManager.BeginBuild(buildParameters); - - // Phase 2: Execute build requests - var buildRequestData1 = new BuildRequestData( - projectFile, - new Dictionary(), - null, - new[] { "Build" }, - null); - - buildManager.BuildRequest(buildRequestData1); - - var buildRequestData2 = new BuildRequestData( - projectFile, - new Dictionary(), - null, - new[] { "Clean" }, - null); - - buildManager.BuildRequest(buildRequestData2); - - // Phase 3: End Build - This puts telemetry to an system.diagnostics activity - buildManager.EndBuild(); - - // Verify build activity were captured by the listener and contain task and target info - capturedActivities.ShouldNotBeEmpty(); - var activity = capturedActivities.FindLast(a => a.DisplayName == "VS/MSBuild/Build").ShouldNotBeNull(); - var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value); - tags.ShouldNotBeNull(); - - tags.ShouldContainKey("VS.MSBuild.BuildTarget"); - tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); - - // Verify task data - tags.ShouldContainKey("VS.MSBuild.Tasks"); - var tasksJson = tags["VS.MSBuild.Tasks"]; - tasksJson.ShouldNotBeNullOrEmpty(); - tasksJson.ShouldContain("Microsoft.Build.Tasks.Message"); - tasksJson.ShouldContain("Microsoft.Build.Tasks.CreateItem"); - - // Parse tasks data for detailed assertions - var tasksData = JsonSerializer.Deserialize(tasksJson); - - // Verify Message task execution metrics - updated for object structure - tasksData.TryGetProperty("Microsoft.Build.Tasks.Message", out var messageTask).ShouldBe(true); - messageTask.GetProperty("ExecutionsCount").GetInt32().ShouldBe(3); - messageTask.GetProperty("TotalMilliseconds").GetDouble().ShouldBeGreaterThan(0); - messageTask.GetProperty("TotalMemoryBytes").GetInt64().ShouldBeGreaterThanOrEqualTo(0); - messageTask.GetProperty(nameof(TaskOrTargetTelemetryKey.IsCustom)).GetBoolean().ShouldBe(false); - messageTask.GetProperty(nameof(TaskOrTargetTelemetryKey.IsCustom)).GetBoolean().ShouldBe(false); - - // Verify CreateItem task execution metrics - updated for object structure - tasksData.TryGetProperty("Microsoft.Build.Tasks.CreateItem", out var createItemTask).ShouldBe(true); - createItemTask.GetProperty("ExecutionsCount").GetInt32().ShouldBe(1); - createItemTask.GetProperty("TotalMilliseconds").GetDouble().ShouldBeGreaterThan(0); - createItemTask.GetProperty("TotalMemoryBytes").GetInt64().ShouldBeGreaterThanOrEqualTo(0); - - // Verify Targets summary information - tags.ShouldContainKey("VS.MSBuild.TargetsSummary"); - var targetsSummaryJson = tags["VS.MSBuild.TargetsSummary"]; - targetsSummaryJson.ShouldNotBeNullOrEmpty(); - var targetsSummary = JsonSerializer.Deserialize(targetsSummaryJson); - - // Verify loaded and executed targets counts - match structure in TargetsSummaryConverter.Write - targetsSummary.GetProperty("Loaded").GetProperty("Total").GetInt32().ShouldBe(2); - targetsSummary.GetProperty("Executed").GetProperty("Total").GetInt32().ShouldBe(2); - - // Verify Tasks summary information - tags.ShouldContainKey("VS.MSBuild.TasksSummary"); - var tasksSummaryJson = tags["VS.MSBuild.TasksSummary"]; - tasksSummaryJson.ShouldNotBeNullOrEmpty(); - var tasksSummary = JsonSerializer.Deserialize(tasksSummaryJson); - - // Verify task execution summary metrics based on TasksSummaryConverter.Write structure - tasksSummary.GetProperty("Microsoft").GetProperty("Total").GetProperty("ExecutionsCount").GetInt32().ShouldBe(4); - tasksSummary.GetProperty("Microsoft").GetProperty("Total").GetProperty("TotalMilliseconds").GetDouble().ShouldBeGreaterThan(0); - // Allowing 0 for TotalMemoryBytes as it is possible for tasks to allocate no memory in certain scenarios. - tasksSummary.GetProperty("Microsoft").GetProperty("Total").GetProperty("TotalMemoryBytes").GetInt64().ShouldBeGreaterThanOrEqualTo(0); - } - // Reset the OpenTelemetryManager state to ensure it doesn't affect other tests - ResetManagerState(); - } - - private void ResetManagerState() - { - var instance = OpenTelemetryManager.Instance; - typeof(OpenTelemetryManager) - .GetField("_telemetryState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(instance, OpenTelemetryManager.TelemetryState.Uninitialized); - - typeof(OpenTelemetryManager) - .GetProperty("DefaultActivitySource", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(instance, null); - } -#endif - } -} diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index c59f0a54a82..e29f2c34925 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -284,7 +284,7 @@ public class BuildManager : INodePacketHandler, IBuildComponentHost, IDisposable /// /// Creates a new unnamed build manager. /// Normally there is only one build manager in a process, and it is the default build manager. - /// Access it with + /// Access it with . /// public BuildManager() : this("Unnamed") @@ -294,7 +294,7 @@ public BuildManager() /// /// Creates a new build manager with an arbitrary distinct name. /// Normally there is only one build manager in a process, and it is the default build manager. - /// Access it with + /// Access it with . /// public BuildManager(string hostName) { @@ -340,12 +340,12 @@ private enum BuildManagerState /// /// This is the state the BuildManager is in after has been called but before has been called. - /// , , , , and may be called in this state. + /// , , , , and may be called in this state. /// Building, /// - /// This is the state the BuildManager is in after has been called but before all existing submissions have completed. + /// This is the state the BuildManager is in after has been called but before all existing submissions have completed. /// WaitingForBuildToComplete } @@ -1167,16 +1167,14 @@ void SerializeCaches() [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - using var activity = TelemetryManager.Instance.StartActivity("Build") - ?.SetTag("GeneralBuildData", _buildTelemetry?.GetActivityProperties()) - ?.SetTag( - "BuildsInsights", - _telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( + using IActivity? activity = TelemetryManager.Instance.StartActivity("Build") + ?.SetTags(_buildTelemetry) + ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, - includeTargetDetails: false)) - ?.SetStatus(ActivityStatusCode.Ok); + includeTargetDetails: false)); } #endif + /// /// Convenience method. Submits a lone build request and blocks until results are available. /// diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 5bb686bf350..33272cb643b 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -112,7 +112,6 @@ internal class BuildTelemetry : TelemetryBase /// /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. /// - /// public Dictionary GetActivityProperties() { Dictionary telemetryItems = new(8); diff --git a/src/Framework/Telemetry/OpenTelemetryManager.cs b/src/Framework/Telemetry/OpenTelemetryManager.cs deleted file mode 100644 index b7e74c760ad..00000000000 --- a/src/Framework/Telemetry/OpenTelemetryManager.cs +++ /dev/null @@ -1,276 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK -using Microsoft.VisualStudio.OpenTelemetry.ClientExtensions; -using Microsoft.VisualStudio.OpenTelemetry.ClientExtensions.Exporters; -using Microsoft.VisualStudio.OpenTelemetry.Collector.Interfaces; -using Microsoft.VisualStudio.OpenTelemetry.Collector.Settings; -using OpenTelemetry; -using OpenTelemetry.Trace; -#endif -using System; -using System.Runtime.CompilerServices; -using System.Threading; - -namespace Microsoft.Build.Framework.Telemetry -{ - - /// - /// Singleton class for configuring and managing the telemetry infrastructure with System.Diagnostics.Activity, - /// OpenTelemetry SDK, and VS OpenTelemetry Collector. - /// - internal class OpenTelemetryManager - { - // Lazy provides thread-safe lazy initialization. - private static readonly Lazy s_instance = - new Lazy(() => new OpenTelemetryManager(), LazyThreadSafetyMode.ExecutionAndPublication); - - /// - /// Globally accessible instance of . - /// - public static OpenTelemetryManager Instance => s_instance.Value; - - private TelemetryState _telemetryState = TelemetryState.Uninitialized; - private readonly LockType _initializeLock = new LockType(); - private double _sampleRate = TelemetryConstants.DefaultSampleRate; - -#if NETFRAMEWORK - private TracerProvider? _tracerProvider; - private IOpenTelemetryCollector? _collector; -#endif - - public string? LoadFailureExceptionMessage { get; set; } - - private OpenTelemetryManager() - { - } - - /// - /// Initializes the telemetry infrastructure. Multiple invocations are no-op, thread-safe. - /// - /// Differentiates between executing as MSBuild.exe or from VS/API. - public void Initialize(bool isStandalone) - { - // for lock free early exit - if (_telemetryState != TelemetryState.Uninitialized) - { - return; - } - - lock (_initializeLock) - { - // for correctness - if (_telemetryState != TelemetryState.Uninitialized) - { - return; - } - - if (IsOptOut()) - { - _telemetryState = TelemetryState.OptOut; - return; - } - - // TODO: temporary until we have green light to enable telemetry perf-wise - if (!IsOptIn()) - { - _telemetryState = TelemetryState.Unsampled; - return; - } - - if (!IsSampled()) - { - _telemetryState = TelemetryState.Unsampled; - return; - } - - InitializeActivitySources(); - } -#if NETFRAMEWORK - try - { - InitializeTracerProvider(); - - // TODO: Enable commented logic when Collector is present in VS - // if (isStandalone) - InitializeCollector(); - - // } - } - catch (Exception ex) when (ex is System.IO.FileNotFoundException or System.IO.FileLoadException) - { - // catch exceptions from loading the OTel SDK or Collector to maintain usability of Microsoft.Build.Framework package in our and downstream tests in VS. - _telemetryState = TelemetryState.Unsampled; - LoadFailureExceptionMessage = ex.ToString(); - return; - } -#endif - } - - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void InitializeActivitySources() - { - _telemetryState = TelemetryState.TracerInitialized; - } - -#if NETFRAMEWORK - /// - /// Initializes the OpenTelemetry SDK TracerProvider with VS default exporter settings. - /// - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void InitializeTracerProvider() - { - var exporterSettings = OpenTelemetryExporterSettingsBuilder - .CreateVSDefault(TelemetryConstants.VSMajorVersion) - .Build(); - - TracerProviderBuilder tracerProviderBuilder = Sdk - .CreateTracerProviderBuilder() - // this adds listeners to ActivitySources with the prefix "Microsoft.VisualStudio.OpenTelemetry." - .AddVisualStudioDefaultTraceExporter(exporterSettings); - - _tracerProvider = tracerProviderBuilder.Build(); - _telemetryState = TelemetryState.ExporterInitialized; - } - - /// - /// Initializes the VS OpenTelemetry Collector with VS default settings. - /// - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void InitializeCollector() - { - IOpenTelemetryCollectorSettings collectorSettings = OpenTelemetryCollectorSettingsBuilder - .CreateVSDefault(TelemetryConstants.VSMajorVersion) - .Build(); - - _collector = OpenTelemetryCollectorProvider.CreateCollector(collectorSettings); - _collector.StartAsync().GetAwaiter().GetResult(); - - _telemetryState = TelemetryState.CollectorInitialized; - } -#endif - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void ForceFlushInner() - { -#if NETFRAMEWORK - _tracerProvider?.ForceFlush(); -#endif - } - - /// - /// Flush the telemetry in TracerProvider/Exporter. - /// - public void ForceFlush() - { - if (ShouldBeCleanedUp()) - { - ForceFlushInner(); - } - } - - // to avoid assembly loading OpenTelemetry in tests - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads - private void ShutdownInner() - { -#if NETFRAMEWORK - _tracerProvider?.Shutdown(); - // Dispose stops the collector, with a default drain timeout of 10s - _collector?.Dispose(); -#endif - } - - /// - /// Shuts down the telemetry infrastructure. - /// - public void Shutdown() - { - lock (_initializeLock) - { - if (ShouldBeCleanedUp()) - { - ShutdownInner(); - } - - _telemetryState = TelemetryState.Disposed; - } - } - - /// - /// Determines if the user has explicitly opted out of telemetry. - /// - private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut || !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14); - - /// - /// TODO: Temporary until perf of loading OTel is agreed to in VS. - /// - private bool IsOptIn() => !IsOptOut() && (Traits.Instance.TelemetryOptIn || Traits.Instance.TelemetrySampleRateOverride.HasValue); - - /// - /// Determines if telemetry should be initialized based on sampling and environment variable overrides. - /// - private bool IsSampled() - { - double? overrideRate = Traits.Instance.TelemetrySampleRateOverride; - if (overrideRate.HasValue) - { - _sampleRate = overrideRate.Value; - } - else - { -#if !NETFRAMEWORK - // In core, OTel infrastructure is not initialized by default. - return false; -#endif - } - - // Simple random sampling, this method is called once, no need to save the Random instance. - Random random = new(); - return random.NextDouble() < _sampleRate; - } - - private bool ShouldBeCleanedUp() => _telemetryState == TelemetryState.CollectorInitialized || _telemetryState == TelemetryState.ExporterInitialized; - - internal bool IsActive() => _telemetryState == TelemetryState.TracerInitialized || _telemetryState == TelemetryState.CollectorInitialized || _telemetryState == TelemetryState.ExporterInitialized; - - /// - /// State of the telemetry infrastructure. - /// - internal enum TelemetryState - { - /// - /// Initial state. - /// - Uninitialized, - - /// - /// Opt out of telemetry. - /// - OptOut, - - /// - /// Run not sampled for telemetry. - /// - Unsampled, - - /// - /// For core hook, ActivitySource is created. - /// - TracerInitialized, - - /// - /// For VS scenario with a collector. ActivitySource, OTel TracerProvider are created. - /// - ExporterInitialized, - - /// - /// For standalone, ActivitySource, OTel TracerProvider, VS OpenTelemetry Collector are created. - /// - CollectorInitialized, - - /// - /// End state. - /// - Disposed - } - } -} diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index f341d5e4cf2..1b53689c6c7 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Security.Cryptography; using System.Text; -using Microsoft.VisualStudio.Telemetry; using static Microsoft.Build.Framework.Telemetry.BuildInsights; namespace Microsoft.Build.Framework.Telemetry @@ -36,8 +35,7 @@ internal static class TelemetryDataUtils GetTasksDetails(telemetryData.TasksExecutionData), GetTargetsDetails(telemetryData.TargetsExecutionData), GetTargetsSummary(targetsSummary), - GetTasksSummary(tasksSummary) - ); + GetTasksSummary(tasksSummary)); return new NodeTelemetry(buildInsights); } @@ -298,12 +296,13 @@ private sealed class NodeTelemetry(BuildInsights insights) : IActivityTelemetryD { Dictionary IActivityTelemetryDataHolder.GetActivityProperties() { - Dictionary properties = new(); - - properties[nameof(BuildInsights.Tasks)] = insights.Tasks; - properties[nameof(BuildInsights.Targets)] = insights.Targets; - properties[nameof(BuildInsights.TargetsSummary)] = insights.TargetsSummary; - properties[nameof(BuildInsights.TasksSummary)] = insights.TasksSummary; + Dictionary properties = new() + { + [nameof(BuildInsights.Tasks)] = insights.Tasks, + [nameof(BuildInsights.Targets)] = insights.Targets, + [nameof(BuildInsights.TargetsSummary)] = insights.TargetsSummary, + [nameof(BuildInsights.TasksSummary)] = insights.TasksSummary, + }; return properties; } diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index e962108d3b6..0639f50f81c 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -13,19 +13,17 @@ internal class TelemetryManager private static TelemetrySession? _telemetrySession; - private static bool _disposed; - - private static readonly TelemetryManager _instance = new TelemetryManager(); + private static bool s_disposed; private TelemetryManager() { } - public static TelemetryManager Instance => _instance; + public static TelemetryManager Instance { get; } = new TelemetryManager(); public void Initialize(bool isStandalone) { - if (_telemetrySession != null) + if (IsOptOut() || _telemetrySession != null) { return; } @@ -52,15 +50,20 @@ public void Initialize(bool isStandalone) public void Dispose() { - if (_disposed) + if (s_disposed) { return; } _telemetrySession?.Dispose(); - _disposed = true; + s_disposed = true; } + + /// + /// Determines if the user has explicitly opted out of telemetry. + /// + private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut || !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14); } } diff --git a/src/Framework/Telemetry/VSTelemetry.cs b/src/Framework/Telemetry/VSTelemetry.cs deleted file mode 100644 index fa05f8490a2..00000000000 --- a/src/Framework/Telemetry/VSTelemetry.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NETFRAMEWORK -using Microsoft.VisualStudio.Telemetry; - -namespace Microsoft.Build.Framework.Telemetry -{ - /// - /// Visual Studio telemetry session implementation using Microsoft.VisualStudio.Telemetry. - /// - internal sealed class VsTelemetrySession - { - - } -} - -#endif diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index 880e46b509f..bc3019b3e90 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Microsoft.Build.Framework.Telemetry; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry @@ -20,31 +21,30 @@ internal class VsTelemetryActivity : IActivity public VsTelemetryActivity(TelemetryScope scope) => _scope = scope; - public IActivity? SetTag(string key, object? value) + public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) { - if (value != null) + Dictionary? tags = dataHolder?.GetActivityProperties(); + + if (tags != null) { - _scope.EndEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{key}"] = new TelemetryComplexProperty(value); + foreach (KeyValuePair tag in tags) + { + _ = SetTag(tag.Key, tag.Value); + } } return this; } - public IActivity? SetStatus(ActivityStatusCode status, string? description = null) + public IActivity? SetTag(string key, object? value) { - // Map ActivityStatusCode to TelemetryResult - _result = status switch + if (value != null) { - ActivityStatusCode.Ok => TelemetryResult.Success, - ActivityStatusCode.Error => TelemetryResult.Failure, - _ => TelemetryResult.None, - }; - - _resultSummary = description; + _scope.EndEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{key}"] = new TelemetryComplexProperty(value); + } return this; } - public IActivity? AddEvent(ActivityEvent activityEvent) { // VS Telemetry doesn't have a direct equivalent to ActivityEvent @@ -81,18 +81,17 @@ internal interface IActivity : IDisposable /// /// Sets a tag on the activity. /// - /// The tag key. - /// The tag value. + /// Telemetry data holder. /// The activity instance for method chaining. - IActivity? SetTag(string key, object? value); + IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder); /// - /// Sets the status of the activity + /// Sets a tag on the activity. /// - /// The status. - /// An optional description. + /// The tag key. + /// The tag value. /// The activity instance for method chaining. - IActivity? SetStatus(ActivityStatusCode status, string? description = null); + IActivity? SetTag(string key, object? value); /// /// Adds an event to the activity. From 210c6c3214129fd344b6901e7119403974fa319f Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 29 Oct 2025 15:21:46 +0100 Subject: [PATCH 08/47] remove OpenTelemetry-related stuff --- NuGet.config | 4 +- THIRDPARTYNOTICES.txt | 6 +-- .../Microsoft.Build.Engine.UnitTests.csproj | 5 +- .../BackEnd/BuildManager/BuildManager.cs | 2 +- .../Microsoft.Build.Framework.csproj | 2 - src/Framework/Telemetry/TelemetryConstants.cs | 24 ++------- src/Framework/Telemetry/TelemetryDataUtils.cs | 49 +++++++++---------- src/Framework/Telemetry/TelemetryItem.cs | 2 +- src/Package/MSBuild.VSSetup/files.swr | 6 +-- src/Package/Microsoft.Build.UnGAC/Program.cs | 3 +- 10 files changed, 37 insertions(+), 66 deletions(-) diff --git a/NuGet.config b/NuGet.config index c181d033061..1fa9b0c95ae 100644 --- a/NuGet.config +++ b/NuGet.config @@ -18,10 +18,10 @@ - + - + diff --git a/THIRDPARTYNOTICES.txt b/THIRDPARTYNOTICES.txt index 49e551d4279..d3e621fb87b 100644 --- a/THIRDPARTYNOTICES.txt +++ b/THIRDPARTYNOTICES.txt @@ -66,10 +66,10 @@ language governing permissions and limitations under the License. ------------------------------- -Notice for Microsoft.VisualStudio.OpenTelemetry.* +Notice for Microsoft.VisualStudio.Telemetry ------------------------------- -MSBuild.exe is distributed with Microsoft.VisualStudio.OpenTelemetry.* binaries. +MSBuild.exe is distributed with Microsoft.VisualStudio.Telemetry binary. -Project: Microsoft.VisualStudio.OpenTelemetry +Project: Microsoft.VisualStudio.Telemetry Copyright: (c) Microsoft Corporation License: https://visualstudio.microsoft.com/license-terms/mt736442/ \ No newline at end of file diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 7125da363e5..09b79da280a 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -27,10 +27,7 @@ all - - - - + diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index e29f2c34925..7cedb1b93c4 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1167,7 +1167,7 @@ void SerializeCaches() [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - using IActivity? activity = TelemetryManager.Instance.StartActivity("Build") + using IActivity? activity = TelemetryManager.Instance.StartActivity(TelemetryConstants.Build) ?.SetTags(_buildTelemetry) ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 3fb5bbc49cb..0e083fe6550 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -25,8 +25,6 @@ - - diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index 461f122ba63..08b383e8b9e 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -7,16 +7,6 @@ namespace Microsoft.Build.Framework.Telemetry; /// internal static class TelemetryConstants { - /// - /// "Microsoft.VisualStudio.OpenTelemetry.*" namespace is required by VS exporting/collection. - /// - public const string ActivitySourceNamespacePrefix = "Microsoft.VisualStudio.OpenTelemetry.MSBuild."; - - /// - /// Namespace of the default ActivitySource handling e.g. End of build telemetry. - /// - public const string DefaultActivitySourceNamespace = $"{ActivitySourceNamespacePrefix}Default"; - /// /// Prefix required by VS exporting/collection. /// @@ -48,14 +38,8 @@ internal static class TelemetryConstants /// public const string InnerBuildDurationPropertyName = "InnerBuildDurationInMilliseconds"; - public const string BuildEvent = nameof(BuildEvent); -} - -internal static class NodeTelemetryTags -{ - // These properties can't use nameof since they're not tied to a specific class property - public const string Tasks = "Tasks"; - public const string Targets = "Targets"; - public const string TargetsSummary = "TargetsSummary"; - public const string TasksSummary = "TasksSummary"; + /// + /// Name of the property for build activity. + /// + public const string Build = "Build"; } diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index 1b53689c6c7..e36496bc914 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -53,10 +53,10 @@ private static List GetTargetsDetails(Dictionary GetTargetsDetails(Dictionary key.IsCustom || key.IsMetaProj; } - internal record TargetDetailInfo(string Name, string WasExecuted, string IsCustom, string IsNuget, string IsMetaProj); + internal record TargetDetailInfo(string Name, bool WasExecuted, bool IsCustom, bool IsNuget, bool IsMetaProj); /// /// Converts tasks details to a list of custom objects for telemetry. @@ -80,11 +80,11 @@ private static List GetTasksDetails( result.Add(new TaskDetailInfo( taskName, - valuePair.Value.CumulativeExecutionTime.TotalMilliseconds.ToString(), - valuePair.Value.ExecutionsCount.ToString(), - valuePair.Value.TotalMemoryBytes.ToString(), - valuePair.Key.IsCustom.ToString(), - valuePair.Key.IsNuget.ToString())); + valuePair.Value.CumulativeExecutionTime.TotalMilliseconds, + valuePair.Value.ExecutionsCount, + valuePair.Value.TotalMemoryBytes, + valuePair.Key.IsCustom, + valuePair.Key.IsNuget)); } return result; @@ -93,10 +93,7 @@ private static List GetTasksDetails( /// /// Depending on the platform, hash the value using an available mechanism. /// - internal static string GetHashed(object value) - { - return Sha256Hasher.Hash(value.ToString() ?? ""); - } + internal static string GetHashed(object value) => Sha256Hasher.Hash(value?.ToString() ?? ""); // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs + workaround for netstandard2.0 private static class Sha256Hasher @@ -136,7 +133,7 @@ public static string Hash(string text) public static string HashWithNormalizedCasing(string text) => Hash(text.ToUpperInvariant()); } - internal record TaskDetailInfo(string Name, string TotalMilliseconds, string ExecutionsCount, string TotalMemoryBytes, string IsCustom, string IsNuget); + internal record TaskDetailInfo(string Name, double TotalMilliseconds, int ExecutionsCount, long TotalMemoryBytes, bool IsCustom, bool IsNuget); /// /// Converts targets summary to a custom object for telemetry. @@ -152,22 +149,22 @@ static TargetStatsInfo CreateTargetStats( TargetsSummaryConverter.TargetInfo customInfo) { var microsoft = builtinInfo.Total > 0 - ? new TargetCategoryInfo(builtinInfo.Total.ToString(), builtinInfo.FromNuget.ToString(), builtinInfo.FromMetaproj.ToString()) + ? new TargetCategoryInfo(builtinInfo.Total, builtinInfo.FromNuget, builtinInfo.FromMetaproj) : null; var custom = customInfo.Total > 0 - ? new TargetCategoryInfo(customInfo.Total.ToString(), customInfo.FromNuget.ToString(), customInfo.FromMetaproj.ToString()) + ? new TargetCategoryInfo(customInfo.Total, customInfo.FromNuget, customInfo.FromMetaproj) : null; - return new TargetStatsInfo((builtinInfo.Total + customInfo.Total).ToString(), microsoft, custom); + return new TargetStatsInfo(builtinInfo.Total + customInfo.Total, microsoft, custom); } } internal record TargetsSummaryInfo(TargetStatsInfo Loaded, TargetStatsInfo Executed); - internal record TargetStatsInfo(string Total, TargetCategoryInfo? Microsoft, TargetCategoryInfo? Custom); + internal record TargetStatsInfo(int Total, TargetCategoryInfo? Microsoft, TargetCategoryInfo? Custom); - internal record TargetCategoryInfo(string Total, string FromNuget, string FromMetaproj); + internal record TargetCategoryInfo(int Total, int FromNuget, int FromMetaproj); /// /// Converts tasks summary to a custom object for telemetry. @@ -183,16 +180,16 @@ private static TasksSummaryInfo GetTasksSummary(TasksSummaryConverter summary) { var totalStats = total.ExecutionsCount > 0 ? new TaskStatsInfo( - total.ExecutionsCount.ToString(), - total.CumulativeExecutionTime.TotalMilliseconds.ToString(), - total.TotalMemoryBytes.ToString()) + total.ExecutionsCount, + total.CumulativeExecutionTime.TotalMilliseconds, + total.TotalMemoryBytes) : null; var nugetStats = fromNuget.ExecutionsCount > 0 ? new TaskStatsInfo( - fromNuget.ExecutionsCount.ToString(), - fromNuget.CumulativeExecutionTime.TotalMilliseconds.ToString(), - fromNuget.TotalMemoryBytes.ToString()) + fromNuget.ExecutionsCount, + fromNuget.CumulativeExecutionTime.TotalMilliseconds, + fromNuget.TotalMemoryBytes) : null; return (totalStats != null || nugetStats != null) diff --git a/src/Framework/Telemetry/TelemetryItem.cs b/src/Framework/Telemetry/TelemetryItem.cs index 08d257bd78b..4b8fa262f98 100644 --- a/src/Framework/Telemetry/TelemetryItem.cs +++ b/src/Framework/Telemetry/TelemetryItem.cs @@ -37,7 +37,7 @@ internal record TasksSummaryInfo(TaskCategoryStats? Microsoft, TaskCategoryStats internal record TaskCategoryStats(TaskStatsInfo? Total, TaskStatsInfo? FromNuget); - internal record TaskStatsInfo(string ExecutionsCount, string TotalMilliseconds, string TotalMemoryBytes); + internal record TaskStatsInfo(int ExecutionsCount, double TotalMilliseconds, long TotalMemoryBytes); } #endif diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 3b75caa7fb1..7f7912e6c34 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -89,12 +89,8 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.ServiceModel.targets file source=$(X86BinPath)Microsoft.WinFx.targets file source=$(X86BinPath)Microsoft.WorkflowBuildExtensions.targets - file source=$(X86BinPath)Microsoft.VisualStudio.OpenTelemetry.ClientExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.VisualStudio.OpenTelemetry.Collector.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 + file source=$(X86BinPath)Microsoft.VisualStudio.Telemetry.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.VisualStudio.Utilities.Internal.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)OpenTelemetry.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)OpenTelemetry.Api.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)OpenTelemetry.Api.ProviderBuilderExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.Extensions.Configuration.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.Extensions.Configuration.Binder.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.Extensions.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 diff --git a/src/Package/Microsoft.Build.UnGAC/Program.cs b/src/Package/Microsoft.Build.UnGAC/Program.cs index a13f518146d..778d099d35d 100644 --- a/src/Package/Microsoft.Build.UnGAC/Program.cs +++ b/src/Package/Microsoft.Build.UnGAC/Program.cs @@ -32,8 +32,7 @@ private static void Main(string[] args) "BuildXL.Utilities.Core, Version=1.0.0.0", "BuildXL.Native, Version=1.0.0.0", "Microsoft.VisualStudio.SolutionPersistence, Version=1.0.0.0", - "Microsoft.VisualStudio.OpenTelemetry.ClientExtensions, Version=0.1.0.0", - "Microsoft.VisualStudio.OpenTelemetry.Collector, Version=0.1.0.0", + "Microsoft.VisualStudio.Telemetry, Version=16.0.0.0", }; uint hresult = NativeMethods.CreateAssemblyCache(out IAssemblyCache assemblyCache, 0); From 6f6ce1f4248ba0f3b5a269058fc29c0b9ecd4e1a Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 27 Nov 2025 18:58:30 +0100 Subject: [PATCH 09/47] cleanup --- NuGet.config | 4 ---- THIRDPARTYNOTICES.txt | 10 +--------- eng/Packages.props | 4 +--- eng/Versions.props | 2 +- src/Framework/Microsoft.Build.Framework.csproj | 2 +- src/Package/MSBuild.VSSetup/files.swr | 1 - src/Package/Microsoft.Build.UnGAC/Program.cs | 1 - 7 files changed, 4 insertions(+), 20 deletions(-) diff --git a/NuGet.config b/NuGet.config index 1fa9b0c95ae..764f9c8ddaa 100644 --- a/NuGet.config +++ b/NuGet.config @@ -18,11 +18,7 @@ - - - - diff --git a/THIRDPARTYNOTICES.txt b/THIRDPARTYNOTICES.txt index d3e621fb87b..03a2249e059 100644 --- a/THIRDPARTYNOTICES.txt +++ b/THIRDPARTYNOTICES.txt @@ -64,12 +64,4 @@ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------- - -Notice for Microsoft.VisualStudio.Telemetry -------------------------------- -MSBuild.exe is distributed with Microsoft.VisualStudio.Telemetry binary. - -Project: Microsoft.VisualStudio.Telemetry -Copyright: (c) Microsoft Corporation -License: https://visualstudio.microsoft.com/license-terms/mt736442/ \ No newline at end of file +------------------------------- \ No newline at end of file diff --git a/eng/Packages.props b/eng/Packages.props index 1b7cccf8001..aac684248db 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -40,9 +40,7 @@ - - - + diff --git a/eng/Versions.props b/eng/Versions.props index 161636543ff..0673c55718b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -55,7 +55,7 @@ 5.0.0 - 0.2.104-beta + 17.14.18 diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 0e083fe6550..dc1ba4af67a 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 7f7912e6c34..9639105ad84 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -89,7 +89,6 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.ServiceModel.targets file source=$(X86BinPath)Microsoft.WinFx.targets file source=$(X86BinPath)Microsoft.WorkflowBuildExtensions.targets - file source=$(X86BinPath)Microsoft.VisualStudio.Telemetry.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.VisualStudio.Utilities.Internal.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.Extensions.Configuration.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)Microsoft.Extensions.Configuration.Binder.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 diff --git a/src/Package/Microsoft.Build.UnGAC/Program.cs b/src/Package/Microsoft.Build.UnGAC/Program.cs index 778d099d35d..d686da3dc75 100644 --- a/src/Package/Microsoft.Build.UnGAC/Program.cs +++ b/src/Package/Microsoft.Build.UnGAC/Program.cs @@ -32,7 +32,6 @@ private static void Main(string[] args) "BuildXL.Utilities.Core, Version=1.0.0.0", "BuildXL.Native, Version=1.0.0.0", "Microsoft.VisualStudio.SolutionPersistence, Version=1.0.0.0", - "Microsoft.VisualStudio.Telemetry, Version=16.0.0.0", }; uint hresult = NativeMethods.CreateAssemblyCache(out IAssemblyCache assemblyCache, 0); From 1a02abca722db509b3d906233ef293e1200c6587 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 3 Dec 2025 15:48:05 +0100 Subject: [PATCH 10/47] return MSBuildActivitySource --- .../BackEnd/BuildManager/BuildManager.cs | 15 +++-- src/Framework/Telemetry/BuildTelemetry.cs | 7 +- src/Framework/Telemetry/DiagnosticActivity.cs | 65 +++++++++++++++++++ src/Framework/Telemetry/IActivity.cs | 36 ++++++++++ .../Telemetry/IActivityTelemetryDataHolder.cs | 4 -- .../Telemetry/MSBuildActivitySource.cs | 62 ++++++++++++++++++ src/Framework/Telemetry/TelemetryConstants.cs | 10 +++ src/Framework/Telemetry/TelemetryDataUtils.cs | 4 +- src/Framework/Telemetry/TelemetryItem.cs | 4 -- src/Framework/Telemetry/TelemetryManager.cs | 51 ++++++++++----- .../Telemetry/VSTelemetryActivity.cs | 30 --------- 11 files changed, 219 insertions(+), 69 deletions(-) create mode 100644 src/Framework/Telemetry/DiagnosticActivity.cs create mode 100644 src/Framework/Telemetry/IActivity.cs create mode 100644 src/Framework/Telemetry/MSBuildActivitySource.cs diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index e23b9cda866..568bbc8822d 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1160,17 +1160,24 @@ void SerializeCaches() } } -#if NETFRAMEWORK - [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - using IActivity? activity = TelemetryManager.Instance.StartActivity(TelemetryConstants.Build) + //OpenTelemetryManager.Instance.DefaultActivitySource? + // .StartActivity("Build")? + // .WithTags(_buildTelemetry) + // .WithTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( + // includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, + // includeTargetDetails: false)) + // .WithStartTime(_buildTelemetry!.InnerStartAt) + // .Dispose(); + + using IActivity? activity = TelemetryManager.Instance.DefaultActivitySource! + .StartActivity(TelemetryConstants.Build) ?.SetTags(_buildTelemetry) ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, includeTargetDetails: false)); } -#endif /// /// Convenience method. Submits a lone build request and blocks until results are available. diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 33272cb643b..9fc53f64d3c 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -10,10 +10,7 @@ namespace Microsoft.Build.Framework.Telemetry /// /// Telemetry of build. /// - internal class BuildTelemetry : TelemetryBase -#if NETFRAMEWORK - , IActivityTelemetryDataHolder -#endif + internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder { public override string EventName => "build"; @@ -108,7 +105,6 @@ internal class BuildTelemetry : TelemetryBase /// public string? BuildEngineFrameworkName { get; set; } -#if NETFRAMEWORK /// /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. /// @@ -145,7 +141,6 @@ void AddIfNotNull(string key, object? value) } } } -#endif public override IDictionary GetProperties() { diff --git a/src/Framework/Telemetry/DiagnosticActivity.cs b/src/Framework/Telemetry/DiagnosticActivity.cs new file mode 100644 index 00000000000..fadb67ad039 --- /dev/null +++ b/src/Framework/Telemetry/DiagnosticActivity.cs @@ -0,0 +1,65 @@ +// 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; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Wraps a and implements . + /// + internal class DiagnosticActivity : IActivity + { + private readonly Activity _activity; + private bool _disposed; + + public DiagnosticActivity(Activity activity) + { + _activity = activity; + } + + public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) + { + Dictionary? tags = dataHolder?.GetActivityProperties(); + if (tags != null) + { + foreach (KeyValuePair tag in tags) + { + SetTag(tag.Key, tag.Value); + } + } + + return this; + } + + public IActivity? SetTag(string key, object? value) + { + if (value != null) + { + _activity.SetTag(key, value); + } + + return this; + } + + public IActivity? AddEvent(ActivityEvent activityEvent) + { + _activity.AddEvent(activityEvent); + + return this; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _activity.Dispose(); + + _disposed = true; + } + } +} diff --git a/src/Framework/Telemetry/IActivity.cs b/src/Framework/Telemetry/IActivity.cs new file mode 100644 index 00000000000..6237fa6dd9d --- /dev/null +++ b/src/Framework/Telemetry/IActivity.cs @@ -0,0 +1,36 @@ +// 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.Diagnostics; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Represents an activity for telemetry tracking. + /// + internal interface IActivity : IDisposable + { + /// + /// Sets a tag on the activity. + /// + /// Telemetry data holder. + /// The activity instance for method chaining. + IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder); + + /// + /// Sets a tag on the activity. + /// + /// The tag key. + /// The tag value. + /// The activity instance for method chaining. + IActivity? SetTag(string key, object? value); + + /// + /// Adds an event to the activity. + /// + /// The event to add. + /// The activity instance for method chaining. + IActivity? AddEvent(ActivityEvent activityEvent); + } +} diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs index 68c7d672f67..90fd7e21875 100644 --- a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK - using System.Collections.Generic; using System.Diagnostics; @@ -15,5 +13,3 @@ internal interface IActivityTelemetryDataHolder { Dictionary GetActivityProperties(); } - -#endif diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs new file mode 100644 index 00000000000..de63312b4ec --- /dev/null +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -0,0 +1,62 @@ +// 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; +#if NETFRAMEWORK +using Microsoft.VisualStudio.Telemetry; +#endif + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Wrapper class for ActivitySource with a method that wraps Activity name with VS prefix. + /// On .NET Framework, activities are also forwarded to VS Telemetry. + /// + internal class MSBuildActivitySource + { + private readonly ActivitySource _source; + +#if NETFRAMEWORK + private readonly TelemetrySession? _telemetrySession; + + public MSBuildActivitySource(string name, TelemetrySession? telemetrySession) + { + _source = new ActivitySource(name); + _telemetrySession = telemetrySession; + } +#else + public MSBuildActivitySource(string name) + { + _source = new ActivitySource(name); + } +#endif + + /// + /// Starts a new activity with the appropriate telemetry prefix. + /// + /// Name of the telemetry event without prefix. + /// An wrapping the underlying Activity, or null if not sampled. + public IActivity? StartActivity(string name) + { + string eventName = $"{TelemetryConstants.EventPrefix}{name}"; + + Activity? activity = Activity.Current?.HasRemoteParent == true + ? _source.StartActivity(eventName, ActivityKind.Internal, parentId: Activity.Current.ParentId) + : _source.StartActivity(eventName); + + if (activity == null) + { + return null; + } + + activity.SetTag("SampleRate", TelemetryConstants.DefaultSampleRate); + +#if NETFRAMEWORK + TelemetryScope? operation = _telemetrySession?.StartOperation(eventName); + return operation != null ? new VsTelemetryActivity(operation) : null; +#else + return new DiagnosticActivity(activity); +#endif + } + } +} diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index 08b383e8b9e..129eca2c3d3 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -17,6 +17,16 @@ internal static class TelemetryConstants /// public const string PropertyPrefix = "VS.MSBuild."; + /// + /// "Microsoft.VisualStudio.OpenTelemetry.*" namespace is required by VS exporting/collection. + /// + public const string ActivitySourceNamespacePrefix = "Microsoft.VisualStudio.OpenTelemetry.MSBuild."; + + /// + /// Namespace of the default ActivitySource handling e.g. End of build telemetry. + /// + public const string DefaultActivitySourceNamespace = $"{ActivitySourceNamespacePrefix}Default"; + /// /// For VS OpenTelemetry Collector to apply the correct privacy policy. /// diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index e36496bc914..01a6a71b829 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK +using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; @@ -306,5 +306,3 @@ Dictionary IActivityTelemetryDataHolder.GetActivityProperties() } } } - -#endif diff --git a/src/Framework/Telemetry/TelemetryItem.cs b/src/Framework/Telemetry/TelemetryItem.cs index 4b8fa262f98..50858c09323 100644 --- a/src/Framework/Telemetry/TelemetryItem.cs +++ b/src/Framework/Telemetry/TelemetryItem.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK - using System.Collections.Generic; using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils; @@ -39,5 +37,3 @@ internal record TaskCategoryStats(TaskStatsInfo? Total, TaskStatsInfo? FromNuget internal record TaskStatsInfo(int ExecutionsCount, double TotalMilliseconds, long TotalMemoryBytes); } - -#endif diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 0639f50f81c..423685d44cd 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -2,28 +2,47 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NETFRAMEWORK - using Microsoft.VisualStudio.Telemetry; +#endif namespace Microsoft.Build.Framework.Telemetry { internal class TelemetryManager { +#if NETFRAMEWORK private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; private static TelemetrySession? _telemetrySession; - +#endif private static bool s_disposed; private TelemetryManager() { } + /// + /// Optional activity source for MSBuild or other telemetry usage. + /// + public MSBuildActivitySource? DefaultActivitySource { get; private set; } + public static TelemetryManager Instance { get; } = new TelemetryManager(); + /// + /// Starts a new telemetry activity. + /// + /// Name of the telemetry event without prefix. + /// An or null if telemetry is not initialized or opted out. + public IActivity? StartActivity(string name) => DefaultActivitySource?.StartActivity(name); + public void Initialize(bool isStandalone) { - if (IsOptOut() || _telemetrySession != null) + if (IsOptOut()) + { + return; + } + +#if NETFRAMEWORK + if (_telemetrySession != null) { return; } @@ -33,19 +52,16 @@ public void Initialize(bool isStandalone) _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); TelemetryService.DefaultSession.IsOptedIn = true; TelemetryService.DefaultSession.Start(); - - return; + } + else + { + _telemetrySession = TelemetryService.DefaultSession; } - _telemetrySession = TelemetryService.DefaultSession; - } - - public IActivity? StartActivity(string name) - { - string eventName = $"{TelemetryConstants.EventPrefix}{name}"; - TelemetryScope? operation = _telemetrySession.StartOperation(eventName); - - return operation != null ? new VsTelemetryActivity(operation) : null; + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, _telemetrySession); +#else + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); +#endif } public void Dispose() @@ -55,16 +71,15 @@ public void Dispose() return; } +#if NETFRAMEWORK _telemetrySession?.Dispose(); - +#endif s_disposed = true; } /// /// Determines if the user has explicitly opted out of telemetry. /// - private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut || !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14); + private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut; } } - -#endif diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index bc3019b3e90..bbba699b9ca 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -3,10 +3,8 @@ #if NETFRAMEWORK -using System; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.Build.Framework.Telemetry; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry @@ -73,32 +71,4 @@ public void Dispose() } } -/// -/// Represents an activity for telemetry tracking. -/// -internal interface IActivity : IDisposable -{ - /// - /// Sets a tag on the activity. - /// - /// Telemetry data holder. - /// The activity instance for method chaining. - IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder); - - /// - /// Sets a tag on the activity. - /// - /// The tag key. - /// The tag value. - /// The activity instance for method chaining. - IActivity? SetTag(string key, object? value); - - /// - /// Adds an event to the activity. - /// - /// The event to add. - /// The activity instance for method chaining. - IActivity? AddEvent(ActivityEvent activityEvent); -} - #endif From 6a3357d1e38398cf50ade9d045cdb67a1d6771b2 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 3 Dec 2025 16:09:07 +0100 Subject: [PATCH 11/47] cleanup --- THIRDPARTYNOTICES.txt | 22 -- documentation/specs/VS-OpenTelemetry.md | 198 ------------------ .../specs/proposed/telemetry-onepager.md | 77 ------- eng/Signing.props | 6 - .../BackEnd/BuildManager/BuildManager.cs | 12 +- src/Build/Resources/Strings.resx | 3 - src/Build/Resources/xlf/Strings.cs.xlf | 5 - src/Build/Resources/xlf/Strings.de.xlf | 5 - src/Build/Resources/xlf/Strings.es.xlf | 5 - src/Build/Resources/xlf/Strings.fr.xlf | 5 - src/Build/Resources/xlf/Strings.it.xlf | 5 - src/Build/Resources/xlf/Strings.ja.xlf | 5 - src/Build/Resources/xlf/Strings.ko.xlf | 5 - src/Build/Resources/xlf/Strings.pl.xlf | 5 - src/Build/Resources/xlf/Strings.pt-BR.xlf | 5 - src/Build/Resources/xlf/Strings.ru.xlf | 5 - src/Build/Resources/xlf/Strings.tr.xlf | 5 - src/Build/Resources/xlf/Strings.zh-Hans.xlf | 5 - src/Build/Resources/xlf/Strings.zh-Hant.xlf | 5 - src/Framework/Telemetry/TelemetryConstants.cs | 4 +- src/Framework/Telemetry/TelemetryDataUtils.cs | 3 +- src/Framework/Telemetry/TelemetryManager.cs | 2 +- .../Telemetry/VSTelemetryActivity.cs | 4 +- src/MSBuild/app.amd64.config | 92 -------- src/MSBuild/app.config | 8 - src/Package/MSBuild.VSSetup/files.swr | 14 -- 26 files changed, 7 insertions(+), 503 deletions(-) delete mode 100644 documentation/specs/VS-OpenTelemetry.md delete mode 100644 documentation/specs/proposed/telemetry-onepager.md diff --git a/THIRDPARTYNOTICES.txt b/THIRDPARTYNOTICES.txt index 03a2249e059..982c50663a3 100644 --- a/THIRDPARTYNOTICES.txt +++ b/THIRDPARTYNOTICES.txt @@ -43,25 +43,3 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -------------------------------- - -Notice for OpenTelemetry .NET -------------------------------- -MSBuild.exe is distributed with OpenTelemetry .NET binaries. - -Copyright (c) OpenTelemetry Authors -Source: https://github.com/open-telemetry/opentelemetry-dotnet - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the specific -language governing permissions and limitations under the License. - -------------------------------- \ No newline at end of file diff --git a/documentation/specs/VS-OpenTelemetry.md b/documentation/specs/VS-OpenTelemetry.md deleted file mode 100644 index 59d1f6e5d17..00000000000 --- a/documentation/specs/VS-OpenTelemetry.md +++ /dev/null @@ -1,198 +0,0 @@ -# Telemetry via OpenTelemetry design - -VS OTel provide packages compatible with ingesting data to their backend if we instrument it via OpenTelemetry traces (System.Diagnostics.Activity). -VS OTel packages are not open source so we need to conditionally include them in our build only for VS and MSBuild.exe - -> this formatting is a comment describing how the implementation turned out in 17.14 when our original goals were different - -[Onepager](https://github.com/dotnet/msbuild/blob/main/documentation/specs/proposed/telemetry-onepager.md) - -## Concepts - -It's a bit confusing how things are named in OpenTelemetry and .NET and VS Telemetry and what they do. - -| OTel concept | .NET/VS | Description | -| --- | --- | --- | -| Span/Trace | System.Diagnostics.Activity | Trace is a tree of Spans. Activities can be nested.| -| Tracer | System.Diagnostics.ActivitySource | Creates activites. | -| Processor/Exporter | VS OTel provided default config | filters and saves telemetry as files in a desired format | -| TracerProvider | OTel SDK TracerProvider | Singleton that is aware of processors, exporters and Tracers and listens (in .NET a bit looser relationship because it does not create Tracers just hooks to them) | -| Collector | VS OTel Collector | Sends to VS backend | - -## Requirements - -### Performance - -- If not sampled, no infra initialization overhead. -- Avoid allocations when not sampled. -- Has to have no impact on Core without opting into tracing, small impact on Framework -- No regression in VS perf ddrit scenarios. - -> there is an allocation regression when sampled, one of the reasons why it's not enabled by default - -### Privacy - -- Hashing data points that could identify customers (e.g. names of targets) -- Opt out capability - -### Security - -- Providing or/and documenting a method for creating a hook in Framework MSBuild -- If custom hooking solution will be used - document the security implications of hooking custom telemetry Exporters/Collectors in Framework -- other security requirements (transportation, rate limiting, sanitization, data access) are implemented by VS Telemetry library or the backend - -> hooking in Framework not implemented - -### Data handling - -- Implement head [Sampling](https://opentelemetry.io/docs/concepts/sampling/) with the granularity of a MSBuild.exe invocation/VS instance. -- VS Data handle tail sampling in their infrastructure not to overwhelm storage with a lot of build events. - -#### Data points - -The data sent via VS OpenTelemetry is neither a subset neither a superset of what is sent to SDK telemetry and it is not a purpose of this design to unify them. - -##### Basic info - -- Build duration -- Host -- Build success/failure -- Version -- Target (hashed) - -##### Evnironment - -- SAC (Smart app control) enabled - -##### Features - -- BuildCheck enabled -- Tasks runtimes and memory usage -- Tasks summary - whether they come from Nuget or are custom -- Targets summary - how many loaded and executed, how many come from nuget, how many come from metaproject - -The design should allow for easy instrumentation of additional data points. -> current implementation has only one datapoint and that is the whole build `vs/msbuild/build`, the instrumentaiton of additional datapoints is gated by first checking that telemetry is running and using `Activity` classes only in helper methods gated by `[MethodImpl(MethodImplOptions.NoInlining)]` to avoid System.Diagnostics.DiagnosticSource dll load. - -## Core `dotnet build` scenario - -- Telemetry should not be collected via VS OpenTelemetry mechanism because it's already collected in sdk. -- opt in to initialize the ActivitySource to avoid degrading performance. -- [baronfel/otel-startup-hook: A .NET CLR Startup Hook that exports OpenTelemetry metrics via the OTLP Exporter to an OpenTelemetry Collector](https://github.com/baronfel/otel-startup-hook/) and similar enable collecting telemetry data locally by listening to the ActivitySource prefix defined in MSBuild. - -> this hook can be used when the customer specifies that they want to listen to the prefix `Microsoft.VisualStudio.OpenTelemetry.MSBuild`, opt in by setting environment variables `MSBUILD_TELEMETRY_OPTIN=1`,`MSBUILD_TELEMETRY_SAMPLE_RATE=1.0` - -## Standalone MSBuild.exe scenario - -- Initialize and finalize in Xmake.cs - ActivitySource, TracerProvider, VS Collector -- overhead of starting VS collector is nonzero -- head sampling should avoid initializing if not sampled - -## VS in proc (devenv) scenario - -- VS can call `BuildManager` in a thread unsafe way the telemetry implementation has to be mindful of [BuildManager instances acquire its own BuildTelemetry instance by rokonec · Pull Request #8444 · dotnet/msbuild](https://github.com/dotnet/msbuild/pull/8444) - - ensure no race conditions in initialization - - only 1 TracerProvider with VS defined processing should exist -- Visual Studio should be responsible for having a running collector, we don't want this overhead in MSBuild and eventually many will use it - -> this was not achieved in 17.14 so we start collector every time - -## Implementation and MSBuild developer experience - -### ActivitySource names - -- Microsoft.VisualStudio.OpenTelemetry.MSBuild.Default - -### Sampling - -Our estimation from VS and SDK data is that there are 10M-100M build events per day. -For proportion estimation (of fairly common occurence in the builds), with not very strict confidnece (95%) and margin for error (5%) sampling 1:25000 would be enough. - -- this would apply for the DefaultActivitySource -- other ActivitySources could be sampled more frequently to get enough data -- Collecting has a cost, especially in standalone scenario where we have to start the collector. We might decide to undersample in standalone to avoid performance frequent impact. -- We want to avoid that cost when not sampled, therefore we prefer head sampling. -- Enables opt-in and opt-out for guaranteed sample or not sampled. -- nullable ActivitySource, using `?` when working with them, we can be initialized but not sampled -> it will not reinitialize but not collect telemetry. - -- for 17.14 we can't use the new OTel assemblies and their dependencies, so everything has to be opt in. -- eventually OpenTelemetry will be available and usable by default -- We can use experiments in VS to pass the environment variable to initialize - -> Targeted notification can be set that samples 100% of customers to which it is sent - -### Initialization at entrypoints - -- There are 2 entrypoints: - - for VS in BuildManager.BeginBuild - - for standalone in Xmake.cs Main - -### Exiting - -Force flush TracerProvider's exporter in BuildManager.EndBuild. -Dispose collector in Xmake.cs at the end of Main. - -### Configuration - -- Class that's responsible for configuring and initializing telemetry and handles optouts, holding tracer and collector. -- Wrapping source so that it has correct prefixes for VS backend to ingest. - -### Instrumenting - -2 ways of instrumenting: - -#### Instrument areas in code running in the main process - -```csharp -using (Activity? myActivity = OpenTelemetryManager.DefaultActivitySource?.StartActivity(TelemetryConstants.NameFromAConstantToAvoidAllocation)) -{ -// something happens here - -// add data to the trace -myActivity?.WithTag("SpecialEvent","fail") -} -``` - -Interface for classes holding telemetry data - -```csharp -IActivityTelemetryDataHolder data = new SomeData(); -... -myActivity?.WithTags(data); -``` - -> currently this should be gated in a separate method to avoid System.DiagnosticDiagnosticsource dll load. - -#### Default Build activity in EndBuild - -- this activity would always be created at the same point when sdk telemetry is sent in Core -- we can add data to it that we want in general builds -- the desired count of data from this should control the sample rate of DefaultActivitySource - -#### Multiple Activity Sources - -We want to create ActivitySources with different sample rates, this requires either implementation server side or a custom Processor. - -We potentially want apart from the Default ActivitySource: - -1. Other activity sources with different sample rates (in order to get significant data for rarer events such as custom tasks). -2. a way to override sampling decision - ad hoc starting telemetry infrastructure to catch rare events - -- Create a way of using a "HighPrioActivitySource" which would override sampling and initialize Collector in MSBuild.exe scenario/tracerprovider in VS. -- this would enable us to catch rare events - -> not implemented - -### Implementation details - -- `OpenTelemetryManager` - singleton that manages lifetime of OpenTelemetry objects listening to `Activity`ies, start by initializing in `Xmake` or `BuildManager`. -- Task and Target data is forwarded from worker nodes via `TelemetryForwarder` and `InternalTelemetryForwardingLogger` and then aggregated to stats and serialized in `TelemetryDataUtils` and attached to the default `vs/msbuild/build` event. - -## Future work when/if we decide to invest in telemetry again - -- avoid initializing/finalizing collector in VS when there is one running -- multiple levels of sampling for different types of events -- running by default with head sampling (simplifies instrumentation with `Activity`ies) -- implement anonymization consistently in an OTel processor and not ad hoc in each usage -- add datapoints helping perf optimization decisions/ reliability investigations diff --git a/documentation/specs/proposed/telemetry-onepager.md b/documentation/specs/proposed/telemetry-onepager.md deleted file mode 100644 index 5bc8f22f9ce..00000000000 --- a/documentation/specs/proposed/telemetry-onepager.md +++ /dev/null @@ -1,77 +0,0 @@ -# Telemetry - -We want to implement telemetry collection for VS/MSBuild.exe scenarios where we are currently not collecting data. VS OpenTelemetry initiative provides a good opportunity to use their infrastructure and library. -There is some data we collect via SDK which we want to make accessible. - -## Goals and Motivation - -We have limited data about usage of MSBuild by our customers in VS and no data about usage of standalone msbuild.exe. -This limits us in prioritization of features and scenarios to optimize performance for. -Over time we want to have comprehensive insight into how MSBuild is used in all scenarios. Collecting such a data without any constraints nor limitations would however be prohibitively expensive (from the data storage PoV and possibly as well from the client side performance impact PoV). Ability to sample / configure the collection is an important factor in deciding the instrumentation and collection tech stack. Implementing telemetry via VS OpenTelemetry initiative would give us this ability in the future. - -Goal: To have relevant data in that is actionable for decisions about development. Measuring real world performance impact of features (e.g. BuildCheck). Easily extensible telemetry infrastructure if we want to measure a new datapoint. - -## Impact -- Better planning of deployment of forces in MSBuild by product/team management. -- Customers can subscribe to telemetry locally to have data in standardized OpenTelemetry format - -## Stakeholders -- @Jan(Krivanek|Provaznik) design and implementation of telemetry via VS OTel. @ - using data we already have from SDK. -- @maridematte - documenting + dashboarding currently existing datapoints. -- MSBuild Team+Management – want insights from builds in VS -- VS OpenTelemetry team – provide support for VS OpenTelemetry collector library, want successful adoption -- SourceBuild – consulting and approving usage of OpenTelemetry -- MSBuild PM @baronfel – representing customers who want to monitor their builds locally - -### V1 Successful handover -- Shipped to Visual Studio -- Data queryable in Kusto -- Dashboards (even for pre-existing data - not introduced by this work) -- Customers are able to monitor with OpenTelemetry collector of choice (can be cut) - -## Risks -- Performance regression risks - it's another thing MSBuild would do and if the perf hit would be too bad it would need mitigation effort. -- It introduces a closed source dependency for VS and MSBuild.exe distribution methods which requires workarounds to remain compatible with SourceBuild policy (conditional compilation/build). -- Using a new VS API - might have gaps -- storage costs -- Potential additional costs and delays due to compliance with SourceBuild/VS data. - -## V1 Cost -5 months of .5 developer's effort ~ 50 dev days (dd) - -20-30dd JanPro OTel design + implementation, 10-15dd JanK design + implementation, 5-10dd Mariana/someone getting available data in order/"data science"/dashboards + external documentation - -Uncertainties: -It’s an exploratory project for VS OpenTelemetry, we'll be their first OSS component, so there might come up issues. SourceBuild compliance could introduce delays. - -## Plan -### V1 scope -- Collected data point definition -- Instrumented data points (as an example how the instrumentation and collection works) -- Telemetry sent to VS Telemetry in acceptable quantity -- Dashboards for collected data -- Hooking of customer's telemetry collection -- Documenting and leveraging pre-existing telemetry - -#### Out of scope -- Unifying telemetry for SDK MSBuild and MSBuild.exe/VS MSBuild. -- Thorough instrumentation of MSBuild -- Using MSBuild server -- Distributed tracing - -### Detailed cost -- Prototyping the libraries/mechanism for collecting telemetry data (month 1) 10dd - -- Defining usful data points (month 1) 5dd - -- Design and approval of hooking VSTelemetry collectors and OTel collectors (month 2) 10dd - -- Formalizing, agreeing to sourcebuild and other external requirements (month 2) 5dd - -- Instrumenting MSBuild with defined datapoints (month 3) 7dd - -- Creating dashboards/insights (month 4) 5dd - -- Documenting for customers how to hook their own telemetry collection (month 4) 3dd - -- Buffer for discovered issues (VSData Platform, SourceBuild, OpenTelemetry) and more investments (month 5) 5dd diff --git a/eng/Signing.props b/eng/Signing.props index b2e4bff8ffe..d46e57e8e34 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -11,12 +11,6 @@ - - - - - - diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 568bbc8822d..bdfee4003ab 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1115,9 +1115,8 @@ public void EndBuild() _buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement; loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties()); -#if NETFRAMEWORK EndBuildTelemetry(); -#endif + // Clean telemetry to make it ready for next build submission. _buildTelemetry = null; } @@ -1162,15 +1161,6 @@ void SerializeCaches() private void EndBuildTelemetry() { - //OpenTelemetryManager.Instance.DefaultActivitySource? - // .StartActivity("Build")? - // .WithTags(_buildTelemetry) - // .WithTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( - // includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, - // includeTargetDetails: false)) - // .WithStartTime(_buildTelemetry!.InnerStartAt) - // .Dispose(); - using IActivity? activity = TelemetryManager.Instance.DefaultActivitySource! .StartActivity(TelemetryConstants.Build) ?.SetTags(_buildTelemetry) diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 68b59de6ee8..a06d23c2862 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2430,9 +2430,6 @@ Utilization: {0} Average Utilization: {1:###.0} succeeded: {0} {0} whole number - - Loading telemetry libraries failed with exception: {0}. - Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 8bee6a2aafa..d8a9c39219f 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -652,11 +652,6 @@ Metoda {0} se nedá zavolat s kolekcí, která obsahuje prázdné cílové názvy nebo názvy null. - - Loading telemetry libraries failed with exception: {0}. - Načítání knihoven telemetrie se nezdařilo s výjimkou: {0}. - - Output Property: Výstupní vlastnost: diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index d645fa63c01..5a9c301f706 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -652,11 +652,6 @@ Die Methode "{0}" kann nicht mit einer Sammlung aufgerufen werden, die NULL oder leere Zielnamen enthält. - - Loading telemetry libraries failed with exception: {0}. - Fehler beim Laden von Telemetriebibliotheken. Ausnahme:{0}. - - Output Property: Ausgabeeigenschaft: diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index 9999f675f1b..0bab8ced519 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -652,11 +652,6 @@ No se puede llamar al método {0} con una colección que contiene nombres de destino nulos o vacíos. - - Loading telemetry libraries failed with exception: {0}. - Error al cargar las bibliotecas de telemetría con la excepción: {0}. - - Output Property: Propiedad de salida: diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 61cb18b44b3..1fd70825bc7 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -652,11 +652,6 @@ Impossible d'appeler la méthode {0} avec une collection contenant des noms de cibles qui ont une valeur null ou qui sont vides. - - Loading telemetry libraries failed with exception: {0}. - Nous n’avons pas pu charger les bibliothèques de télémétrie avec l’exception : {0}. - - Output Property: Propriété de sortie : diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 6d747503398..0d7bd82eceb 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -652,11 +652,6 @@ Non è possibile chiamare il metodo {0} con una raccolta contenente nomi di destinazione Null o vuoti. - - Loading telemetry libraries failed with exception: {0}. - Caricamento delle librerie di telemetria non riuscito con eccezione: {0}. - - Output Property: Proprietà di output: diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index 16e5023c578..8a0e3d17a13 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -652,11 +652,6 @@ Null または空のターゲット名を含むコレクションを指定してメソッド {0} を呼び出すことはできません。 - - Loading telemetry libraries failed with exception: {0}. - テレメトリ ライブラリの読み込みが次の例外で失敗しました: {0}。 - - Output Property: プロパティの出力: diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index 32ffaf8a51b..121fa05f8ec 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -652,11 +652,6 @@ null 또는 빈 대상 이름을 포함하는 컬렉션을 사용하여 {0} 메서드를 호출할 수 없습니다. - - Loading telemetry libraries failed with exception: {0}. - 예외 {0}(으)로 인해 원격 분석 라이브러리를 로드하지 못했습니다. - - Output Property: 출력 속성: diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 9cb87fdc994..7aca39c9198 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -652,11 +652,6 @@ Metody {0} nie można wywołać przy użyciu kolekcji zawierającej nazwy docelowe o wartości null lub puste. - - Loading telemetry libraries failed with exception: {0}. - Ładowanie bibliotek telemetrii nie powiodło się. Wyjątek: {0}. - - Output Property: Właściwość danych wyjściowych: diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index 6269fc1664d..d07633c489a 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -652,11 +652,6 @@ O método {0} não pode ser chamado com uma coleção que contém nomes de destino nulos ou vazios. - - Loading telemetry libraries failed with exception: {0}. - Falha ao carregar as bibliotecas de telemetria com a exceção: {0}. - - Output Property: Propriedade de Saída: diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 8cf9e87e539..4c151a9d29a 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -652,11 +652,6 @@ Метод {0} не может быть вызван с коллекцией, содержащей целевые имена, которые пусты или равны NULL. - - Loading telemetry libraries failed with exception: {0}. - Не удалось загрузить библиотеки телеметрии с исключением: {0}. - - Output Property: Выходное свойство: diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 475bd25e46e..97cb97c6eef 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -652,11 +652,6 @@ {0} metosu null veya boş hedef adları içeren bir koleksiyonla çağrılamaz. - - Loading telemetry libraries failed with exception: {0}. - Telemetri kitaplıklarının yüklenmesi şu hayatla başarısız oldu: {0}. - - Output Property: Çıkış Özelliği: diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index dcbd2aeb783..e2107b0f3ce 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -652,11 +652,6 @@ 无法使用包含 null 或空目标名称的集合调用方法 {0}。 - - Loading telemetry libraries failed with exception: {0}. - 加载遥测库失败,出现异常: {0}。 - - Output Property: 输出属性: diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index d0bf2ee9add..905e965c76c 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -652,11 +652,6 @@ 無法使用內含 null 或空白目標名稱的集合呼叫方法 {0}。 - - Loading telemetry libraries failed with exception: {0}. - 載入遙測程式庫時發生例外狀況: {0}。 - - Output Property: 輸出屬性: diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index 129eca2c3d3..14592865a68 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -18,9 +18,9 @@ internal static class TelemetryConstants public const string PropertyPrefix = "VS.MSBuild."; /// - /// "Microsoft.VisualStudio.OpenTelemetry.*" namespace is required by VS exporting/collection. + /// "Microsoft.Build.Telemetry.*" namespace is required by VS exporting/collection. /// - public const string ActivitySourceNamespacePrefix = "Microsoft.VisualStudio.OpenTelemetry.MSBuild."; + public const string ActivitySourceNamespacePrefix = "Microsoft.Build.Telemetry"; /// /// Namespace of the default ActivitySource handling e.g. End of build telemetry. diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index 01a6a71b829..31dd22fdee4 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -1,7 +1,6 @@ // 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.Security.Cryptography; using System.Text; @@ -107,7 +106,7 @@ public static string Hash(string text) #if NET byte[] hash = SHA256.HashData(bytes); #if NET9_0_OR_GREATER - return Convert.ToHexStringLower(hash); + return System.Convert.ToHexStringLower(hash); #else return Convert.ToHexString(hash).ToLowerInvariant(); #endif diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 423685d44cd..1328264fc62 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -80,6 +80,6 @@ public void Dispose() /// /// Determines if the user has explicitly opted out of telemetry. /// - private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut; + private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut; } } diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index bbba699b9ca..cdbfe58472c 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -13,7 +13,6 @@ internal class VsTelemetryActivity : IActivity { private readonly TelemetryScope _scope; private TelemetryResult _result = TelemetryResult.Success; - private string? _resultSummary; private bool _disposed; @@ -43,6 +42,7 @@ internal class VsTelemetryActivity : IActivity return this; } + public IActivity? AddEvent(ActivityEvent activityEvent) { // VS Telemetry doesn't have a direct equivalent to ActivityEvent @@ -65,7 +65,7 @@ public void Dispose() } // End the operation - _scope.End(_result, _resultSummary); + _scope.End(_result); _disposed = true; } } diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 9bf8b014e38..977daa4650f 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -56,14 +56,6 @@ - - - - - - - - @@ -104,90 +96,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/MSBuild/app.config b/src/MSBuild/app.config index 9c41d0b862c..6f2cba28e6c 100644 --- a/src/MSBuild/app.config +++ b/src/MSBuild/app.config @@ -39,10 +39,6 @@ - - - - @@ -72,10 +68,6 @@ - - - - diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 9639105ad84..4d2e84f524e 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -41,7 +41,6 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.VisualStudio.SolutionPersistence.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)RuntimeContracts.dll file source=$(X86BinPath)System.Buffers.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 - file source=$(X86BinPath)System.Diagnostics.DiagnosticSource.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)System.Formats.Nrbf.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.IO.Pipelines.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.Memory.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 @@ -90,19 +89,6 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.WinFx.targets file source=$(X86BinPath)Microsoft.WorkflowBuildExtensions.targets file source=$(X86BinPath)Microsoft.VisualStudio.Utilities.Internal.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.Binder.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.DependencyInjection.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.DependencyInjection.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Options.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Options.ConfigurationExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Primitives.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Diagnostics.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Newtonsoft.Json.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 folder InstallDir:\MSBuild\Current\Bin\MSBuild file source=$(X86BinPath)\MSBuild\Microsoft.Build.Core.xsd From 752a51dd3834d3aae0a6ccb37edc493027240e2d Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 3 Dec 2025 16:15:03 +0100 Subject: [PATCH 12/47] add documentation --- src/Framework/Telemetry/TelemetryManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 1328264fc62..12d86ccb0be 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -7,6 +7,15 @@ namespace Microsoft.Build.Framework.Telemetry { + /// + /// Manages telemetry collection and reporting for MSBuild. + /// This class provides a centralized way to initialize, configure, and manage telemetry sessions. + /// + /// + /// The TelemetryManager is a singleton that handles both standalone and integrated telemetry scenarios. + /// On .NET Framework, it integrates with Visual Studio telemetry services. + /// On .NET Core it provides a lightweight telemetry implementation though exposing an activity source. + /// internal class TelemetryManager { #if NETFRAMEWORK From 2f76960b7c636cb6fc3372bf34d1e63437976fd6 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 3 Dec 2025 16:15:50 +0100 Subject: [PATCH 13/47] cleanup --- src/Framework/Telemetry/TelemetryManager.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 12d86ccb0be..f642fa9c2d6 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -36,13 +36,6 @@ private TelemetryManager() public static TelemetryManager Instance { get; } = new TelemetryManager(); - /// - /// Starts a new telemetry activity. - /// - /// Name of the telemetry event without prefix. - /// An or null if telemetry is not initialized or opted out. - public IActivity? StartActivity(string name) => DefaultActivitySource?.StartActivity(name); - public void Initialize(bool isStandalone) { if (IsOptOut()) From dc1dc4b8f32448a23fc2d6d5a667531969c61ad7 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 3 Dec 2025 16:38:11 +0100 Subject: [PATCH 14/47] remove extra logic from VS telemetry --- src/Framework/Telemetry/MSBuildActivitySource.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs index de63312b4ec..bd91774bbd1 100644 --- a/src/Framework/Telemetry/MSBuildActivitySource.cs +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -40,6 +40,10 @@ public MSBuildActivitySource(string name) { string eventName = $"{TelemetryConstants.EventPrefix}{name}"; +#if NETFRAMEWORK + TelemetryScope? operation = _telemetrySession?.StartOperation(eventName); + return operation != null ? new VsTelemetryActivity(operation) : null; +#else Activity? activity = Activity.Current?.HasRemoteParent == true ? _source.StartActivity(eventName, ActivityKind.Internal, parentId: Activity.Current.ParentId) : _source.StartActivity(eventName); @@ -51,10 +55,6 @@ public MSBuildActivitySource(string name) activity.SetTag("SampleRate", TelemetryConstants.DefaultSampleRate); -#if NETFRAMEWORK - TelemetryScope? operation = _telemetrySession?.StartOperation(eventName); - return operation != null ? new VsTelemetryActivity(operation) : null; -#else return new DiagnosticActivity(activity); #endif } From 34a2b436fedc1cc787226fe3130655cfc95f665e Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 4 Dec 2025 10:50:47 +0100 Subject: [PATCH 15/47] cleanup --- src/Framework/Telemetry/MSBuildActivitySource.cs | 9 ++++----- src/Framework/Telemetry/TelemetryManager.cs | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs index bd91774bbd1..4da748ad918 100644 --- a/src/Framework/Telemetry/MSBuildActivitySource.cs +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -9,22 +9,21 @@ namespace Microsoft.Build.Framework.Telemetry { /// - /// Wrapper class for ActivitySource with a method that wraps Activity name with VS prefix. + /// Wrapper class for ActivitySource with a method that wraps Activity name with MSBuild prefix. /// On .NET Framework, activities are also forwarded to VS Telemetry. /// internal class MSBuildActivitySource { - private readonly ActivitySource _source; - #if NETFRAMEWORK private readonly TelemetrySession? _telemetrySession; - public MSBuildActivitySource(string name, TelemetrySession? telemetrySession) + public MSBuildActivitySource(TelemetrySession? telemetrySession) { - _source = new ActivitySource(name); _telemetrySession = telemetrySession; } #else + private readonly ActivitySource _source; + public MSBuildActivitySource(string name) { _source = new ActivitySource(name); diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index f642fa9c2d6..86184b31a08 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -60,7 +60,7 @@ public void Initialize(bool isStandalone) _telemetrySession = TelemetryService.DefaultSession; } - DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, _telemetrySession); + DefaultActivitySource = new MSBuildActivitySource(_telemetrySession); #else DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); #endif From 494ee525f1d6df31a99a17cbfcbe8b81054130c9 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 4 Dec 2025 11:20:51 +0100 Subject: [PATCH 16/47] namespace --- src/Framework/Telemetry/MSBuildActivitySource.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs index 4da748ad918..891e85c781f 100644 --- a/src/Framework/Telemetry/MSBuildActivitySource.cs +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -1,9 +1,10 @@ // 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; #if NETFRAMEWORK using Microsoft.VisualStudio.Telemetry; +#else +using System.Diagnostics; #endif namespace Microsoft.Build.Framework.Telemetry From 4539aa2005957d8825ed75b86c0519f76c4b479c Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 4 Dec 2025 11:39:59 +0100 Subject: [PATCH 17/47] nullable --- src/Build/BackEnd/BuildManager/BuildManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index bdfee4003ab..eef83905c06 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1161,8 +1161,8 @@ void SerializeCaches() private void EndBuildTelemetry() { - using IActivity? activity = TelemetryManager.Instance.DefaultActivitySource! - .StartActivity(TelemetryConstants.Build) + using IActivity? activity = TelemetryManager.Instance?.DefaultActivitySource + ?.StartActivity(TelemetryConstants.Build) ?.SetTags(_buildTelemetry) ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, From 79f17126126438c4564dd6e257e4fa8e62802f3b Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 4 Dec 2025 15:51:10 +0100 Subject: [PATCH 18/47] return ActivitySource + more polishing --- THIRDPARTYNOTICES.txt | 22 -- documentation/specs/VS-OpenTelemetry.md | 198 ------------------ .../specs/proposed/telemetry-onepager.md | 77 ------- eng/Signing.props | 6 - .../BackEnd/BuildManager/BuildManager.cs | 16 +- src/Build/Resources/Strings.resx | 3 - src/Build/Resources/xlf/Strings.cs.xlf | 5 - src/Build/Resources/xlf/Strings.de.xlf | 5 - src/Build/Resources/xlf/Strings.es.xlf | 5 - src/Build/Resources/xlf/Strings.fr.xlf | 5 - src/Build/Resources/xlf/Strings.it.xlf | 5 - src/Build/Resources/xlf/Strings.ja.xlf | 5 - src/Build/Resources/xlf/Strings.ko.xlf | 5 - src/Build/Resources/xlf/Strings.pl.xlf | 5 - src/Build/Resources/xlf/Strings.pt-BR.xlf | 5 - src/Build/Resources/xlf/Strings.ru.xlf | 5 - src/Build/Resources/xlf/Strings.tr.xlf | 5 - src/Build/Resources/xlf/Strings.zh-Hans.xlf | 5 - src/Build/Resources/xlf/Strings.zh-Hant.xlf | 5 - .../{TelemetryItem.cs => BuildInsights.cs} | 4 - src/Framework/Telemetry/BuildTelemetry.cs | 9 +- src/Framework/Telemetry/DiagnosticActivity.cs | 65 ++++++ src/Framework/Telemetry/IActivity.cs | 36 ++++ .../Telemetry/IActivityTelemetryDataHolder.cs | 4 - .../Telemetry/MSBuildActivitySource.cs | 62 ++++++ src/Framework/Telemetry/TelemetryConstants.cs | 10 + src/Framework/Telemetry/TelemetryDataUtils.cs | 5 +- src/Framework/Telemetry/TelemetryManager.cs | 54 +++-- .../Telemetry/VSTelemetryActivity.cs | 39 +--- src/MSBuild/XMake.cs | 9 +- src/MSBuild/app.amd64.config | 92 -------- src/MSBuild/app.config | 8 - src/Package/MSBuild.VSSetup/files.swr | 14 -- 33 files changed, 229 insertions(+), 569 deletions(-) delete mode 100644 documentation/specs/VS-OpenTelemetry.md delete mode 100644 documentation/specs/proposed/telemetry-onepager.md rename src/Framework/Telemetry/{TelemetryItem.cs => BuildInsights.cs} (98%) create mode 100644 src/Framework/Telemetry/DiagnosticActivity.cs create mode 100644 src/Framework/Telemetry/IActivity.cs create mode 100644 src/Framework/Telemetry/MSBuildActivitySource.cs diff --git a/THIRDPARTYNOTICES.txt b/THIRDPARTYNOTICES.txt index 03a2249e059..982c50663a3 100644 --- a/THIRDPARTYNOTICES.txt +++ b/THIRDPARTYNOTICES.txt @@ -43,25 +43,3 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -------------------------------- - -Notice for OpenTelemetry .NET -------------------------------- -MSBuild.exe is distributed with OpenTelemetry .NET binaries. - -Copyright (c) OpenTelemetry Authors -Source: https://github.com/open-telemetry/opentelemetry-dotnet - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the specific -language governing permissions and limitations under the License. - -------------------------------- \ No newline at end of file diff --git a/documentation/specs/VS-OpenTelemetry.md b/documentation/specs/VS-OpenTelemetry.md deleted file mode 100644 index 59d1f6e5d17..00000000000 --- a/documentation/specs/VS-OpenTelemetry.md +++ /dev/null @@ -1,198 +0,0 @@ -# Telemetry via OpenTelemetry design - -VS OTel provide packages compatible with ingesting data to their backend if we instrument it via OpenTelemetry traces (System.Diagnostics.Activity). -VS OTel packages are not open source so we need to conditionally include them in our build only for VS and MSBuild.exe - -> this formatting is a comment describing how the implementation turned out in 17.14 when our original goals were different - -[Onepager](https://github.com/dotnet/msbuild/blob/main/documentation/specs/proposed/telemetry-onepager.md) - -## Concepts - -It's a bit confusing how things are named in OpenTelemetry and .NET and VS Telemetry and what they do. - -| OTel concept | .NET/VS | Description | -| --- | --- | --- | -| Span/Trace | System.Diagnostics.Activity | Trace is a tree of Spans. Activities can be nested.| -| Tracer | System.Diagnostics.ActivitySource | Creates activites. | -| Processor/Exporter | VS OTel provided default config | filters and saves telemetry as files in a desired format | -| TracerProvider | OTel SDK TracerProvider | Singleton that is aware of processors, exporters and Tracers and listens (in .NET a bit looser relationship because it does not create Tracers just hooks to them) | -| Collector | VS OTel Collector | Sends to VS backend | - -## Requirements - -### Performance - -- If not sampled, no infra initialization overhead. -- Avoid allocations when not sampled. -- Has to have no impact on Core without opting into tracing, small impact on Framework -- No regression in VS perf ddrit scenarios. - -> there is an allocation regression when sampled, one of the reasons why it's not enabled by default - -### Privacy - -- Hashing data points that could identify customers (e.g. names of targets) -- Opt out capability - -### Security - -- Providing or/and documenting a method for creating a hook in Framework MSBuild -- If custom hooking solution will be used - document the security implications of hooking custom telemetry Exporters/Collectors in Framework -- other security requirements (transportation, rate limiting, sanitization, data access) are implemented by VS Telemetry library or the backend - -> hooking in Framework not implemented - -### Data handling - -- Implement head [Sampling](https://opentelemetry.io/docs/concepts/sampling/) with the granularity of a MSBuild.exe invocation/VS instance. -- VS Data handle tail sampling in their infrastructure not to overwhelm storage with a lot of build events. - -#### Data points - -The data sent via VS OpenTelemetry is neither a subset neither a superset of what is sent to SDK telemetry and it is not a purpose of this design to unify them. - -##### Basic info - -- Build duration -- Host -- Build success/failure -- Version -- Target (hashed) - -##### Evnironment - -- SAC (Smart app control) enabled - -##### Features - -- BuildCheck enabled -- Tasks runtimes and memory usage -- Tasks summary - whether they come from Nuget or are custom -- Targets summary - how many loaded and executed, how many come from nuget, how many come from metaproject - -The design should allow for easy instrumentation of additional data points. -> current implementation has only one datapoint and that is the whole build `vs/msbuild/build`, the instrumentaiton of additional datapoints is gated by first checking that telemetry is running and using `Activity` classes only in helper methods gated by `[MethodImpl(MethodImplOptions.NoInlining)]` to avoid System.Diagnostics.DiagnosticSource dll load. - -## Core `dotnet build` scenario - -- Telemetry should not be collected via VS OpenTelemetry mechanism because it's already collected in sdk. -- opt in to initialize the ActivitySource to avoid degrading performance. -- [baronfel/otel-startup-hook: A .NET CLR Startup Hook that exports OpenTelemetry metrics via the OTLP Exporter to an OpenTelemetry Collector](https://github.com/baronfel/otel-startup-hook/) and similar enable collecting telemetry data locally by listening to the ActivitySource prefix defined in MSBuild. - -> this hook can be used when the customer specifies that they want to listen to the prefix `Microsoft.VisualStudio.OpenTelemetry.MSBuild`, opt in by setting environment variables `MSBUILD_TELEMETRY_OPTIN=1`,`MSBUILD_TELEMETRY_SAMPLE_RATE=1.0` - -## Standalone MSBuild.exe scenario - -- Initialize and finalize in Xmake.cs - ActivitySource, TracerProvider, VS Collector -- overhead of starting VS collector is nonzero -- head sampling should avoid initializing if not sampled - -## VS in proc (devenv) scenario - -- VS can call `BuildManager` in a thread unsafe way the telemetry implementation has to be mindful of [BuildManager instances acquire its own BuildTelemetry instance by rokonec · Pull Request #8444 · dotnet/msbuild](https://github.com/dotnet/msbuild/pull/8444) - - ensure no race conditions in initialization - - only 1 TracerProvider with VS defined processing should exist -- Visual Studio should be responsible for having a running collector, we don't want this overhead in MSBuild and eventually many will use it - -> this was not achieved in 17.14 so we start collector every time - -## Implementation and MSBuild developer experience - -### ActivitySource names - -- Microsoft.VisualStudio.OpenTelemetry.MSBuild.Default - -### Sampling - -Our estimation from VS and SDK data is that there are 10M-100M build events per day. -For proportion estimation (of fairly common occurence in the builds), with not very strict confidnece (95%) and margin for error (5%) sampling 1:25000 would be enough. - -- this would apply for the DefaultActivitySource -- other ActivitySources could be sampled more frequently to get enough data -- Collecting has a cost, especially in standalone scenario where we have to start the collector. We might decide to undersample in standalone to avoid performance frequent impact. -- We want to avoid that cost when not sampled, therefore we prefer head sampling. -- Enables opt-in and opt-out for guaranteed sample or not sampled. -- nullable ActivitySource, using `?` when working with them, we can be initialized but not sampled -> it will not reinitialize but not collect telemetry. - -- for 17.14 we can't use the new OTel assemblies and their dependencies, so everything has to be opt in. -- eventually OpenTelemetry will be available and usable by default -- We can use experiments in VS to pass the environment variable to initialize - -> Targeted notification can be set that samples 100% of customers to which it is sent - -### Initialization at entrypoints - -- There are 2 entrypoints: - - for VS in BuildManager.BeginBuild - - for standalone in Xmake.cs Main - -### Exiting - -Force flush TracerProvider's exporter in BuildManager.EndBuild. -Dispose collector in Xmake.cs at the end of Main. - -### Configuration - -- Class that's responsible for configuring and initializing telemetry and handles optouts, holding tracer and collector. -- Wrapping source so that it has correct prefixes for VS backend to ingest. - -### Instrumenting - -2 ways of instrumenting: - -#### Instrument areas in code running in the main process - -```csharp -using (Activity? myActivity = OpenTelemetryManager.DefaultActivitySource?.StartActivity(TelemetryConstants.NameFromAConstantToAvoidAllocation)) -{ -// something happens here - -// add data to the trace -myActivity?.WithTag("SpecialEvent","fail") -} -``` - -Interface for classes holding telemetry data - -```csharp -IActivityTelemetryDataHolder data = new SomeData(); -... -myActivity?.WithTags(data); -``` - -> currently this should be gated in a separate method to avoid System.DiagnosticDiagnosticsource dll load. - -#### Default Build activity in EndBuild - -- this activity would always be created at the same point when sdk telemetry is sent in Core -- we can add data to it that we want in general builds -- the desired count of data from this should control the sample rate of DefaultActivitySource - -#### Multiple Activity Sources - -We want to create ActivitySources with different sample rates, this requires either implementation server side or a custom Processor. - -We potentially want apart from the Default ActivitySource: - -1. Other activity sources with different sample rates (in order to get significant data for rarer events such as custom tasks). -2. a way to override sampling decision - ad hoc starting telemetry infrastructure to catch rare events - -- Create a way of using a "HighPrioActivitySource" which would override sampling and initialize Collector in MSBuild.exe scenario/tracerprovider in VS. -- this would enable us to catch rare events - -> not implemented - -### Implementation details - -- `OpenTelemetryManager` - singleton that manages lifetime of OpenTelemetry objects listening to `Activity`ies, start by initializing in `Xmake` or `BuildManager`. -- Task and Target data is forwarded from worker nodes via `TelemetryForwarder` and `InternalTelemetryForwardingLogger` and then aggregated to stats and serialized in `TelemetryDataUtils` and attached to the default `vs/msbuild/build` event. - -## Future work when/if we decide to invest in telemetry again - -- avoid initializing/finalizing collector in VS when there is one running -- multiple levels of sampling for different types of events -- running by default with head sampling (simplifies instrumentation with `Activity`ies) -- implement anonymization consistently in an OTel processor and not ad hoc in each usage -- add datapoints helping perf optimization decisions/ reliability investigations diff --git a/documentation/specs/proposed/telemetry-onepager.md b/documentation/specs/proposed/telemetry-onepager.md deleted file mode 100644 index 5bc8f22f9ce..00000000000 --- a/documentation/specs/proposed/telemetry-onepager.md +++ /dev/null @@ -1,77 +0,0 @@ -# Telemetry - -We want to implement telemetry collection for VS/MSBuild.exe scenarios where we are currently not collecting data. VS OpenTelemetry initiative provides a good opportunity to use their infrastructure and library. -There is some data we collect via SDK which we want to make accessible. - -## Goals and Motivation - -We have limited data about usage of MSBuild by our customers in VS and no data about usage of standalone msbuild.exe. -This limits us in prioritization of features and scenarios to optimize performance for. -Over time we want to have comprehensive insight into how MSBuild is used in all scenarios. Collecting such a data without any constraints nor limitations would however be prohibitively expensive (from the data storage PoV and possibly as well from the client side performance impact PoV). Ability to sample / configure the collection is an important factor in deciding the instrumentation and collection tech stack. Implementing telemetry via VS OpenTelemetry initiative would give us this ability in the future. - -Goal: To have relevant data in that is actionable for decisions about development. Measuring real world performance impact of features (e.g. BuildCheck). Easily extensible telemetry infrastructure if we want to measure a new datapoint. - -## Impact -- Better planning of deployment of forces in MSBuild by product/team management. -- Customers can subscribe to telemetry locally to have data in standardized OpenTelemetry format - -## Stakeholders -- @Jan(Krivanek|Provaznik) design and implementation of telemetry via VS OTel. @ - using data we already have from SDK. -- @maridematte - documenting + dashboarding currently existing datapoints. -- MSBuild Team+Management – want insights from builds in VS -- VS OpenTelemetry team – provide support for VS OpenTelemetry collector library, want successful adoption -- SourceBuild – consulting and approving usage of OpenTelemetry -- MSBuild PM @baronfel – representing customers who want to monitor their builds locally - -### V1 Successful handover -- Shipped to Visual Studio -- Data queryable in Kusto -- Dashboards (even for pre-existing data - not introduced by this work) -- Customers are able to monitor with OpenTelemetry collector of choice (can be cut) - -## Risks -- Performance regression risks - it's another thing MSBuild would do and if the perf hit would be too bad it would need mitigation effort. -- It introduces a closed source dependency for VS and MSBuild.exe distribution methods which requires workarounds to remain compatible with SourceBuild policy (conditional compilation/build). -- Using a new VS API - might have gaps -- storage costs -- Potential additional costs and delays due to compliance with SourceBuild/VS data. - -## V1 Cost -5 months of .5 developer's effort ~ 50 dev days (dd) - -20-30dd JanPro OTel design + implementation, 10-15dd JanK design + implementation, 5-10dd Mariana/someone getting available data in order/"data science"/dashboards + external documentation - -Uncertainties: -It’s an exploratory project for VS OpenTelemetry, we'll be their first OSS component, so there might come up issues. SourceBuild compliance could introduce delays. - -## Plan -### V1 scope -- Collected data point definition -- Instrumented data points (as an example how the instrumentation and collection works) -- Telemetry sent to VS Telemetry in acceptable quantity -- Dashboards for collected data -- Hooking of customer's telemetry collection -- Documenting and leveraging pre-existing telemetry - -#### Out of scope -- Unifying telemetry for SDK MSBuild and MSBuild.exe/VS MSBuild. -- Thorough instrumentation of MSBuild -- Using MSBuild server -- Distributed tracing - -### Detailed cost -- Prototyping the libraries/mechanism for collecting telemetry data (month 1) 10dd - -- Defining usful data points (month 1) 5dd - -- Design and approval of hooking VSTelemetry collectors and OTel collectors (month 2) 10dd - -- Formalizing, agreeing to sourcebuild and other external requirements (month 2) 5dd - -- Instrumenting MSBuild with defined datapoints (month 3) 7dd - -- Creating dashboards/insights (month 4) 5dd - -- Documenting for customers how to hook their own telemetry collection (month 4) 3dd - -- Buffer for discovered issues (VSData Platform, SourceBuild, OpenTelemetry) and more investments (month 5) 5dd diff --git a/eng/Signing.props b/eng/Signing.props index b2e4bff8ffe..d46e57e8e34 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -11,12 +11,6 @@ - - - - - - diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index e23b9cda866..9ea881f0fb4 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -463,9 +463,6 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) /// Thrown if a build is already in progress. public void BeginBuild(BuildParameters parameters) { -#if NETFRAMEWORK - TelemetryManager.Instance.Initialize(isStandalone: false); -#endif if (_previousLowPriority != null) { if (parameters.LowPriority != _previousLowPriority) @@ -1115,9 +1112,8 @@ public void EndBuild() _buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement; loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties()); -#if NETFRAMEWORK EndBuildTelemetry(); -#endif + // Clean telemetry to make it ready for next build submission. _buildTelemetry = null; } @@ -1160,17 +1156,15 @@ void SerializeCaches() } } -#if NETFRAMEWORK - [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - using IActivity? activity = TelemetryManager.Instance.StartActivity(TelemetryConstants.Build) + using IActivity? activity = TelemetryManager.Instance?.DefaultActivitySource + ?.StartActivity(TelemetryConstants.Build) ?.SetTags(_buildTelemetry) ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( includeTasksDetails: !Traits.Instance.ExcludeTasksDetailsFromTelemetry, includeTargetDetails: false)); } -#endif /// /// Convenience method. Submits a lone build request and blocks until results are available. @@ -3013,6 +3007,8 @@ private ILoggingService CreateLoggingService( if (_buildParameters.IsTelemetryEnabled) { + TelemetryManager.Instance.Initialize(isStandalone: false); + // We do want to dictate our own forwarding logger (otherwise CentralForwardingLogger with minimum transferred importance MessageImportance.Low is used) // In the future we might optimize for single, in-node build scenario - where forwarding logger is not needed (but it's just quick pass-through) LoggerDescription forwardingLoggerDescription = new LoggerDescription( @@ -3245,6 +3241,8 @@ private void Dispose(bool disposing) s_singletonInstance = null; } + TelemetryManager.Instance?.Dispose(); + _disposed = true; } } diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 68b59de6ee8..a06d23c2862 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2430,9 +2430,6 @@ Utilization: {0} Average Utilization: {1:###.0} succeeded: {0} {0} whole number - - Loading telemetry libraries failed with exception: {0}. - Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 8bee6a2aafa..d8a9c39219f 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -652,11 +652,6 @@ Metoda {0} se nedá zavolat s kolekcí, která obsahuje prázdné cílové názvy nebo názvy null. - - Loading telemetry libraries failed with exception: {0}. - Načítání knihoven telemetrie se nezdařilo s výjimkou: {0}. - - Output Property: Výstupní vlastnost: diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index d645fa63c01..5a9c301f706 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -652,11 +652,6 @@ Die Methode "{0}" kann nicht mit einer Sammlung aufgerufen werden, die NULL oder leere Zielnamen enthält. - - Loading telemetry libraries failed with exception: {0}. - Fehler beim Laden von Telemetriebibliotheken. Ausnahme:{0}. - - Output Property: Ausgabeeigenschaft: diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index 9999f675f1b..0bab8ced519 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -652,11 +652,6 @@ No se puede llamar al método {0} con una colección que contiene nombres de destino nulos o vacíos. - - Loading telemetry libraries failed with exception: {0}. - Error al cargar las bibliotecas de telemetría con la excepción: {0}. - - Output Property: Propiedad de salida: diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 61cb18b44b3..1fd70825bc7 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -652,11 +652,6 @@ Impossible d'appeler la méthode {0} avec une collection contenant des noms de cibles qui ont une valeur null ou qui sont vides. - - Loading telemetry libraries failed with exception: {0}. - Nous n’avons pas pu charger les bibliothèques de télémétrie avec l’exception : {0}. - - Output Property: Propriété de sortie : diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 6d747503398..0d7bd82eceb 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -652,11 +652,6 @@ Non è possibile chiamare il metodo {0} con una raccolta contenente nomi di destinazione Null o vuoti. - - Loading telemetry libraries failed with exception: {0}. - Caricamento delle librerie di telemetria non riuscito con eccezione: {0}. - - Output Property: Proprietà di output: diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index 16e5023c578..8a0e3d17a13 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -652,11 +652,6 @@ Null または空のターゲット名を含むコレクションを指定してメソッド {0} を呼び出すことはできません。 - - Loading telemetry libraries failed with exception: {0}. - テレメトリ ライブラリの読み込みが次の例外で失敗しました: {0}。 - - Output Property: プロパティの出力: diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index 32ffaf8a51b..121fa05f8ec 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -652,11 +652,6 @@ null 또는 빈 대상 이름을 포함하는 컬렉션을 사용하여 {0} 메서드를 호출할 수 없습니다. - - Loading telemetry libraries failed with exception: {0}. - 예외 {0}(으)로 인해 원격 분석 라이브러리를 로드하지 못했습니다. - - Output Property: 출력 속성: diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 9cb87fdc994..7aca39c9198 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -652,11 +652,6 @@ Metody {0} nie można wywołać przy użyciu kolekcji zawierającej nazwy docelowe o wartości null lub puste. - - Loading telemetry libraries failed with exception: {0}. - Ładowanie bibliotek telemetrii nie powiodło się. Wyjątek: {0}. - - Output Property: Właściwość danych wyjściowych: diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index 6269fc1664d..d07633c489a 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -652,11 +652,6 @@ O método {0} não pode ser chamado com uma coleção que contém nomes de destino nulos ou vazios. - - Loading telemetry libraries failed with exception: {0}. - Falha ao carregar as bibliotecas de telemetria com a exceção: {0}. - - Output Property: Propriedade de Saída: diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 8cf9e87e539..4c151a9d29a 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -652,11 +652,6 @@ Метод {0} не может быть вызван с коллекцией, содержащей целевые имена, которые пусты или равны NULL. - - Loading telemetry libraries failed with exception: {0}. - Не удалось загрузить библиотеки телеметрии с исключением: {0}. - - Output Property: Выходное свойство: diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 475bd25e46e..97cb97c6eef 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -652,11 +652,6 @@ {0} metosu null veya boş hedef adları içeren bir koleksiyonla çağrılamaz. - - Loading telemetry libraries failed with exception: {0}. - Telemetri kitaplıklarının yüklenmesi şu hayatla başarısız oldu: {0}. - - Output Property: Çıkış Özelliği: diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index dcbd2aeb783..e2107b0f3ce 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -652,11 +652,6 @@ 无法使用包含 null 或空目标名称的集合调用方法 {0}。 - - Loading telemetry libraries failed with exception: {0}. - 加载遥测库失败,出现异常: {0}。 - - Output Property: 输出属性: diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index d0bf2ee9add..905e965c76c 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -652,11 +652,6 @@ 無法使用內含 null 或空白目標名稱的集合呼叫方法 {0}。 - - Loading telemetry libraries failed with exception: {0}. - 載入遙測程式庫時發生例外狀況: {0}。 - - Output Property: 輸出屬性: diff --git a/src/Framework/Telemetry/TelemetryItem.cs b/src/Framework/Telemetry/BuildInsights.cs similarity index 98% rename from src/Framework/Telemetry/TelemetryItem.cs rename to src/Framework/Telemetry/BuildInsights.cs index 4b8fa262f98..50858c09323 100644 --- a/src/Framework/Telemetry/TelemetryItem.cs +++ b/src/Framework/Telemetry/BuildInsights.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK - using System.Collections.Generic; using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils; @@ -39,5 +37,3 @@ internal record TaskCategoryStats(TaskStatsInfo? Total, TaskStatsInfo? FromNuget internal record TaskStatsInfo(int ExecutionsCount, double TotalMilliseconds, long TotalMemoryBytes); } - -#endif diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 33272cb643b..1db7edbaba2 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -10,10 +10,7 @@ namespace Microsoft.Build.Framework.Telemetry /// /// Telemetry of build. /// - internal class BuildTelemetry : TelemetryBase -#if NETFRAMEWORK - , IActivityTelemetryDataHolder -#endif + internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder { public override string EventName => "build"; @@ -108,9 +105,8 @@ internal class BuildTelemetry : TelemetryBase /// public string? BuildEngineFrameworkName { get; set; } -#if NETFRAMEWORK /// - /// Create a list of properties sent to VS telemetry with the information whether they should be hashed. + /// Create a list of properties sent to VS telemetry. /// public Dictionary GetActivityProperties() { @@ -145,7 +141,6 @@ void AddIfNotNull(string key, object? value) } } } -#endif public override IDictionary GetProperties() { diff --git a/src/Framework/Telemetry/DiagnosticActivity.cs b/src/Framework/Telemetry/DiagnosticActivity.cs new file mode 100644 index 00000000000..fadb67ad039 --- /dev/null +++ b/src/Framework/Telemetry/DiagnosticActivity.cs @@ -0,0 +1,65 @@ +// 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; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Wraps a and implements . + /// + internal class DiagnosticActivity : IActivity + { + private readonly Activity _activity; + private bool _disposed; + + public DiagnosticActivity(Activity activity) + { + _activity = activity; + } + + public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) + { + Dictionary? tags = dataHolder?.GetActivityProperties(); + if (tags != null) + { + foreach (KeyValuePair tag in tags) + { + SetTag(tag.Key, tag.Value); + } + } + + return this; + } + + public IActivity? SetTag(string key, object? value) + { + if (value != null) + { + _activity.SetTag(key, value); + } + + return this; + } + + public IActivity? AddEvent(ActivityEvent activityEvent) + { + _activity.AddEvent(activityEvent); + + return this; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _activity.Dispose(); + + _disposed = true; + } + } +} diff --git a/src/Framework/Telemetry/IActivity.cs b/src/Framework/Telemetry/IActivity.cs new file mode 100644 index 00000000000..6237fa6dd9d --- /dev/null +++ b/src/Framework/Telemetry/IActivity.cs @@ -0,0 +1,36 @@ +// 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.Diagnostics; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Represents an activity for telemetry tracking. + /// + internal interface IActivity : IDisposable + { + /// + /// Sets a tag on the activity. + /// + /// Telemetry data holder. + /// The activity instance for method chaining. + IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder); + + /// + /// Sets a tag on the activity. + /// + /// The tag key. + /// The tag value. + /// The activity instance for method chaining. + IActivity? SetTag(string key, object? value); + + /// + /// Adds an event to the activity. + /// + /// The event to add. + /// The activity instance for method chaining. + IActivity? AddEvent(ActivityEvent activityEvent); + } +} diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs index 68c7d672f67..90fd7e21875 100644 --- a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK - using System.Collections.Generic; using System.Diagnostics; @@ -15,5 +13,3 @@ internal interface IActivityTelemetryDataHolder { Dictionary GetActivityProperties(); } - -#endif diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs new file mode 100644 index 00000000000..891e85c781f --- /dev/null +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK +using Microsoft.VisualStudio.Telemetry; +#else +using System.Diagnostics; +#endif + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Wrapper class for ActivitySource with a method that wraps Activity name with MSBuild prefix. + /// On .NET Framework, activities are also forwarded to VS Telemetry. + /// + internal class MSBuildActivitySource + { +#if NETFRAMEWORK + private readonly TelemetrySession? _telemetrySession; + + public MSBuildActivitySource(TelemetrySession? telemetrySession) + { + _telemetrySession = telemetrySession; + } +#else + private readonly ActivitySource _source; + + public MSBuildActivitySource(string name) + { + _source = new ActivitySource(name); + } +#endif + + /// + /// Starts a new activity with the appropriate telemetry prefix. + /// + /// Name of the telemetry event without prefix. + /// An wrapping the underlying Activity, or null if not sampled. + public IActivity? StartActivity(string name) + { + string eventName = $"{TelemetryConstants.EventPrefix}{name}"; + +#if NETFRAMEWORK + TelemetryScope? operation = _telemetrySession?.StartOperation(eventName); + return operation != null ? new VsTelemetryActivity(operation) : null; +#else + Activity? activity = Activity.Current?.HasRemoteParent == true + ? _source.StartActivity(eventName, ActivityKind.Internal, parentId: Activity.Current.ParentId) + : _source.StartActivity(eventName); + + if (activity == null) + { + return null; + } + + activity.SetTag("SampleRate", TelemetryConstants.DefaultSampleRate); + + return new DiagnosticActivity(activity); +#endif + } + } +} diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index 08b383e8b9e..14592865a68 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -17,6 +17,16 @@ internal static class TelemetryConstants /// public const string PropertyPrefix = "VS.MSBuild."; + /// + /// "Microsoft.Build.Telemetry.*" namespace is required by VS exporting/collection. + /// + public const string ActivitySourceNamespacePrefix = "Microsoft.Build.Telemetry"; + + /// + /// Namespace of the default ActivitySource handling e.g. End of build telemetry. + /// + public const string DefaultActivitySourceNamespace = $"{ActivitySourceNamespacePrefix}Default"; + /// /// For VS OpenTelemetry Collector to apply the correct privacy policy. /// diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index e36496bc914..31dd22fdee4 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if NETFRAMEWORK using System.Collections.Generic; using System.Security.Cryptography; @@ -107,7 +106,7 @@ public static string Hash(string text) #if NET byte[] hash = SHA256.HashData(bytes); #if NET9_0_OR_GREATER - return Convert.ToHexStringLower(hash); + return System.Convert.ToHexStringLower(hash); #else return Convert.ToHexString(hash).ToLowerInvariant(); #endif @@ -306,5 +305,3 @@ Dictionary IActivityTelemetryDataHolder.GetActivityProperties() } } } - -#endif diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 0639f50f81c..44df3d27555 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -2,28 +2,50 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NETFRAMEWORK - using Microsoft.VisualStudio.Telemetry; +#endif namespace Microsoft.Build.Framework.Telemetry { + /// + /// Manages telemetry collection and reporting for MSBuild. + /// This class provides a centralized way to initialize, configure, and manage telemetry sessions. + /// + /// + /// The TelemetryManager is a singleton that handles both standalone and integrated telemetry scenarios. + /// On .NET Framework, it integrates with Visual Studio telemetry services. + /// On .NET Core it provides a lightweight telemetry implementation though exposing an activity source. + /// internal class TelemetryManager { +#if NETFRAMEWORK + // Telemetry API key for Visual Studio telemetry service. private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; private static TelemetrySession? _telemetrySession; - +#endif private static bool s_disposed; private TelemetryManager() { } + /// + /// Optional activity source for MSBuild or other telemetry usage. + /// + public MSBuildActivitySource? DefaultActivitySource { get; private set; } + public static TelemetryManager Instance { get; } = new TelemetryManager(); public void Initialize(bool isStandalone) { - if (IsOptOut() || _telemetrySession != null) + if (IsOptOut()) + { + return; + } + +#if NETFRAMEWORK + if (_telemetrySession != null) { return; } @@ -33,19 +55,16 @@ public void Initialize(bool isStandalone) _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); TelemetryService.DefaultSession.IsOptedIn = true; TelemetryService.DefaultSession.Start(); - - return; + } + else + { + _telemetrySession = TelemetryService.DefaultSession; } - _telemetrySession = TelemetryService.DefaultSession; - } - - public IActivity? StartActivity(string name) - { - string eventName = $"{TelemetryConstants.EventPrefix}{name}"; - TelemetryScope? operation = _telemetrySession.StartOperation(eventName); - - return operation != null ? new VsTelemetryActivity(operation) : null; + DefaultActivitySource = new MSBuildActivitySource(_telemetrySession); +#else + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); +#endif } public void Dispose() @@ -55,16 +74,15 @@ public void Dispose() return; } +#if NETFRAMEWORK _telemetrySession?.Dispose(); - +#endif s_disposed = true; } /// /// Determines if the user has explicitly opted out of telemetry. /// - private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut || !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14); + private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut; } } - -#endif diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index bc3019b3e90..3d765ee056c 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -3,19 +3,21 @@ #if NETFRAMEWORK -using System; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.Build.Framework.Telemetry; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry { + /// + /// Represents a Visual Studio telemetry activity that wraps a . + /// This class provides an implementation of for the VS Telemetry system, + /// allowing telemetry data to be collected and sent when running on .NET Framework. + /// internal class VsTelemetryActivity : IActivity { private readonly TelemetryScope _scope; private TelemetryResult _result = TelemetryResult.Success; - private string? _resultSummary; private bool _disposed; @@ -45,6 +47,7 @@ internal class VsTelemetryActivity : IActivity return this; } + public IActivity? AddEvent(ActivityEvent activityEvent) { // VS Telemetry doesn't have a direct equivalent to ActivityEvent @@ -67,38 +70,10 @@ public void Dispose() } // End the operation - _scope.End(_result, _resultSummary); + _scope.End(_result); _disposed = true; } } } -/// -/// Represents an activity for telemetry tracking. -/// -internal interface IActivity : IDisposable -{ - /// - /// Sets a tag on the activity. - /// - /// Telemetry data holder. - /// The activity instance for method chaining. - IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder); - - /// - /// Sets a tag on the activity. - /// - /// The tag key. - /// The tag value. - /// The activity instance for method chaining. - IActivity? SetTag(string key, object? value); - - /// - /// Adds an event to the activity. - /// - /// The event to add. - /// The activity instance for method chaining. - IActivity? AddEvent(ActivityEvent activityEvent); -} - #endif diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 4ae8746977b..323009abb41 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -251,9 +251,7 @@ string[] args KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true}; // Initialize VSTelemetry -#if NETFRAMEWORK - TelemetryManager.Instance.Initialize(isStandalone: true); -#endif + TelemetryManager.Instance?.Initialize(isStandalone: true); using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); @@ -302,9 +300,8 @@ string[] args DumpCounters(false /* log to console */); } -#if NETFRAMEWORK - TelemetryManager.Instance.Dispose(); -#endif + TelemetryManager.Instance?.Dispose(); + return exitCode; } diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 9bf8b014e38..977daa4650f 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -56,14 +56,6 @@ - - - - - - - - @@ -104,90 +96,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/MSBuild/app.config b/src/MSBuild/app.config index 9c41d0b862c..6f2cba28e6c 100644 --- a/src/MSBuild/app.config +++ b/src/MSBuild/app.config @@ -39,10 +39,6 @@ - - - - @@ -72,10 +68,6 @@ - - - - diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 9639105ad84..4d2e84f524e 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -41,7 +41,6 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.VisualStudio.SolutionPersistence.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)RuntimeContracts.dll file source=$(X86BinPath)System.Buffers.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 - file source=$(X86BinPath)System.Diagnostics.DiagnosticSource.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 file source=$(X86BinPath)System.Formats.Nrbf.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.IO.Pipelines.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)System.Memory.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 @@ -90,19 +89,6 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.WinFx.targets file source=$(X86BinPath)Microsoft.WorkflowBuildExtensions.targets file source=$(X86BinPath)Microsoft.VisualStudio.Utilities.Internal.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.Binder.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.DependencyInjection.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.DependencyInjection.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Logging.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Options.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Options.ConfigurationExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Primitives.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Microsoft.Extensions.Diagnostics.Abstractions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 - file source=$(X86BinPath)Newtonsoft.Json.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 folder InstallDir:\MSBuild\Current\Bin\MSBuild file source=$(X86BinPath)\MSBuild\Microsoft.Build.Core.xsd From f736dd875d5fd14f01b358629245977956830510 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 4 Dec 2025 16:19:29 +0100 Subject: [PATCH 19/47] temp bind to MSBUILD_TELEMETRY_OPTIN --- src/Build/BackEnd/BuildManager/BuildManager.cs | 3 +++ src/Framework/Telemetry/TelemetryManager.cs | 7 ++++++- src/Framework/Traits.cs | 2 +- src/MSBuild/XMake.cs | 8 ++++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 9ea881f0fb4..1a0cf277725 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -3005,6 +3005,9 @@ private ILoggingService CreateLoggingService( forwardingLoggers = forwardingLoggers?.Concat(forwardingLogger) ?? forwardingLogger; } + // respect value coming from environment variable. + _buildParameters.IsTelemetryEnabled |= Traits.Instance.TelemetryOptIn; + if (_buildParameters.IsTelemetryEnabled) { TelemetryManager.Instance.Initialize(isStandalone: false); diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 44df3d27555..1451b13ce7f 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -83,6 +83,11 @@ public void Dispose() /// /// Determines if the user has explicitly opted out of telemetry. /// - private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut; + private bool IsOptOut() => +#if NETFRAMEWORK + Traits.Instance.FrameworkTelemetryOptOut; +#else + Traits.Instance.SdkTelemetryOptOut; +#endif } } diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 8cbf21feef1..cbea92608f1 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -160,7 +160,7 @@ public Traits() public bool EnableTargetOutputLogging = IsEnvVarOneOrTrue("MSBUILDTARGETOUTPUTLOGGING"); - // for VS17.14 + // for VS18.* public readonly bool TelemetryOptIn = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTIN"); public readonly bool SlnParsingWithSolutionPersistenceOptIn = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE")); diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 323009abb41..1a3788a9eb6 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -250,8 +250,12 @@ string[] args // Initialize new build telemetry and record start of this build. KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true}; - // Initialize VSTelemetry - TelemetryManager.Instance?.Initialize(isStandalone: true); + // Initialize Telemetry + // Temporarily only enable telemetry when environment variable set to "1". + if (Traits.Instance.TelemetryOptIn) + { + TelemetryManager.Instance?.Initialize(isStandalone: true); + } using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); From 833708738fb3d529c3f299e09505e9c6a397d2a8 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 5 Dec 2025 10:54:00 +0100 Subject: [PATCH 20/47] prevents the JIT from inlining and loading the VS assembly prematurely --- src/Framework/Telemetry/TelemetryManager.cs | 111 +++++++++++++++--- src/Framework/Telemetry/VsTelemetrySession.cs | 63 ++++++++++ 2 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 src/Framework/Telemetry/VsTelemetrySession.cs diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 1451b13ce7f..54ca550913f 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NETFRAMEWORK +using System; +using System.IO; +using System.Runtime.CompilerServices; using Microsoft.VisualStudio.Telemetry; #endif @@ -14,16 +17,11 @@ namespace Microsoft.Build.Framework.Telemetry /// /// The TelemetryManager is a singleton that handles both standalone and integrated telemetry scenarios. /// On .NET Framework, it integrates with Visual Studio telemetry services. - /// On .NET Core it provides a lightweight telemetry implementation though exposing an activity source. + /// On .NET Core it provides a lightweight telemetry implementation through exposing an activity source. /// internal class TelemetryManager { -#if NETFRAMEWORK - // Telemetry API key for Visual Studio telemetry service. - private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; - - private static TelemetrySession? _telemetrySession; -#endif + private static bool s_initialized; private static bool s_disposed; private TelemetryManager() @@ -39,34 +37,48 @@ private TelemetryManager() public void Initialize(bool isStandalone) { - if (IsOptOut()) + if (s_initialized) { return; } -#if NETFRAMEWORK - if (_telemetrySession != null) + s_initialized = true; + + if (IsOptOut()) { return; } - if (isStandalone) +#if NETFRAMEWORK + try { - _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); - TelemetryService.DefaultSession.IsOptedIn = true; - TelemetryService.DefaultSession.Start(); + InitializeVsTelemetry(isStandalone); } - else + catch (Exception ex) when ( + ex is FileNotFoundException or + FileLoadException or + TypeLoadException) { - _telemetrySession = TelemetryService.DefaultSession; + // Microsoft.VisualStudio.Telemetry is not available outside VS. + // This is expected in standalone MSBuild.exe scenarios. + DefaultActivitySource = null; } - - DefaultActivitySource = new MSBuildActivitySource(_telemetrySession); #else DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); #endif } +#if NETFRAMEWORK + /// + /// Initializes Visual Studio telemetry. + /// This method is deliberately not inlined to ensure + /// the Microsoft.VisualStudio.Telemetry assembly is only loaded when this method is called, + /// allowing the calling code to catch assembly loading exceptions. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void InitializeVsTelemetry(bool isStandalone) => DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); +#endif + public void Dispose() { if (s_disposed) @@ -75,11 +87,26 @@ public void Dispose() } #if NETFRAMEWORK - _telemetrySession?.Dispose(); + try + { + DisposeVsTelemetry(); + } + catch (Exception ex) when ( + ex is FileNotFoundException or + FileLoadException or + TypeLoadException) + { + // Assembly was never loaded, nothing to dispose. + } #endif s_disposed = true; } +#if NETFRAMEWORK + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DisposeVsTelemetry() => VsTelemetryInitializer.Dispose(); +#endif + /// /// Determines if the user has explicitly opted out of telemetry. /// @@ -90,4 +117,50 @@ private bool IsOptOut() => Traits.Instance.SdkTelemetryOptOut; #endif } + +#if NETFRAMEWORK + /// + /// Isolated class that references Microsoft.VisualStudio.Telemetry types. + /// This separation ensures the VS Telemetry assembly is only loaded when methods + /// on this class are actually invoked. + /// + internal static class VsTelemetryInitializer + { + // Telemetry API key for Visual Studio telemetry service. + private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; + + private static TelemetrySession? _telemetrySession; + private static bool _ownsSession; + + public static MSBuildActivitySource Initialize(bool isStandalone) + { + if (isStandalone) + { + _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); + TelemetryService.DefaultSession.IsOptedIn = true; + TelemetryService.DefaultSession.Start(); + _ownsSession = true; + } + else + { + _telemetrySession = TelemetryService.DefaultSession; + _ownsSession = false; + } + + return new MSBuildActivitySource(_telemetrySession); + } + + public static void Dispose() + { + // Only dispose the session if we created it (standalone scenario). + // In VS, the session is owned by VS and should not be disposed by MSBuild. + if (_ownsSession) + { + _telemetrySession?.Dispose(); + } + + _telemetrySession = null; + } + } +#endif } diff --git a/src/Framework/Telemetry/VsTelemetrySession.cs b/src/Framework/Telemetry/VsTelemetrySession.cs new file mode 100644 index 00000000000..862952acd87 --- /dev/null +++ b/src/Framework/Telemetry/VsTelemetrySession.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETFRAMEWORK +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// VS Telemetry implementation. This class is in a separate file to ensure + /// the Microsoft.VisualStudio.Telemetry assembly is only loaded when this type is accessed. + /// + internal sealed class VsTelemetrySession : ITelemetrySession + { + private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; + + private readonly TelemetrySession _session; + private bool _disposed; + private readonly bool _ownsSession; + + private VsTelemetrySession(TelemetrySession session, bool ownsSession) + { + _session = session; + _ownsSession = ownsSession; + } + + public static ITelemetrySession Create(bool isStandalone) + { + if (isStandalone) + { + TelemetrySession session = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); + TelemetryService.DefaultSession.IsOptedIn = true; + TelemetryService.DefaultSession.Start(); + return new VsTelemetrySession(session, ownsSession: true); + } + + return new VsTelemetrySession(TelemetryService.DefaultSession, ownsSession: false); + } + + public IActivity? StartActivity(string name) + { + string eventName = $"{TelemetryConstants.EventPrefix}{name}"; + TelemetryScope? operation = _session.StartOperation(eventName); + return operation != null ? new VsTelemetryActivity(operation) : null; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + if (_ownsSession) + { + _session.Dispose(); + } + + _disposed = true; + } + } +} +#endif From 1157cd939864bb5372c74528dbe56568168d107f Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 5 Dec 2025 10:54:28 +0100 Subject: [PATCH 21/47] remove extra file --- src/Framework/Telemetry/VsTelemetrySession.cs | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 src/Framework/Telemetry/VsTelemetrySession.cs diff --git a/src/Framework/Telemetry/VsTelemetrySession.cs b/src/Framework/Telemetry/VsTelemetrySession.cs deleted file mode 100644 index 862952acd87..00000000000 --- a/src/Framework/Telemetry/VsTelemetrySession.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NETFRAMEWORK -using Microsoft.VisualStudio.Telemetry; - -namespace Microsoft.Build.Framework.Telemetry -{ - /// - /// VS Telemetry implementation. This class is in a separate file to ensure - /// the Microsoft.VisualStudio.Telemetry assembly is only loaded when this type is accessed. - /// - internal sealed class VsTelemetrySession : ITelemetrySession - { - private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; - - private readonly TelemetrySession _session; - private bool _disposed; - private readonly bool _ownsSession; - - private VsTelemetrySession(TelemetrySession session, bool ownsSession) - { - _session = session; - _ownsSession = ownsSession; - } - - public static ITelemetrySession Create(bool isStandalone) - { - if (isStandalone) - { - TelemetrySession session = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); - TelemetryService.DefaultSession.IsOptedIn = true; - TelemetryService.DefaultSession.Start(); - return new VsTelemetrySession(session, ownsSession: true); - } - - return new VsTelemetrySession(TelemetryService.DefaultSession, ownsSession: false); - } - - public IActivity? StartActivity(string name) - { - string eventName = $"{TelemetryConstants.EventPrefix}{name}"; - TelemetryScope? operation = _session.StartOperation(eventName); - return operation != null ? new VsTelemetryActivity(operation) : null; - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - if (_ownsSession) - { - _session.Dispose(); - } - - _disposed = true; - } - } -} -#endif From a2bb6dad50edef86325391211fc135a4890e1c16 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 5 Dec 2025 16:48:58 +0100 Subject: [PATCH 22/47] remove System.Diagnostics.DiagnosticSource reference --- src/Framework/Microsoft.Build.Framework.csproj | 1 - src/MSBuild/MSBuild.csproj | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index dc1ba4af67a..606d6855d00 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -37,7 +37,6 @@ - diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index e43039e8e6c..6394ac6ffb9 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -191,7 +191,6 @@ - From cfb6fa391347b28383adf0dd0c73ce61a1d53c5d Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:47:34 +0100 Subject: [PATCH 23/47] Add PackageReference for DiagnosticSource Added PackageReference for System.Diagnostics.DiagnosticSource for consistency. --- src/MSBuild/MSBuild.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 6394ac6ffb9..e43039e8e6c 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -191,6 +191,7 @@ + From 9e2e6dc8712d90330d33d4516cacb3963fddc80b Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 8 Dec 2025 13:50:07 +0100 Subject: [PATCH 24/47] fix reference resolution --- .../Microsoft.Build.Framework.csproj | 1 + src/Framework/Telemetry/TelemetryConstants.cs | 7 +- src/Framework/Telemetry/TelemetryManager.cs | 114 ++++++++++-------- src/MSBuild/XMake.cs | 2 +- 4 files changed, 68 insertions(+), 56 deletions(-) diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 606d6855d00..dc1ba4af67a 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index 14592865a68..94e194b9e48 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -3,7 +3,7 @@ namespace Microsoft.Build.Framework.Telemetry; /// -/// Constants for VS OpenTelemetry for basic configuration and appropriate naming for VS exporting/collection. +/// Constants for VS Telemetry for basic configuration and appropriate naming for VS exporting/collection. /// internal static class TelemetryConstants { @@ -27,11 +27,6 @@ internal static class TelemetryConstants /// public const string DefaultActivitySourceNamespace = $"{ActivitySourceNamespacePrefix}Default"; - /// - /// For VS OpenTelemetry Collector to apply the correct privacy policy. - /// - public const string VSMajorVersion = "18.0"; - /// /// Sample rate for the default namespace. /// 1:25000 gives us sample size of sufficient confidence with the assumption we collect the order of 1e7 - 1e8 events per day. diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 54ca550913f..c49cf792008 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -21,6 +21,11 @@ namespace Microsoft.Build.Framework.Telemetry /// internal class TelemetryManager { + /// + /// Lock object for thread-safe initialization and disposal. + /// + private static readonly object s_lock = new object(); + private static bool s_initialized; private static bool s_disposed; @@ -37,35 +42,38 @@ private TelemetryManager() public void Initialize(bool isStandalone) { - if (s_initialized) + lock (s_lock) { - return; - } + if (s_initialized) + { + return; + } - s_initialized = true; + s_initialized = true; - if (IsOptOut()) - { - return; - } + if (IsOptOut()) + { + return; + } #if NETFRAMEWORK - try - { - InitializeVsTelemetry(isStandalone); - } - catch (Exception ex) when ( - ex is FileNotFoundException or - FileLoadException or - TypeLoadException) - { - // Microsoft.VisualStudio.Telemetry is not available outside VS. - // This is expected in standalone MSBuild.exe scenarios. - DefaultActivitySource = null; - } + try + { + InitializeVsTelemetry(isStandalone); + } + catch (Exception ex) when ( + ex is FileNotFoundException or + FileLoadException or + TypeLoadException) + { + // Microsoft.VisualStudio.Telemetry is not available outside VS. + // This is expected in standalone MSBuild.exe scenarios. + DefaultActivitySource = null; + } #else - DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); #endif + } } #if NETFRAMEWORK @@ -81,25 +89,28 @@ FileLoadException or public void Dispose() { - if (s_disposed) + lock (s_lock) { - return; - } + if (s_disposed) + { + return; + } #if NETFRAMEWORK - try - { - DisposeVsTelemetry(); - } - catch (Exception ex) when ( - ex is FileNotFoundException or - FileLoadException or - TypeLoadException) - { - // Assembly was never loaded, nothing to dispose. - } + try + { + DisposeVsTelemetry(); + } + catch (Exception ex) when ( + ex is FileNotFoundException or + FileLoadException or + TypeLoadException) + { + // Assembly was never loaded, nothing to dispose. + } #endif - s_disposed = true; + s_disposed = true; + } } #if NETFRAMEWORK @@ -110,7 +121,7 @@ FileLoadException or /// /// Determines if the user has explicitly opted out of telemetry. /// - private bool IsOptOut() => + private static bool IsOptOut() => #if NETFRAMEWORK Traits.Instance.FrameworkTelemetryOptOut; #else @@ -124,43 +135,48 @@ private bool IsOptOut() => /// This separation ensures the VS Telemetry assembly is only loaded when methods /// on this class are actually invoked. /// + /// + /// Thread-safety: All public methods on this class must be called under the + /// lock to ensure thread-safe access to static state. + /// Callers must not invoke or concurrently. + /// internal static class VsTelemetryInitializer { // Telemetry API key for Visual Studio telemetry service. private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; - private static TelemetrySession? _telemetrySession; - private static bool _ownsSession; + private static TelemetrySession? s_telemetrySession; + private static bool s_ownsSession; public static MSBuildActivitySource Initialize(bool isStandalone) { if (isStandalone) { - _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); + s_telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); TelemetryService.DefaultSession.IsOptedIn = true; TelemetryService.DefaultSession.Start(); - _ownsSession = true; + s_ownsSession = true; } else { - _telemetrySession = TelemetryService.DefaultSession; - _ownsSession = false; + s_telemetrySession = TelemetryService.DefaultSession; + s_ownsSession = false; } - return new MSBuildActivitySource(_telemetrySession); + return new MSBuildActivitySource(s_telemetrySession); } public static void Dispose() { // Only dispose the session if we created it (standalone scenario). // In VS, the session is owned by VS and should not be disposed by MSBuild. - if (_ownsSession) + if (s_ownsSession) { - _telemetrySession?.Dispose(); + s_telemetrySession?.Dispose(); } - _telemetrySession = null; + s_telemetrySession = null; } } #endif -} +} \ No newline at end of file diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 1a3788a9eb6..ed918842879 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -248,7 +248,7 @@ string[] args DebuggerLaunchCheck(); // Initialize new build telemetry and record start of this build. - KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true}; + KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true }; // Initialize Telemetry // Temporarily only enable telemetry when environment variable set to "1". From 8fdc90c84b5828129c6e9007a80e789049c497ce Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 8 Dec 2025 15:16:08 +0100 Subject: [PATCH 25/47] fix review comments --- .../Telemetry/MSBuildActivitySource.cs | 2 +- .../Telemetry/VSTelemetryActivity.cs | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs index 891e85c781f..50d5c214c2b 100644 --- a/src/Framework/Telemetry/MSBuildActivitySource.cs +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -42,7 +42,7 @@ public MSBuildActivitySource(string name) #if NETFRAMEWORK TelemetryScope? operation = _telemetrySession?.StartOperation(eventName); - return operation != null ? new VsTelemetryActivity(operation) : null; + return operation != null ? new VsTelemetryActivity(operation, _telemetrySession) : null; #else Activity? activity = Activity.Current?.HasRemoteParent == true ? _source.StartActivity(eventName, ActivityKind.Internal, parentId: Activity.Current.ParentId) diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index 3d765ee056c..129605ba386 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -17,11 +17,22 @@ namespace Microsoft.Build.Framework.Telemetry internal class VsTelemetryActivity : IActivity { private readonly TelemetryScope _scope; + private readonly TelemetrySession _session; private TelemetryResult _result = TelemetryResult.Success; private bool _disposed; - public VsTelemetryActivity(TelemetryScope scope) => _scope = scope; + public VsTelemetryActivity(TelemetryScope scope, TelemetrySession session) + { + _scope = scope; + _session = session; + } + + public IActivity? SetResult(TelemetryResult result) + { + _result = result; + return this; + } public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) { @@ -50,15 +61,15 @@ internal class VsTelemetryActivity : IActivity public IActivity? AddEvent(ActivityEvent activityEvent) { - // VS Telemetry doesn't have a direct equivalent to ActivityEvent - // We could create and immediately post a custom event if needed. + // VS Telemetry doesn't have a direct equivalent to ActivityEvent. + // We create and post a custom event to the session associated with this activity. var telemetryEvent = new TelemetryEvent(activityEvent.Name); foreach (KeyValuePair tag in activityEvent.Tags) { telemetryEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{tag.Key}"] = tag.Value; } - TelemetryService.DefaultSession.PostEvent(telemetryEvent); + _session.PostEvent(telemetryEvent); return this; } @@ -69,11 +80,10 @@ public void Dispose() return; } - // End the operation _scope.End(_result); _disposed = true; } } } -#endif +#endif \ No newline at end of file From 167f1fcf0aff423615653f9aeaea6d08e367339a Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 8 Dec 2025 15:26:17 +0100 Subject: [PATCH 26/47] fix error --- src/Framework/Telemetry/VSTelemetryActivity.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index 129605ba386..82f6bf70569 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -17,12 +17,12 @@ namespace Microsoft.Build.Framework.Telemetry internal class VsTelemetryActivity : IActivity { private readonly TelemetryScope _scope; - private readonly TelemetrySession _session; + private readonly TelemetrySession? _session; private TelemetryResult _result = TelemetryResult.Success; private bool _disposed; - public VsTelemetryActivity(TelemetryScope scope, TelemetrySession session) + public VsTelemetryActivity(TelemetryScope scope, TelemetrySession? session) { _scope = scope; _session = session; @@ -69,7 +69,8 @@ public VsTelemetryActivity(TelemetryScope scope, TelemetrySessio telemetryEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{tag.Key}"] = tag.Value; } - _session.PostEvent(telemetryEvent); + _session?.PostEvent(telemetryEvent); + return this; } @@ -86,4 +87,4 @@ public void Dispose() } } -#endif \ No newline at end of file +#endif From 24fbf04da663df9448124f045c4f0f483fa802f5 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 10:56:38 +0100 Subject: [PATCH 27/47] add MethodImplOptions.NoInlining for MSBuildActivitySource --- src/Framework/Telemetry/TelemetryManager.cs | 49 +++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index c49cf792008..4403f9eda51 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -2,12 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NETFRAMEWORK -using System; -using System.IO; using System.Runtime.CompilerServices; using Microsoft.VisualStudio.Telemetry; #endif +using System; + +using System.IO; +using System.Runtime.CompilerServices; + namespace Microsoft.Build.Framework.Telemetry { /// @@ -56,36 +59,34 @@ public void Initialize(bool isStandalone) return; } -#if NETFRAMEWORK - try - { - InitializeVsTelemetry(isStandalone); - } - catch (Exception ex) when ( - ex is FileNotFoundException or - FileLoadException or - TypeLoadException) - { - // Microsoft.VisualStudio.Telemetry is not available outside VS. - // This is expected in standalone MSBuild.exe scenarios. - DefaultActivitySource = null; - } -#else - DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); -#endif + TryInitializeTelemetry(isStandalone); } } -#if NETFRAMEWORK /// - /// Initializes Visual Studio telemetry. + /// Initializes MSBuild telemetry. /// This method is deliberately not inlined to ensure - /// the Microsoft.VisualStudio.Telemetry assembly is only loaded when this method is called, + /// the Telemetry related assemblies are only loaded when this method is called, /// allowing the calling code to catch assembly loading exceptions. /// [MethodImpl(MethodImplOptions.NoInlining)] - private void InitializeVsTelemetry(bool isStandalone) => DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); + private void TryInitializeTelemetry(bool isStandalone) + { + try + { +#if NETFRAMEWORK + DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); +#else + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); #endif + } + catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or TypeLoadException) + { + // Microsoft.VisualStudio.Telemetry or System.Diagnostics.DiagnosticSource might not be available outside of VS or dotnet. + // This is expected in standalone application scenarios. + DefaultActivitySource = null; + } + } public void Dispose() { @@ -179,4 +180,4 @@ public static void Dispose() } } #endif -} \ No newline at end of file +} From 502e272d4713a96ea52b43b972b4d162b0be61f8 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 10:59:04 +0100 Subject: [PATCH 28/47] usings cleanup --- src/Framework/Telemetry/TelemetryManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 4403f9eda51..4c29c7bc49e 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -7,9 +7,7 @@ #endif using System; - using System.IO; -using System.Runtime.CompilerServices; namespace Microsoft.Build.Framework.Telemetry { From 50b03cd3807f0ba281ef26b4b5ba481fda93a70c Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 10:59:35 +0100 Subject: [PATCH 29/47] usings cleanup --- src/Framework/Telemetry/TelemetryManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 4403f9eda51..4c29c7bc49e 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -7,9 +7,7 @@ #endif using System; - using System.IO; -using System.Runtime.CompilerServices; namespace Microsoft.Build.Framework.Telemetry { From c92814134ef9a7a9ac9485b37a9ff1f1ff47d783 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 11:25:06 +0100 Subject: [PATCH 30/47] fix usings --- src/Framework/Telemetry/TelemetryManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 4c29c7bc49e..0bc8931a862 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NETFRAMEWORK -using System.Runtime.CompilerServices; using Microsoft.VisualStudio.Telemetry; #endif using System; using System.IO; +using System.Runtime.CompilerServices; namespace Microsoft.Build.Framework.Telemetry { From 8f52bcc3ca431d1507967b63b13619a736bd77a1 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 11:26:06 +0100 Subject: [PATCH 31/47] fix usings --- src/Framework/Telemetry/TelemetryManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 0472e16106d..0bc8931a862 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -7,6 +7,7 @@ using System; using System.IO; +using System.Runtime.CompilerServices; namespace Microsoft.Build.Framework.Telemetry { From 884a1d086555f6c044fc6c7febf71e1c1d551aba Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:00:07 +0100 Subject: [PATCH 32/47] Add binding redirects for new dependencies --- src/MSBuild/app.amd64.config | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 977daa4650f..85f752a8a64 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -60,6 +60,16 @@ + + + + + + + + + + From a0d81a84eaa98d3314227cb39fc83ac42b5f1424 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 12:57:04 +0100 Subject: [PATCH 33/47] return telemetry tests --- .../Telemetry/Telemetry_Tests.cs | 302 ++++++++++++++++++ src/Framework/Telemetry/DiagnosticActivity.cs | 2 +- src/Framework/Traits.cs | 4 +- 3 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 src/Build.UnitTests/Telemetry/Telemetry_Tests.cs diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs new file mode 100644 index 00000000000..2f83939d588 --- /dev/null +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -0,0 +1,302 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text.Json; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Telemetry; +using Microsoft.Build.TelemetryInfra; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using Xunit.Abstractions; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; +using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils; + +namespace Microsoft.Build.Engine.UnitTests +{ + [Collection("TelemetryManagerTests")] + public class Telemetry_Tests + { + private readonly ITestOutputHelper _output; + + public Telemetry_Tests(ITestOutputHelper output) + { + _output = output; + } + + private sealed class ProjectFinishedCapturingLogger : ILogger + { + private readonly List _projectFinishedEventArgs = []; + public LoggerVerbosity Verbosity { get; set; } + public string? Parameters { get; set; } + + public IReadOnlyList ProjectFinishedEventArgsReceived => + _projectFinishedEventArgs; + + public void Initialize(IEventSource eventSource) + { + eventSource.ProjectFinished += EventSource_ProjectFinished; + } + + private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) + { + _projectFinishedEventArgs.Add(e); + } + + public void Shutdown() + { } + } + + [Fact] + public void WorkerNodeTelemetryCollection_BasicTarget() + { + WorkerNodeTelemetryData? workerNodeTelemetryData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; + + var testProject = """ + + + + + + + + + + """; + + MockLogger logger = new MockLogger(_output); + Helpers.BuildProjectContentUsingBuildManager(testProject, logger, + new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); + + workerNodeTelemetryData!.ShouldNotBeNull(); + var buildTargetKey = new TaskOrTargetTelemetryKey("Build", true, false); + workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(buildTargetKey); + workerNodeTelemetryData.TargetsExecutionData[buildTargetKey].ShouldBeTrue(); + workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(1); + + workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); + ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(2); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + + workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsCustom && !k.IsNuget); + workerNodeTelemetryData.TasksExecutionData.Values + .Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(2); + } + + [Fact] + public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() + { + WorkerNodeTelemetryData? workerNodeTelemetryData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; + + var testProject = """ + + + + + + Log.LogMessage(MessageImportance.Low, "Hello, world!"); + + + + + + + + + Log.LogMessage(MessageImportance.High, "Hello, world!"); + + + + + + + + + + + + + + + + + + + + + + + """; + MockLogger logger = new MockLogger(_output); + Helpers.BuildProjectContentUsingBuildManager(testProject, logger, + new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); + + workerNodeTelemetryData!.ShouldNotBeNull(); + workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("Build", true, false)); + workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("Build", true, false)].ShouldBeTrue(); + workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("BeforeBuild", true, false)); + workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("BeforeBuild", true, false)].ShouldBeTrue(); + workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("NotExecuted", true, false)); + workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("NotExecuted", true, false)].ShouldBeFalse(); + workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(3); + + workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); + ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(3); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + + ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].ExecutionsCount).ShouldBe(2); + workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + + ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].ExecutionsCount).ShouldBe(0); + workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].CumulativeExecutionTime.ShouldBe(TimeSpan.Zero); + + workerNodeTelemetryData.TasksExecutionData.Values + .Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(3); + + workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); + } + +#if NET + // test in .net core with telemetry opted in to avoid sending it but enable listening to it + [Fact] + public void NodeTelemetryE2E() + { + using TestEnvironment env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTIN", "1"); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE", "1.0"); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); + env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); + + // track activities through an ActivityListener + var capturedActivities = new List(); + using var listener = new ActivityListener + { + ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.DefaultActivitySourceNamespace), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = capturedActivities.Add, + ActivityStopped = _ => { } + }; + ActivitySource.AddActivityListener(listener); + + var testProject = @" + + + + + + + + + + + + "; + + using var testEnv = TestEnvironment.Create(_output); + var projectFile = testEnv.CreateFile("test.proj", testProject).Path; + + // Set up loggers + var projectFinishedLogger = new ProjectFinishedCapturingLogger(); + var buildParameters = new BuildParameters + { + Loggers = new ILogger[] { projectFinishedLogger }, + IsTelemetryEnabled = true + }; + + // Act + using (var buildManager = new BuildManager()) + { + // Phase 1: Begin Build - This initializes telemetry infrastructure + buildManager.BeginBuild(buildParameters); + + // Phase 2: Execute build requests + var buildRequestData1 = new BuildRequestData( + projectFile, + new Dictionary(), + null, + new[] { "Build" }, + null); + + buildManager.BuildRequest(buildRequestData1); + + var buildRequestData2 = new BuildRequestData( + projectFile, + new Dictionary(), + null, + new[] { "Clean" }, + null); + + buildManager.BuildRequest(buildRequestData2); + + // Phase 3: End Build - This puts telemetry to an system.diagnostics activity + buildManager.EndBuild(); + + // Verify build activity were captured by the listener and contain task and target info + capturedActivities.ShouldNotBeEmpty(); + var activity = capturedActivities.FindLast(a => a.DisplayName == "VS/MSBuild/Build").ShouldNotBeNull(); + var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value); + tags.ShouldNotBeNull(); + + tags.ShouldContainKey("VS.MSBuild.BuildTarget"); + tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); + + // Verify task data + var tasks = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.Tasks")); + + var tasksData = tasks.Value as List; + var messageTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.Message"); + messageTaskData.ShouldNotBeNull(); + + // Verify Message task execution metrics + messageTaskData.ExecutionsCount.ShouldBe(3); + messageTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + messageTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + messageTaskData.IsCustom.ShouldBe(false); + + // Verify CreateItem task execution metrics + var createItemTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.CreateItem"); + createItemTaskData.ShouldNotBeNull(); + createItemTaskData.ExecutionsCount.ShouldBe(1); + createItemTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + createItemTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + + // Verify Targets summary information + var targetsSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TargetsSummary")); + var targetsSummary = targetsSummaryTagObject.Value as TargetsSummaryInfo; + targetsSummary.ShouldNotBeNull(); + targetsSummary.Loaded.Total.ShouldBe(2); + targetsSummary.Executed.Total.ShouldBe(2); + + // Verify Tasks summary information + var tasksSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TasksSummary")); + var tasksSummary = tasksSummaryTagObject.Value as TasksSummaryInfo; + tasksSummary.ShouldNotBeNull(); + + tasksSummary.Microsoft.ShouldNotBeNull(); + tasksSummary.Microsoft!.Total!.ExecutionsCount.ShouldBe(4); + tasksSummary.Microsoft!.Total!.TotalMilliseconds.ShouldBeGreaterThan(0); + + // Allowing 0 for TotalMemoryBytes as it is possible for tasks to allocate no memory in certain scenarios. + tasksSummary.Microsoft.Total.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + } + } +#endif + } +} diff --git a/src/Framework/Telemetry/DiagnosticActivity.cs b/src/Framework/Telemetry/DiagnosticActivity.cs index fadb67ad039..8878dd86086 100644 --- a/src/Framework/Telemetry/DiagnosticActivity.cs +++ b/src/Framework/Telemetry/DiagnosticActivity.cs @@ -37,7 +37,7 @@ public DiagnosticActivity(Activity activity) { if (value != null) { - _activity.SetTag(key, value); + _activity.SetTag($"{TelemetryConstants.PropertyPrefix}{key}", value); } return this; diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index cbea92608f1..f28fcba0620 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -160,9 +160,11 @@ public Traits() public bool EnableTargetOutputLogging = IsEnvVarOneOrTrue("MSBUILDTARGETOUTPUTLOGGING"); + // for VS17.14 + public readonly bool SlnParsingWithSolutionPersistenceOptIn = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE")); + // for VS18.* public readonly bool TelemetryOptIn = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTIN"); - public readonly bool SlnParsingWithSolutionPersistenceOptIn = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE")); public static void UpdateFromEnvironment() { From 50b356e79d0c58fab4bc801534f5d400b69713e9 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 16:03:41 +0100 Subject: [PATCH 34/47] play with NodeTelemetryE2E setup --- .../Telemetry/Telemetry_Tests.cs | 215 +++++++++--------- src/Framework/Traits.cs | 1 - 2 files changed, 107 insertions(+), 109 deletions(-) diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs index 2f83939d588..d2e5047b0cb 100644 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -29,46 +29,24 @@ public Telemetry_Tests(ITestOutputHelper output) _output = output; } - private sealed class ProjectFinishedCapturingLogger : ILogger - { - private readonly List _projectFinishedEventArgs = []; - public LoggerVerbosity Verbosity { get; set; } - public string? Parameters { get; set; } - - public IReadOnlyList ProjectFinishedEventArgsReceived => - _projectFinishedEventArgs; - - public void Initialize(IEventSource eventSource) - { - eventSource.ProjectFinished += EventSource_ProjectFinished; - } - - private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) - { - _projectFinishedEventArgs.Add(e); - } - - public void Shutdown() - { } - } - [Fact] public void WorkerNodeTelemetryCollection_BasicTarget() { WorkerNodeTelemetryData? workerNodeTelemetryData = null; InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; - var testProject = """ - - - - - - - - - - """; + var testProject = + """ + + + + + + + + + + """; MockLogger logger = new MockLogger(_output); Helpers.BuildProjectContentUsingBuildManager(testProject, logger, @@ -81,9 +59,9 @@ public void WorkerNodeTelemetryCollection_BasicTarget() workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(1); workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(2); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount.ShouldBe(2); workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount.ShouldBe(1); workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsCustom && !k.IsNuget); @@ -94,83 +72,39 @@ public void WorkerNodeTelemetryCollection_BasicTarget() [Fact] public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() { - WorkerNodeTelemetryData? workerNodeTelemetryData = null; - InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; + WorkerNodeTelemetryData? workerNodeData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeData = dt; - var testProject = """ - - - - - - Log.LogMessage(MessageImportance.Low, "Hello, world!"); - - - - - - - - - Log.LogMessage(MessageImportance.High, "Hello, world!"); - - - - - - - - - - - - - - - - - - - - - - - """; MockLogger logger = new MockLogger(_output); - Helpers.BuildProjectContentUsingBuildManager(testProject, logger, + Helpers.BuildProjectContentUsingBuildManager( + GetTestProject(), + logger, new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); - workerNodeTelemetryData!.ShouldNotBeNull(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("Build", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("Build", true, false)].ShouldBeTrue(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("BeforeBuild", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("BeforeBuild", true, false)].ShouldBeTrue(); - workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("NotExecuted", true, false)); - workerNodeTelemetryData.TargetsExecutionData[new TaskOrTargetTelemetryKey("NotExecuted", true, false)].ShouldBeFalse(); - workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(3); + workerNodeData!.ShouldNotBeNull(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("Build", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("Build", true, false)].ShouldBeTrue(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("BeforeBuild", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("BeforeBuild", true, false)].ShouldBeTrue(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("NotExecuted", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("NotExecuted", true, false)].ShouldBeFalse(); + workerNodeData.TargetsExecutionData.Keys.Count.ShouldBe(3); - workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount).ShouldBe(3); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount).ShouldBe(1); - workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount.ShouldBe(3); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount.ShouldBe(1); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].ExecutionsCount).ShouldBe(2); - workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].ExecutionsCount.ShouldBe(2); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); - ((int)workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].ExecutionsCount).ShouldBe(0); - workerNodeTelemetryData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].CumulativeExecutionTime.ShouldBe(TimeSpan.Zero); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].ExecutionsCount.ShouldBe(0); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].CumulativeExecutionTime.ShouldBe(TimeSpan.Zero); - workerNodeTelemetryData.TasksExecutionData.Values - .Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(3); + workerNodeData.TasksExecutionData.Values.Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(3); - workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); + workerNodeData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); } #if NET @@ -180,17 +114,15 @@ public void NodeTelemetryE2E() { using TestEnvironment env = TestEnvironment.Create(); env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTIN", "1"); - env.SetEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE", "1.0"); env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); - // track activities through an ActivityListener var capturedActivities = new List(); using var listener = new ActivityListener { ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.DefaultActivitySourceNamespace), - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, - ActivityStarted = capturedActivities.Add, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = a => { lock (capturedActivities) { capturedActivities.Add(a); } }, ActivityStopped = _ => { } }; ActivitySource.AddActivityListener(listener); @@ -298,5 +230,72 @@ public void NodeTelemetryE2E() } } #endif + + private sealed class ProjectFinishedCapturingLogger : ILogger + { + private readonly List _projectFinishedEventArgs = []; + + public LoggerVerbosity Verbosity { get; set; } + + public string? Parameters { get; set; } + + public IReadOnlyList ProjectFinishedEventArgsReceived => _projectFinishedEventArgs; + + public void Initialize(IEventSource eventSource) => eventSource.ProjectFinished += EventSource_ProjectFinished; + + private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) => _projectFinishedEventArgs.Add(e); + + public void Shutdown() { } + } + +#region test project + private static string GetTestProject() => + """ + + + + + + Log.LogMessage(MessageImportance.Low, "Hello, world!"); + + + + + + + + + Log.LogMessage(MessageImportance.High, "Hello, world!"); + + + + + + + < CreateItem Include="foo.bar"> + + + + + + + < Target Name="BeforeBuild"> + + < Task01 /> + + + < Target Name="NotExecuted"> + + + + """; +#endregion + } } diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index f28fcba0620..9378e5c8eeb 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -154,7 +154,6 @@ public Traits() /// public bool SdkTelemetryOptOut = IsEnvVarOneOrTrue("DOTNET_CLI_TELEMETRY_OPTOUT"); public bool FrameworkTelemetryOptOut = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTOUT"); - public double? TelemetrySampleRateOverride = ParseDoubleFromEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE"); public bool ExcludeTasksDetailsFromTelemetry = IsEnvVarOneOrTrue("MSBUILDTELEMETRYEXCLUDETASKSDETAILS"); public bool FlushNodesTelemetryIntoConsole = IsEnvVarOneOrTrue("MSBUILDFLUSHNODESTELEMETRYINTOCONSOLE"); From 293ff44094a1c4e4b48b07368ed4e40537133658 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 16:10:38 +0100 Subject: [PATCH 35/47] cleanup --- src/Framework/Traits.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 9378e5c8eeb..7ff2fd0d7b7 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -181,19 +181,6 @@ private static int ParseIntFromEnvironmentVariableOrDefault(string environmentVa : defaultValue; } - /// - /// Parse a double from an environment variable with invariant culture. - /// - private static double? ParseDoubleFromEnvironmentVariable(string environmentVariable) - { - return double.TryParse(Environment.GetEnvironmentVariable(environmentVariable), - NumberStyles.Float, - CultureInfo.InvariantCulture, - out double result) - ? result - : null; - } - internal static bool IsEnvVarOneOrTrue(string name) { string? value = Environment.GetEnvironmentVariable(name); From 2280b9b49c8380b4c8b7153cff8952a7d6d59eeb Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 16:29:31 +0100 Subject: [PATCH 36/47] cleanup --- src/Framework/Telemetry/BuildTelemetry.cs | 47 ++++++++++++----------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 1db7edbaba2..0c31c7c16ca 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Runtime.CompilerServices; namespace Microsoft.Build.Framework.Telemetry { @@ -122,18 +123,18 @@ public Dictionary GetActivityProperties() telemetryItems.Add(TelemetryConstants.InnerBuildDurationPropertyName, (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds); } - AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); - AddIfNotNull(nameof(BuildSuccess), BuildSuccess); - AddIfNotNull(nameof(BuildTarget), BuildTarget); - AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion); - AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled); - AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled); - AddIfNotNull(nameof(SACEnabled), SACEnabled); - AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution); + AddIfNotNull(BuildEngineHost); + AddIfNotNull(BuildSuccess); + AddIfNotNull(BuildTarget); + AddIfNotNull(BuildEngineVersion); + AddIfNotNull(BuildCheckEnabled); + AddIfNotNull(MultiThreadedModeEnabled); + AddIfNotNull(SACEnabled); + AddIfNotNull(IsStandaloneExecution); return telemetryItems; - void AddIfNotNull(string key, object? value) + void AddIfNotNull(object? value, [CallerArgumentExpression(nameof(value))] string key = "") { if (value != null) { @@ -146,19 +147,19 @@ public override IDictionary GetProperties() { var properties = new Dictionary(); - AddIfNotNull(nameof(BuildEngineDisplayVersion), BuildEngineDisplayVersion); - AddIfNotNull(nameof(BuildEngineFrameworkName), BuildEngineFrameworkName); - AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); - AddIfNotNull(nameof(InitialMSBuildServerState), InitialMSBuildServerState); - AddIfNotNull(nameof(ProjectPath), ProjectPath); - AddIfNotNull(nameof(ServerFallbackReason), ServerFallbackReason); - AddIfNotNull(nameof(BuildTarget), BuildTarget); - AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion?.ToString()); - AddIfNotNull(nameof(BuildSuccess), BuildSuccess?.ToString()); - AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled?.ToString()); - AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled?.ToString()); - AddIfNotNull(nameof(SACEnabled), SACEnabled?.ToString()); - AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution?.ToString()); + AddIfNotNull(BuildEngineDisplayVersion); + AddIfNotNull(BuildEngineFrameworkName); + AddIfNotNull(BuildEngineHost); + AddIfNotNull(InitialMSBuildServerState); + AddIfNotNull(ProjectPath); + AddIfNotNull(ServerFallbackReason); + AddIfNotNull(BuildTarget); + AddIfNotNull(BuildEngineVersion?.ToString()); + AddIfNotNull(BuildSuccess?.ToString()); + AddIfNotNull(BuildCheckEnabled?.ToString()); + AddIfNotNull(MultiThreadedModeEnabled?.ToString()); + AddIfNotNull(SACEnabled?.ToString()); + AddIfNotNull(IsStandaloneExecution?.ToString()); // Calculate durations if (StartAt.HasValue && FinishedAt.HasValue) @@ -175,7 +176,7 @@ public override IDictionary GetProperties() return properties; - void AddIfNotNull(string key, string? value) + void AddIfNotNull(string? value, [CallerArgumentExpression(nameof(value))] string key = "") { if (value != null) { From 7b133a59487b2ebc4bf07846a8d2a20d82842c7b Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 17:06:29 +0100 Subject: [PATCH 37/47] more cleanup --- .../Microsoft.Build.Framework.csproj | 3 +-- src/Framework/Telemetry/DiagnosticActivity.cs | 11 +++----- src/Framework/Telemetry/IActivity.cs | 8 ------ .../Telemetry/IActivityTelemetryDataHolder.cs | 3 +-- .../Telemetry/MSBuildActivitySource.cs | 2 +- .../Telemetry/VSTelemetryActivity.cs | 26 +------------------ 6 files changed, 8 insertions(+), 45 deletions(-) diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index dc1ba4af67a..2820f9af784 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -31,13 +31,12 @@ - + - diff --git a/src/Framework/Telemetry/DiagnosticActivity.cs b/src/Framework/Telemetry/DiagnosticActivity.cs index 8878dd86086..3bb2ed30f8e 100644 --- a/src/Framework/Telemetry/DiagnosticActivity.cs +++ b/src/Framework/Telemetry/DiagnosticActivity.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if !NETFRAMEWORK + using System.Collections.Generic; using System.Diagnostics; @@ -43,13 +45,6 @@ public DiagnosticActivity(Activity activity) return this; } - public IActivity? AddEvent(ActivityEvent activityEvent) - { - _activity.AddEvent(activityEvent); - - return this; - } - public void Dispose() { if (_disposed) @@ -63,3 +58,5 @@ public void Dispose() } } } + +#endif diff --git a/src/Framework/Telemetry/IActivity.cs b/src/Framework/Telemetry/IActivity.cs index 6237fa6dd9d..6118e50f7e8 100644 --- a/src/Framework/Telemetry/IActivity.cs +++ b/src/Framework/Telemetry/IActivity.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics; namespace Microsoft.Build.Framework.Telemetry { @@ -25,12 +24,5 @@ internal interface IActivity : IDisposable /// The tag value. /// The activity instance for method chaining. IActivity? SetTag(string key, object? value); - - /// - /// Adds an event to the activity. - /// - /// The event to add. - /// The activity instance for method chaining. - IActivity? AddEvent(ActivityEvent activityEvent); } } diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs index 90fd7e21875..e660f191695 100644 --- a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; namespace Microsoft.Build.Framework.Telemetry; /// -/// Interface for classes that hold telemetry data that should be added as tags to an . +/// Interface for classes that hold telemetry data that should be added as tags to an . /// internal interface IActivityTelemetryDataHolder { diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs index 50d5c214c2b..891e85c781f 100644 --- a/src/Framework/Telemetry/MSBuildActivitySource.cs +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -42,7 +42,7 @@ public MSBuildActivitySource(string name) #if NETFRAMEWORK TelemetryScope? operation = _telemetrySession?.StartOperation(eventName); - return operation != null ? new VsTelemetryActivity(operation, _telemetrySession) : null; + return operation != null ? new VsTelemetryActivity(operation) : null; #else Activity? activity = Activity.Current?.HasRemoteParent == true ? _source.StartActivity(eventName, ActivityKind.Internal, parentId: Activity.Current.ParentId) diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index 82f6bf70569..f9f21374d1b 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -4,7 +4,6 @@ #if NETFRAMEWORK using System.Collections.Generic; -using System.Diagnostics; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry @@ -17,21 +16,13 @@ namespace Microsoft.Build.Framework.Telemetry internal class VsTelemetryActivity : IActivity { private readonly TelemetryScope _scope; - private readonly TelemetrySession? _session; private TelemetryResult _result = TelemetryResult.Success; private bool _disposed; - public VsTelemetryActivity(TelemetryScope scope, TelemetrySession? session) + public VsTelemetryActivity(TelemetryScope scope) { _scope = scope; - _session = session; - } - - public IActivity? SetResult(TelemetryResult result) - { - _result = result; - return this; } public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) @@ -59,21 +50,6 @@ public VsTelemetryActivity(TelemetryScope scope, TelemetrySessio return this; } - public IActivity? AddEvent(ActivityEvent activityEvent) - { - // VS Telemetry doesn't have a direct equivalent to ActivityEvent. - // We create and post a custom event to the session associated with this activity. - var telemetryEvent = new TelemetryEvent(activityEvent.Name); - foreach (KeyValuePair tag in activityEvent.Tags) - { - telemetryEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{tag.Key}"] = tag.Value; - } - - _session?.PostEvent(telemetryEvent); - - return this; - } - public void Dispose() { if (_disposed) From f581c4234e2d7da5db7847d94a88c883b75663af Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 17:09:07 +0100 Subject: [PATCH 38/47] update comment --- src/Framework/Telemetry/TelemetryDataUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index 31dd22fdee4..95ef2067d39 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -16,7 +16,7 @@ internal static class TelemetryDataUtils /// Data about tasks and target forwarded from nodes. /// Controls whether Task details should attached to the telemetry. /// Controls whether Target details should be attached to the telemetry. - /// Node Telemetry data wrapped in a list of properties that can be attached as tags to a . + /// Node Telemetry data wrapped in a list of properties that can be attached as tags to a . public static IActivityTelemetryDataHolder? AsActivityDataHolder(this IWorkerNodeTelemetryData? telemetryData, bool includeTasksDetails, bool includeTargetDetails) { if (telemetryData == null) From 7ca2245a50eefb5b60c67734c8e6da78d608d16d Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 17:14:30 +0100 Subject: [PATCH 39/47] fix review comments & merge --- .vsts-dotnet-ci.yml | 11 +- Directory.Build.props | 4 - Directory.Build.targets | 3 +- ...Packages.props => Directory.Packages.props | 9 +- eng/Version.Details.props | 6 +- eng/Version.Details.xml | 14 +- .../job/publish-build-assets.yml | 13 + .../job/source-index-stage1.yml | 8 +- .../core-templates/post-build/post-build.yml | 17 +- .../steps/install-microbuild.yml | 34 +- .../core-templates/steps/publish-logs.yml | 4 +- .../core-templates/steps/source-build.yml | 2 +- eng/common/post-build/publish-using-darc.ps1 | 4 +- eng/common/post-build/redact-logs.ps1 | 5 +- eng/common/sdk-task.ps1 | 4 +- eng/common/tools.ps1 | 4 +- ...ackages.props => Directory.Packages.props} | 3 +- eng/dependabot/dependabot.csproj | 4 - eng/dependabot/global.json | 1 + global.json | 5 +- .../Telemetry/Telemetry_Tests.cs | 301 ++++++++++++++++++ src/Framework/MSBuildEventSource.cs | 4 +- .../Microsoft.Build.Framework.csproj | 2 +- src/Framework/Telemetry/BuildTelemetry.cs | 47 +-- src/Framework/Telemetry/DiagnosticActivity.cs | 13 +- src/Framework/Telemetry/IActivity.cs | 8 - .../Telemetry/IActivityTelemetryDataHolder.cs | 3 +- src/Framework/Telemetry/TelemetryConstants.cs | 7 +- src/Framework/Telemetry/TelemetryManager.cs | 127 ++++---- .../Telemetry/VSTelemetryActivity.cs | 21 +- src/Framework/Traits.cs | 18 +- src/MSBuild/XMake.cs | 2 +- src/MSBuild/app.amd64.config | 10 + .../Directory.Packages.props | 3 + 34 files changed, 530 insertions(+), 191 deletions(-) rename eng/Packages.props => Directory.Packages.props (91%) rename eng/dependabot/{Packages.props => Directory.Packages.props} (97%) delete mode 100644 eng/dependabot/dependabot.csproj create mode 100644 eng/dependabot/global.json create mode 100644 src/Build.UnitTests/Telemetry/Telemetry_Tests.cs create mode 100644 src/Shared/EmptyDirectoryBuildFiles/Directory.Packages.props diff --git a/.vsts-dotnet-ci.yml b/.vsts-dotnet-ci.yml index 0114cc99d49..eb38f6d96a3 100644 --- a/.vsts-dotnet-ci.yml +++ b/.vsts-dotnet-ci.yml @@ -533,11 +533,14 @@ jobs: - job: CodeCoverage displayName: "Code Coverage" dependsOn: + - IfOnlyDocumentionChanged - BootstrapMSBuildOnFullFrameworkWindows - BootstrapMSBuildOnCoreWindows - FullReleaseOnWindows - CoreBootstrappedOnLinux - CoreOnMac + variables: + onlyDocChanged: $[ dependencies.IfOnlyDocumentionChanged.outputs['SetIfOnlyDocumentionChangedVaribale.onlyDocChanged'] ] pool: vmImage: 'windows-2022' steps: @@ -562,11 +565,13 @@ jobs: buildType: 'current' artifactName: 'LinuxCoreCoverage' targetPath: '$(Build.SourcesDirectory)/artifacts/TestResults/CoverageResults/LinuxCore' + condition: eq(variables.onlyDocChanged, 0) - task: DownloadPipelineArtifact@2 inputs: buildType: 'current' artifactName: 'MacCoreCoverage' targetPath: '$(Build.SourcesDirectory)/artifacts/TestResults/CoverageResults/MacCore' + condition: eq(variables.onlyDocChanged, 0) - task: PowerShell@2 displayName: Process coverage reports @@ -574,20 +579,22 @@ jobs: filePath: $(Build.SourcesDirectory)\eng\process-coverage.ps1 arguments: -repoRoot $(Build.SourcesDirectory) -coverageArtifactsDir $(Build.SourcesDirectory)/artifacts/CoverageResults pwsh: true + condition: eq(variables.onlyDocChanged, 0) - task: PublishBuildArtifacts@1 displayName: Publish Artifact $(Build.BuildNumber) Coverage inputs: PathtoPublish: '$(Build.SourcesDirectory)/artifacts/CoverageResults/merged.coverage' ArtifactName: '$(Build.BuildNumber) Coverage' - condition: succeededOrFailed() + condition: and(succeededOrFailed(), eq(variables.onlyDocChanged, 0)) - task: PublishBuildArtifacts@1 displayName: Publish Artifact $(Build.BuildNumber) Cobertura inputs: PathtoPublish: '$(Build.SourcesDirectory)/artifacts/CoverageResults/merged.cobertura.xml' ArtifactName: '$(Build.BuildNumber) Cobertura' - condition: succeededOrFailed() + condition: and(succeededOrFailed(), eq(variables.onlyDocChanged, 0)) - task: PublishCodeCoverageResults@2 inputs: summaryFileLocation: '$(Build.SourcesDirectory)/artifacts/CoverageResults/merged.coverage' pathToSources: $(Build.SourcesDirectory) + condition: eq(variables.onlyDocChanged, 0) - template: /eng/common/templates/jobs/source-build.yml diff --git a/Directory.Build.props b/Directory.Build.props index 0742dc9970c..2d0433af560 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -84,10 +84,6 @@ ates https://learn.microsoft.com/en-gb/dotnet/fundamentals/syslib-diagnostics/sy true - true - true - $([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'eng', 'Packages.props')) - true diff --git a/Directory.Build.targets b/Directory.Build.targets index 374fe0fc145..761f9fe2563 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -18,8 +18,7 @@ - - + diff --git a/eng/Packages.props b/Directory.Packages.props similarity index 91% rename from eng/Packages.props rename to Directory.Packages.props index aac684248db..117d0ddf276 100644 --- a/eng/Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,13 @@ + - + + + + true + true + + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index b19b0b87ecd..7e0f7bb8c69 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -24,12 +24,12 @@ This file should be imported by eng/Versions.props 9.0.11 9.0.11 - 10.0.0-beta.25555.6 - 10.0.0-beta.25555.6 + 10.0.0-beta.25605.3 + 10.0.0-beta.25605.3 7.0.0-rc.288 - 5.3.0-2.25580.2 + 5.3.0-2.25605.1 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index fff544f4a95..d63feb3370f 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,6 +1,6 @@ - + @@ -106,21 +106,21 @@ - + https://github.com/dotnet/arcade - 987d1a73ea67d323c0fc7537bce8ec65d87eb43f + 774a2ef8d2777c50d047d6776ced33260822cad6 https://github.com/nuget/nuget.client 5514d935e3e77d90d931758cf9e2589735b905a3 - + https://github.com/dotnet/roslyn - c14edd18895fe53efd010d4517da332f30784df6 + 9031dea89451823b7aab55f80025eee451e688f3 - + https://github.com/dotnet/arcade - 987d1a73ea67d323c0fc7537bce8ec65d87eb43f + 774a2ef8d2777c50d047d6776ced33260822cad6 diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index e7daa6d2faf..3437087c80f 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -120,6 +120,14 @@ jobs: - task: NuGetAuthenticate@1 + # Populate internal runtime variables. + - template: /eng/common/templates/steps/enable-internal-sources.yml + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + + - template: /eng/common/templates/steps/enable-internal-runtimes.yml + - task: AzureCLI@2 displayName: Publish Build Assets inputs: @@ -132,6 +140,9 @@ jobs: /p:IsAssetlessBuild=${{ parameters.isAssetlessBuild }} /p:MaestroApiEndpoint=https://maestro.dot.net /p:OfficialBuildId=$(OfficialBuildId) + -runtimeSourceFeed https://ci.dot.net/internal + -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' + condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} @@ -200,6 +211,8 @@ jobs: -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' + -runtimeSourceFeed https://ci.dot.net/internal + -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: - template: /eng/common/core-templates/steps/publish-logs.yml diff --git a/eng/common/core-templates/job/source-index-stage1.yml b/eng/common/core-templates/job/source-index-stage1.yml index 30530359a5d..76baf5c2725 100644 --- a/eng/common/core-templates/job/source-index-stage1.yml +++ b/eng/common/core-templates/job/source-index-stage1.yml @@ -3,7 +3,7 @@ parameters: sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] binlogPath: artifacts/log/Debug/Build.binlog - condition: '' + condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') dependsOn: '' pool: '' is1ESPipeline: '' @@ -25,10 +25,10 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $(DncEngPublicBuildPool) - image: windows.vs2022.amd64.open + image: windows.vs2026preview.scout.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $(DncEngInternalBuildPool) - image: windows.vs2022.amd64 + image: windows.vs2026preview.scout.amd64 steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: @@ -41,4 +41,4 @@ jobs: - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml parameters: - binLogPath: ${{ parameters.binLogPath }} \ No newline at end of file + binLogPath: ${{ parameters.binLogPath }} diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index 55361908c2e..9423d71ca3a 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -127,11 +127,11 @@ stages: ${{ else }}: ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) - image: windows.vs2022.amd64 + image: windows.vs2026preview.scout.amd64 os: windows ${{ else }}: name: $(DncEngInternalBuildPool) - demands: ImageOverride -equals windows.vs2022.amd64 + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml @@ -175,7 +175,7 @@ stages: os: windows ${{ else }}: name: $(DncEngInternalBuildPool) - demands: ImageOverride -equals windows.vs2022.amd64 + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: @@ -236,7 +236,7 @@ stages: os: windows ${{ else }}: name: $(DncEngInternalBuildPool) - demands: ImageOverride -equals windows.vs2022.amd64 + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml parameters: @@ -307,6 +307,13 @@ stages: - task: NuGetAuthenticate@1 + # Populate internal runtime variables. + - template: /eng/common/templates/steps/enable-internal-sources.yml + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + + - template: /eng/common/templates/steps/enable-internal-runtimes.yml + # Darc is targeting 8.0, so make sure it's installed - task: UseDotNet@2 inputs: @@ -328,3 +335,5 @@ stages: -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}' + -runtimeSourceFeed https://ci.dot.net/internal + -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' diff --git a/eng/common/core-templates/steps/install-microbuild.yml b/eng/common/core-templates/steps/install-microbuild.yml index f2248ebfd73..553fce66b94 100644 --- a/eng/common/core-templates/steps/install-microbuild.yml +++ b/eng/common/core-templates/steps/install-microbuild.yml @@ -11,22 +11,41 @@ parameters: # Unfortunately, _SignType can't be used to exclude the use of the service connection in non-real sign scenarios. The # variable is not available in template expression. _SignType has a very large proliferation across .NET, so replacing it is tough. microbuildUseESRP: true + # Microbuild installation directory + microBuildOutputFolder: $(Agent.TempDirectory)/MicroBuild continueOnError: false steps: - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, 'true') }}: - # Installing .NET 8 is required to use the MicroBuild signing plugin on non-Windows platforms + # Needed to download the MicroBuild plugin nupkgs on Mac and Linux when nuget.exe is unavailable - task: UseDotNet@2 displayName: Install .NET 8.0 SDK for MicroBuild Plugin inputs: packageType: sdk version: 8.0.x - # Installing the SDK in a '.dotnet-microbuild' directory is required for signing. - # See target FindDotNetPathForMicroBuild in arcade/src/Microsoft.DotNet.Arcade.Sdk/tools/Sign.proj - # Do not remove '.dotnet-microbuild' from the path without changing the corresponding logic. - installationPath: $(Agent.TempDirectory)/.dotnet-microbuild + installationPath: ${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) + + - script: | + set -euo pipefail + + # UseDotNet@2 prepends the dotnet executable path to the PATH variable, so we can call dotnet directly + version=$(dotnet --version) + cat << 'EOF' > ${{ parameters.microBuildOutputFolder }}/global.json + { + "sdk": { + "version": "$version", + "paths": [ + "${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild" + ], + "errorMessage": "The .NET SDK version $version is required to install the MicroBuild signing plugin." + } + } + EOF + displayName: 'Add global.json to MicroBuild Installation path' + workingDirectory: ${{ parameters.microBuildOutputFolder }} condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) - script: | @@ -64,7 +83,7 @@ steps: ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca env: TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: $(Agent.TempDirectory)/MicroBuild + MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) continueOnError: ${{ parameters.continueOnError }} condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) @@ -76,6 +95,7 @@ steps: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + workingDirectory: ${{ parameters.microBuildOutputFolder }} ${{ if eq(parameters.microbuildUseESRP, true) }}: ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: @@ -84,7 +104,7 @@ steps: ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc env: TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: $(Agent.TempDirectory)/MicroBuild + MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) continueOnError: ${{ parameters.continueOnError }} condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 10f825e270a..5a927b4c7bc 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -26,8 +26,10 @@ steps: # If the file exists - sensitive data for redaction will be sourced from it # (single entry per line, lines starting with '# ' are considered comments and skipped) arguments: -InputPath '$(System.DefaultWorkingDirectory)/PostBuildLogs' - -BinlogToolVersion ${{parameters.BinlogToolVersion}} + -BinlogToolVersion '${{parameters.BinlogToolVersion}}' -TokensFilePath '$(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt' + -runtimeSourceFeed https://ci.dot.net/internal + -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' '$(publishing-dnceng-devdiv-code-r-build-re)' '$(MaestroAccessToken)' '$(dn-bot-all-orgs-artifact-feeds-rw)' diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index acf16ed3496..b9c86c18ae4 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -24,7 +24,7 @@ steps: # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey '$(dotnetbuilds-internal-container-read-token-base64)'' fi buildConfig=Release diff --git a/eng/common/post-build/publish-using-darc.ps1 b/eng/common/post-build/publish-using-darc.ps1 index 1eda208a3bb..48e55598bdd 100644 --- a/eng/common/post-build/publish-using-darc.ps1 +++ b/eng/common/post-build/publish-using-darc.ps1 @@ -7,7 +7,9 @@ param( [Parameter(Mandatory=$false)][string] $ArtifactsPublishingAdditionalParameters, [Parameter(Mandatory=$false)][string] $SymbolPublishingAdditionalParameters, [Parameter(Mandatory=$false)][string] $RequireDefaultChannels, - [Parameter(Mandatory=$false)][string] $SkipAssetsPublishing + [Parameter(Mandatory=$false)][string] $SkipAssetsPublishing, + [Parameter(Mandatory=$false)][string] $runtimeSourceFeed, + [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey ) try { diff --git a/eng/common/post-build/redact-logs.ps1 b/eng/common/post-build/redact-logs.ps1 index b7fc1959150..472d5bb562c 100644 --- a/eng/common/post-build/redact-logs.ps1 +++ b/eng/common/post-build/redact-logs.ps1 @@ -7,8 +7,9 @@ param( # File with strings to redact - separated by newlines. # For comments start the line with '# ' - such lines are ignored [Parameter(Mandatory=$false)][string] $TokensFilePath, - [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact -) + [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact, + [Parameter(Mandatory=$false)][string] $runtimeSourceFeed, + [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey) try { $ErrorActionPreference = 'Stop' diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1 index b62e132d32a..b64b66a6275 100644 --- a/eng/common/sdk-task.ps1 +++ b/eng/common/sdk-task.ps1 @@ -9,6 +9,8 @@ Param( [switch][Alias('nobl')]$excludeCIBinaryLog, [switch]$noWarnAsError, [switch] $help, + [string] $runtimeSourceFeed = '', + [string] $runtimeSourceFeedKey = '', [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) @@ -68,7 +70,7 @@ try { $GlobalJson.tools | Add-Member -Name "vs" -Value (ConvertFrom-Json "{ `"version`": `"16.5`" }") -MemberType NoteProperty } if( -not ($GlobalJson.tools.PSObject.Properties.Name -match "xcopy-msbuild" )) { - $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.13.0" -MemberType NoteProperty + $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "18.0.0" -MemberType NoteProperty } if ($GlobalJson.tools."xcopy-msbuild".Trim() -ine "none") { $xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools."xcopy-msbuild" -install $true diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 06b44de7870..578705ee4db 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -394,8 +394,8 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = # If the version of msbuild is going to be xcopied, # use this version. Version matches a package here: - # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.13.0 - $defaultXCopyMSBuildVersion = '17.13.0' + # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/18.0.0 + $defaultXCopyMSBuildVersion = '18.0.0' if (!$vsRequirements) { if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { diff --git a/eng/dependabot/Packages.props b/eng/dependabot/Directory.Packages.props similarity index 97% rename from eng/dependabot/Packages.props rename to eng/dependabot/Directory.Packages.props index 1d888c580e0..6ebbc6b1ccc 100644 --- a/eng/dependabot/Packages.props +++ b/eng/dependabot/Directory.Packages.props @@ -49,10 +49,9 @@ - - + diff --git a/eng/dependabot/dependabot.csproj b/eng/dependabot/dependabot.csproj deleted file mode 100644 index fdb8d223906..00000000000 --- a/eng/dependabot/dependabot.csproj +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/eng/dependabot/global.json b/eng/dependabot/global.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/eng/dependabot/global.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/global.json b/global.json index 9cfa84c652c..9525136f142 100644 --- a/global.json +++ b/global.json @@ -2,7 +2,6 @@ "sdk": { "allowPrerelease": true, "paths": [ - ".dotnet", "$host$" ], "errorMessage": "The .NET SDK could not be found, please run a command-line build with ./build.cmd." @@ -15,6 +14,6 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25578.106" + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25605.116" } -} +} \ No newline at end of file diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs new file mode 100644 index 00000000000..d2e5047b0cb --- /dev/null +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -0,0 +1,301 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text.Json; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Telemetry; +using Microsoft.Build.TelemetryInfra; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using Xunit.Abstractions; +using static Microsoft.Build.Framework.Telemetry.BuildInsights; +using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils; + +namespace Microsoft.Build.Engine.UnitTests +{ + [Collection("TelemetryManagerTests")] + public class Telemetry_Tests + { + private readonly ITestOutputHelper _output; + + public Telemetry_Tests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void WorkerNodeTelemetryCollection_BasicTarget() + { + WorkerNodeTelemetryData? workerNodeTelemetryData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; + + var testProject = + """ + + + + + + + + + + """; + + MockLogger logger = new MockLogger(_output); + Helpers.BuildProjectContentUsingBuildManager(testProject, logger, + new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); + + workerNodeTelemetryData!.ShouldNotBeNull(); + var buildTargetKey = new TaskOrTargetTelemetryKey("Build", true, false); + workerNodeTelemetryData.TargetsExecutionData.ShouldContainKey(buildTargetKey); + workerNodeTelemetryData.TargetsExecutionData[buildTargetKey].ShouldBeTrue(); + workerNodeTelemetryData.TargetsExecutionData.Keys.Count.ShouldBe(1); + + workerNodeTelemetryData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount.ShouldBe(2); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount.ShouldBe(1); + workerNodeTelemetryData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + + workerNodeTelemetryData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsCustom && !k.IsNuget); + workerNodeTelemetryData.TasksExecutionData.Values + .Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(2); + } + + [Fact] + public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() + { + WorkerNodeTelemetryData? workerNodeData = null; + InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeData = dt; + + MockLogger logger = new MockLogger(_output); + Helpers.BuildProjectContentUsingBuildManager( + GetTestProject(), + logger, + new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); + + workerNodeData!.ShouldNotBeNull(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("Build", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("Build", true, false)].ShouldBeTrue(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("BeforeBuild", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("BeforeBuild", true, false)].ShouldBeTrue(); + workerNodeData.TargetsExecutionData.ShouldContainKey(new TaskOrTargetTelemetryKey("NotExecuted", true, false)); + workerNodeData.TargetsExecutionData[new TaskOrTargetTelemetryKey("NotExecuted", true, false)].ShouldBeFalse(); + workerNodeData.TargetsExecutionData.Keys.Count.ShouldBe(3); + + workerNodeData.TasksExecutionData.Keys.Count.ShouldBeGreaterThan(2); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].ExecutionsCount.ShouldBe(3); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.Message"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].ExecutionsCount.ShouldBe(1); + workerNodeData.TasksExecutionData[(TaskOrTargetTelemetryKey)"Microsoft.Build.Tasks.CreateItem"].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].ExecutionsCount.ShouldBe(2); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task01", true, false)].CumulativeExecutionTime.ShouldBeGreaterThan(TimeSpan.Zero); + + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].ExecutionsCount.ShouldBe(0); + workerNodeData.TasksExecutionData[new TaskOrTargetTelemetryKey("Task02", true, false)].CumulativeExecutionTime.ShouldBe(TimeSpan.Zero); + + workerNodeData.TasksExecutionData.Values.Count(v => v.CumulativeExecutionTime > TimeSpan.Zero || v.ExecutionsCount > 0).ShouldBe(3); + + workerNodeData.TasksExecutionData.Keys.ShouldAllBe(k => !k.IsNuget); + } + +#if NET + // test in .net core with telemetry opted in to avoid sending it but enable listening to it + [Fact] + public void NodeTelemetryE2E() + { + using TestEnvironment env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTIN", "1"); + env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); + env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); + + var capturedActivities = new List(); + using var listener = new ActivityListener + { + ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.DefaultActivitySourceNamespace), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = a => { lock (capturedActivities) { capturedActivities.Add(a); } }, + ActivityStopped = _ => { } + }; + ActivitySource.AddActivityListener(listener); + + var testProject = @" + + + + + + + + + + + + "; + + using var testEnv = TestEnvironment.Create(_output); + var projectFile = testEnv.CreateFile("test.proj", testProject).Path; + + // Set up loggers + var projectFinishedLogger = new ProjectFinishedCapturingLogger(); + var buildParameters = new BuildParameters + { + Loggers = new ILogger[] { projectFinishedLogger }, + IsTelemetryEnabled = true + }; + + // Act + using (var buildManager = new BuildManager()) + { + // Phase 1: Begin Build - This initializes telemetry infrastructure + buildManager.BeginBuild(buildParameters); + + // Phase 2: Execute build requests + var buildRequestData1 = new BuildRequestData( + projectFile, + new Dictionary(), + null, + new[] { "Build" }, + null); + + buildManager.BuildRequest(buildRequestData1); + + var buildRequestData2 = new BuildRequestData( + projectFile, + new Dictionary(), + null, + new[] { "Clean" }, + null); + + buildManager.BuildRequest(buildRequestData2); + + // Phase 3: End Build - This puts telemetry to an system.diagnostics activity + buildManager.EndBuild(); + + // Verify build activity were captured by the listener and contain task and target info + capturedActivities.ShouldNotBeEmpty(); + var activity = capturedActivities.FindLast(a => a.DisplayName == "VS/MSBuild/Build").ShouldNotBeNull(); + var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value); + tags.ShouldNotBeNull(); + + tags.ShouldContainKey("VS.MSBuild.BuildTarget"); + tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); + + // Verify task data + var tasks = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.Tasks")); + + var tasksData = tasks.Value as List; + var messageTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.Message"); + messageTaskData.ShouldNotBeNull(); + + // Verify Message task execution metrics + messageTaskData.ExecutionsCount.ShouldBe(3); + messageTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + messageTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + messageTaskData.IsCustom.ShouldBe(false); + + // Verify CreateItem task execution metrics + var createItemTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.CreateItem"); + createItemTaskData.ShouldNotBeNull(); + createItemTaskData.ExecutionsCount.ShouldBe(1); + createItemTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + createItemTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + + // Verify Targets summary information + var targetsSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TargetsSummary")); + var targetsSummary = targetsSummaryTagObject.Value as TargetsSummaryInfo; + targetsSummary.ShouldNotBeNull(); + targetsSummary.Loaded.Total.ShouldBe(2); + targetsSummary.Executed.Total.ShouldBe(2); + + // Verify Tasks summary information + var tasksSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TasksSummary")); + var tasksSummary = tasksSummaryTagObject.Value as TasksSummaryInfo; + tasksSummary.ShouldNotBeNull(); + + tasksSummary.Microsoft.ShouldNotBeNull(); + tasksSummary.Microsoft!.Total!.ExecutionsCount.ShouldBe(4); + tasksSummary.Microsoft!.Total!.TotalMilliseconds.ShouldBeGreaterThan(0); + + // Allowing 0 for TotalMemoryBytes as it is possible for tasks to allocate no memory in certain scenarios. + tasksSummary.Microsoft.Total.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + } + } +#endif + + private sealed class ProjectFinishedCapturingLogger : ILogger + { + private readonly List _projectFinishedEventArgs = []; + + public LoggerVerbosity Verbosity { get; set; } + + public string? Parameters { get; set; } + + public IReadOnlyList ProjectFinishedEventArgsReceived => _projectFinishedEventArgs; + + public void Initialize(IEventSource eventSource) => eventSource.ProjectFinished += EventSource_ProjectFinished; + + private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e) => _projectFinishedEventArgs.Add(e); + + public void Shutdown() { } + } + +#region test project + private static string GetTestProject() => + """ + + + + + + Log.LogMessage(MessageImportance.Low, "Hello, world!"); + + + + + + + + + Log.LogMessage(MessageImportance.High, "Hello, world!"); + + + + + + + < CreateItem Include="foo.bar"> + + + + + + + < Target Name="BeforeBuild"> + + < Task01 /> + + + < Target Name="NotExecuted"> + + + + """; +#endregion + + } +} diff --git a/src/Framework/MSBuildEventSource.cs b/src/Framework/MSBuildEventSource.cs index e6b691a2105..78c7eaa55f2 100644 --- a/src/Framework/MSBuildEventSource.cs +++ b/src/Framework/MSBuildEventSource.cs @@ -12,7 +12,7 @@ namespace Microsoft.Build.Eventing /// Changes to existing event method signatures will not be reflected unless you update the property or assign a new event ID. /// [EventSource(Name = "Microsoft-Build")] - internal sealed class MSBuildEventSource : EventSource + internal sealed partial class MSBuildEventSource : EventSource { public static class Keywords { @@ -40,8 +40,6 @@ public static class Keywords /// public static MSBuildEventSource Log = new MSBuildEventSource(); - private MSBuildEventSource() { } - #region Events /// diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 606d6855d00..2820f9af784 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -31,11 +31,11 @@ - + diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 1db7edbaba2..0c31c7c16ca 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Runtime.CompilerServices; namespace Microsoft.Build.Framework.Telemetry { @@ -122,18 +123,18 @@ public Dictionary GetActivityProperties() telemetryItems.Add(TelemetryConstants.InnerBuildDurationPropertyName, (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds); } - AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); - AddIfNotNull(nameof(BuildSuccess), BuildSuccess); - AddIfNotNull(nameof(BuildTarget), BuildTarget); - AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion); - AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled); - AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled); - AddIfNotNull(nameof(SACEnabled), SACEnabled); - AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution); + AddIfNotNull(BuildEngineHost); + AddIfNotNull(BuildSuccess); + AddIfNotNull(BuildTarget); + AddIfNotNull(BuildEngineVersion); + AddIfNotNull(BuildCheckEnabled); + AddIfNotNull(MultiThreadedModeEnabled); + AddIfNotNull(SACEnabled); + AddIfNotNull(IsStandaloneExecution); return telemetryItems; - void AddIfNotNull(string key, object? value) + void AddIfNotNull(object? value, [CallerArgumentExpression(nameof(value))] string key = "") { if (value != null) { @@ -146,19 +147,19 @@ public override IDictionary GetProperties() { var properties = new Dictionary(); - AddIfNotNull(nameof(BuildEngineDisplayVersion), BuildEngineDisplayVersion); - AddIfNotNull(nameof(BuildEngineFrameworkName), BuildEngineFrameworkName); - AddIfNotNull(nameof(BuildEngineHost), BuildEngineHost); - AddIfNotNull(nameof(InitialMSBuildServerState), InitialMSBuildServerState); - AddIfNotNull(nameof(ProjectPath), ProjectPath); - AddIfNotNull(nameof(ServerFallbackReason), ServerFallbackReason); - AddIfNotNull(nameof(BuildTarget), BuildTarget); - AddIfNotNull(nameof(BuildEngineVersion), BuildEngineVersion?.ToString()); - AddIfNotNull(nameof(BuildSuccess), BuildSuccess?.ToString()); - AddIfNotNull(nameof(BuildCheckEnabled), BuildCheckEnabled?.ToString()); - AddIfNotNull(nameof(MultiThreadedModeEnabled), MultiThreadedModeEnabled?.ToString()); - AddIfNotNull(nameof(SACEnabled), SACEnabled?.ToString()); - AddIfNotNull(nameof(IsStandaloneExecution), IsStandaloneExecution?.ToString()); + AddIfNotNull(BuildEngineDisplayVersion); + AddIfNotNull(BuildEngineFrameworkName); + AddIfNotNull(BuildEngineHost); + AddIfNotNull(InitialMSBuildServerState); + AddIfNotNull(ProjectPath); + AddIfNotNull(ServerFallbackReason); + AddIfNotNull(BuildTarget); + AddIfNotNull(BuildEngineVersion?.ToString()); + AddIfNotNull(BuildSuccess?.ToString()); + AddIfNotNull(BuildCheckEnabled?.ToString()); + AddIfNotNull(MultiThreadedModeEnabled?.ToString()); + AddIfNotNull(SACEnabled?.ToString()); + AddIfNotNull(IsStandaloneExecution?.ToString()); // Calculate durations if (StartAt.HasValue && FinishedAt.HasValue) @@ -175,7 +176,7 @@ public override IDictionary GetProperties() return properties; - void AddIfNotNull(string key, string? value) + void AddIfNotNull(string? value, [CallerArgumentExpression(nameof(value))] string key = "") { if (value != null) { diff --git a/src/Framework/Telemetry/DiagnosticActivity.cs b/src/Framework/Telemetry/DiagnosticActivity.cs index fadb67ad039..3bb2ed30f8e 100644 --- a/src/Framework/Telemetry/DiagnosticActivity.cs +++ b/src/Framework/Telemetry/DiagnosticActivity.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if !NETFRAMEWORK + using System.Collections.Generic; using System.Diagnostics; @@ -37,19 +39,12 @@ public DiagnosticActivity(Activity activity) { if (value != null) { - _activity.SetTag(key, value); + _activity.SetTag($"{TelemetryConstants.PropertyPrefix}{key}", value); } return this; } - public IActivity? AddEvent(ActivityEvent activityEvent) - { - _activity.AddEvent(activityEvent); - - return this; - } - public void Dispose() { if (_disposed) @@ -63,3 +58,5 @@ public void Dispose() } } } + +#endif diff --git a/src/Framework/Telemetry/IActivity.cs b/src/Framework/Telemetry/IActivity.cs index 6237fa6dd9d..6118e50f7e8 100644 --- a/src/Framework/Telemetry/IActivity.cs +++ b/src/Framework/Telemetry/IActivity.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics; namespace Microsoft.Build.Framework.Telemetry { @@ -25,12 +24,5 @@ internal interface IActivity : IDisposable /// The tag value. /// The activity instance for method chaining. IActivity? SetTag(string key, object? value); - - /// - /// Adds an event to the activity. - /// - /// The event to add. - /// The activity instance for method chaining. - IActivity? AddEvent(ActivityEvent activityEvent); } } diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs index 90fd7e21875..e660f191695 100644 --- a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; namespace Microsoft.Build.Framework.Telemetry; /// -/// Interface for classes that hold telemetry data that should be added as tags to an . +/// Interface for classes that hold telemetry data that should be added as tags to an . /// internal interface IActivityTelemetryDataHolder { diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs index 14592865a68..94e194b9e48 100644 --- a/src/Framework/Telemetry/TelemetryConstants.cs +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -3,7 +3,7 @@ namespace Microsoft.Build.Framework.Telemetry; /// -/// Constants for VS OpenTelemetry for basic configuration and appropriate naming for VS exporting/collection. +/// Constants for VS Telemetry for basic configuration and appropriate naming for VS exporting/collection. /// internal static class TelemetryConstants { @@ -27,11 +27,6 @@ internal static class TelemetryConstants /// public const string DefaultActivitySourceNamespace = $"{ActivitySourceNamespacePrefix}Default"; - /// - /// For VS OpenTelemetry Collector to apply the correct privacy policy. - /// - public const string VSMajorVersion = "18.0"; - /// /// Sample rate for the default namespace. /// 1:25000 gives us sample size of sufficient confidence with the assumption we collect the order of 1e7 - 1e8 events per day. diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 54ca550913f..0bc8931a862 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. #if NETFRAMEWORK +using Microsoft.VisualStudio.Telemetry; +#endif + using System; using System.IO; using System.Runtime.CompilerServices; -using Microsoft.VisualStudio.Telemetry; -#endif namespace Microsoft.Build.Framework.Telemetry { @@ -21,6 +22,11 @@ namespace Microsoft.Build.Framework.Telemetry /// internal class TelemetryManager { + /// + /// Lock object for thread-safe initialization and disposal. + /// + private static readonly object s_lock = new object(); + private static bool s_initialized; private static bool s_disposed; @@ -37,69 +43,73 @@ private TelemetryManager() public void Initialize(bool isStandalone) { - if (s_initialized) + lock (s_lock) { - return; - } + if (s_initialized) + { + return; + } - s_initialized = true; + s_initialized = true; - if (IsOptOut()) - { - return; - } + if (IsOptOut()) + { + return; + } -#if NETFRAMEWORK - try - { - InitializeVsTelemetry(isStandalone); - } - catch (Exception ex) when ( - ex is FileNotFoundException or - FileLoadException or - TypeLoadException) - { - // Microsoft.VisualStudio.Telemetry is not available outside VS. - // This is expected in standalone MSBuild.exe scenarios. - DefaultActivitySource = null; + TryInitializeTelemetry(isStandalone); } -#else - DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); -#endif } -#if NETFRAMEWORK /// - /// Initializes Visual Studio telemetry. + /// Initializes MSBuild telemetry. /// This method is deliberately not inlined to ensure - /// the Microsoft.VisualStudio.Telemetry assembly is only loaded when this method is called, + /// the Telemetry related assemblies are only loaded when this method is called, /// allowing the calling code to catch assembly loading exceptions. /// [MethodImpl(MethodImplOptions.NoInlining)] - private void InitializeVsTelemetry(bool isStandalone) => DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); + private void TryInitializeTelemetry(bool isStandalone) + { + try + { +#if NETFRAMEWORK + DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); +#else + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); #endif + } + catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or TypeLoadException) + { + // Microsoft.VisualStudio.Telemetry or System.Diagnostics.DiagnosticSource might not be available outside of VS or dotnet. + // This is expected in standalone application scenarios. + DefaultActivitySource = null; + } + } public void Dispose() { - if (s_disposed) + lock (s_lock) { - return; - } + if (s_disposed) + { + return; + } #if NETFRAMEWORK - try - { - DisposeVsTelemetry(); - } - catch (Exception ex) when ( - ex is FileNotFoundException or - FileLoadException or - TypeLoadException) - { - // Assembly was never loaded, nothing to dispose. - } + try + { + DisposeVsTelemetry(); + } + catch (Exception ex) when ( + ex is FileNotFoundException or + FileLoadException or + TypeLoadException) + { + // Assembly was never loaded, nothing to dispose. + } #endif - s_disposed = true; + s_disposed = true; + } } #if NETFRAMEWORK @@ -110,7 +120,7 @@ FileLoadException or /// /// Determines if the user has explicitly opted out of telemetry. /// - private bool IsOptOut() => + private static bool IsOptOut() => #if NETFRAMEWORK Traits.Instance.FrameworkTelemetryOptOut; #else @@ -124,42 +134,47 @@ private bool IsOptOut() => /// This separation ensures the VS Telemetry assembly is only loaded when methods /// on this class are actually invoked. /// + /// + /// Thread-safety: All public methods on this class must be called under the + /// lock to ensure thread-safe access to static state. + /// Callers must not invoke or concurrently. + /// internal static class VsTelemetryInitializer { // Telemetry API key for Visual Studio telemetry service. private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; - private static TelemetrySession? _telemetrySession; - private static bool _ownsSession; + private static TelemetrySession? s_telemetrySession; + private static bool s_ownsSession; public static MSBuildActivitySource Initialize(bool isStandalone) { if (isStandalone) { - _telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); + s_telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); TelemetryService.DefaultSession.IsOptedIn = true; TelemetryService.DefaultSession.Start(); - _ownsSession = true; + s_ownsSession = true; } else { - _telemetrySession = TelemetryService.DefaultSession; - _ownsSession = false; + s_telemetrySession = TelemetryService.DefaultSession; + s_ownsSession = false; } - return new MSBuildActivitySource(_telemetrySession); + return new MSBuildActivitySource(s_telemetrySession); } public static void Dispose() { // Only dispose the session if we created it (standalone scenario). // In VS, the session is owned by VS and should not be disposed by MSBuild. - if (_ownsSession) + if (s_ownsSession) { - _telemetrySession?.Dispose(); + s_telemetrySession?.Dispose(); } - _telemetrySession = null; + s_telemetrySession = null; } } #endif diff --git a/src/Framework/Telemetry/VSTelemetryActivity.cs b/src/Framework/Telemetry/VSTelemetryActivity.cs index 3d765ee056c..f9f21374d1b 100644 --- a/src/Framework/Telemetry/VSTelemetryActivity.cs +++ b/src/Framework/Telemetry/VSTelemetryActivity.cs @@ -4,7 +4,6 @@ #if NETFRAMEWORK using System.Collections.Generic; -using System.Diagnostics; using Microsoft.VisualStudio.Telemetry; namespace Microsoft.Build.Framework.Telemetry @@ -21,7 +20,10 @@ internal class VsTelemetryActivity : IActivity private bool _disposed; - public VsTelemetryActivity(TelemetryScope scope) => _scope = scope; + public VsTelemetryActivity(TelemetryScope scope) + { + _scope = scope; + } public IActivity? SetTags(IActivityTelemetryDataHolder? dataHolder) { @@ -48,20 +50,6 @@ internal class VsTelemetryActivity : IActivity return this; } - public IActivity? AddEvent(ActivityEvent activityEvent) - { - // VS Telemetry doesn't have a direct equivalent to ActivityEvent - // We could create and immediately post a custom event if needed. - var telemetryEvent = new TelemetryEvent(activityEvent.Name); - foreach (KeyValuePair tag in activityEvent.Tags) - { - telemetryEvent.Properties[$"{TelemetryConstants.PropertyPrefix}{tag.Key}"] = tag.Value; - } - - TelemetryService.DefaultSession.PostEvent(telemetryEvent); - return this; - } - public void Dispose() { if (_disposed) @@ -69,7 +57,6 @@ public void Dispose() return; } - // End the operation _scope.End(_result); _disposed = true; } diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index cbea92608f1..7ff2fd0d7b7 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -154,15 +154,16 @@ public Traits() /// public bool SdkTelemetryOptOut = IsEnvVarOneOrTrue("DOTNET_CLI_TELEMETRY_OPTOUT"); public bool FrameworkTelemetryOptOut = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTOUT"); - public double? TelemetrySampleRateOverride = ParseDoubleFromEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE"); public bool ExcludeTasksDetailsFromTelemetry = IsEnvVarOneOrTrue("MSBUILDTELEMETRYEXCLUDETASKSDETAILS"); public bool FlushNodesTelemetryIntoConsole = IsEnvVarOneOrTrue("MSBUILDFLUSHNODESTELEMETRYINTOCONSOLE"); public bool EnableTargetOutputLogging = IsEnvVarOneOrTrue("MSBUILDTARGETOUTPUTLOGGING"); + // for VS17.14 + public readonly bool SlnParsingWithSolutionPersistenceOptIn = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE")); + // for VS18.* public readonly bool TelemetryOptIn = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTIN"); - public readonly bool SlnParsingWithSolutionPersistenceOptIn = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE")); public static void UpdateFromEnvironment() { @@ -180,19 +181,6 @@ private static int ParseIntFromEnvironmentVariableOrDefault(string environmentVa : defaultValue; } - /// - /// Parse a double from an environment variable with invariant culture. - /// - private static double? ParseDoubleFromEnvironmentVariable(string environmentVariable) - { - return double.TryParse(Environment.GetEnvironmentVariable(environmentVariable), - NumberStyles.Float, - CultureInfo.InvariantCulture, - out double result) - ? result - : null; - } - internal static bool IsEnvVarOneOrTrue(string name) { string? value = Environment.GetEnvironmentVariable(name); diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 1a3788a9eb6..ed918842879 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -248,7 +248,7 @@ string[] args DebuggerLaunchCheck(); // Initialize new build telemetry and record start of this build. - KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true}; + KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true }; // Initialize Telemetry // Temporarily only enable telemetry when environment variable set to "1". diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 977daa4650f..85f752a8a64 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -60,6 +60,16 @@ + + + + + + + + + + diff --git a/src/Shared/EmptyDirectoryBuildFiles/Directory.Packages.props b/src/Shared/EmptyDirectoryBuildFiles/Directory.Packages.props new file mode 100644 index 00000000000..3fdd23271ea --- /dev/null +++ b/src/Shared/EmptyDirectoryBuildFiles/Directory.Packages.props @@ -0,0 +1,3 @@ + + + From 0cdcbcf15f1d2493a3fd2429b0f36a28a4d1669e Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 17:51:23 +0100 Subject: [PATCH 40/47] return inlining in tests --- .../Telemetry/Telemetry_Tests.cs | 99 +++++++++---------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs index d2e5047b0cb..59ca59635f7 100644 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -35,9 +35,8 @@ public void WorkerNodeTelemetryCollection_BasicTarget() WorkerNodeTelemetryData? workerNodeTelemetryData = null; InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeTelemetryData = dt; - var testProject = - """ - + var testProject = """ + @@ -75,9 +74,51 @@ public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() WorkerNodeTelemetryData? workerNodeData = null; InternalTelemetryConsumingLogger.TestOnly_InternalTelemetryAggregted += dt => workerNodeData = dt; + var testProject = """ + + + + + + Log.LogMessage(MessageImportance.Low, "Hello, world!"); + + + + + + + + Log.LogMessage(MessageImportance.High, "Hello, world!"); + + + + + + + + + + + + + + + + + + + + """; + MockLogger logger = new MockLogger(_output); Helpers.BuildProjectContentUsingBuildManager( - GetTestProject(), + testProject, logger, new BuildParameters() { IsTelemetryEnabled = true }).OverallResult.ShouldBe(BuildResultCode.Success); @@ -247,55 +288,5 @@ private sealed class ProjectFinishedCapturingLogger : ILogger public void Shutdown() { } } - -#region test project - private static string GetTestProject() => - """ - - - - - - Log.LogMessage(MessageImportance.Low, "Hello, world!"); - - - - - - - - - Log.LogMessage(MessageImportance.High, "Hello, world!"); - - - - - - - < CreateItem Include="foo.bar"> - - - - - - - < Target Name="BeforeBuild"> - - < Task01 /> - - - < Target Name="NotExecuted"> - - - - """; -#endregion - } } From 31c1ff3a50e9d990a4c017492455f8404d9e1609 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 18:22:15 +0100 Subject: [PATCH 41/47] fix the key name --- src/Framework/Telemetry/BuildTelemetry.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 0c31c7c16ca..acaf6033f97 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -154,12 +154,12 @@ public override IDictionary GetProperties() AddIfNotNull(ProjectPath); AddIfNotNull(ServerFallbackReason); AddIfNotNull(BuildTarget); - AddIfNotNull(BuildEngineVersion?.ToString()); - AddIfNotNull(BuildSuccess?.ToString()); - AddIfNotNull(BuildCheckEnabled?.ToString()); - AddIfNotNull(MultiThreadedModeEnabled?.ToString()); - AddIfNotNull(SACEnabled?.ToString()); - AddIfNotNull(IsStandaloneExecution?.ToString()); + AddIfNotNull(BuildEngineVersion?.ToString(), nameof(BuildEngineVersion)); + AddIfNotNull(BuildSuccess?.ToString(), nameof(BuildSuccess)); + AddIfNotNull(BuildCheckEnabled?.ToString(), nameof(BuildCheckEnabled)); + AddIfNotNull(MultiThreadedModeEnabled?.ToString(), nameof(MultiThreadedModeEnabled)); + AddIfNotNull(SACEnabled?.ToString(), nameof(SACEnabled)); + AddIfNotNull(IsStandaloneExecution?.ToString(), nameof(IsStandaloneExecution)); // Calculate durations if (StartAt.HasValue && FinishedAt.HasValue) From 04f6372fdf09c96641736ebd7b828ff4cca97b48 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 18:23:55 +0100 Subject: [PATCH 42/47] fix keys naming --- src/Framework/Telemetry/BuildTelemetry.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index 0c31c7c16ca..acaf6033f97 100644 --- a/src/Framework/Telemetry/BuildTelemetry.cs +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -154,12 +154,12 @@ public override IDictionary GetProperties() AddIfNotNull(ProjectPath); AddIfNotNull(ServerFallbackReason); AddIfNotNull(BuildTarget); - AddIfNotNull(BuildEngineVersion?.ToString()); - AddIfNotNull(BuildSuccess?.ToString()); - AddIfNotNull(BuildCheckEnabled?.ToString()); - AddIfNotNull(MultiThreadedModeEnabled?.ToString()); - AddIfNotNull(SACEnabled?.ToString()); - AddIfNotNull(IsStandaloneExecution?.ToString()); + AddIfNotNull(BuildEngineVersion?.ToString(), nameof(BuildEngineVersion)); + AddIfNotNull(BuildSuccess?.ToString(), nameof(BuildSuccess)); + AddIfNotNull(BuildCheckEnabled?.ToString(), nameof(BuildCheckEnabled)); + AddIfNotNull(MultiThreadedModeEnabled?.ToString(), nameof(MultiThreadedModeEnabled)); + AddIfNotNull(SACEnabled?.ToString(), nameof(SACEnabled)); + AddIfNotNull(IsStandaloneExecution?.ToString(), nameof(IsStandaloneExecution)); // Calculate durations if (StartAt.HasValue && FinishedAt.HasValue) From 41b5e12639b25aadbef94558a18e3ea2e158f611 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 9 Dec 2025 21:20:01 +0100 Subject: [PATCH 43/47] fix listener in the test --- .../Telemetry/Telemetry_Tests.cs | 110 ++++++++++-------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs index 59ca59635f7..4328e5fa909 100644 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text.Json; +using System.Threading; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; @@ -159,12 +159,19 @@ public void NodeTelemetryE2E() env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); var capturedActivities = new List(); + using var activityStoppedEvent = new ManualResetEventSlim(false); using var listener = new ActivityListener { ShouldListenTo = source => source.Name.StartsWith(TelemetryConstants.DefaultActivitySourceNamespace), Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, ActivityStarted = a => { lock (capturedActivities) { capturedActivities.Add(a); } }, - ActivityStopped = _ => { } + ActivityStopped = a => + { + if (a.DisplayName == "VS/MSBuild/Build") + { + activityStoppedEvent.Set(); + } + }, }; ActivitySource.AddActivityListener(listener); @@ -220,55 +227,58 @@ public void NodeTelemetryE2E() // Phase 3: End Build - This puts telemetry to an system.diagnostics activity buildManager.EndBuild(); - - // Verify build activity were captured by the listener and contain task and target info - capturedActivities.ShouldNotBeEmpty(); - var activity = capturedActivities.FindLast(a => a.DisplayName == "VS/MSBuild/Build").ShouldNotBeNull(); - var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value); - tags.ShouldNotBeNull(); - - tags.ShouldContainKey("VS.MSBuild.BuildTarget"); - tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); - - // Verify task data - var tasks = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.Tasks")); - - var tasksData = tasks.Value as List; - var messageTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.Message"); - messageTaskData.ShouldNotBeNull(); - - // Verify Message task execution metrics - messageTaskData.ExecutionsCount.ShouldBe(3); - messageTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); - messageTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); - messageTaskData.IsCustom.ShouldBe(false); - - // Verify CreateItem task execution metrics - var createItemTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.CreateItem"); - createItemTaskData.ShouldNotBeNull(); - createItemTaskData.ExecutionsCount.ShouldBe(1); - createItemTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); - createItemTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); - - // Verify Targets summary information - var targetsSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TargetsSummary")); - var targetsSummary = targetsSummaryTagObject.Value as TargetsSummaryInfo; - targetsSummary.ShouldNotBeNull(); - targetsSummary.Loaded.Total.ShouldBe(2); - targetsSummary.Executed.Total.ShouldBe(2); - - // Verify Tasks summary information - var tasksSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TasksSummary")); - var tasksSummary = tasksSummaryTagObject.Value as TasksSummaryInfo; - tasksSummary.ShouldNotBeNull(); - - tasksSummary.Microsoft.ShouldNotBeNull(); - tasksSummary.Microsoft!.Total!.ExecutionsCount.ShouldBe(4); - tasksSummary.Microsoft!.Total!.TotalMilliseconds.ShouldBeGreaterThan(0); - - // Allowing 0 for TotalMemoryBytes as it is possible for tasks to allocate no memory in certain scenarios. - tasksSummary.Microsoft.Total.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); } + + // Wait for the activity to be fully processed + activityStoppedEvent.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue("Timed out waiting for build activity to stop"); + + // Verify build activity were captured by the listener and contain task and target info + capturedActivities.ShouldNotBeEmpty(); + var activity = capturedActivities.FindLast(a => a.DisplayName == "VS/MSBuild/Build").ShouldNotBeNull(); + var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value); + tags.ShouldNotBeNull(); + + tags.ShouldContainKey("VS.MSBuild.BuildTarget"); + tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); + + // Verify task data + var tasks = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.Tasks")); + + var tasksData = tasks.Value as List; + var messageTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.Message"); + messageTaskData.ShouldNotBeNull(); + + // Verify Message task execution metrics + messageTaskData.ExecutionsCount.ShouldBe(3); + messageTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + messageTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + messageTaskData.IsCustom.ShouldBe(false); + + // Verify CreateItem task execution metrics + var createItemTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.CreateItem"); + createItemTaskData.ShouldNotBeNull(); + createItemTaskData.ExecutionsCount.ShouldBe(1); + createItemTaskData.TotalMilliseconds.ShouldBeGreaterThan(0); + createItemTaskData.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); + + // Verify Targets summary information + var targetsSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TargetsSummary")); + var targetsSummary = targetsSummaryTagObject.Value as TargetsSummaryInfo; + targetsSummary.ShouldNotBeNull(); + targetsSummary.Loaded.Total.ShouldBe(2); + targetsSummary.Executed.Total.ShouldBe(2); + + // Verify Tasks summary information + var tasksSummaryTagObject = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.TasksSummary")); + var tasksSummary = tasksSummaryTagObject.Value as TasksSummaryInfo; + tasksSummary.ShouldNotBeNull(); + + tasksSummary.Microsoft.ShouldNotBeNull(); + tasksSummary.Microsoft!.Total!.ExecutionsCount.ShouldBe(4); + tasksSummary.Microsoft!.Total!.TotalMilliseconds.ShouldBeGreaterThan(0); + + // Allowing 0 for TotalMemoryBytes as it is possible for tasks to allocate no memory in certain scenarios. + tasksSummary.Microsoft.Total.TotalMemoryBytes.ShouldBeGreaterThanOrEqualTo(0); } #endif From b7e8c2cf13a48025d3e9f3b51e979d6776e68932 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 10 Dec 2025 10:25:46 +0100 Subject: [PATCH 44/47] add reset to ensure that when NodeTelemetryE2E runs the TelemetryManager will create a new ActivitySource --- src/Build.UnitTests/Telemetry/Telemetry_Tests.cs | 3 +++ src/Framework/Telemetry/TelemetryManager.cs | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs index 4328e5fa909..511e3a8398a 100644 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -175,6 +175,9 @@ public void NodeTelemetryE2E() }; ActivitySource.AddActivityListener(listener); + // Reset TelemetryManager to force re-initialization with our listener active + TelemetryManager.ResetForTest(); + var testProject = @" diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 0bc8931a862..a4946b0e4c6 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -61,6 +61,19 @@ public void Initialize(bool isStandalone) } } + /// + /// Resets the TelemetryManager state for TESTING purposes. + /// + internal static void ResetForTest() + { + lock (s_lock) + { + s_initialized = false; + s_disposed = false; + Instance.DefaultActivitySource = null; + } + } + /// /// Initializes MSBuild telemetry. /// This method is deliberately not inlined to ensure From ccd29ba1da32fc2e18152efa4c02b43c91b31039 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 10 Dec 2025 17:33:37 +0100 Subject: [PATCH 45/47] update optin logic for standalone execution --- src/Framework/Telemetry/TelemetryManager.cs | 2 +- src/MSBuild/XMake.cs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index a4946b0e4c6..0ab0881de7f 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -165,7 +165,7 @@ public static MSBuildActivitySource Initialize(bool isStandalone) if (isStandalone) { s_telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); - TelemetryService.DefaultSession.IsOptedIn = true; + TelemetryService.DefaultSession.IsOptedIn = Traits.Instance.TelemetryOptIn; TelemetryService.DefaultSession.Start(); s_ownsSession = true; } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index ed918842879..eda19a97bad 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -250,12 +250,7 @@ string[] args // Initialize new build telemetry and record start of this build. KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true }; - // Initialize Telemetry - // Temporarily only enable telemetry when environment variable set to "1". - if (Traits.Instance.TelemetryOptIn) - { - TelemetryManager.Instance?.Initialize(isStandalone: true); - } + TelemetryManager.Instance?.Initialize(isStandalone: true); using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); From 376a9f1a467a9507c1217e830a14feddedb07371 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 30 Dec 2025 11:04:33 +0100 Subject: [PATCH 46/47] update telemetry init logic --- .../BackEnd/BuildManager/BuildManager.cs | 8 +++--- src/Framework/Telemetry/TelemetryManager.cs | 26 ++++++++++++++----- src/Framework/Traits.cs | 3 --- src/MSBuild/XMake.cs | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 1a0cf277725..a0629dcd00f 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -3005,13 +3005,11 @@ private ILoggingService CreateLoggingService( forwardingLoggers = forwardingLoggers?.Concat(forwardingLogger) ?? forwardingLogger; } - // respect value coming from environment variable. - _buildParameters.IsTelemetryEnabled |= Traits.Instance.TelemetryOptIn; + TelemetryManager.Instance.Initialize(isStandalone: false, isExplicitlyRequested: _buildParameters.IsTelemetryEnabled); - if (_buildParameters.IsTelemetryEnabled) + // The telemetry is enabled - we need to add our consuming logger + if (TelemetryManager.Instance.DefaultActivitySource != null) { - TelemetryManager.Instance.Initialize(isStandalone: false); - // We do want to dictate our own forwarding logger (otherwise CentralForwardingLogger with minimum transferred importance MessageImportance.Low is used) // In the future we might optimize for single, in-node build scenario - where forwarding logger is not needed (but it's just quick pass-through) LoggerDescription forwardingLoggerDescription = new LoggerDescription( diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index 0ab0881de7f..f8c43a1522c 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -41,7 +41,19 @@ private TelemetryManager() public static TelemetryManager Instance { get; } = new TelemetryManager(); - public void Initialize(bool isStandalone) + /// + /// Initializes the telemetry manager with the specified configuration. + /// + /// + /// Indicates whether MSBuild is running in standalone mode (e.g., MSBuild.exe directly invoked) + /// versus integrated mode (e.g., running within Visual Studio or dotnet CLI). + /// When true, creates and manages its own telemetry session on .NET Framework. + /// + /// + /// Indicates whether telemetry was explicitly requested through command line arguments. + /// On .NET, telemetry is only enabled when this is true. + /// + public void Initialize(bool isStandalone, bool isExplicitlyRequested) { lock (s_lock) { @@ -57,7 +69,7 @@ public void Initialize(bool isStandalone) return; } - TryInitializeTelemetry(isStandalone); + TryInitializeTelemetry(isStandalone, isExplicitlyRequested); } } @@ -81,20 +93,22 @@ internal static void ResetForTest() /// allowing the calling code to catch assembly loading exceptions. /// [MethodImpl(MethodImplOptions.NoInlining)] - private void TryInitializeTelemetry(bool isStandalone) + private void TryInitializeTelemetry(bool isStandalone, bool isExplicitlyRequested) { try { #if NETFRAMEWORK DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); #else - DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); + DefaultActivitySource = isExplicitlyRequested + ? new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace) + : null; #endif } catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or TypeLoadException) { // Microsoft.VisualStudio.Telemetry or System.Diagnostics.DiagnosticSource might not be available outside of VS or dotnet. - // This is expected in standalone application scenarios. + // This is expected in standalone application scenarios (when MSBuild.exe is invoked directly). DefaultActivitySource = null; } } @@ -165,7 +179,7 @@ public static MSBuildActivitySource Initialize(bool isStandalone) if (isStandalone) { s_telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); - TelemetryService.DefaultSession.IsOptedIn = Traits.Instance.TelemetryOptIn; + TelemetryService.DefaultSession.UseVsIsOptedIn(); TelemetryService.DefaultSession.Start(); s_ownsSession = true; } diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 7ff2fd0d7b7..d02e95ce944 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -162,9 +162,6 @@ public Traits() // for VS17.14 public readonly bool SlnParsingWithSolutionPersistenceOptIn = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE")); - // for VS18.* - public readonly bool TelemetryOptIn = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTIN"); - public static void UpdateFromEnvironment() { // Re-create Traits instance to update values in Traits according to current environment. diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index b5ad7405d7e..7d5491ea05a 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -250,7 +250,7 @@ string[] args // Initialize new build telemetry and record start of this build. KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true }; - TelemetryManager.Instance?.Initialize(isStandalone: true); + TelemetryManager.Instance?.Initialize(isStandalone: true, isExplicitlyRequested: false); using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); From 4e6d0b369c4a7935f32fe9075747f8db9dded375 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Tue, 6 Jan 2026 13:13:37 +0100 Subject: [PATCH 47/47] fix logger message importance and other cleanup --- eng/Signing.props | 3 + .../BackEnd/BuildManager_Tests.cs | 1 - .../Telemetry/Telemetry_Tests.cs | 3 +- .../BackEnd/BuildManager/BuildManager.cs | 25 +++++--- .../Components/Logging/LoggingService.cs | 5 ++ .../RequestBuilder/RequestBuilder.cs | 9 ++- src/Framework/Telemetry/TelemetryDataUtils.cs | 20 +++--- src/Framework/Telemetry/TelemetryManager.cs | 61 ++++++++----------- src/MSBuild/XMake.cs | 2 +- src/MSBuild/app.amd64.config | 4 ++ src/Package/MSBuild.VSSetup/files.swr | 1 + 11 files changed, 74 insertions(+), 60 deletions(-) diff --git a/eng/Signing.props b/eng/Signing.props index d46e57e8e34..00e8367eb86 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -11,6 +11,9 @@ + + + diff --git a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs index 06cf011f5a2..ab4af5b59d6 100644 --- a/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs +++ b/src/Build.UnitTests/BackEnd/BuildManager_Tests.cs @@ -1801,7 +1801,6 @@ public void OverlappingBuildsOfTheSameProjectDifferentTargetsAreAllowed() "); - Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true); ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); _buildManager.BeginBuild(_parameters); diff --git a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs index 511e3a8398a..fb2459d683b 100644 --- a/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs +++ b/src/Build.UnitTests/Telemetry/Telemetry_Tests.cs @@ -154,7 +154,6 @@ public void WorkerNodeTelemetryCollection_CustomTargetsAndTasks() public void NodeTelemetryE2E() { using TestEnvironment env = TestEnvironment.Create(); - env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTIN", "1"); env.SetEnvironmentVariable("MSBUILD_TELEMETRY_OPTOUT", null); env.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", null); @@ -245,7 +244,7 @@ public void NodeTelemetryE2E() tags["VS.MSBuild.BuildTarget"].ShouldNotBeNullOrEmpty(); // Verify task data - var tasks = activity.TagObjects.FirstOrDefault(to => to.Key.Contains("VS.MSBuild.Tasks")); + var tasks = activity.TagObjects.FirstOrDefault(to => to.Key == "VS.MSBuild.Tasks"); var tasksData = tasks.Value as List; var messageTaskData = tasksData!.FirstOrDefault(t => t.Name == "Microsoft.Build.Tasks.Message"); diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index a0629dcd00f..887ce2d15ca 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -13,11 +13,7 @@ using System.IO; using System.Linq; using System.Reflection; - -#if FEATURE_REPORTFILEACCESSES using System.Runtime.CompilerServices; -#endif - using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; @@ -30,13 +26,13 @@ using Microsoft.Build.Exceptions; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.Experimental.BuildCheck.Infrastructure; -using Microsoft.Build.ProjectCache; using Microsoft.Build.FileAccesses; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Graph; using Microsoft.Build.Internal; using Microsoft.Build.Logging; +using Microsoft.Build.ProjectCache; using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; @@ -299,6 +295,7 @@ public BuildManager() public BuildManager(string hostName) { ErrorUtilities.VerifyThrowArgumentNull(hostName); + _hostName = hostName; _buildManagerState = BuildManagerState.Idle; _buildSubmissions = new Dictionary(); @@ -463,6 +460,11 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) /// Thrown if a build is already in progress. public void BeginBuild(BuildParameters parameters) { +#if NETFRAMEWORK + // Collect telemetry unless explicitly opted out via environment variable. + // The decision to send telemetry is made at EndBuild to avoid eager loading of telemetry assemblies. + parameters.IsTelemetryEnabled |= !TelemetryManager.IsOptOut(); +#endif if (_previousLowPriority != null) { if (parameters.LowPriority != _previousLowPriority) @@ -1103,6 +1105,7 @@ public void EndBuild() { host = "VSCode"; } + _buildTelemetry.BuildEngineHost = host; _buildTelemetry.BuildCheckEnabled = _buildParameters!.IsBuildCheckEnabled; @@ -1112,6 +1115,7 @@ public void EndBuild() _buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement; loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties()); + EndBuildTelemetry(); // Clean telemetry to make it ready for next build submission. @@ -1156,9 +1160,13 @@ void SerializeCaches() } } + [MethodImpl(MethodImplOptions.NoInlining)] private void EndBuildTelemetry() { - using IActivity? activity = TelemetryManager.Instance?.DefaultActivitySource + TelemetryManager.Instance.Initialize(isStandalone: false); + + using IActivity? activity = TelemetryManager.Instance + ?.DefaultActivitySource ?.StartActivity(TelemetryConstants.Build) ?.SetTags(_buildTelemetry) ?.SetTags(_telemetryConsumingLogger?.WorkerNodeTelemetryData.AsActivityDataHolder( @@ -3005,10 +3013,7 @@ private ILoggingService CreateLoggingService( forwardingLoggers = forwardingLoggers?.Concat(forwardingLogger) ?? forwardingLogger; } - TelemetryManager.Instance.Initialize(isStandalone: false, isExplicitlyRequested: _buildParameters.IsTelemetryEnabled); - - // The telemetry is enabled - we need to add our consuming logger - if (TelemetryManager.Instance.DefaultActivitySource != null) + if (_buildParameters.IsTelemetryEnabled) { // We do want to dictate our own forwarding logger (otherwise CentralForwardingLogger with minimum transferred importance MessageImportance.Low is used) // In the future we might optimize for single, in-node build scenario - where forwarding logger is not needed (but it's just quick pass-through) diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 487c10b69b0..d2067256d9c 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -1848,6 +1848,11 @@ private void UpdateMinimumMessageImportance(ILogger logger) // The null logger has no effect on minimum verbosity. Execution.BuildManager.NullLogger => null, + // Telemetry loggers only consume WorkerNodeTelemetryLogged events, not message events. + // They have no effect on minimum message verbosity. + TelemetryInfra.InternalTelemetryConsumingLogger => null, + Framework.Telemetry.InternalTelemetryForwardingLogger => null, + TerminalLogger terminalLogger => terminalLogger.GetMinimumMessageImportance(), _ => innerLogger.GetType().FullName == "Microsoft.Build.Logging.TerminalLogger" diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index d99df97edb7..f4eeca2c1ef 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -1267,9 +1267,9 @@ private void UpdateStatisticsPostBuild() { ITelemetryForwarder telemetryForwarder = ((TelemetryForwarderProvider)_componentHost.GetComponent(BuildComponentType.TelemetryForwarder)) - .Instance; + ?.Instance; - if (!telemetryForwarder.IsTelemetryCollected) + if (telemetryForwarder == null || !telemetryForwarder.IsTelemetryCollected) { return; } @@ -1279,6 +1279,11 @@ private void UpdateStatisticsPostBuild() // Hence we need to fetch the original result from the cache - to get the data for all executed targets. BuildResult unfilteredResult = resultsCache.GetResultsForConfiguration(_requestEntry.Request.ConfigurationId); + if (unfilteredResult?.ResultsByTarget == null || _requestEntry.RequestConfiguration.Project?.Targets == null) + { + return; + } + foreach (var projectTargetInstance in _requestEntry.RequestConfiguration.Project.Targets) { bool wasExecuted = diff --git a/src/Framework/Telemetry/TelemetryDataUtils.cs b/src/Framework/Telemetry/TelemetryDataUtils.cs index 95ef2067d39..b7202bd897b 100644 --- a/src/Framework/Telemetry/TelemetryDataUtils.cs +++ b/src/Framework/Telemetry/TelemetryDataUtils.cs @@ -31,8 +31,8 @@ internal static class TelemetryDataUtils tasksSummary.Process(telemetryData.TasksExecutionData); var buildInsights = new BuildInsights( - GetTasksDetails(telemetryData.TasksExecutionData), - GetTargetsDetails(telemetryData.TargetsExecutionData), + includeTasksDetails ? GetTasksDetails(telemetryData.TasksExecutionData) : [], + includeTargetDetails ? GetTargetsDetails(telemetryData.TargetsExecutionData) : [], GetTargetsSummary(targetsSummary), GetTasksSummary(tasksSummary)); @@ -128,8 +128,6 @@ public static string Hash(string text) } #endif } - - public static string HashWithNormalizedCasing(string text) => Hash(text.ToUpperInvariant()); } internal record TaskDetailInfo(string Name, double TotalMilliseconds, int ExecutionsCount, long TotalMemoryBytes, bool IsCustom, bool IsNuget); @@ -230,7 +228,7 @@ private TargetInfo GetTargetInfo(TaskOrTargetTelemetryKey key, bool isExecuted) (true, true) => ExecutedCustomTargetInfo, (true, false) => LoadedCustomTargetInfo, (false, true) => ExecutedBuiltinTargetInfo, - (false, false) => LoadedBuiltinTargetInfo + (false, false) => LoadedBuiltinTargetInfo, }; internal class TargetInfo @@ -294,12 +292,20 @@ Dictionary IActivityTelemetryDataHolder.GetActivityProperties() { Dictionary properties = new() { - [nameof(BuildInsights.Tasks)] = insights.Tasks, - [nameof(BuildInsights.Targets)] = insights.Targets, [nameof(BuildInsights.TargetsSummary)] = insights.TargetsSummary, [nameof(BuildInsights.TasksSummary)] = insights.TasksSummary, }; + if (insights.Targets.Count > 0) + { + properties[nameof(BuildInsights.Targets)] = insights.Targets; + } + + if (insights.Tasks.Count > 0) + { + properties[nameof(BuildInsights.Tasks)] = insights.Tasks; + } + return properties; } } diff --git a/src/Framework/Telemetry/TelemetryManager.cs b/src/Framework/Telemetry/TelemetryManager.cs index f8c43a1522c..4b55507436b 100644 --- a/src/Framework/Telemetry/TelemetryManager.cs +++ b/src/Framework/Telemetry/TelemetryManager.cs @@ -25,7 +25,7 @@ internal class TelemetryManager /// /// Lock object for thread-safe initialization and disposal. /// - private static readonly object s_lock = new object(); + private static readonly LockType s_lock = new(); private static bool s_initialized; private static bool s_disposed; @@ -49,11 +49,8 @@ private TelemetryManager() /// versus integrated mode (e.g., running within Visual Studio or dotnet CLI). /// When true, creates and manages its own telemetry session on .NET Framework. /// - /// - /// Indicates whether telemetry was explicitly requested through command line arguments. - /// On .NET, telemetry is only enabled when this is true. - /// - public void Initialize(bool isStandalone, bool isExplicitlyRequested) + [MethodImpl(MethodImplOptions.NoInlining)] + public void Initialize(bool isStandalone) { lock (s_lock) { @@ -69,7 +66,7 @@ public void Initialize(bool isStandalone, bool isExplicitlyRequested) return; } - TryInitializeTelemetry(isStandalone, isExplicitlyRequested); + TryInitializeTelemetry(isStandalone); } } @@ -93,16 +90,14 @@ internal static void ResetForTest() /// allowing the calling code to catch assembly loading exceptions. /// [MethodImpl(MethodImplOptions.NoInlining)] - private void TryInitializeTelemetry(bool isStandalone, bool isExplicitlyRequested) + private void TryInitializeTelemetry(bool isStandalone) { try { #if NETFRAMEWORK DefaultActivitySource = VsTelemetryInitializer.Initialize(isStandalone); #else - DefaultActivitySource = isExplicitlyRequested - ? new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace) - : null; + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); #endif } catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or TypeLoadException) @@ -139,66 +134,58 @@ FileLoadException or } } -#if NETFRAMEWORK - [MethodImpl(MethodImplOptions.NoInlining)] - private static void DisposeVsTelemetry() => VsTelemetryInitializer.Dispose(); -#endif - /// /// Determines if the user has explicitly opted out of telemetry. /// - private static bool IsOptOut() => + internal static bool IsOptOut() => #if NETFRAMEWORK Traits.Instance.FrameworkTelemetryOptOut; #else Traits.Instance.SdkTelemetryOptOut; #endif + +#if NETFRAMEWORK + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DisposeVsTelemetry() => VsTelemetryInitializer.Dispose(); +#endif } #if NETFRAMEWORK - /// - /// Isolated class that references Microsoft.VisualStudio.Telemetry types. - /// This separation ensures the VS Telemetry assembly is only loaded when methods - /// on this class are actually invoked. - /// - /// - /// Thread-safety: All public methods on this class must be called under the - /// lock to ensure thread-safe access to static state. - /// Callers must not invoke or concurrently. - /// internal static class VsTelemetryInitializer { // Telemetry API key for Visual Studio telemetry service. private const string CollectorApiKey = "f3e86b4023cc43f0be495508d51f588a-f70d0e59-0fb0-4473-9f19-b4024cc340be-7296"; - private static TelemetrySession? s_telemetrySession; - private static bool s_ownsSession; + // Store as object to avoid type reference at class load time + private static object? s_telemetrySession; + private static bool s_ownsSession = false; + [MethodImpl(MethodImplOptions.NoInlining)] public static MSBuildActivitySource Initialize(bool isStandalone) { + TelemetrySession session; if (isStandalone) { - s_telemetrySession = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); + session = TelemetryService.CreateAndGetDefaultSession(CollectorApiKey); TelemetryService.DefaultSession.UseVsIsOptedIn(); TelemetryService.DefaultSession.Start(); s_ownsSession = true; } else { - s_telemetrySession = TelemetryService.DefaultSession; - s_ownsSession = false; + session = TelemetryService.DefaultSession; } - return new MSBuildActivitySource(s_telemetrySession); + s_telemetrySession = session; + return new MSBuildActivitySource(session); } + [MethodImpl(MethodImplOptions.NoInlining)] public static void Dispose() { - // Only dispose the session if we created it (standalone scenario). - // In VS, the session is owned by VS and should not be disposed by MSBuild. - if (s_ownsSession) + if (s_ownsSession && s_telemetrySession is TelemetrySession session) { - s_telemetrySession?.Dispose(); + session.Dispose(); } s_telemetrySession = null; diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 7d5491ea05a..b5ad7405d7e 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -250,7 +250,7 @@ string[] args // Initialize new build telemetry and record start of this build. KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow, IsStandaloneExecution = true }; - TelemetryManager.Instance?.Initialize(isStandalone: true, isExplicitlyRequested: false); + TelemetryManager.Instance?.Initialize(isStandalone: true); using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 85f752a8a64..00194107526 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -106,6 +106,10 @@ + + + + diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 4d2e84f524e..4e091c37e8e 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -89,6 +89,7 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.WinFx.targets file source=$(X86BinPath)Microsoft.WorkflowBuildExtensions.targets file source=$(X86BinPath)Microsoft.VisualStudio.Utilities.Internal.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=3 + file source=$(X86BinPath)Newtonsoft.Json.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 folder InstallDir:\MSBuild\Current\Bin\MSBuild file source=$(X86BinPath)\MSBuild\Microsoft.Build.Core.xsd