diff --git a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs b/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs index 966ea12c87c4..76f8cba3ee60 100644 --- a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs +++ b/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using Microsoft.Build.Graph; +using Microsoft.DotNet.Cli.Extensions; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; @@ -75,7 +76,7 @@ public void WatchFiles(FileWatcher fileWatcher) { using (var loggers = buildReporter.GetLoggers(rootNode.ProjectInstance.FullPath, "Restore")) { - if (!rootNode.ProjectInstance.Build([TargetNames.Restore], loggers)) + if (!rootNode.ProjectInstance.BuildWithTelemetry([TargetNames.Restore], loggers)) { logger.LogError("Failed to restore project '{Path}'.", rootProjectPath); loggers.ReportOutput(); @@ -103,7 +104,7 @@ public void WatchFiles(FileWatcher fileWatcher) using (var loggers = buildReporter.GetLoggers(projectInstance.FullPath, "DesignTimeBuild")) { - if (!projectInstance.Build([TargetNames.Compile, .. customCollectWatchItems], loggers)) + if (!projectInstance.BuildWithTelemetry([TargetNames.Compile, .. customCollectWatchItems], loggers)) { logger.LogError("Failed to build project '{Path}'.", projectInstance.FullPath); loggers.ReportOutput(); diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs index 6f4b8803ed91..496cd6b389fa 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Microsoft.Build.Graph; using Microsoft.CodeAnalysis; +using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; @@ -578,7 +579,7 @@ private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray items, ChangeKind kind) - => items is [{Item: var item }] + => items is [{ Item: var item }] ? GetSingularMessage(kind) + ": " + GetRelativeFilePath(item.FilePath) : GetPluralMessage(kind) + ": " + string.Join(", ", items.Select(f => GetRelativeFilePath(f.Item.FilePath))); diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs index 09e33759e7a4..645bea82d951 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs @@ -3,6 +3,7 @@ using Microsoft.Build.Graph; +using Microsoft.DotNet.Cli.Extensions; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch @@ -61,7 +62,7 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList files, using var loggers = buildReporter.GetLoggers(projectNode.ProjectInstance.FullPath, BuildTargetName); // Deep copy so that we don't pollute the project graph: - if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, loggers)) + if (!projectNode.ProjectInstance.DeepCopy().BuildWithTelemetry([BuildTargetName], loggers)) { loggers.ReportOutput(); return null; diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs index dc94f892abd1..32ffeba93baf 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs @@ -190,7 +190,10 @@ private int ExecuteForFileBasedApp(string path) NoCache = true, NoBuild = true, }; - var projectCollection = new ProjectCollection(); + + // Include telemetry logger for project evaluation + var (loggersWithTelemetry, _) = ProjectInstanceExtensions.CreateLoggersWithTelemetry([]); + var projectCollection = new ProjectCollection(globalProperties: null, loggersWithTelemetry, ToolsetDefinitionLocations.Default); var projectInstance = command.CreateProjectInstance(projectCollection); // Set initial version to Directory.Packages.props and/or C# file diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 183569417ae2..c82f55d2d453 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -402,29 +402,36 @@ internal ICommand GetTargetCommand(Func? pro Reporter.Verbose.WriteLine("Getting target command: evaluating project."); FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. MSBuildArgs.OtherMSBuildArgs], "dotnet-run"); - var project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger); + var (project, telemetryCentralLogger) = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger); ValidatePreconditions(project); - InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs); + InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs, telemetryCentralLogger); logger?.ReallyShutdown(); var runProperties = RunProperties.FromProject(project).WithApplicationArguments(ApplicationArgs); var command = CreateCommandFromRunProperties(runProperties); return command; - static ProjectInstance EvaluateProject(string? projectFilePath, Func? projectFactory, MSBuildArgs msbuildArgs, ILogger? binaryLogger) + static (ProjectInstance project, ILogger? telemetryCentralLogger) EvaluateProject(string? projectFilePath, Func? projectFactory, MSBuildArgs msbuildArgs, ILogger? binaryLogger) { Debug.Assert(projectFilePath is not null || projectFactory is not null); var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs); - var collection = new ProjectCollection(globalProperties: globalProperties, loggers: binaryLogger is null ? null : [binaryLogger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); + // Include telemetry logger for evaluation and capture it for reuse in builds + var (loggers, telemetryCentralLogger) = ProjectInstanceExtensions.CreateLoggersWithTelemetry(binaryLogger is null ? null : [binaryLogger]); + var collection = new ProjectCollection(globalProperties: globalProperties, loggers: loggers, toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); + ProjectInstance projectInstance; if (projectFilePath is not null) { - return collection.LoadProject(projectFilePath).CreateProjectInstance(); + projectInstance = collection.LoadProject(projectFilePath).CreateProjectInstance(); + } + else + { + Debug.Assert(projectFactory is not null); + projectInstance = projectFactory(collection); } - Debug.Assert(projectFactory is not null); - return projectFactory(collection); + return (projectInstance, telemetryCentralLogger); } static void ValidatePreconditions(ProjectInstance project) @@ -480,7 +487,7 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s return command; } - static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs) + static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs, ILogger? telemetryCentralLogger) { List loggersForBuild = [ TerminalLogger.CreateTerminalOrConsoleLogger([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs]) @@ -490,7 +497,7 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, Faca loggersForBuild.Add(binaryLogger); } - if (!project.Build([Constants.ComputeRunArguments], loggers: loggersForBuild, remoteLoggers: null, out _)) + if (!project.BuildWithTelemetry([Constants.ComputeRunArguments], loggersForBuild, null, out _, telemetryCentralLogger)) { throw new GracefulException(CliCommandStrings.RunCommandEvaluationExceptionBuildFailed, Constants.ComputeRunArguments); } diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 17f5ce1b226e..374eb9663f5e 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -24,6 +24,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Cli.Commands.Clean.FileBasedAppArtifacts; using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; @@ -297,14 +298,17 @@ public override int Execute() // Set up MSBuild. ReadOnlySpan binaryLoggers = binaryLogger is null ? [] : [binaryLogger.Value]; - IEnumerable loggers = [.. binaryLoggers, consoleLogger]; + IEnumerable existingLoggers = [.. binaryLoggers, consoleLogger]; + + // Include telemetry logger for evaluation and capture it for potential future use + var (loggersWithTelemetry, telemetryCentralLogger) = ProjectInstanceExtensions.CreateLoggersWithTelemetry(existingLoggers); var projectCollection = new ProjectCollection( MSBuildArgs.GlobalProperties, - loggers, + loggersWithTelemetry, ToolsetDefinitionLocations.Default); var parameters = new BuildParameters(projectCollection) { - Loggers = loggers, + Loggers = loggersWithTelemetry, LogTaskInputs = binaryLoggers.Length != 0, }; diff --git a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs index c83efdecce8d..f950dee0a815 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs @@ -6,6 +6,7 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Execution; +using Microsoft.Build.Framework; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; @@ -37,9 +38,11 @@ public static (IEnumerable projects = GetProjectsProperties(collection, evaluationContext, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions); + ConcurrentBag projects = GetProjectsProperties(collection, evaluationContext, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions, telemetryCentralLogger); logger?.ReallyShutdown(); collection.UnloadAllProjects(); @@ -59,9 +62,11 @@ public static (IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions); + IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions, telemetryCentralLogger); logger?.ReallyShutdown(); collection.UnloadAllProjects(); return (projects, isBuiltOrRestored); @@ -130,7 +135,7 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption return result == (int)BuildResultCode.Success; } - private static ConcurrentBag GetProjectsProperties(ProjectCollection projectCollection, EvaluationContext evaluationContext, IEnumerable projects, BuildOptions buildOptions) + private static ConcurrentBag GetProjectsProperties(ProjectCollection projectCollection, EvaluationContext evaluationContext, IEnumerable projects, BuildOptions buildOptions, ILogger? telemetryCentralLogger) { var allProjects = new ConcurrentBag(); @@ -141,7 +146,7 @@ private static ConcurrentBag { - IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, evaluationContext, buildOptions); + IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, evaluationContext, buildOptions, telemetryCentralLogger); foreach (var projectMetadata in projectsMetadata) { allProjects.Add(projectMetadata); diff --git a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs index c5ec75139a91..8f530a12de52 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs @@ -6,8 +6,10 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Execution; +using Microsoft.Build.Framework; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; +using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; @@ -216,7 +218,7 @@ public static string GetRootDirectory(string solutionOrProjectFilePath) return string.IsNullOrEmpty(fileDirectory) ? Directory.GetCurrentDirectory() : fileDirectory; } - public static IEnumerable GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, EvaluationContext evaluationContext, BuildOptions buildOptions) + public static IEnumerable GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, EvaluationContext evaluationContext, BuildOptions buildOptions, ILogger? telemetryCentralLogger = null) { var projects = new List(); ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, null); @@ -228,7 +230,7 @@ public static IEnumerable(); innerModules.Add(module); @@ -287,7 +289,7 @@ public static IEnumerable RunTargetToGetWorkloadIds(IEnumerable allProjec continue; } - bool buildResult = project.Build([GetRequiredWorkloadsTargetName], + bool buildResult = project.BuildWithTelemetry([GetRequiredWorkloadsTargetName], loggers: [ new ConsoleLogger(Verbosity.ToLoggerVerbosity()) ], diff --git a/src/Cli/dotnet/Extensions/ProjectInstanceExtensions.cs b/src/Cli/dotnet/Extensions/ProjectInstanceExtensions.cs index 749007923950..41a99dcb3285 100644 --- a/src/Cli/dotnet/Extensions/ProjectInstanceExtensions.cs +++ b/src/Cli/dotnet/Extensions/ProjectInstanceExtensions.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.DotNet.Cli.Commands.MSBuild; namespace Microsoft.DotNet.Cli.Extensions; @@ -47,4 +50,200 @@ public static IEnumerable GetConfigurations(this ProjectInstance project .Where(c => !string.IsNullOrWhiteSpace(c)) .DefaultIfEmpty("Debug"); } + + /// + /// Creates the central telemetry logger for API-based MSBuild usage if telemetry is enabled. + /// This logger should be used for evaluation (ProjectCollection) and as a central logger for builds. + /// Returns null if telemetry is not enabled or if there's an error creating the logger. + /// + /// The central telemetry logger, or null if telemetry is disabled. + public static ILogger? CreateTelemetryCentralLogger() + { + if (Telemetry.Telemetry.CurrentSessionId != null) + { + try + { + return new MSBuildLogger(); + } + catch (Exception) + { + // Exceptions during telemetry shouldn't cause anything else to fail + } + } + return null; + } + + /// + /// Creates the forwarding logger record for distributed builds using the provided central logger. + /// This should be used with the remoteLoggers parameter of ProjectInstance.Build. + /// The same central logger instance from ProjectCollection should be reused here. + /// Returns an empty collection if the central logger is null or if there's an error. + /// + /// The central logger instance (typically the same one used in ProjectCollection). + /// An array containing the forwarding logger record, or empty array if central logger is null. + public static ForwardingLoggerRecord[] CreateTelemetryForwardingLoggerRecords(ILogger? centralLogger) + { + if (centralLogger is MSBuildLogger msbuildLogger) + { + try + { + // LoggerDescription describes the forwarding logger that worker nodes will create + var forwardingLoggerDescription = new Microsoft.Build.Logging.LoggerDescription( + loggerClassName: typeof(MSBuildForwardingLogger).FullName!, + loggerAssemblyName: typeof(MSBuildForwardingLogger).Assembly.Location, + loggerAssemblyFile: null, + loggerSwitchParameters: null, + verbosity: LoggerVerbosity.Normal); + + var loggerRecord = new ForwardingLoggerRecord(msbuildLogger, forwardingLoggerDescription); + return [loggerRecord]; + } + catch (Exception) + { + // Exceptions during telemetry shouldn't cause anything else to fail + } + } + return []; + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The project instance to build. + /// The targets to build. + /// Additional loggers to include. + /// Optional telemetry central logger from ProjectCollection. If null, creates a new one. + public static bool BuildWithTelemetry( + this ProjectInstance projectInstance, + string[] targets, + IEnumerable? additionalLoggers = null, + ILogger? telemetryCentralLogger = null) + { + var loggers = new List(); + var forwardingLoggers = new List(); + + // Add telemetry as a distributed logger via ForwardingLoggerRecord + // Use provided central logger or create a new one + var centralLogger = telemetryCentralLogger ?? CreateTelemetryCentralLogger(); + forwardingLoggers.AddRange(CreateTelemetryForwardingLoggerRecords(centralLogger)); + + if (additionalLoggers != null) + { + loggers.AddRange(additionalLoggers); + } + + // Use the overload that accepts forwarding loggers for proper distributed logging + return projectInstance.Build( + targets, + loggers.Count > 0 ? loggers : null, + forwardingLoggers.Count > 0 ? forwardingLoggers : null, + out _); + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The project instance to build. + /// The targets to build. + /// Loggers to include. + /// The outputs from the build. + /// Optional telemetry central logger from ProjectCollection. If null, creates a new one. + public static bool BuildWithTelemetry( + this ProjectInstance projectInstance, + string[] targets, + IEnumerable? loggers, + out IDictionary targetOutputs, + ILogger? telemetryCentralLogger = null) + { + var allLoggers = new List(); + var forwardingLoggers = new List(); + + // Add telemetry as a distributed logger via ForwardingLoggerRecord + // Use provided central logger or create a new one + var centralLogger = telemetryCentralLogger ?? CreateTelemetryCentralLogger(); + forwardingLoggers.AddRange(CreateTelemetryForwardingLoggerRecords(centralLogger)); + + if (loggers != null) + { + allLoggers.AddRange(loggers); + } + + // Use the overload that accepts forwarding loggers for proper distributed logging + return projectInstance.Build( + targets, + allLoggers.Count > 0 ? allLoggers : null, + forwardingLoggers.Count > 0 ? forwardingLoggers : null, + out targetOutputs); + } + + /// + /// Builds the project with the specified targets, automatically including telemetry loggers + /// as a distributed logger (central logger + forwarding logger). + /// + /// The project instance to build. + /// The targets to build. + /// Loggers to include. + /// Remote/forwarding loggers to include. + /// The outputs from the build. + /// Optional telemetry central logger from ProjectCollection. If null, creates a new one. + public static bool BuildWithTelemetry( + this ProjectInstance projectInstance, + string[] targets, + IEnumerable? loggers, + IEnumerable? remoteLoggers, + out IDictionary targetOutputs, + ILogger? telemetryCentralLogger = null) + { + var allLoggers = new List(); + var allForwardingLoggers = new List(); + + // Add telemetry as a distributed logger via ForwardingLoggerRecord + // Use provided central logger or create a new one + var centralLogger = telemetryCentralLogger ?? CreateTelemetryCentralLogger(); + allForwardingLoggers.AddRange(CreateTelemetryForwardingLoggerRecords(centralLogger)); + + if (loggers != null) + { + allLoggers.AddRange(loggers); + } + + if (remoteLoggers != null) + { + allForwardingLoggers.AddRange(remoteLoggers); + } + + return projectInstance.Build( + targets, + allLoggers.Count > 0 ? allLoggers : null, + allForwardingLoggers.Count > 0 ? allForwardingLoggers : null, + out targetOutputs); + } + + /// + /// Creates a logger collection that includes the telemetry central logger. + /// This is useful for ProjectCollection scenarios where evaluation needs telemetry. + /// Returns both the logger array and the telemetry central logger instance for reuse in subsequent builds. + /// + /// Additional loggers to include in the collection. + /// A tuple containing the logger array and the telemetry central logger (or null if no telemetry). + public static (ILogger[]? loggers, ILogger? telemetryCentralLogger) CreateLoggersWithTelemetry(IEnumerable? additionalLoggers = null) + { + var loggers = new List(); + + // Add central telemetry logger for evaluation + var telemetryCentralLogger = CreateTelemetryCentralLogger(); + if (telemetryCentralLogger != null) + { + loggers.Add(telemetryCentralLogger); + } + + if (additionalLoggers != null) + { + loggers.AddRange(additionalLoggers); + } + + return (loggers.Count > 0 ? loggers.ToArray() : null, telemetryCentralLogger); + } } diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenProjectInstanceExtensions.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenProjectInstanceExtensions.cs new file mode 100644 index 000000000000..c02964617bef --- /dev/null +++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenProjectInstanceExtensions.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.Telemetry; + +namespace Microsoft.DotNet.Cli.MSBuild.Tests; + +public class GivenProjectInstanceExtensions +{ + [Fact] + public void CreateTelemetryCentralLogger_WhenTelemetryDisabled_ReturnsNull() + { + // Ensure telemetry is disabled + Telemetry.Telemetry.CurrentSessionId = null; + + var logger = ProjectInstanceExtensions.CreateTelemetryCentralLogger(); + + logger.Should().BeNull(); + } + + [Fact] + public void CreateTelemetryCentralLogger_WhenTelemetryEnabled_ReturnsLogger() + { + // Enable telemetry with a session ID + var originalSessionId = Telemetry.Telemetry.CurrentSessionId; + try + { + Telemetry.Telemetry.CurrentSessionId = Guid.NewGuid().ToString(); + + var logger = ProjectInstanceExtensions.CreateTelemetryCentralLogger(); + + logger.Should().NotBeNull(); + logger.Should().BeOfType(); + } + finally + { + // Restore original session ID + Telemetry.Telemetry.CurrentSessionId = originalSessionId; + } + } + + [Fact] + public void CreateTelemetryForwardingLoggerRecords_WhenTelemetryDisabled_ReturnsEmpty() + { + // Ensure telemetry is disabled + Telemetry.Telemetry.CurrentSessionId = null; + + var centralLogger = ProjectInstanceExtensions.CreateTelemetryCentralLogger(); + var loggerRecords = ProjectInstanceExtensions.CreateTelemetryForwardingLoggerRecords(centralLogger); + + loggerRecords.Should().BeEmpty(); + } + + [Fact] + public void CreateTelemetryForwardingLoggerRecords_WhenTelemetryEnabled_ReturnsLoggerRecords() + { + // Enable telemetry with a session ID + var originalSessionId = Telemetry.Telemetry.CurrentSessionId; + try + { + Telemetry.Telemetry.CurrentSessionId = Guid.NewGuid().ToString(); + + var centralLogger = ProjectInstanceExtensions.CreateTelemetryCentralLogger(); + var loggerRecords = ProjectInstanceExtensions.CreateTelemetryForwardingLoggerRecords(centralLogger); + + loggerRecords.Should().NotBeEmpty(); + loggerRecords.Should().HaveCount(1); + // ForwardingLoggerRecord contains the ForwardingLogger and LoggerDescription + loggerRecords[0].Should().NotBeNull(); + } + finally + { + // Restore original session ID + Telemetry.Telemetry.CurrentSessionId = originalSessionId; + } + } + + [Fact] + public void BuildWithTelemetry_WhenTelemetryEnabled_CreatesDistributedLogger() + { + // Enable telemetry with a session ID + var originalSessionId = Telemetry.Telemetry.CurrentSessionId; + try + { + Telemetry.Telemetry.CurrentSessionId = Guid.NewGuid().ToString(); + + // CreateTelemetryCentralLogger should return logger when telemetry is enabled + var centralLogger = ProjectInstanceExtensions.CreateTelemetryCentralLogger(); + centralLogger.Should().NotBeNull(); + + // CreateTelemetryForwardingLoggerRecords should return forwarding logger when telemetry is enabled + // using the same central logger instance + var forwardingLoggers = ProjectInstanceExtensions.CreateTelemetryForwardingLoggerRecords(centralLogger); + forwardingLoggers.Should().NotBeEmpty(); + } + finally + { + // Restore original session ID + Telemetry.Telemetry.CurrentSessionId = originalSessionId; + } + } + + [Fact] + public void TelemetryLogger_ReceivesEventsFromAPIBasedBuild() + { + // Enable telemetry with a session ID + var originalSessionId = Telemetry.Telemetry.CurrentSessionId; + try + { + Telemetry.Telemetry.CurrentSessionId = Guid.NewGuid().ToString(); + + // Create a simple in-memory project + string projectContent = @" + + + + +"; + + // Create ProjectCollection with telemetry logger + var (loggers, telemetryCentralLogger) = ProjectInstanceExtensions.CreateLoggersWithTelemetry(); + using var collection = new ProjectCollection( + globalProperties: null, + loggers: loggers, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); + + // Create a temporary project file + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, projectContent); + + // Load and build the project using API-based MSBuild with telemetry + var project = collection.LoadProject(tempFile); + var projectInstance = project.CreateProjectInstance(); + + // Use a test logger to capture events + var testLogger = new TestEventLogger(); + + // Build directly without distributed logger for simpler test + // The telemetry logger is already attached to the ProjectCollection + var result = projectInstance.Build(new[] { "TestTarget" }, new[] { testLogger }); + + // Verify build succeeded + result.Should().BeTrue(); + + // Verify the test logger received events (indicating build actually ran) + testLogger.BuildStartedCount.Should().BeGreaterThan(0); + testLogger.BuildFinishedCount.Should().BeGreaterThan(0); + + // Verify telemetry logger was created and attached to collection + telemetryCentralLogger.Should().NotBeNull(); + loggers.Should().Contain(telemetryCentralLogger); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + finally + { + // Restore original session ID + Telemetry.Telemetry.CurrentSessionId = originalSessionId; + } + } + + /// + /// Simple logger to track build events for testing + /// + private class TestEventLogger : ILogger + { + public int BuildStartedCount { get; private set; } + public int BuildFinishedCount { get; private set; } + public int TargetStartedCount { get; private set; } + public int TargetFinishedCount { get; private set; } + + public LoggerVerbosity Verbosity { get; set; } + public string Parameters { get; set; } + + public void Initialize(IEventSource eventSource) + { + eventSource.BuildStarted += (sender, e) => BuildStartedCount++; + eventSource.BuildFinished += (sender, e) => BuildFinishedCount++; + eventSource.TargetStarted += (sender, e) => TargetStartedCount++; + eventSource.TargetFinished += (sender, e) => TargetFinishedCount++; + } + + public void Shutdown() + { + } + } +}