File: EventSourceLogger.cs
Web Access
Project: src\src\libraries\Microsoft.Extensions.Logging.EventSource\src\Microsoft.Extensions.Logging.EventSource.csproj (Microsoft.Extensions.Logging.EventSource)
// 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.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
 
namespace Microsoft.Extensions.Logging.EventSource
{
    /// <summary>
    /// A logger that writes messages to EventSource instance.
    /// </summary>
    /// <remarks>
    /// On Windows platforms EventSource will deliver messages using Event Tracing for Windows (ETW) events.
    /// On Linux EventSource will use LTTng (http://lttng.org) to deliver messages.
    /// </remarks>
    internal sealed class EventSourceLogger : ILogger
    {
        private static int _activityIds;
        private readonly LoggingEventSource _eventSource;
        private readonly int _factoryID;
 
        public EventSourceLogger(string categoryName, int factoryID, LoggingEventSource eventSource, EventSourceLogger? next)
        {
            CategoryName = categoryName;
 
            // Default is to turn on all the logging
            Level = LogLevel.Trace;
 
            _factoryID = factoryID;
            _eventSource = eventSource;
            Next = next;
        }
 
        public string CategoryName { get; }
 
        public LogLevel Level { get; set; }
 
        // Loggers created by a single provider form a linked list
        public EventSourceLogger? Next { get; }
 
        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel != LogLevel.None && logLevel >= Level;
        }
 
        /// <inheritdoc />
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }
 
            bool formattedMessageEventEnabled = _eventSource.IsEnabled(EventLevel.Critical, LoggingEventSource.Keywords.FormattedMessage);
            bool messageEventEnabled = _eventSource.IsEnabled(EventLevel.Critical, LoggingEventSource.Keywords.Message);
            bool jsonMessageEventEnabled = _eventSource.IsEnabled(EventLevel.Critical, LoggingEventSource.Keywords.JsonMessage);
 
            if (!formattedMessageEventEnabled
                && !messageEventEnabled
                && !jsonMessageEventEnabled)
            {
                return;
            }
 
            string? message = null;
 
            Activity? activity = Activity.Current;
            string activityTraceId;
            string activitySpanId;
            string activityTraceFlags;
            if (activity != null && activity.IdFormat == ActivityIdFormat.W3C)
            {
                activityTraceId = activity.TraceId.ToHexString();
                activitySpanId = activity.SpanId.ToHexString();
                activityTraceFlags = activity.ActivityTraceFlags == ActivityTraceFlags.None
                    ? "0"
                    : "1";
            }
            else
            {
                activityTraceId = string.Empty;
                activitySpanId = string.Empty;
                activityTraceFlags = string.Empty;
            }
 
            // See if they want the formatted message
            if (formattedMessageEventEnabled)
            {
                message = formatter(state, exception);
 
                _eventSource.FormattedMessage(
                    logLevel,
                    _factoryID,
                    CategoryName,
                    eventId.Id,
                    eventId.Name,
                    message,
                    activityTraceId,
                    activitySpanId,
                    activityTraceFlags);
            }
 
            // See if they want the message as its component parts.
            if (messageEventEnabled)
            {
                ExceptionInfo exceptionInfo = GetExceptionInfo(exception);
                IReadOnlyList<KeyValuePair<string, string?>> arguments = GetProperties(state);
 
                _eventSource.Message(
                    logLevel,
                    _factoryID,
                    CategoryName,
                    eventId.Id,
                    eventId.Name,
                    exceptionInfo,
                    arguments,
                    activityTraceId,
                    activitySpanId,
                    activityTraceFlags);
            }
 
            // See if they want the json message
            if (jsonMessageEventEnabled)
            {
                string exceptionJson = "{}";
                if (exception != null)
                {
                    ExceptionInfo exceptionInfo = GetExceptionInfo(exception);
                    KeyValuePair<string, string?>[] exceptionInfoData = new[]
                    {
                        new KeyValuePair<string, string?>("TypeName", exceptionInfo.TypeName),
                        new KeyValuePair<string, string?>("Message", exceptionInfo.Message),
                        new KeyValuePair<string, string?>("HResult", exceptionInfo.HResult.ToString()),
                        new KeyValuePair<string, string?>("VerboseMessage", exceptionInfo.VerboseMessage),
                    };
                    exceptionJson = ToJson(exceptionInfoData);
                }
 
                IReadOnlyList<KeyValuePair<string, string?>> arguments = GetProperties(state);
 
                _eventSource.MessageJson(
                    logLevel,
                    _factoryID,
                    CategoryName,
                    eventId.Id,
                    eventId.Name,
                    exceptionJson,
                    ToJson(arguments),
                    message ?? formatter(state, exception),
                    activityTraceId,
                    activitySpanId,
                    activityTraceFlags);
            }
        }
 
        public IDisposable BeginScope<TState>(TState state) where TState : notnull
        {
            if (!IsEnabled(LogLevel.Critical))
            {
                return NullScope.Instance;
            }
 
            int id = Interlocked.Increment(ref _activityIds);
 
            // If JsonMessage is on, use JSON format
            if (_eventSource.IsEnabled(EventLevel.Critical, LoggingEventSource.Keywords.JsonMessage))
            {
                IReadOnlyList<KeyValuePair<string, string?>> arguments = GetProperties(state);
                _eventSource.ActivityJsonStart(id, _factoryID, CategoryName, ToJson(arguments));
                return new ActivityScope(_eventSource, CategoryName, id, _factoryID, true);
            }
 
            if (_eventSource.IsEnabled(EventLevel.Critical, LoggingEventSource.Keywords.Message) ||
                _eventSource.IsEnabled(EventLevel.Critical, LoggingEventSource.Keywords.FormattedMessage))
            {
                IReadOnlyList<KeyValuePair<string, string?>> arguments = GetProperties(state);
                _eventSource.ActivityStart(id, _factoryID, CategoryName, arguments);
                return new ActivityScope(_eventSource, CategoryName, id, _factoryID, false);
            }
 
            return NullScope.Instance;
        }
 
        /// <summary>
        /// ActivityScope is just a IDisposable that knows how to send the ActivityStop event when it is
        /// desposed.  It is part of the BeginScope() support.
        /// </summary>
        private sealed class ActivityScope : IDisposable
        {
            private readonly string _categoryName;
            private readonly int _activityID;
            private readonly int _factoryID;
            private readonly bool _isJsonStop;
            private readonly LoggingEventSource _eventSource;
 
            public ActivityScope(LoggingEventSource eventSource, string categoryName, int activityID, int factoryID, bool isJsonStop)
            {
                _categoryName = categoryName;
                _activityID = activityID;
                _factoryID = factoryID;
                _isJsonStop = isJsonStop;
                _eventSource = eventSource;
            }
 
            public void Dispose()
            {
                if (_isJsonStop)
                {
                    _eventSource.ActivityJsonStop(_activityID, _factoryID, _categoryName);
                }
                else
                {
                    _eventSource.ActivityStop(_activityID, _factoryID, _categoryName);
                }
            }
        }
 
        /// <summary>
        /// 'serializes' a given exception into an ExceptionInfo (that EventSource knows how to serialize)
        /// </summary>
        /// <param name="exception">The exception to get information for.</param>
        /// <returns>ExceptionInfo object represending a .NET Exception</returns>
        /// <remarks>ETW does not support a concept of a null value. So we use an un-initialized object if there is no exception in the event data.</remarks>
        private static ExceptionInfo GetExceptionInfo(Exception? exception)
        {
            return exception != null ? new ExceptionInfo(exception) : ExceptionInfo.Empty;
        }
 
        /// <summary>
        /// Converts an ILogger state object into a set of key-value pairs (That can be send to a EventSource)
        /// </summary>
        private static KeyValuePair<string, string?>[] GetProperties(object? state)
        {
            if (state is IReadOnlyList<KeyValuePair<string, object?>> keyValuePairs)
            {
                var arguments = new KeyValuePair<string, string?>[keyValuePairs.Count];
                for (int i = 0; i < keyValuePairs.Count; i++)
                {
                    KeyValuePair<string, object?> keyValuePair = keyValuePairs[i];
                    arguments[i] = new KeyValuePair<string, string?>(keyValuePair.Key, keyValuePair.Value?.ToString());
                }
                return arguments;
            }
 
            return Array.Empty<KeyValuePair<string, string?>>();
        }
 
        private static string ToJson(IReadOnlyList<KeyValuePair<string, string?>> keyValues)
        {
            using var stream = new MemoryStream();
            using var writer = new Utf8JsonWriter(stream);
 
            writer.WriteStartObject();
            foreach (KeyValuePair<string, string?> keyValue in keyValues)
            {
                writer.WriteString(keyValue.Key, keyValue.Value);
            }
            writer.WriteEndObject();
 
            writer.Flush();
 
            if (!stream.TryGetBuffer(out ArraySegment<byte> buffer))
            {
                buffer = new ArraySegment<byte>(stream.ToArray());
            }
 
            return Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count);
        }
    }
}