File: BackEnd\Components\Logging\LoggingServiceLogMethods.cs
Web Access
Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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;
using System.Collections.Generic;
using Microsoft.Build.BackEnd.Shared;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Experimental.BuildCheck.Infrastructure;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.Profiler;
using Microsoft.Build.Logging;
using Microsoft.Build.Shared;

using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem;

#nullable disable

namespace Microsoft.Build.BackEnd.Logging
{
    /// <summary>
    /// Partial class half of LoggingService which contains the Logging methods.
    /// </summary>
    internal partial class LoggingService : ILoggingService, INodePacketHandler, IBuildComponent
    {
        #region Log comments

        /// <summary>
        /// Logs a comment (BuildMessageEventArgs) with a certain MessageImportance level
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="importance">How important is the message, this will determine which verbosities the message will show up on.
        /// The higher the importance the lower the verbosity needs to be for the message to be seen</param>
        /// <param name="messageResourceName">String which identifies the message in the string resx</param>
        /// <param name="messageArgs">Arguments for the format string indexed by messageResourceName</param>
        /// <exception cref="InternalErrorException">MessageResourceName is null</exception>
        public void LogComment(BuildEventContext buildEventContext, MessageImportance importance, string messageResourceName, params object[] messageArgs)
        {
            if (!OnlyLogCriticalEvents)
            {
                Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for comment message.");

                LogCommentFromText(buildEventContext, importance, ResourceUtilities.GetResourceString(messageResourceName), messageArgs);
            }
        }

        /// <summary>
        /// Log a comment
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="importance">How important is the message, this will determine which verbosities the message will show up on.
        /// The higher the importance the lower the verbosity needs to be for the message to be seen</param>
        /// <param name="message">Message to log</param>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        /// <exception cref="InternalErrorException">Message is null</exception>
        public void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message)
        {
            this.LogCommentFromText(buildEventContext, importance, message, messageArgs: null);
        }

        /// <summary>
        /// Log a comment
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="importance">How important is the message, this will determine which verbosities the message will show up on.
        /// The higher the importance the lower the verbosity needs to be for the message to be seen</param>
        /// <param name="message">Message to log</param>
        /// <param name="messageArgs">Message formatting arguments</param>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        /// <exception cref="InternalErrorException">Message is null</exception>
        public void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message, params object[] messageArgs)
        {
            if (!OnlyLogCriticalEvents)
            {
                BuildMessageEventArgs buildEvent = EventsCreatorHelper.CreateMessageEventFromText(buildEventContext, importance, message, messageArgs);

                ProcessLoggingEvent(buildEvent);
            }
        }
        #endregion

        #region Log errors
        /**************************************************************************************************************************
         * WARNING: Do not add overloads that allow raising events without specifying a file. In general ALL events should have a
         * file associated with them. We've received a LOT of feedback from dogfooders about the lack of information in our
         * events. If an event TRULY does not have an associated file, then String.Empty can be passed in for the file. However,
         * that burden should lie on the caller -- these wrapper methods should NOT make it easy to skip the filename.
         *************************************************************************************************************************/

        /// <summary>
        /// Logs an error with all registered loggers using the specified resource string.
        /// </summary>
        /// <param name="location">Event context information which describes who is logging the event</param>
        /// <param name="file">File information where the error happened</param>
        /// <param name="messageResourceName">String key to find the correct string resource</param>
        /// <param name="messageArgs">Arguments for the string resource</param>
        public void LogError(BuildEventContext location, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs)
        {
            LogError(location, null, file, messageResourceName, messageArgs);
        }

        /// <summary>
        /// Logs an error
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="subcategoryResourceName">Can be null.</param>
        /// <param name="file">File information about where the error happened</param>
        /// <param name="messageResourceName">String index into the string.resx file</param>
        /// <param name="messageArgs">Arguments for the format string in the resource file</param>
        /// <exception cref="InternalErrorException">MessageResourceName is null</exception>
        public void LogError(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs)
        {
            Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for error message.");

            string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out string helpKeyword, messageResourceName, messageArgs);

            LogErrorFromText(buildEventContext, subcategoryResourceName, errorCode, helpKeyword, file, message);
        }

        /// <summary>
        /// Logs an error with a given message
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="subcategoryResourceName">Can be null.</param>
        /// <param name="errorCode">Can be null.</param>
        /// <param name="helpKeyword">Can be null.</param>
        /// <param name="file">File information about where the error happened</param>
        /// <param name="message">Error message which will be displayed</param>
        /// <exception cref="InternalErrorException">File is null</exception>
        /// <exception cref="InternalErrorException">Message is null</exception>
        public void LogErrorFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string errorCode, string helpKeyword, BuildEventFileInfo file, string message)
        {
            BuildErrorEventArgs buildEvent = EventsCreatorHelper.CreateErrorEventFromText(buildEventContext, subcategoryResourceName, errorCode, helpKeyword, file, message);

            if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId)
            {
                _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out string projectFile);
                Assumed.NotNull(projectFile, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!");
                buildEvent.ProjectFile = projectFile;
            }

            ProcessLoggingEvent(buildEvent);
        }

        /// <summary>
        /// Logs an error regarding an invalid project file . Since this method may be multiple times for the same InvalidProjectException
        /// we do not want to log the error multiple times. Once the exception has been logged we set a flag on the exception to note that
        /// it has already been logged.
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="invalidProjectFileException">Exception which is causing the error</param>
        /// <exception cref="InternalErrorException">InvalidProjectFileException is null</exception>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public void LogInvalidProjectFileError(BuildEventContext buildEventContext, InvalidProjectFileException invalidProjectFileException)
        {
            Assumed.NotNull(invalidProjectFileException, "Need exception context.");
            Assumed.NotNull(buildEventContext, "buildEventContext is null");

            // Don't log the exception more than once.
            if (!invalidProjectFileException.HasBeenLogged)
            {
                BuildErrorEventArgs buildEvent =
                    new BuildErrorEventArgs(
                        invalidProjectFileException.ErrorSubcategory,
                        invalidProjectFileException.ErrorCode,
                        invalidProjectFileException.ProjectFile,
                        invalidProjectFileException.LineNumber,
                        invalidProjectFileException.ColumnNumber,
                        invalidProjectFileException.EndLineNumber,
                        invalidProjectFileException.EndColumnNumber,
                        invalidProjectFileException.BaseMessage,
                        invalidProjectFileException.HelpKeyword,
                        "MSBuild");
                buildEvent.BuildEventContext = buildEventContext;
                if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId)
                {
                    _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out string projectFile);
                    Assumed.NotNull(projectFile, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!");
                    buildEvent.ProjectFile = projectFile;
                }

                ProcessLoggingEvent(buildEvent);
                invalidProjectFileException.HasBeenLogged = true;
            }
        }

        /// <summary>
        /// Logs an error regarding an unexpected build failure
        /// This will include a stack dump.
        /// </summary>
        /// <param name="buildEventContext">BuildEventContext of the error</param>
        /// <param name="exception">Exception wihch caused the build error</param>
        /// <param name="file">Provides file information about where the build error happened</param>
        public void LogFatalBuildError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file)
        {
            LogFatalError(buildEventContext, exception, file, "FatalBuildError");
        }

        /// <summary>
        /// Logs an error regarding an unexpected task failure.
        /// This will include a stack dump.
        /// </summary>
        /// <param name="buildEventContext">BuildEventContext of the error</param>
        /// <param name="exception">Exceptionm which caused the error</param>
        /// <param name="file">File information which indicates which file the error is happening in</param>
        /// <param name="taskName">Task which the error is happening in</param>
        /// <exception cref="InternalErrorException">TaskName is null</exception>
        public void LogFatalTaskError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName)
        {
            Assumed.NotNull(taskName, "Must specify the name of the task that failed.");

            LogFatalError(buildEventContext, exception, file, "FatalTaskError", taskName);
        }

        /// <summary>
        /// Logs an error regarding an unexpected failure using the specified resource string.
        /// This will include a stack dump.
        /// </summary>
        /// <param name="buildEventContext">BuildEventContext of the error</param>
        /// <param name="exception">Exception which will be used to generate the error message</param>
        /// <param name="file">File information which describes where the error happened</param>
        /// <param name="messageResourceName">String name for the resource string to be used</param>
        /// <param name="messageArgs">Arguments for messageResourceName</param>
        /// <exception cref="InternalErrorException">MessageResourceName is null</exception>
        public void LogFatalError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs)
        {
            Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for error message.");

            string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out string helpKeyword, messageResourceName, messageArgs);
#if DEBUG
            message += Environment.NewLine + "This is an unhandled exception from a task -- PLEASE OPEN A BUG AGAINST THE TASK OWNER.";
#endif
            if (exception != null)
            {
                message += Environment.NewLine + exception.ToString();
            }

            LogErrorFromText(buildEventContext, null, errorCode, helpKeyword, file, message);
        }

        #endregion

        #region Log warnings
        /**************************************************************************************************************************
         * WARNING: Do not add overloads that allow raising events without specifying a file. In general ALL events should have a
         * file associated with them. We've received a LOT of feedback from dogfooders about the lack of information in our
         * events. If an event TRULY does not have an associated file, then String.Empty can be passed in for the file. However,
         * that burden should lie on the caller -- these wrapper methods should NOT make it easy to skip the filename.
         *************************************************************************************************************************/

        /// <summary>
        /// Logs an warning regarding an unexpected task failure
        /// This will include a stack dump.
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="exception">The exception to be used to create the warning text</param>
        /// <param name="file">The file information which indicates where the warning happened</param>
        /// <param name="taskName">Name of the task which the warning is being raised from</param>
        public void LogTaskWarningFromException(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName)
        {
            Assumed.NotNullOrEmpty(taskName, "Must specify the name of the task that failed.");

            string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out string helpKeyword, "FatalTaskError", taskName);
#if DEBUG
            message += Environment.NewLine + "This is an unhandled exception from a task -- PLEASE OPEN A BUG AGAINST THE TASK OWNER.";
#endif

            if (exception != null)
            {
                message += Environment.NewLine + exception.ToString();
            }

            LogWarningFromText(buildEventContext, null, warningCode, helpKeyword, file, message);
        }

        /// <summary>
        /// Logs a warning using the specified resource string.
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="subcategoryResourceName">Can be null.</param>
        /// <param name="file">File information which describes where the warning happened</param>
        /// <param name="messageResourceName">String name for the resource string to be used</param>
        /// <param name="messageArgs">Arguments for messageResourceName</param>
        public void LogWarning(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs)
        {
            Assumed.NotNullOrEmpty(messageResourceName, "Need resource string for warning message.");

            string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out string helpKeyword, messageResourceName, messageArgs);
            LogWarningFromText(buildEventContext, subcategoryResourceName, warningCode, helpKeyword, file, message);
        }

        /// <summary>
        /// Logs a warning
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="subcategoryResourceName">Subcategory resource Name. Can be null.</param>
        /// <param name="warningCode">The warning code of the message. Can be null.</param>
        /// <param name="helpKeyword">Help keyword for the message. Can be null.</param>
        /// <param name="file">The file information which will describe where the warning happened</param>
        /// <param name="message">Warning message to log</param>
        public void LogWarningFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string warningCode, string helpKeyword, BuildEventFileInfo file, string message)
        {
            Assumed.NotNull(file, "Must specify the associated file.");
            Assumed.NotNull(message, "Need warning message.");
            Assumed.NotNull(buildEventContext, "Need a BuildEventContext");

            string subcategory = null;

            if (!string.IsNullOrWhiteSpace(subcategoryResourceName))
            {
                subcategory = AssemblyResources.GetString(subcategoryResourceName);
            }

            BuildWarningEventArgs buildEvent = new BuildWarningEventArgs(
                    subcategory,
                    warningCode,
                    file.File,
                    file.Line,
                    file.Column,
                    file.EndLine,
                    file.EndColumn,
                    message,
                    helpKeyword,
                    "MSBuild");

            buildEvent.BuildEventContext = buildEventContext;
            if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId)
            {
                _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out string projectFile);
                Assumed.NotNull(projectFile, $"ContextID {buildEventContext.ProjectContextId} should have been in the ID-to-project file mapping but wasn't!");
                buildEvent.ProjectFile = projectFile;
            }

            ProcessLoggingEvent(buildEvent);
        }

        #endregion

        #region Log status

        /// <summary>
        /// Logs that the build has started
        /// </summary>
        public void LogBuildStarted()
        {
            // If we're only logging critical events, don't risk causing all the resources to load by formatting
            // a string that won't get emitted anyway.
            string message = String.Empty;
            if (!OnlyLogCriticalEvents)
            {
                message = ResourceUtilities.GetResourceString("BuildStarted");
            }

            IDictionary<string, string> environmentProperties = _componentHost?.BuildParameters?.BuildProcessEnvironment;

            BuildStartedEventArgs buildEvent = new(message, helpKeyword: null, environmentProperties);

            // Raise the event with the filters
            ProcessLoggingEvent(buildEvent);

            // Make sure we process this event before going any further
            WaitForLoggingToProcessEvents();

            // Register Loggers and print out all the enabled loggers.
            // Gated behind ChangeWaves.Wave18_8 so that disabling the wave fully suppresses
            // the new "Enabled loggers" message and the new LoggersRegisteredEventArgs,
            // not just their rendering in the console/terminal loggers.
            if (!OnlyLogCriticalEvents && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8))
            {
                LogAndRegisterLoggers();
            }
        }

        /// <summary>
        /// Logs that the build has finished
        /// </summary>
        /// <param name="success">Did the build pass or fail</param>
        public void LogBuildFinished(bool success)
        {
            // If we're only logging critical events, don't risk causing all the resources to load by formatting
            // a string that won't get emitted anyway.
            string message = String.Empty;
            if (!OnlyLogCriticalEvents)
            {
                if (Question)
                {
                    message = ResourceUtilities.GetResourceString(success ? "BuildFinishedQuestionSuccess" : "BuildFinishedQuestionFailure");
                }
                else
                {
                    message = ResourceUtilities.GetResourceString(success ? "BuildFinishedSuccess" : "BuildFinishedFailure");
                }
            }

            BuildFinishedEventArgs buildEvent = new BuildFinishedEventArgs(message, null /* no help keyword */, success);

            ProcessLoggingEvent(buildEvent);

            // Make sure we process this event before going any further
            WaitForLoggingToProcessEvents();
        }

        /// <summary>
        /// Emits a message listing the enabled logger
        /// type names and a <see cref="LoggersRegisteredEventArgs"/> describing each logger (including
        /// any output file paths for <see cref="IFileOutputLogger"/> implementations).
        /// </summary>
        private void LogAndRegisterLoggers()
        {
            List<string> listOfLoggers = new();
            var loggerDescriptions = new List<RegisteredLoggerInfo>();

            foreach (ILogger logger in Loggers)
            {
                ILogger actualLogger = UnwrapLogger(logger);
                Type loggerType = actualLogger.GetType();

                listOfLoggers.Add(loggerType.Name);

                var outputFilePaths = new List<string>();
                if (actualLogger is IFileOutputLogger fileLogger)
                {
                    foreach (string outputFilePath in fileLogger.OutputFilePaths)
                    {
                        if (!string.IsNullOrEmpty(outputFilePath))
                        {
                            outputFilePaths.Add(outputFilePath);
                        }
                    }
                }

                loggerDescriptions.Add(new RegisteredLoggerInfo(
                    loggerName: loggerType.Name,
                    outputFilePaths: outputFilePaths.Count > 0 ? outputFilePaths : null,
                    verbosity: actualLogger.Verbosity,
                    parameters: actualLogger.Parameters));
            }

            if (listOfLoggers.Count != 0)
            {
                var msgEvent = new BuildMessageEventArgs(
                    ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LogEnabledLogs", string.Join(", ", listOfLoggers)),
                    null, null, MessageImportance.Low);
                msgEvent.BuildEventContext = BuildEventContext.Invalid;
                ProcessLoggingEvent(msgEvent);
            }

            if (loggerDescriptions.Count > 0)
            {
                var registerEvent = new LoggersRegisteredEventArgs(loggerDescriptions);
                registerEvent.BuildEventContext = BuildEventContext.Invalid;
                ProcessLoggingEvent(registerEvent);
            }
        }
        private ILogger UnwrapLogger(ILogger logger)
        {
            return logger is ReusableLogger reusable ? reusable.OriginalLogger : logger;
        }

        /// <inheritdoc />
        public void LogBuildCanceled()
        {
            string message = ResourceUtilities.GetResourceString("AbortingBuild");
            BuildCanceledEventArgs buildEvent = new BuildCanceledEventArgs(message);

            ProcessLoggingEvent(buildEvent);
        }

        /// <inheritdoc />
        public BuildEventContext CreateEvaluationBuildEventContext(int nodeId, int submissionId)
            => new BuildEventContext(submissionId, nodeId, NextEvaluationId, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);

        /// <inheritdoc />
        public BuildEventContext CreateProjectCacheBuildEventContext(
            int submissionId,
            int evaluationId,
            int projectInstanceId,
            string projectFile)
        {
            int projectContextId = NextProjectId;

            // In the future if some LogProjectCacheStarted event is created, move this there to align with evaluation and build execution.
            _projectFileMap[projectContextId] = projectFile;

            // Because the project cache runs in the BuildManager, it makes some sense to associate logging with the in-proc node.
            // If a invalid node id is used the messages become deferred in the console logger and spit out at the end.
            int nodeId = Scheduler.InProcNodeId;

            return new BuildEventContext(submissionId, nodeId, evaluationId, projectInstanceId, projectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
        }

        /// <inheritdoc />
        public void LogProjectEvaluationStarted(BuildEventContext projectEvaluationEventContext, string projectFile)
        {
            ProjectEvaluationStartedEventArgs evaluationEvent =
                new ProjectEvaluationStartedEventArgs(ResourceUtilities.GetResourceString("EvaluationStarted"),
                    projectFile)
                {
                    BuildEventContext = projectEvaluationEventContext,
                    ProjectFile = projectFile
                };

            ProcessLoggingEvent(evaluationEvent);
        }

        /// <summary>
        /// Logs that a project evaluation has finished
        /// </summary>
        /// <param name="projectEvaluationEventContext">Event context for the project.</param>
        /// <param name="projectFile">Project file being built</param>
        /// <param name="globalProperties">Global properties used for the evaluation.</param>
        /// <param name="properties">Properties produced by the evaluation.</param>
        /// <param name="items">Items produced by the evaluation.</param>
        /// <param name="profilerResult">Profiler results if evaluation profiling was enabled.</param>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public void LogProjectEvaluationFinished(
            BuildEventContext projectEvaluationEventContext,
            string projectFile,
            IEnumerable globalProperties,
            IEnumerable properties,
            IEnumerable items,
            ProfilerResult? profilerResult)
        {
            Assumed.NotNull(projectEvaluationEventContext, "projectBuildEventContext");

            ProjectEvaluationFinishedEventArgs buildEvent =
                new ProjectEvaluationFinishedEventArgs(ResourceUtilities.GetResourceString("EvaluationFinished"), projectFile)
                {
                    BuildEventContext = projectEvaluationEventContext,
                    ProjectFile = projectFile,
                    ProfilerResult = profilerResult,
                    GlobalProperties = globalProperties,
                    Properties = properties,
                    Items = items
                };
            ProcessLoggingEvent(buildEvent);
        }

        /// <summary>
        /// Logs that a project build has started
        /// </summary>
        /// <param name="nodeBuildEventContext">The event context of the node which is spawning this project.</param>
        /// <param name="submissionId">The id of the submission.</param>
        /// <param name="configurationId">The id of the project configuration which is about to start</param>
        /// <param name="parentBuildEventContext">BuildEventContext of the project who is requesting "projectFile" to build</param>
        /// <param name="projectFile">Project file to build</param>
        /// <param name="targetNames">Target names to build</param>
        /// <param name="properties">Initial property list</param>
        /// <param name="items">Initial items list</param>
        /// <param name="evaluationId">EvaluationId of the project instance</param>
        /// <param name="projectContextId">The project context id</param>
        /// <returns>The build event context for the project.</returns>
        /// <exception cref="InternalErrorException">parentBuildEventContext is null</exception>
        /// <exception cref="InternalErrorException">projectBuildEventContext is null</exception>
        public BuildEventContext LogProjectStarted(
            BuildEventContext nodeBuildEventContext,
            int submissionId,
            int configurationId,
            BuildEventContext parentBuildEventContext,
            string projectFile,
            string targetNames,
            IEnumerable<DictionaryEntry> properties,
            IEnumerable<DictionaryEntry> items,
            int evaluationId = BuildEventContext.InvalidEvaluationId,
            int projectContextId = BuildEventContext.InvalidProjectContextId)
        {
            var args = CreateProjectStarted(nodeBuildEventContext,
                submissionId,
                configurationId,
                parentBuildEventContext,
                projectFile,
                targetNames,
                properties,
                items,
                evaluationId,
                projectContextId);

            this.LogProjectStarted(args);

            return args.BuildEventContext;
        }

        public void LogProjectStarted(ProjectStartedEventArgs buildEvent)
        {
            ProcessLoggingEvent(buildEvent);
        }

        public ProjectStartedEventArgs CreateProjectStarted(
            BuildEventContext nodeBuildEventContext,
            int submissionId,
            int configurationId,
            BuildEventContext parentBuildEventContext,
            string projectFile,
            string targetNames,
            IEnumerable<DictionaryEntry> properties,
            IEnumerable<DictionaryEntry> items,
            int evaluationId = BuildEventContext.InvalidEvaluationId,
            int projectContextId = BuildEventContext.InvalidProjectContextId)
        {
            Assumed.NotNull(nodeBuildEventContext, "Need a nodeBuildEventContext");

            if (projectContextId == BuildEventContext.InvalidProjectContextId)
            {
                projectContextId = NextProjectId;

                Assumed.False(_projectFileMap.ContainsKey(projectContextId), $"ContextID {projectContextId} for project {projectFile} should not already be in the ID-to-file mapping!");

                _projectFileMap[projectContextId] = projectFile;
            }
            else
            {
                // A projectContextId was provided, so use it with some sanity checks
                if (_projectFileMap.TryGetValue(projectContextId, out string existingProjectFile))
                {
                    Assumed.Equal(
                        projectFile,
                        existingProjectFile,
                        StringComparison.OrdinalIgnoreCase,
                        $"ContextID {projectContextId} was already in the ID-to-project file mapping but the project file {existingProjectFile} did not match the provided one {projectFile}!");
                }
                else
                {
                    // Currently, an existing projectContextId can only be provided in the project cache scenario, which runs on the in-proc node.
                    // If there was a cache miss and the build was scheduled on a worker node, it may not have seen this projectContextId yet.
                    // So we only need this sanity check for the in-proc node.
                    Assumed.NotEqual(
                        nodeBuildEventContext.NodeId,
                        Scheduler.InProcNodeId,
                        $"ContextID {projectContextId} should have been in the ID-to-project file mapping but wasn't!");

                    _projectFileMap[projectContextId] = projectFile;
                }
            }

            BuildEventContext projectBuildEventContext = new BuildEventContext(submissionId, nodeBuildEventContext.NodeId, evaluationId, configurationId, projectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);

            Assumed.NotNull(parentBuildEventContext, "Need a parentBuildEventContext");

            Assumed.True(_configCache.Value.HasConfiguration(configurationId), "Cannot find the project configuration while injecting non-serialized data from out-of-proc node.");
            var buildRequestConfiguration = _configCache.Value[configurationId];

            // Always log GlobalProperties on ProjectStarted
            // See https://github.com/dotnet/msbuild/issues/6341 for details
            IDictionary<string, string> globalProperties = buildRequestConfiguration.GlobalProperties.ToDictionary();

            var buildEvent = new ProjectStartedEventArgs(
                    configurationId,
                    message: null,
                    helpKeyword: null,
                    projectFile,
                    targetNames,
                    properties,
                    items,
                    parentBuildEventContext,
                    globalProperties,
                    buildRequestConfiguration.ToolsVersion);
            buildEvent.BuildEventContext = projectBuildEventContext;

            return buildEvent;
        }

        /// <summary>
        /// Logs that a project has finished
        /// </summary>
        /// <param name="projectBuildEventContext">Event context for the project.</param>
        /// <param name="projectFile">Project file being built</param>
        /// <param name="success">Did the project pass or fail</param>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public void LogProjectFinished(BuildEventContext projectBuildEventContext, string projectFile, bool success)
        {
            Assumed.NotNull(projectBuildEventContext, "projectBuildEventContext");

            ProjectFinishedEventArgs buildEvent = new ProjectFinishedEventArgs(
                    message: null,
                    helpKeyword: null,
                    projectFile,
                    success);
            buildEvent.BuildEventContext = projectBuildEventContext;
            ProcessLoggingEvent(buildEvent);

            // BuildCheck can still emit some LogBuildEvent(s) after ProjectFinishedEventArgs was reported.
            // Due to GetAndVerifyProjectFileFromContext validation, these checks break the build.
            if (!_buildCheckEnabled)
            {
                Assumed.True(_projectFileMap.TryRemove(projectBuildEventContext.ProjectContextId, out _), $"ContextID {projectBuildEventContext.ProjectContextId} for project {projectFile} should be in the ID-to-file mapping!");
            }
        }

        /// <summary>
        /// Logs that a target started
        /// </summary>
        /// <param name="projectBuildEventContext">Event context for the project spawning this target</param>
        /// <param name="targetName">Name of target</param>
        /// <param name="projectFile">Project file being built</param>
        /// <param name="projectFileOfTargetElement">Project file which contains the target</param>
        /// <param name="parentTargetName">The name of the parent target.</param>
        /// <param name="buildReason">The reason the parent target built the target.</param>
        /// <returns>The build event context for the target.</returns>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public BuildEventContext LogTargetStarted(BuildEventContext projectBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, string parentTargetName, TargetBuiltReason buildReason)
        {
            Assumed.NotNull(projectBuildEventContext, "projectBuildEventContext is null");
            BuildEventContext targetBuildEventContext = new BuildEventContext(
                    projectBuildEventContext.SubmissionId,
                    projectBuildEventContext.NodeId,
                    projectBuildEventContext.ProjectInstanceId,
                    projectBuildEventContext.ProjectContextId,
                    NextTargetId,
                    BuildEventContext.InvalidTaskId);

            if (!OnlyLogCriticalEvents)
            {
                TargetStartedEventArgs buildEvent = new TargetStartedEventArgs(
                        message: null,
                        helpKeyword: null,
                        targetName,
                        projectFile,
                        projectFileOfTargetElement,
                        parentTargetName,
                        buildReason,
                        DateTime.UtcNow);
                buildEvent.BuildEventContext = targetBuildEventContext;
                ProcessLoggingEvent(buildEvent);
            }

            return targetBuildEventContext;
        }

        /// <summary>
        /// Logs that a target has finished.
        /// </summary>
        /// <param name="targetBuildEventContext">Event context for the target</param>
        /// <param name="targetName">Target which has just finished</param>
        /// <param name="projectFile">Project file being built</param>
        /// <param name="projectFileOfTargetElement">Project file which contains the target</param>
        /// <param name="success">Did the target pass or fail</param>
        /// <param name="targetOutputs">Target outputs for the target.</param>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public void LogTargetFinished(BuildEventContext targetBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, bool success, IEnumerable<TaskItem> targetOutputs)
        {
            if (!OnlyLogCriticalEvents)
            {
                Assumed.NotNull(targetBuildEventContext, "targetBuildEventContext is null");

                TargetFinishedEventArgs buildEvent = new TargetFinishedEventArgs(
                        message: null,
                        helpKeyword: null,
                        targetName,
                        projectFile,
                        projectFileOfTargetElement,
                        success,
                        targetOutputs);

                buildEvent.BuildEventContext = targetBuildEventContext;
                ProcessLoggingEvent(buildEvent);
            }
        }

        /// <summary>
        /// Logs that task execution has started.
        /// </summary>
        /// <param name="taskBuildEventContext">Event context for the task</param>
        /// <param name="taskName">Task Name</param>
        /// <param name="projectFile">Project file being built</param>
        /// <param name="projectFileOfTaskNode">Project file which contains the task</param>
        /// <param name="taskAssemblyLocation">>The location of the assembly containing the implementation of the task.</param>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public void LogTaskStarted(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, string taskAssemblyLocation)
        {
            Assumed.NotNull(taskBuildEventContext, "targetBuildEventContext is null");
            if (!OnlyLogCriticalEvents)
            {
                TaskStartedEventArgs buildEvent = new TaskStartedEventArgs(
                        message: null,
                        helpKeyword: null,
                        projectFile,
                        projectFileOfTaskNode,
                        taskName,
                        taskAssemblyLocation);
                buildEvent.BuildEventContext = taskBuildEventContext;
                ProcessLoggingEvent(buildEvent);
            }
        }

        /// <summary>
        /// Logs that task execution has started.
        /// </summary>
        /// <param name="targetBuildEventContext">Event context for the target spawning this task.</param>
        /// <param name="taskName">Task Name</param>
        /// <param name="projectFile">Project file being built</param>
        /// <param name="projectFileOfTaskNode">Project file which contains the task</param>
        /// <param name="line">The line number in the file where the task invocation is located.</param>
        /// <param name="column">The column number in the file where the task invocation is located.</param>
        /// <param name="taskAssemblyLocation">>The location of the assembly containing the implementation of the task.</param>
        /// <returns>The build event context for the task.</returns>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public BuildEventContext LogTaskStarted2(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, int line, int column, string taskAssemblyLocation)
        {
            Assumed.NotNull(targetBuildEventContext, "targetBuildEventContext is null");
            BuildEventContext taskBuildEventContext = new BuildEventContext(
                    targetBuildEventContext.SubmissionId,
                    targetBuildEventContext.NodeId,
                    targetBuildEventContext.ProjectInstanceId,
                    targetBuildEventContext.ProjectContextId,
                    targetBuildEventContext.TargetId,
                    NextTaskId);

            if (!OnlyLogCriticalEvents)
            {
                TaskStartedEventArgs buildEvent = new TaskStartedEventArgs(
                        message: null,
                        helpKeyword: null,
                        projectFile,
                        projectFileOfTaskNode,
                        taskName,
                        taskAssemblyLocation);
                buildEvent.BuildEventContext = taskBuildEventContext;
                buildEvent.LineNumber = line;
                buildEvent.ColumnNumber = column;
                ProcessLoggingEvent(buildEvent);
            }

            return taskBuildEventContext;
        }

        /// <summary>
        /// Logs that a task has finished executing.
        /// </summary>
        /// <param name="taskBuildEventContext">Event context for the task</param>
        /// <param name="taskName">Name of the task</param>
        /// <param name="projectFile">Project which is being processed</param>
        /// <param name="projectFileOfTaskNode">Project file which contains the task</param>
        /// <param name="success">Did the task pass or fail</param>
        /// <exception cref="InternalErrorException">BuildEventContext is null</exception>
        public void LogTaskFinished(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, bool success)
        {
            if (!OnlyLogCriticalEvents)
            {
                Assumed.NotNull(taskBuildEventContext, "taskBuildEventContext is null");

                TaskFinishedEventArgs buildEvent = new TaskFinishedEventArgs(
                        message: null,
                        helpKeyword: null,
                        projectFile,
                        projectFileOfTaskNode,
                        taskName,
                        success);
                buildEvent.BuildEventContext = taskBuildEventContext;
                ProcessLoggingEvent(buildEvent);
            }
        }

        #endregion

        #region Log telemetry

        /// <summary>
        /// Logs a telemetry event.
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="eventName">The event name.</param>
        /// <param name="properties">The list of properties assocated with the event.</param>
        public void LogTelemetry(BuildEventContext buildEventContext, string eventName, IDictionary<string, string> properties)
        {
            Assumed.NotNull(eventName, "eventName is null");

            TelemetryEventArgs telemetryEvent = new TelemetryEventArgs
            {
                BuildEventContext = buildEventContext,
                EventName = eventName,
                Properties = properties == null ? new Dictionary<string, string>() : new Dictionary<string, string>(properties)
            };

            ProcessLoggingEvent(telemetryEvent);
        }

        #endregion

        #region log response files
        /// <summary>
        /// Logs a file to include in the binlogs
        /// </summary>
        /// <param name="buildEventContext">Event context information which describes who is logging the event</param>
        /// <param name="filePath">Full path to response file</param>
        public void LogIncludeFile(BuildEventContext buildEventContext, string filePath)
        {
            Assumed.NotNull(buildEventContext, "buildEventContext was null");
            Assumed.NotNull(filePath, "response file path was null");
            ResponseFileUsedEventArgs responseFileUsedEvent = new ResponseFileUsedEventArgs(filePath);
            responseFileUsedEvent.BuildEventContext = buildEventContext;
            ProcessLoggingEvent(responseFileUsedEvent);
        }

        #endregion

#nullable enable
        private IBuildEngineDataRouter? _buildEngineDataRouter;

        public void ProcessPropertyRead(PropertyReadInfo propertyReadInfo, CheckLoggingContext checkContext)
            => _buildEngineDataRouter?.ProcessPropertyRead(propertyReadInfo, checkContext);

        public void ProcessPropertyWrite(PropertyWriteInfo propertyWriteInfo, CheckLoggingContext checkContext)
            => _buildEngineDataRouter?.ProcessPropertyWrite(propertyWriteInfo, checkContext);

        public void ProcessProjectEvaluationStarted(ICheckContext checkContext, string projectFullPath)
            => _buildEngineDataRouter?.ProcessProjectEvaluationStarted(checkContext, projectFullPath);
#nullable disable
    }
}