File: Commands\MSBuild\MSBuildLogger.cs
Web Access
Project: ..\..\..\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Globalization;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Cli.Telemetry;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
 
namespace Microsoft.DotNet.Cli.Commands.MSBuild;
 
public sealed class MSBuildLogger : INodeLogger
{
    private readonly IFirstTimeUseNoticeSentinel _sentinel =
        new FirstTimeUseNoticeSentinel();
    private readonly ITelemetry _telemetry;
 
    internal const string TargetFrameworkTelemetryEventName = "targetframeworkeval";
    internal const string BuildTelemetryEventName = "build";
    internal const string LoggingConfigurationTelemetryEventName = "loggingConfiguration";
    internal const string BuildcheckAcquisitionFailureEventName = "buildcheck/acquisitionfailure";
    internal const string BuildcheckRunEventName = "buildcheck/run";
    internal const string BuildcheckRuleStatsEventName = "buildcheck/rule";
 
    internal const string SdkTaskBaseCatchExceptionTelemetryEventName = "taskBaseCatchException";
    internal const string PublishPropertiesTelemetryEventName = "PublishProperties";
    internal const string WorkloadPublishPropertiesTelemetryEventName = "WorkloadPublishProperties";
    internal const string ReadyToRunTelemetryEventName = "ReadyToRun";
 
    internal const string TargetFrameworkVersionTelemetryPropertyKey = "TargetFrameworkVersion";
    internal const string RuntimeIdentifierTelemetryPropertyKey = "RuntimeIdentifier";
    internal const string SelfContainedTelemetryPropertyKey = "SelfContained";
    internal const string UseApphostTelemetryPropertyKey = "UseApphost";
    internal const string OutputTypeTelemetryPropertyKey = "OutputType";
    internal const string UseArtifactsOutputTelemetryPropertyKey = "UseArtifactsOutput";
    internal const string ArtifactsPathLocationTypeTelemetryPropertyKey = "ArtifactsPathLocationType";
 
    /// <summary>
    /// This is defined in <see cref="ComputeDotnetBaseImageAndTag.cs"/>
    /// </summary>
    internal const string SdkContainerPublishBaseImageInferenceEventName = "sdk/container/inference";
    /// <summary>
    /// This is defined in <see cref="CreateNewImage.cs"/>
    /// </summary>
    internal const string SdkContainerPublishSuccessEventName = "sdk/container/publish/success";
    /// <summary>
    /// This is defined in <see cref="CreateNewImage.cs"/>
    /// </summary>
    internal const string SdkContainerPublishErrorEventName = "sdk/container/publish/error";
 
    public MSBuildLogger()
    {
        try
        {
            string sessionId =
                Environment.GetEnvironmentVariable(MSBuildForwardingApp.TelemetrySessionIdEnvironmentVariableName);
 
            if (sessionId != null)
            {
                // senderCount: 0 to disable sender.
                // When senders in different process running at the same
                // time they will read from the same global queue and cause
                // sending duplicated events. Disable sender to reduce it.
                _telemetry = new Telemetry.Telemetry(
                    _sentinel,
                    sessionId,
                    senderCount: 0);
            }
        }
        catch (Exception)
        {
            // Exceptions during telemetry shouldn't cause anything else to fail
        }
    }
 
    public void Initialize(IEventSource eventSource, int nodeCount)
    {
        Initialize(eventSource);
    }
 
    public void Initialize(IEventSource eventSource)
    {
        // Declare lack of dependency on having properties/items in ProjectStarted events
        // (since this logger doesn't ever care about those events it's irrelevant)
        if (eventSource is IEventSource4 eventSource4)
        {
            eventSource4.IncludeEvaluationPropertiesAndItems();
        }
 
        try
        {
            if (_telemetry != null && _telemetry.Enabled)
            {
                if (eventSource is IEventSource2 eventSource2)
                {
                    eventSource2.TelemetryLogged += OnTelemetryLogged;
                }
            }
        }
        catch (Exception)
        {
            // Exceptions during telemetry shouldn't cause anything else to fail
        }
    }
 
    internal static void FormatAndSend(ITelemetry telemetry, TelemetryEventArgs args)
    {
        switch (args.EventName)
        {
            case TargetFrameworkTelemetryEventName:
                {
                    var newEventName = $"msbuild/{TargetFrameworkTelemetryEventName}";
                    Dictionary<string, string> maskedProperties = [];
 
                    foreach (var key in new[] {
                        TargetFrameworkVersionTelemetryPropertyKey,
                        RuntimeIdentifierTelemetryPropertyKey,
                        SelfContainedTelemetryPropertyKey,
                        UseApphostTelemetryPropertyKey,
                        OutputTypeTelemetryPropertyKey,
                        UseArtifactsOutputTelemetryPropertyKey,
                        ArtifactsPathLocationTypeTelemetryPropertyKey
                    })
                    {
                        if (args.Properties.TryGetValue(key, out string value))
                        {
                            maskedProperties.Add(key, Sha256Hasher.HashWithNormalizedCasing(value));
                        }
                    }
 
                    telemetry.TrackEvent(newEventName, maskedProperties, measurements: null);
                    break;
                }
            case BuildTelemetryEventName:
                TrackEvent(telemetry, $"msbuild/{BuildTelemetryEventName}", args.Properties,
                    toBeHashed: ["ProjectPath", "BuildTarget"],
                    toBeMeasured: ["BuildDurationInMilliseconds", "InnerBuildDurationInMilliseconds"]);
                break;
            case LoggingConfigurationTelemetryEventName:
                TrackEvent(telemetry, $"msbuild/{LoggingConfigurationTelemetryEventName}", args.Properties,
                    toBeHashed: [],
                    toBeMeasured: []);
                break;
            case BuildcheckAcquisitionFailureEventName:
                TrackEvent(telemetry, $"msbuild/{BuildcheckAcquisitionFailureEventName}", args.Properties,
                    toBeHashed: ["AssemblyName", "ExceptionType", "ExceptionMessage"],
                    toBeMeasured: []);
                break;
            case BuildcheckRunEventName:
                TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties,
                    toBeHashed: [],
                    toBeMeasured: ["TotalRuntimeInMilliseconds"]);
                break;
            case BuildcheckRuleStatsEventName:
                TrackEvent(telemetry, $"msbuild/{BuildcheckRuleStatsEventName}", args.Properties,
                    toBeHashed: ["RuleId", "CheckFriendlyName"],
                    toBeMeasured: ["TotalRuntimeInMilliseconds"]);
                break;
            // Pass through events that don't need special handling
            case SdkTaskBaseCatchExceptionTelemetryEventName:
            case PublishPropertiesTelemetryEventName:
            case ReadyToRunTelemetryEventName:
            case WorkloadPublishPropertiesTelemetryEventName:
            case SdkContainerPublishBaseImageInferenceEventName:
            case SdkContainerPublishSuccessEventName:
            case SdkContainerPublishErrorEventName:
                TrackEvent(telemetry, args.EventName, args.Properties, [], []);
                break;
            default:
                // Ignore unknown events
                break;
        }
    }
 
    private static void TrackEvent(ITelemetry telemetry, string eventName, IDictionary<string, string> eventProperties, string[] toBeHashed, string[] toBeMeasured)
    {
        Dictionary<string, string> properties = null;
        Dictionary<string, double> measurements = null;
 
        foreach (var propertyToBeHashed in toBeHashed)
        {
            if (eventProperties.TryGetValue(propertyToBeHashed, out string value))
            {
                // Lets lazy allocate in case there is tons of telemetry
                properties ??= new Dictionary<string, string>(eventProperties);
                properties[propertyToBeHashed] = Sha256Hasher.HashWithNormalizedCasing(value);
            }
        }
 
        foreach (var propertyToBeMeasured in toBeMeasured)
        {
            if (eventProperties.TryGetValue(propertyToBeMeasured, out string value))
            {
                // Lets lazy allocate in case there is tons of telemetry
                properties ??= new Dictionary<string, string>(eventProperties);
                properties.Remove(propertyToBeMeasured);
                if (double.TryParse(value, CultureInfo.InvariantCulture, out double realValue))
                {
                    // Lets lazy allocate in case there is tons of telemetry
                    measurements ??= [];
                    measurements[propertyToBeMeasured] = realValue;
                }
            }
        }
 
        telemetry.TrackEvent(eventName, properties ?? eventProperties, measurements);
    }
 
    private void OnTelemetryLogged(object sender, TelemetryEventArgs args)
    {
        FormatAndSend(_telemetry, args);
    }
 
    public void Shutdown()
    {
        try
        {
            _sentinel?.Dispose();
        }
        catch (Exception)
        {
            // Exceptions during telemetry shouldn't cause anything else to fail
        }
    }
 
    public LoggerVerbosity Verbosity { get; set; }
 
    public string Parameters { get; set; }
}