diff --git a/NuGet.config b/NuGet.config index 107cd4542dc..c9bda385cf9 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,4 +1,4 @@ - + @@ -13,6 +13,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages.props b/eng/Packages.props index 87cf3b78909..cfc7e34a83c 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -33,5 +33,11 @@ + + + + + + diff --git a/eng/Versions.props b/eng/Versions.props index a773ffaf11f..148132bbb1f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -54,6 +54,11 @@ 8.0.5 8.0.0 8.0.0 + 0.1.700-beta + 1.10.0 + 9.0.0 + 9.0.0 + 9.0.0 diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 4ee166bb5e2..4240dc2e9fd 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -492,6 +492,7 @@ public void BeginBuild(BuildParameters parameters) parameters.DetailedSummary = true; parameters.LogTaskInputs = true; } + OpenTelemetryManager.Initialize(false); lock (_syncLock) { @@ -1071,6 +1072,14 @@ public void EndBuild() _buildTelemetry.SACEnabled = sacState == NativeMethodsShared.SAC_State.Evaluation || sacState == NativeMethodsShared.SAC_State.Enforcement; loggingService.LogTelemetry(buildEventContext: null, _buildTelemetry.EventName, _buildTelemetry.GetProperties()); + Activity? endOfBuildTelemetry = OpenTelemetryManager.DefaultActivitySource? + .StartActivity("Build")? + .WithTags(_buildTelemetry) + .WithStartTime(_buildTelemetry.InnerStartAt); + + endOfBuildTelemetry?.Dispose(); + OpenTelemetryManager.ForceFlush(); + // Clean telemetry to make it ready for next build submission. _buildTelemetry = null; } diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index 2395b09f44d..c1080c96c0a 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -25,6 +25,7 @@ using Task = System.Threading.Tasks.Task; using Microsoft.Build.Framework; using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework.Telemetry; #nullable disable @@ -333,6 +334,7 @@ bool StartNewNode(int nodeId) #endif // Create the node process INodeLauncher nodeLauncher = (INodeLauncher)_componentHost.GetComponent(BuildComponentType.NodeLauncher); + var activity = OpenTelemetryManager.DefaultActivitySource?.StartActivity("NodeLaunching"); Process msbuildProcess = nodeLauncher.Start(msbuildLocation, commandLineArgs, nodeId); _processesToIgnore.TryAdd(GetProcessesToIgnoreKey(hostHandshake, msbuildProcess.Id), default); @@ -342,6 +344,7 @@ bool StartNewNode(int nodeId) // Now try to connect to it. Stream nodeStream = TryConnectToProcess(msbuildProcess.Id, TimeoutForNewNodeCreation, hostHandshake); + activity?.Dispose(); if (nodeStream != null) { // Connection successful, use this node. diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 736cccac2f1..735264448f0 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -22,6 +22,23 @@ + + + + + + + + + + + + @@ -43,6 +60,7 @@ Shared\IMSBuildElementLocation.cs + diff --git a/src/Framework/Telemetry/ActivityInstrumentation/IActivityTelemetryDataHolder.cs b/src/Framework/Telemetry/ActivityInstrumentation/IActivityTelemetryDataHolder.cs new file mode 100644 index 00000000000..ec29dbd0d72 --- /dev/null +++ b/src/Framework/Telemetry/ActivityInstrumentation/IActivityTelemetryDataHolder.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. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Build.Framework.Telemetry; + +internal record TelemetryItem(string Name, object Value, bool Hashed); + +/// +/// +/// +internal interface IActivityTelemetryDataHolder +{ + IList GetActivityProperties(); +} \ No newline at end of file diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs index c23d9269c9b..f877c6bcf6e 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"; @@ -167,5 +167,51 @@ public override IDictionary GetProperties() return properties; } + public IList GetActivityProperties() + { + List telemetryItems = new(); + + if (StartAt.HasValue && FinishedAt.HasValue) + { + telemetryItems.Add(new TelemetryItem("BuildDurationInMilliseconds", (FinishedAt.Value - StartAt.Value).TotalMilliseconds, false)); + } + + if (InnerStartAt.HasValue && FinishedAt.HasValue) + { + telemetryItems.Add(new TelemetryItem("InnerBuildDurationInMilliseconds", (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds, false)); + } + + if (Host != null) + { + telemetryItems.Add(new TelemetryItem("BuildEngineHost", Host, false)); + } + + if (Success.HasValue) + { + telemetryItems.Add(new TelemetryItem("BuildSuccess", Success, false)); + } + + if (Target != null) + { + telemetryItems.Add(new TelemetryItem("BuildTarget", Target, true)); + } + + if (Version != null) + { + telemetryItems.Add(new TelemetryItem("BuildEngineVersion", Version.ToString(), false)); + } + + if (BuildCheckEnabled != null) + { + telemetryItems.Add(new TelemetryItem("BuildCheckEnabled", BuildCheckEnabled, false)); + } + + if (SACEnabled != null) + { + telemetryItems.Add(new TelemetryItem("SACEnabled", SACEnabled, false)); + } + + return telemetryItems; + } } } diff --git a/src/Framework/Telemetry/OpenTelemetryManager.cs b/src/Framework/Telemetry/OpenTelemetryManager.cs new file mode 100644 index 00000000000..43e6f640b1a --- /dev/null +++ b/src/Framework/Telemetry/OpenTelemetryManager.cs @@ -0,0 +1,222 @@ +// 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.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +#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 +// #if DEBUG && NETFRAMEWORK +// using OpenTelemetry.Exporter; +// #endif + +namespace Microsoft.Build.Framework.Telemetry +{ + + /// + /// Class for configuring and managing the telemetry infrastructure with System.Diagnostics.Activity, OpenTelemetry SDK and VS OpenTelemetry Collector. + /// + internal static class OpenTelemetryManager + { + private static bool _initialized = false; + private static readonly object s_initialize_lock = new(); + +#if NETFRAMEWORK + private static TracerProvider? s_tracerProvider; + private static IOpenTelemetryCollector? s_collector; +#endif + + public static MSBuildActivitySource? DefaultActivitySource { get; set; } + + public static void Initialize(bool isStandalone) + { + lock (s_initialize_lock) + { + if (!ShouldInitialize()) + { + return; + } + + // create activity source + DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace); + + // create trace exporter in framework +#if NETFRAMEWORK + var exporterSettings = OpenTelemetryExporterSettingsBuilder + .CreateVSDefault(TelemetryConstants.VSMajorVersion) + .Build(); + + TracerProviderBuilder tracerProviderBuilder = OpenTelemetry.Sdk + .CreateTracerProviderBuilder() + .AddSource(TelemetryConstants.DefaultActivitySourceNamespace) + .AddVisualStudioDefaultTraceExporter(exporterSettings); + + s_tracerProvider = + tracerProviderBuilder + /* +#if DEBUG + .AddOtlpExporter() +#endif + */ + .Build(); + + // create collector if not in vs + if (isStandalone) + { + IOpenTelemetryCollectorSettings collectorSettings = OpenTelemetryCollectorSettingsBuilder + .CreateVSDefault(TelemetryConstants.VSMajorVersion) + .Build(); + + s_collector = OpenTelemetryCollectorProvider + .CreateCollector(collectorSettings); + s_collector.StartAsync().Wait(); + } +#endif + _initialized = true; + } + } + + public static void ForceFlush() + { + lock (s_initialize_lock) + { + if (_initialized) + { +#if NETFRAMEWORK + s_tracerProvider?.ForceFlush(); +#endif + } + } + } + private static bool ShouldInitialize() + { + // only initialize once + if (_initialized) + { + return false; + } + + string? dotnetCliTelemetryOptOut = Environment.GetEnvironmentVariable(TelemetryConstants.DotnetOptOut); + if (dotnetCliTelemetryOptOut == "1" || dotnetCliTelemetryOptOut == "true") + { + return false; + } +#if NETFRAMEWORK + string? telemetryMSBuildOptOut = Environment.GetEnvironmentVariable(TelemetryConstants.MSBuildFxOptout); + if (telemetryMSBuildOptOut == "1" || telemetryMSBuildOptOut == "true") + { + return false; + } + return true; +#else + string? telemetryOptIn = Environment.GetEnvironmentVariable(TelemetryConstants.MSBuildCoreOptin); + if (telemetryOptIn == "1" || telemetryOptIn == "true") + { + return true; + } + return false; + + +#endif + + } + + public static void Shutdown() + { + lock (s_initialize_lock) + { + if (_initialized) + { +#if NETFRAMEWORK + s_tracerProvider?.Shutdown(); + s_collector?.Dispose(); +#endif + } + } + } + } + + internal class MSBuildActivitySource + { + private readonly ActivitySource _source; + + public MSBuildActivitySource(string name) + { + _source = new ActivitySource(name); + } + + 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}"); + return activity; + } + } + + internal static class ActivityExtensions + { + public static Activity WithTags(this Activity activity, IActivityTelemetryDataHolder dataHolder) + { + if (dataHolder != null) + { + foreach ((string name, object value, bool hashed) in dataHolder.GetActivityProperties()) + { + object? hashedValue = null; + if (hashed) + { + // TODO: make this work + hashedValue = value; + + // Hash the value via Visual Studio mechanism in Framework & same algo as in core telemetry hashing + // https://github.com/dotnet/sdk/blob/8bd19a2390a6bba4aa80d1ac3b6c5385527cc311/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs +#if NETFRAMEWORK + // hashedValue = new Microsoft.VisualStudio.Telemetry.TelemetryHashedProperty(value +#endif + } + + activity.SetTag($"{TelemetryConstants.PropertyPrefix}{name}", hashed ? hashedValue : value); + } + } + return activity; + } + + public static Activity WithTags(this Activity activity, IDictionary? tags) + { + if (tags != null) + { + foreach (var tag in tags) + { + activity.SetTag($"{TelemetryConstants.PropertyPrefix}{tag.Key}", tag.Value); + } + } + + return activity; + } + + public static Activity WithTag(this Activity activity, string name, object value, bool hashed = false) + { + activity.SetTag($"{TelemetryConstants.PropertyPrefix}{name}", hashed ? value.GetHashCode() : value); + return activity; + } + + public static Activity WithStartTime(this Activity activity, DateTime? startTime) + { + if (startTime.HasValue) + { + activity.SetStartTime(startTime.Value); + } + return activity; + } + } +} diff --git a/src/Framework/Telemetry/TelemetryConstants.cs b/src/Framework/Telemetry/TelemetryConstants.cs new file mode 100644 index 00000000000..1f456b34ff2 --- /dev/null +++ b/src/Framework/Telemetry/TelemetryConstants.cs @@ -0,0 +1,24 @@ +// 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 static class TelemetryConstants +{ + /// + /// "Microsoft.VisualStudio.OpenTelemetry.*" namespace is required by VS exporting/collection. + /// + public const string DefaultActivitySourceNamespace = "Microsoft.VisualStudio.OpenTelemetry.MSBuild"; + public const string EventPrefix = "VS/MSBuild/"; + public const string PropertyPrefix = "VS.MSBuild."; + /// + /// For VS OpenTelemetry Collector to apply the correct privacy policy. + /// + public const string VSMajorVersion = "17.0"; + + /// + /// https://learn.microsoft.com/en-us/dotnet/core/tools/telemetry + /// + public const string DotnetOptOut = "DOTNET_CLI_TELEMETRY_OPTOUT"; + public const string MSBuildFxOptout = "MSBUILD_TELEMETRY_OPTOUT"; + public const string MSBuildCoreOptin = "MSBUILD_TELEMETRY_OPTIN"; +} diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index aeddef7aba4..85717fb952e 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -246,6 +246,7 @@ string[] args DebuggerLaunchCheck(); + OpenTelemetryManager.Initialize(true); // Initialize new build telemetry and record start of this build. KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow }; @@ -296,6 +297,7 @@ string[] args DumpCounters(false /* log to console */); } + OpenTelemetryManager.Shutdown(); return exitCode; } diff --git a/src/MSBuild/app.amd64.config b/src/MSBuild/app.amd64.config index 339dfe620bf..b2ff35a1606 100644 --- a/src/MSBuild/app.amd64.config +++ b/src/MSBuild/app.amd64.config @@ -62,8 +62,8 @@ - - + + diff --git a/src/MSBuild/app.config b/src/MSBuild/app.config index 9bc9a4c595c..1e15f91cb19 100644 --- a/src/MSBuild/app.config +++ b/src/MSBuild/app.config @@ -33,7 +33,7 @@ - + @@ -60,6 +60,18 @@ + + + + + + + + + + + +