From 072c659ff4d49bdf70069cd36df6dc9e44719432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Fri, 21 Feb 2025 13:55:43 +0100 Subject: [PATCH] Implement OpenTelemetry infrastructure (#11255) --- NuGet.config | 8 + THIRDPARTYNOTICES.txt | 45 ++- documentation/release-checklist.md | 6 + documentation/release.md | 8 + documentation/wiki/ChangeWaves.md | 1 + eng/Packages.props | 3 + eng/Signing.props | 6 + eng/SourceBuildPrebuiltBaseline.xml | 2 + eng/Version.Details.xml | 4 + eng/Versions.props | 2 + .../BackEnd/KnownTelemetry_Tests.cs | 32 +- .../BackEnd/OpenTelemetryActivities_Tests.cs | 196 ++++++++++++ .../BackEnd/OpenTelemetryManager_Tests.cs | 152 ++++++++++ .../Microsoft.Build.Engine.UnitTests.csproj | 7 +- .../BackEnd/BuildManager/BuildManager.cs | 31 +- src/Build/BackEnd/Client/MSBuildClient.cs | 4 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 2 +- .../Microsoft.Build.Framework.csproj | 9 + src/Framework/Telemetry/ActivityExtensions.cs | 108 +++++++ src/Framework/Telemetry/BuildTelemetry.cs | 111 +++++-- .../Telemetry/IActivityTelemetryDataHolder.cs | 15 + .../Telemetry/MSBuildActivitySource.cs | 35 +++ .../Telemetry/OpenTelemetryManager.cs | 281 ++++++++++++++++++ src/Framework/Telemetry/TelemetryConstants.cs | 50 ++++ src/Framework/Telemetry/TelemetryItem.cs | 6 + src/Framework/Traits.cs | 41 ++- src/MSBuild/MSBuild.csproj | 7 + src/MSBuild/XMake.cs | 3 + src/MSBuild/app.amd64.config | 82 +++++ src/MSBuild/app.config | 8 + src/Package/MSBuild.VSSetup/files.swr | 20 ++ src/Package/Microsoft.Build.UnGAC/Program.cs | 2 + 32 files changed, 1213 insertions(+), 74 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/OpenTelemetryActivities_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/OpenTelemetryManager_Tests.cs create mode 100644 src/Framework/Telemetry/ActivityExtensions.cs create mode 100644 src/Framework/Telemetry/IActivityTelemetryDataHolder.cs create mode 100644 src/Framework/Telemetry/MSBuildActivitySource.cs create mode 100644 src/Framework/Telemetry/OpenTelemetryManager.cs create mode 100644 src/Framework/Telemetry/TelemetryConstants.cs create mode 100644 src/Framework/Telemetry/TelemetryItem.cs diff --git a/NuGet.config b/NuGet.config index 107cd4542dc..beebd60e603 100644 --- a/NuGet.config +++ b/NuGet.config @@ -13,6 +13,14 @@ + + + + + + + + diff --git a/THIRDPARTYNOTICES.txt b/THIRDPARTYNOTICES.txt index 28661c086ad..49e551d4279 100644 --- a/THIRDPARTYNOTICES.txt +++ b/THIRDPARTYNOTICES.txt @@ -1,7 +1,7 @@ -MSBuild uses third-party material as listed below. The attached notices are -provided for informational purposes only. +MSBuild uses third-party material as listed below. The attached notices are +provided for informational purposes only. -Notice for LockCheck +Notice for LockCheck ------------------------------- The MIT License (MIT) @@ -27,20 +27,49 @@ SOFTWARE. ------------------------------- -Notice for Samples for xUnit.net +Notice for Samples for xUnit.net ------------------------------- -Copyright (c) .NET Foundation and Contributors +Copyright (c) .NET Foundation and Contributors -All Rights Reserved +All Rights Reserved Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. +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 +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. + +------------------------------- + +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. + +------------------------------- + +Notice for Microsoft.VisualStudio.OpenTelemetry.* ------------------------------- +MSBuild.exe is distributed with Microsoft.VisualStudio.OpenTelemetry.* binaries. + +Project: Microsoft.VisualStudio.OpenTelemetry +Copyright: (c) Microsoft Corporation +License: https://visualstudio.microsoft.com/license-terms/mt736442/ \ No newline at end of file diff --git a/documentation/release-checklist.md b/documentation/release-checklist.md index f7c8d95742b..7e077ebfa42 100644 --- a/documentation/release-checklist.md +++ b/documentation/release-checklist.md @@ -130,3 +130,9 @@ Timing based on the [(Microsoft-internal) release schedule](https://dev.azure.co git push upstream v{{THIS_RELEASE_VERSION}}.3 ``` - [ ] Create Release in Github with `Create Release from Tag` GH option (https://github.com/dotnet/msbuild/releases/new?tag=v17.9.3) - the release notes can be prepopulated (`Generate Release Notes`) + +## After release + +If v{{NEXT_VERSION}} is a new major version + +- [ ] do Major version extra update steps from [release.md](./release.md) diff --git a/documentation/release.md b/documentation/release.md index 7ef016408f5..febe2cc1317 100644 --- a/documentation/release.md +++ b/documentation/release.md @@ -17,3 +17,11 @@ As of [#7018](https://github.com/dotnet/msbuild/pull/7018), MSBuild uses a Rosly 3. At release time, we must manually promote the `Unshipped` public API to `Shipped`. That is a new step in our release process for each formal release (including patch releases if they change API surface). + +## Major version extra update steps + +Update major version of VS in + +- [BuildEnvironmentHelper.cs](../src/Shared/BuildEnvironmentHelper.cs) +- [Constants.cs](../src/Shared/Constants.cs) +- [TelemetryConstants.cs](../src/Framework/Telemetry/TelemetryConstants.cs) diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 1dfe8a21f13..304e6b3a8b3 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -27,6 +27,7 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t ### 17.14 - [.SLNX support - use the new parser for .sln and .slnx](https://github.com/dotnet/msbuild/pull/10836) - [Support custom culture in RAR](https://github.com/dotnet/msbuild/pull/11000) +- [VS Telemetry](https://github.com/dotnet/msbuild/pull/11255) ### 17.12 - [Log TaskParameterEvent for scalar parameters](https://github.com/dotnet/msbuild/pull/9908) diff --git a/eng/Packages.props b/eng/Packages.props index 87cf3b78909..a677edd736e 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -33,5 +33,8 @@ + + + diff --git a/eng/Signing.props b/eng/Signing.props index b3d45b6fcf3..6b21c5d2ef4 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -7,6 +7,12 @@ + + + + + + diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml index 1421f999956..15779fde2b2 100644 --- a/eng/SourceBuildPrebuiltBaseline.xml +++ b/eng/SourceBuildPrebuiltBaseline.xml @@ -19,6 +19,8 @@ + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1d22dc041f2..545a0ee8353 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -69,6 +69,10 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 2aade6beb02ea367fd97c4070a4198802fe61c03 + + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime + 2d7eea252964e69be94cb9c847b371b23e4dd470 + diff --git a/eng/Versions.props b/eng/Versions.props index 987b385ed57..616a114d933 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -54,6 +54,8 @@ 8.0.5 8.0.0 8.0.0 + 8.0.1 + 0.2.104-beta diff --git a/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs index cfbf63deebd..5f25dc3a248 100644 --- a/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs +++ b/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs @@ -35,19 +35,19 @@ public void BuildTelemetryConstructedHasNoProperties() { BuildTelemetry buildTelemetry = new BuildTelemetry(); - buildTelemetry.DisplayVersion.ShouldBeNull(); + buildTelemetry.BuildEngineDisplayVersion.ShouldBeNull(); buildTelemetry.EventName.ShouldBe("build"); buildTelemetry.FinishedAt.ShouldBeNull(); - buildTelemetry.FrameworkName.ShouldBeNull(); - buildTelemetry.Host.ShouldBeNull(); - buildTelemetry.InitialServerState.ShouldBeNull(); + buildTelemetry.BuildEngineFrameworkName.ShouldBeNull(); + buildTelemetry.BuildEngineHost.ShouldBeNull(); + buildTelemetry.InitialMSBuildServerState.ShouldBeNull(); buildTelemetry.InnerStartAt.ShouldBeNull(); - buildTelemetry.Project.ShouldBeNull(); + buildTelemetry.ProjectPath.ShouldBeNull(); buildTelemetry.ServerFallbackReason.ShouldBeNull(); buildTelemetry.StartAt.ShouldBeNull(); - buildTelemetry.Success.ShouldBeNull(); - buildTelemetry.Target.ShouldBeNull(); - buildTelemetry.Version.ShouldBeNull(); + buildTelemetry.BuildSuccess.ShouldBeNull(); + buildTelemetry.BuildTarget.ShouldBeNull(); + buildTelemetry.BuildEngineVersion.ShouldBeNull(); buildTelemetry.GetProperties().ShouldBeEmpty(); } @@ -61,18 +61,18 @@ public void BuildTelemetryCreateProperProperties() DateTime innerStartAt = new DateTime(2023, 01, 02, 10, 20, 30); DateTime finishedAt = new DateTime(2023, 12, 13, 14, 15, 16); - buildTelemetry.DisplayVersion = "Some Display Version"; + buildTelemetry.BuildEngineDisplayVersion = "Some Display Version"; buildTelemetry.FinishedAt = finishedAt; - buildTelemetry.FrameworkName = "new .NET"; - buildTelemetry.Host = "Host description"; - buildTelemetry.InitialServerState = "hot"; + buildTelemetry.BuildEngineFrameworkName = "new .NET"; + buildTelemetry.BuildEngineHost = "Host description"; + buildTelemetry.InitialMSBuildServerState = "hot"; buildTelemetry.InnerStartAt = innerStartAt; - buildTelemetry.Project = @"C:\\dev\\theProject"; + buildTelemetry.ProjectPath = @"C:\\dev\\theProject"; buildTelemetry.ServerFallbackReason = "busy"; buildTelemetry.StartAt = startAt; - buildTelemetry.Success = true; - buildTelemetry.Target = "clean"; - buildTelemetry.Version = new Version(1, 2, 3, 4); + buildTelemetry.BuildSuccess = true; + buildTelemetry.BuildTarget = "clean"; + buildTelemetry.BuildEngineVersion = new Version(1, 2, 3, 4); var properties = buildTelemetry.GetProperties(); diff --git a/src/Build.UnitTests/BackEnd/OpenTelemetryActivities_Tests.cs b/src/Build.UnitTests/BackEnd/OpenTelemetryActivities_Tests.cs new file mode 100644 index 00000000000..cd041632de2 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/OpenTelemetryActivities_Tests.cs @@ -0,0 +1,196 @@ +// 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; +using Xunit; +using Shouldly; + +namespace Microsoft.Build.Framework.Telemetry.Tests +{ + 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/BackEnd/OpenTelemetryManager_Tests.cs b/src/Build.UnitTests/BackEnd/OpenTelemetryManager_Tests.cs new file mode 100644 index 00000000000..a2ec5161797 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/OpenTelemetryManager_Tests.cs @@ -0,0 +1,152 @@ +// 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.Reflection; +using Xunit; +using Shouldly; +using Xunit.Abstractions; +using Microsoft.Build.UnitTests.Shared; +using Microsoft.Build.UnitTests; + +namespace Microsoft.Build.Framework.Telemetry.Tests +{ + /// + /// Ensures tests run serially so environment variables and the singleton do not interfere with parallel test runs. + /// + [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"; + + private string? preTestFxOptout; + private string? preTestDotnetOptout; + private string? preTestSampleRate; + private string? preTestVS1714TelemetryOptIn; + + public OpenTelemetryManagerTests() + { + // control environment state before each test + SaveEnvVars(); + ResetManagerState(); + ResetEnvVars(); + } + + private void SaveEnvVars() + { + preTestFxOptout = Environment.GetEnvironmentVariable(TelemetryFxOptoutEnvVarName); + preTestDotnetOptout = Environment.GetEnvironmentVariable(DotnetOptOut); + preTestSampleRate = Environment.GetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName); + preTestVS1714TelemetryOptIn = Environment.GetEnvironmentVariable(VS1714TelemetryOptInEnvVarName); + } + + private void RestoreEnvVars() + { + Environment.SetEnvironmentVariable(TelemetryFxOptoutEnvVarName, preTestFxOptout); + Environment.SetEnvironmentVariable(DotnetOptOut, preTestDotnetOptout); + Environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, preTestSampleRate); + Environment.SetEnvironmentVariable(VS1714TelemetryOptInEnvVarName, preTestVS1714TelemetryOptIn); + } + + private void ResetEnvVars() + { + Environment.SetEnvironmentVariable(DotnetOptOut, null); + Environment.SetEnvironmentVariable(TelemetryFxOptoutEnvVarName, null); + Environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, null); + Environment.SetEnvironmentVariable(VS1714TelemetryOptInEnvVarName, null); + } + + public void Dispose() + { + RestoreEnvVars(); + } + + [Theory] + [InlineData(DotnetOptOut, "true")] + [InlineData(TelemetryFxOptoutEnvVarName, "true")] + [InlineData(DotnetOptOut, "1")] + [InlineData(TelemetryFxOptoutEnvVarName, "1")] + public void Initialize_ShouldSetStateToOptOut_WhenOptOutEnvVarIsTrue(string optoutVar, string value) + { + // Arrange + Environment.SetEnvironmentVariable(optoutVar, value); + + // Act + OpenTelemetryManager.Instance.Initialize(isStandalone: false); + + // Assert + OpenTelemetryManager.Instance.IsActive().ShouldBeFalse(); + } + +#if NET + [Fact] + public void Initialize_ShouldSetStateToUnsampled_WhenNoOverrideOnNetCore() + { + Environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, 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 + Environment.SetEnvironmentVariable(VS1714TelemetryOptInEnvVarName, "1"); + Environment.SetEnvironmentVariable(TelemetrySampleRateOverrideEnvVarName, "1.0"); + + // Act + OpenTelemetryManager.Instance.Initialize(isStandalone: standalone); + + // Assert + OpenTelemetryManager.Instance.IsActive().ShouldBeTrue(); + OpenTelemetryManager.Instance.DefaultActivitySource.ShouldNotBeNull(); + } + + [Fact] + public void Initialize_ShouldNoOp_WhenCalledMultipleTimes() + { + 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); + } + + /* 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.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index a5f9ba12a47..c49cd75150a 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -26,6 +26,9 @@ all + + + @@ -34,7 +37,7 @@ - + TargetFramework=$(FullFrameworkTFM) TargetFramework=$(LatestDotNetCoreForMSBuild) @@ -45,7 +48,7 @@ TargetFramework=$(FullFrameworkTFM) TargetFramework=$(LatestDotNetCoreForMSBuild) - + diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 39eefd96415..947c7764de6 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -453,6 +453,7 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) /// Thrown if a build is already in progress. public void BeginBuild(BuildParameters parameters) { + OpenTelemetryManager.Instance.Initialize(isStandalone: false); if (_previousLowPriority != null) { if (parameters.LowPriority != _previousLowPriority) @@ -910,8 +911,8 @@ private BuildSubmissionBase PendBuildRequest /// Convenience method. Submits a lone build request and blocks until results are available. /// diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 4a496ed7f8d..0abf86a2aa2 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -176,7 +176,7 @@ public MSBuildClientExitResult Execute(CancellationToken cancellationToken) bool serverIsAlreadyRunning = ServerIsRunning(); if (KnownTelemetry.PartialBuildTelemetry != null) { - KnownTelemetry.PartialBuildTelemetry.InitialServerState = serverIsAlreadyRunning ? "hot" : "cold"; + KnownTelemetry.PartialBuildTelemetry.InitialMSBuildServerState = serverIsAlreadyRunning ? "hot" : "cold"; } if (!serverIsAlreadyRunning) { @@ -521,7 +521,7 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand() ? null : new PartialBuildTelemetry( startedAt: KnownTelemetry.PartialBuildTelemetry.StartAt.GetValueOrDefault(), - initialServerState: KnownTelemetry.PartialBuildTelemetry.InitialServerState, + initialServerState: KnownTelemetry.PartialBuildTelemetry.InitialMSBuildServerState, serverFallbackReason: KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason); return new ServerNodeBuildCommand( diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index ab17e3b7ce1..bda79d588cd 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -381,7 +381,7 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) BuildTelemetry buildTelemetry = KnownTelemetry.PartialBuildTelemetry ??= new BuildTelemetry(); buildTelemetry.StartAt = command.PartialBuildTelemetry.StartedAt; - buildTelemetry.InitialServerState = command.PartialBuildTelemetry.InitialServerState; + buildTelemetry.InitialMSBuildServerState = command.PartialBuildTelemetry.InitialServerState; buildTelemetry.ServerFallbackReason = command.PartialBuildTelemetry.ServerFallbackReason; } diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 911198afdf5..95240faa44b 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -23,6 +23,15 @@ + + + + + + + + + diff --git a/src/Framework/Telemetry/ActivityExtensions.cs b/src/Framework/Telemetry/ActivityExtensions.cs new file mode 100644 index 00000000000..91648067ae2 --- /dev/null +++ b/src/Framework/Telemetry/ActivityExtensions.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; +using System.Text; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +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) + { + 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. + /// + private static object 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 c23d9269c9b..c1a5541def3 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 + internal class BuildTelemetry : TelemetryBase, IActivityTelemetryDataHolder { public override string EventName => "build"; @@ -40,12 +40,12 @@ internal class BuildTelemetry : TelemetryBase /// /// Overall build success. /// - public bool? Success { get; set; } + public bool? BuildSuccess { get; set; } /// /// Build Target. /// - public string? Target { get; set; } + public string? BuildTarget { get; set; } /// /// MSBuild server fallback reason. @@ -56,23 +56,23 @@ internal class BuildTelemetry : TelemetryBase /// /// Version of MSBuild. /// - public Version? Version { get; set; } + public Version? BuildEngineVersion { get; set; } /// /// Display version of the Engine suitable for display to a user. /// - public string? DisplayVersion { get; set; } + public string? BuildEngineDisplayVersion { get; set; } /// /// Path to project file. /// - public string? Project { get; set; } + public string? ProjectPath { get; set; } /// /// Host in which MSBuild build was executed. /// For example: "VS", "VSCode", "Azure DevOps", "GitHub Action", "CLI", ... /// - public string? Host { get; set; } + public string? BuildEngineHost { get; set; } /// /// True if buildcheck was used. @@ -88,84 +88,135 @@ internal class BuildTelemetry : TelemetryBase /// State of MSBuild server process before this build. /// One of 'cold', 'hot', null (if not run as server) /// - public string? InitialServerState { get; set; } + public string? InitialMSBuildServerState { get; set; } /// /// Framework name suitable for display to a user. /// - public string? FrameworkName { get; set; } + public string? BuildEngineFrameworkName { get; set; } public override IDictionary GetProperties() { var properties = new Dictionary(); // populate property values - if (DisplayVersion != null) + if (BuildEngineDisplayVersion != null) { - properties["BuildEngineDisplayVersion"] = DisplayVersion; + properties[nameof(BuildEngineDisplayVersion)] = BuildEngineDisplayVersion; } if (StartAt.HasValue && FinishedAt.HasValue) { - properties["BuildDurationInMilliseconds"] = (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["InnerBuildDurationInMilliseconds"] = (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + properties[TelemetryConstants.InnerBuildDurationPropertyName] = (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); } - if (FrameworkName != null) + if (BuildEngineFrameworkName != null) { - properties["BuildEngineFrameworkName"] = FrameworkName; + properties[nameof(BuildEngineFrameworkName)] = BuildEngineFrameworkName; } - if (Host != null) + if (BuildEngineHost != null) { - properties["BuildEngineHost"] = Host; + properties[nameof(BuildEngineHost)] = BuildEngineHost; } - if (InitialServerState != null) + if (InitialMSBuildServerState != null) { - properties["InitialMSBuildServerState"] = InitialServerState; + properties[nameof(InitialMSBuildServerState)] = InitialMSBuildServerState; } - if (Project != null) + if (ProjectPath != null) { - properties["ProjectPath"] = Project; + properties[nameof(ProjectPath)] = ProjectPath; } if (ServerFallbackReason != null) { - properties["ServerFallbackReason"] = ServerFallbackReason; + properties[nameof(ServerFallbackReason)] = ServerFallbackReason; } - if (Success.HasValue) + if (BuildSuccess.HasValue) { - properties["BuildSuccess"] = Success.HasValue.ToString(CultureInfo.InvariantCulture); + properties[nameof(BuildSuccess)] = BuildSuccess.Value.ToString(CultureInfo.InvariantCulture); } - if (Target != null) + if (BuildTarget != null) { - properties["BuildTarget"] = Target; + properties[nameof(BuildTarget)] = BuildTarget; } - if (Version != null) + if (BuildEngineVersion != null) { - properties["BuildEngineVersion"] = Version.ToString(); + properties[nameof(BuildEngineVersion)] = BuildEngineVersion.ToString(); } if (BuildCheckEnabled != null) { - properties["BuildCheckEnabled"] = BuildCheckEnabled.Value.ToString(CultureInfo.InvariantCulture); + properties[nameof(BuildCheckEnabled)] = BuildCheckEnabled.Value.ToString(CultureInfo.InvariantCulture); } if (SACEnabled != null) { - properties["SACEnabled"] = SACEnabled.Value.ToString(CultureInfo.InvariantCulture); + properties[nameof(SACEnabled)] = SACEnabled.Value.ToString(CultureInfo.InvariantCulture); } return properties; } + + /// + /// 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)); + } + + 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)); + } + + if (SACEnabled != null) + { + telemetryItems.Add(new TelemetryItem(nameof(SACEnabled), SACEnabled, false)); + } + + return telemetryItems; + } } } diff --git a/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs new file mode 100644 index 00000000000..9eeb0a7509f --- /dev/null +++ b/src/Framework/Telemetry/IActivityTelemetryDataHolder.cs @@ -0,0 +1,15 @@ +// 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; + +/// +/// Interface for classes that hold telemetry data that should be added as tags to an . +/// +internal interface IActivityTelemetryDataHolder +{ + IList GetActivityProperties(); +} diff --git a/src/Framework/Telemetry/MSBuildActivitySource.cs b/src/Framework/Telemetry/MSBuildActivitySource.cs new file mode 100644 index 00000000000..33668c0926f --- /dev/null +++ b/src/Framework/Telemetry/MSBuildActivitySource.cs @@ -0,0 +1,35 @@ +// 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 new file mode 100644 index 00000000000..f392e1c24e3 --- /dev/null +++ b/src/Framework/Telemetry/OpenTelemetryManager.cs @@ -0,0 +1,281 @@ +// 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.Diagnostics; +using System.Threading; +using System.Globalization; +using System.Runtime.CompilerServices; + +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 object _initializeLock = new(); + private double _sampleRate = TelemetryConstants.DefaultSampleRate; + +#if NETFRAMEWORK + private TracerProvider? _tracerProvider; + private IOpenTelemetryCollector? _collector; +#endif + + /// + /// Optional activity source for MSBuild or other telemetry usage. + /// + public MSBuildActivitySource? DefaultActivitySource { get; private 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; + return; + } +#endif + } + + [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads + private void InitializeActivitySources() + { + _telemetryState = TelemetryState.TracerInitialized; + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, _sampleRate); + } + +#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/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs new file mode 100644 index 00000000000..87df7c68e1c --- /dev/null +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +namespace Microsoft.Build.Framework.Telemetry; + +/// +/// Constants for VS OpenTelemetry for basic configuration and appropriate naming for VS exporting/collection. +/// +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. + /// + public const string EventPrefix = "VS/MSBuild/"; + + /// + /// Prefix required by VS exporting/collection. + /// + public const string PropertyPrefix = "VS.MSBuild."; + + /// + /// For VS OpenTelemetry Collector to apply the correct privacy policy. + /// + public const string VSMajorVersion = "17.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. + /// + public const double DefaultSampleRate = 4e-5; + + /// + /// Name of the property for build duration. + /// + public const string BuildDurationPropertyName = "BuildDurationInMilliseconds"; + + /// + /// Name of the property for inner build duration. + /// + public const string InnerBuildDurationPropertyName = "InnerBuildDurationInMilliseconds"; +} diff --git a/src/Framework/Telemetry/TelemetryItem.cs b/src/Framework/Telemetry/TelemetryItem.cs new file mode 100644 index 00000000000..f037d7ddbea --- /dev/null +++ b/src/Framework/Telemetry/TelemetryItem.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework.Telemetry; + +internal record TelemetryItem(string Name, object Value, bool NeedsHashing); diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 4519271a8b3..4dd997eefed 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -4,8 +4,6 @@ using System; using System.Globalization; -#nullable disable - namespace Microsoft.Build.Framework { /// @@ -36,7 +34,7 @@ public Traits() public EscapeHatches EscapeHatches { get; } - internal readonly string MSBuildDisableFeaturesFromVersion = Environment.GetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION"); + internal readonly string? MSBuildDisableFeaturesFromVersion = Environment.GetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION"); /// /// Do not expand wildcards that match a certain pattern @@ -67,7 +65,7 @@ public Traits() /// /// Allow the user to specify that two processes should not be communicating via an environment variable. /// - public static readonly string MSBuildNodeHandshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT"); + public static readonly string? MSBuildNodeHandshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT"); /// /// Override property "MSBuildRuntimeType" to "Full", ignoring the actual runtime type of MSBuild. @@ -134,6 +132,19 @@ public Traits() public readonly bool InProcNodeDisabled = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") == "1"; + + /// + /// Variables controlling opt out at the level of not initializing telemetry infrastructure. Set to "1" or "true" to opt out. + /// mirroring + /// https://learn.microsoft.com/en-us/dotnet/core/tools/telemetry + /// + public bool SdkTelemetryOptOut = IsEnvVarOneOrTrue("DOTNET_CLI_TELEMETRY_OPTOUT"); + public bool FrameworkTelemetryOptOut = IsEnvVarOneOrTrue("MSBUILD_TELEMETRY_OPTOUT"); + public double? TelemetrySampleRateOverride = ParseDoubleFromEnvironmentVariable("MSBUILD_TELEMETRY_SAMPLE_RATE"); + + // for VS17.14 + public readonly bool TelemetryOptIn = Environment.GetEnvironmentVariable("MSBUILD_TELEMETRY_OPTIN") == "1"; + public static void UpdateFromEnvironment() { // Re-create Traits instance to update values in Traits according to current environment. @@ -149,6 +160,21 @@ private static int ParseIntFromEnvironmentVariableOrDefault(string environmentVa ? result : defaultValue; } + + private static double? ParseDoubleFromEnvironmentVariable(string environmentVariable) + { + return double.TryParse(Environment.GetEnvironmentVariable(environmentVariable), out double result) + ? result + : null; + } + + private static bool IsEnvVarOneOrTrue(string name) + { + string? value = Environment.GetEnvironmentVariable(name); + return value != null && + (value.Equals("1", StringComparison.OrdinalIgnoreCase) || + value.Equals("true", StringComparison.OrdinalIgnoreCase)); + } } internal class EscapeHatches @@ -400,7 +426,6 @@ public bool UnquoteTargetSwitchParameters } } - private static bool? ParseNullableBoolFromEnvironmentVariable(string environmentVariable) { var value = Environment.GetEnvironmentVariable(environmentVariable); @@ -516,7 +541,7 @@ internal static void ThrowInternalError(string message) /// /// Clone from ErrorUtilities which isn't available in Framework. /// - internal static void ThrowInternalError(string message, params object[] args) + internal static void ThrowInternalError(string message, params object?[] args) { throw new InternalErrorException(FormatString(message, args)); } @@ -535,7 +560,7 @@ internal static void ThrowInternalError(string message, params object[] args) /// /// Clone from ResourceUtilities which isn't available in Framework. /// - internal static string FormatString(string unformatted, params object[] args) + internal static string FormatString(string unformatted, params object?[] args) { string formatted = unformatted; @@ -545,7 +570,7 @@ internal static string FormatString(string unformatted, params object[] args) #if DEBUG // If you accidentally pass some random type in that can't be converted to a string, // FormatResourceString calls ToString() which returns the full name of the type! - foreach (object param in args) + foreach (object? param in args) { // Check it has a real implementation of ToString() and the type is not actually System.String if (param != null) diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 2ce4c96e4bf..64d885fa5c7 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -176,6 +176,7 @@ + @@ -206,6 +207,12 @@ + + + + + + diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index c94490ede17..cf7db1d4ddb 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -248,6 +248,8 @@ string[] args // 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); using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); @@ -295,6 +297,7 @@ string[] args { DumpCounters(false /* log to console */); } + OpenTelemetryManager.Instance.Shutdown(); return exitCode; } diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 339dfe620bf..96d2e3dbc1d 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -56,6 +56,14 @@ + + + + + + + + @@ -91,6 +99,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MSBuild/app.config b/src/MSBuild/app.config index 9bc9a4c595c..d63a1782ca4 100644 --- a/src/MSBuild/app.config +++ b/src/MSBuild/app.config @@ -35,6 +35,10 @@ + + + + @@ -56,6 +60,10 @@ + + + + diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index fa1e17b716c..8df9f8d8d90 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -42,6 +42,7 @@ 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=1 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=1 + file source=$(X86BinPath)System.Diagnostics.DiagnosticSource.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 file source=$(X86BinPath)System.Memory.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 file source=$(X86BinPath)System.Reflection.Metadata.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 file source=$(X86BinPath)System.Reflection.MetadataLoadContext.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 @@ -86,6 +87,25 @@ 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=1 + 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=1 + 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=1 + file source=$(X86BinPath)OpenTelemetry.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + file source=$(X86BinPath)OpenTelemetry.Api.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + file source=$(X86BinPath)OpenTelemetry.Api.ProviderBuilderExtensions.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + 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=1 + 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=1 + file source=$(X86BinPath)Microsoft.Extensions.Configuration.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + 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=1 + file source=$(X86BinPath)Microsoft.Extensions.DependencyInjection.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + 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=1 + 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=1 + file source=$(X86BinPath)Microsoft.Extensions.Logging.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + file source=$(X86BinPath)Microsoft.Extensions.Options.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + 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=1 + file source=$(X86BinPath)Microsoft.Extensions.Primitives.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + 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=1 + file source=$(X86BinPath)Newtonsoft.Json.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\amd64\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 folder InstallDir:\MSBuild\Current\Bin\MSBuild file source=$(X86BinPath)\MSBuild\Microsoft.Build.Core.xsd diff --git a/src/Package/Microsoft.Build.UnGAC/Program.cs b/src/Package/Microsoft.Build.UnGAC/Program.cs index d686da3dc75..a13f518146d 100644 --- a/src/Package/Microsoft.Build.UnGAC/Program.cs +++ b/src/Package/Microsoft.Build.UnGAC/Program.cs @@ -32,6 +32,8 @@ 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", }; uint hresult = NativeMethods.CreateAssemblyCache(out IAssemblyCache assemblyCache, 0);