File: LogBuffering\SerializedLogRecordFactory.cs
Web Access
Project: src\src\Shared\Shared.csproj (Shared)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if NET9_0_OR_GREATER
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Shared.Pools;
 
namespace Microsoft.Extensions.Diagnostics.Buffering;
 
internal static class SerializedLogRecordFactory
{
    private const string OriginalFormat = "{OriginalFormat}";
 
    // The approximate size of a SerializedLogRecord instance (112 bytes) and of its slot in the buffer (16 bytes).
    private const int SerializedLogRecordSize = 128;
 
    private static readonly ObjectPool<SerializedLogRecord> _recordPool =
        PoolFactory.CreateResettingPool<SerializedLogRecord>();
 
    public static SerializedLogRecord Create(
        LogLevel logLevel,
        EventId eventId,
        DateTimeOffset timestamp,
        IReadOnlyList<KeyValuePair<string, object?>> attributes,
        Exception? exception,
        string formattedMessage,
        IExternalScopeProvider? scopeProvider)
    {
        SerializedLogRecord record = _recordPool.Get();
        int sizeInBytes = SerializedLogRecordSize;
        string? messageTemplate = null;
        List<KeyValuePair<string, object?>> serializedAttributes = record.Attributes;
        _ = serializedAttributes.EnsureCapacity(attributes.Count);
        for (int i = 0; i < attributes.Count; i++)
        {
            KeyValuePair<string, object?> attribute = attributes[i];
            string key = attribute.Key;
            string value = attribute.Value?.ToString() ?? string.Empty;
 
            // deliberately not counting the size of the key,
            // as it is constant strings in the vast majority of cases
 
            sizeInBytes += CalculateStringSize(value);
 
            if (key == OriginalFormat && attribute.Value is string template)
            {
                messageTemplate = template;
            }
 
            serializedAttributes.Add(new KeyValuePair<string, object?>(key, value));
        }
 
        if (scopeProvider is not null)
        {
            sizeInBytes += AddScopes(serializedAttributes, scopeProvider);
        }
 
        string? exceptionMessage = null;
        if (exception is not null)
        {
            exceptionMessage = exception.Message;
            sizeInBytes += CalculateStringSize(exceptionMessage);
        }
 
        sizeInBytes += CalculateStringSize(formattedMessage);
 
        // Capturing the state of the thread that creates the record, as it's no longer available when the record is flushed.
        // Trace and span IDs are not counted towards the size, because they wrap strings cached by the activity.
        ActivityTraceId activityTraceId = default;
        ActivitySpanId activitySpanId = default;
        Activity? activity = Activity.Current;
        if (activity is not null && activity.IdFormat == ActivityIdFormat.W3C)
        {
            activityTraceId = activity.TraceId;
            activitySpanId = activity.SpanId;
        }
 
        record.Set(
            logLevel,
            eventId,
            timestamp,
            exceptionMessage,
            formattedMessage,
            messageTemplate,
            activityTraceId,
            activitySpanId,
            Environment.CurrentManagedThreadId,
            sizeInBytes);
 
        return record;
    }
 
    /// <summary>
    /// Returns a record to the pool, so that the record can be reused.
    /// </summary>
    /// <remarks>
    /// Must only be called for records which have not been emitted, as loggers can still hold on to emitted records' attributes.
    /// </remarks>
    public static void Return(SerializedLogRecord bufferedRecord)
    {
        _recordPool.Return(bufferedRecord);
    }
 
    /// <summary>
    /// Adds the name/value pairs of the current scopes to the attributes of a log record.
    /// </summary>
    /// <returns>The approximate size in bytes of the added values.</returns>
    private static int AddScopes(List<KeyValuePair<string, object?>> serializedAttributes, IExternalScopeProvider scopeProvider)
    {
        int count = serializedAttributes.Count;
        scopeProvider.ForEachScope(static (scope, attributes) =>
        {
            if (scope is IReadOnlyList<KeyValuePair<string, object?>> list)
            {
                for (int i = 0; i < list.Count; i++)
                {
                    AddScopeItem(attributes, list[i]);
                }
            }
            else if (scope is IEnumerable<KeyValuePair<string, object?>> items)
            {
                foreach (KeyValuePair<string, object?> item in items)
                {
                    AddScopeItem(attributes, item);
                }
            }
        }, serializedAttributes);
 
        int sizeInBytes = 0;
        for (int i = count; i < serializedAttributes.Count; i++)
        {
            sizeInBytes += CalculateStringSize((string)serializedAttributes[i].Value!);
        }
 
        // "{OriginalFormat}" needs to stay the last attribute.
        if (count > 0 && serializedAttributes[count - 1].Key == OriginalFormat)
        {
            KeyValuePair<string, object?> originalFormat = serializedAttributes[count - 1];
            serializedAttributes.RemoveAt(count - 1);
            serializedAttributes.Add(originalFormat);
        }
 
        return sizeInBytes;
    }
 
    private static void AddScopeItem(List<KeyValuePair<string, object?>> attributes, KeyValuePair<string, object?> item)
    {
        // Skips the scope's {OriginalFormat} in favor of the log record's own {OriginalFormat}.
        if (item.Key != OriginalFormat)
        {
            attributes.Add(new KeyValuePair<string, object?>(item.Key, item.Value?.ToString() ?? string.Empty));
        }
    }
 
    private static int CalculateStringSize(string str)
    {
        if (string.IsNullOrEmpty(str))
        {
            return 0;
        }
 
        // Base size: object overhead (16 bytes) + other stuff.
        const int BaseSize = 26;
 
        // Strings are aligned to 8-byte boundaries
        const int Alignment = 7;
 
        int charSize = str.Length * sizeof(char);
        return (BaseSize + charSize + Alignment) & ~Alignment;
    }
}
#endif