diff --git a/src/Arc4u.Diagnostics/Arc4u.Diagnostics.csproj b/src/Arc4u.Diagnostics/Arc4u.Diagnostics.csproj index ab8a8a51..e86c4951 100644 --- a/src/Arc4u.Diagnostics/Arc4u.Diagnostics.csproj +++ b/src/Arc4u.Diagnostics/Arc4u.Diagnostics.csproj @@ -6,12 +6,15 @@ + + + diff --git a/src/Arc4u.Diagnostics/Logging/ApplicationBuilderExtensions.cs b/src/Arc4u.Diagnostics/Logging/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..911882da --- /dev/null +++ b/src/Arc4u.Diagnostics/Logging/ApplicationBuilderExtensions.cs @@ -0,0 +1,12 @@ +using Arc4u.Diagnostics.Logging; +using Microsoft.AspNetCore.Builder; + +namespace Arc4u.Diagnostics; +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder UseArc4uLogging(this IApplicationBuilder app) + { + LoggerWrapperContext.Initialize(app.ApplicationServices); + return app; + } +} diff --git a/src/Arc4u.Diagnostics/Logging/HostBuilderExtensions.cs b/src/Arc4u.Diagnostics/Logging/HostBuilderExtensions.cs new file mode 100644 index 00000000..9d41de8a --- /dev/null +++ b/src/Arc4u.Diagnostics/Logging/HostBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Arc4u.Diagnostics.Logging; + +public static class HostBuilderExtensions +{ + public static IHostBuilder UseArc4uLogging( + this IHostBuilder builder, + Action? configure = null) + { + builder.ConfigureLogging((context, loggingBuilder) => + { + var services = loggingBuilder.Services; + configure?.Invoke(context, loggingBuilder); + }); + + return builder; + } +} diff --git a/src/Arc4u.Diagnostics/Logging/ILoggerExtensions.cs b/src/Arc4u.Diagnostics/Logging/ILoggerExtensions.cs new file mode 100644 index 00000000..7f384d47 --- /dev/null +++ b/src/Arc4u.Diagnostics/Logging/ILoggerExtensions.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Arc4u.Diagnostics.Logging; +public static class ILoggerExtensions +{ + public static LoggerWrapper Technical(this ILogger logger, [CallerMemberName] string methodName = "") => + new(logger, MessageCategory.Technical, typeof(T), methodName); + + public static LoggerWrapper Business(this ILogger logger, [CallerMemberName] string methodName = "") => + new(logger, MessageCategory.Business, typeof(T), methodName); + + public static LoggerWrapper Monitoring(this ILogger logger, [CallerMemberName] string methodName = "") => + new(logger, MessageCategory.Monitoring, typeof(T), methodName); + + public static LoggerWrapper Technical(this ILogger logger, [CallerMemberName] string methodName = "") => + new(logger, MessageCategory.Technical, typeof(T), methodName); + + public static LoggerWrapper Business(this ILogger logger, [CallerMemberName] string methodName = "") => + new(logger, MessageCategory.Business, typeof(T), methodName); + + public static LoggerWrapper Monitoring(this ILogger logger, [CallerMemberName] string methodName = "") => + new(logger, MessageCategory.Monitoring, typeof(T), methodName); +} diff --git a/src/Arc4u.Diagnostics/Logging/LoggerWrapper.cs b/src/Arc4u.Diagnostics/Logging/LoggerWrapper.cs new file mode 100644 index 00000000..8457af87 --- /dev/null +++ b/src/Arc4u.Diagnostics/Logging/LoggerWrapper.cs @@ -0,0 +1,244 @@ +using System.Reflection; +using System.Text; +using Arc4u.Diagnostics; +using Arc4u.Diagnostics.Logging; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection; + +public sealed class LoggerWrapper : ILogger +{ + private readonly ILogger _logger; + private readonly MessageCategory _category; + private readonly Dictionary _additionalFields = []; + private readonly Type? _contextType; + private readonly string _caller = string.Empty; + private bool _disposed; + private bool _includeStackTrace; + private readonly Lazy> _providers = new(Enumerable.Empty().ToList()); + + internal Dictionary AdditionalFields => _additionalFields; + internal bool IncludeStackTrace { get => _includeStackTrace; set => _includeStackTrace = value; } + + internal static int ProcessId + { + get + { + try + { + return Environment.ProcessId; + } + catch (PlatformNotSupportedException) + { + return -1; + } + } + } + + public LoggerWrapper(ILoggerFactory loggerFactory) => + _logger = loggerFactory.CreateLogger(); + + internal LoggerWrapper( + ILogger logger, + MessageCategory category, + Type? contextType = null, + string caller = "") + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _category = category; + _contextType = contextType; + _caller = caller; + + _providers = new Lazy>(() => + { + using var scope = LoggerWrapperContext.CreateScope(); + return scope.ServiceProvider.GetServices().ToList(); + }); + } + + private static class LoggerMessages + { + public static readonly EventId TechnicalEventId = new(1000, "Technical"); + public static readonly EventId BusinessEventId = new(2000, "Business"); + public static readonly EventId MonitoringEventId = new(3000, "Monitoring"); + + public static readonly Func> TechnicalLog = + level => Logging.LoggerMessage.Define( + level, + TechnicalEventId, + "{Category} [{Context}] [{@State}] {Message}"); + + public static readonly Func> BusinessLog = + level => Logging.LoggerMessage.Define( + level, + BusinessEventId, + "{Category} [{Context}] [{@State}] {Message}"); + + public static readonly Func> MonitoringLog = + level => Logging.LoggerMessage.Define( + level, + MonitoringEventId, + "{Category} [{Context}] [{@State}] {Message}"); + } + + private void Log(LogLevel level, EventId eventId, string? message, Exception? exception = null) + { + ThrowIfDisposed(); + + if (!IsEnabled(level)) + { + return; + } + + try + { + var properties = AddAdditionalProperties(); + if (IncludeStackTrace) + { + properties.AddIfNotExist(LoggingConstants.Stacktrace, exception?.StackTrace ?? Environment.StackTrace); + } + + if (exception is not null) + { + properties.AddIfNotExist(LoggingConstants.UnwrappedException, exception.ToFormattedstring()); + } + + properties.AddIfNotExist(LoggingConstants.SubEventId, eventId.Id); + properties.AddIfNotExist(LoggingConstants.MethodName, _caller); + properties.AddIfNotExist(LoggingConstants.Class, _contextType?.FullName ?? nameof(_contextType)); + properties.AddIfNotExist(LoggingConstants.Category, (short)_category); + properties.AddIfNotExist(LoggingConstants.Application, Assembly.GetEntryAssembly()?.GetName().Name ?? "Unknown App"); + properties.AddIfNotExist(LoggingConstants.ThreadId, Environment.CurrentManagedThreadId); + properties.AddIfNotExist(LoggingConstants.ProcessId, ProcessId); + + var enrichedState = new + { + Caller = _caller, + TimeStamp = DateTime.UtcNow, + AdditionalFields = properties, + Level = level + }; + + var logAction = _category switch + { + MessageCategory.Technical => LoggerMessages.TechnicalLog(level), + MessageCategory.Business => LoggerMessages.BusinessLog(level), + MessageCategory.Monitoring => LoggerMessages.MonitoringLog(level), + _ => LoggerMessages.TechnicalLog(level) + }; + + logAction( + _logger, + _category.ToString(), + _contextType?.Name ?? GetType().Name, + enrichedState, + message ?? string.Empty, + exception); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to log message with property providers and eventId"); + } + } + + internal void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + private Dictionary AddAdditionalProperties() + { + Dictionary properties; + + try + { + properties = new Dictionary(AdditionalFields); + var definedProperties = _providers.Value.Select(x => x.GetProperties()).SelectMany(x => x); + if (definedProperties != null) + { + foreach (var property in definedProperties) + { + if (property.Value != null) + { + this.AddIfNotExist(property.Key, property.Value); + } + } + + return properties; + } + + return new Dictionary(AdditionalFields); + } + catch (Exception ex) + { + Log(LogLevel.Error, default, "Error getting property providers. Logging without additional properties.", ex); + return new Dictionary(AdditionalFields); + } + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (state is IEnumerable> pairs) + { + foreach (var pair in pairs) + { + AdditionalFields.AddOrReplace(pair.Key, pair.Value); + } + } + + Log(logLevel, eventId, formatter(state, exception), exception); + } + + public bool IsEnabled(LogLevel logLevel) => _logger.IsEnabled(logLevel); + + public IDisposable? BeginScope(TState state) where TState : notnull => _logger.BeginScope(state); +} + +internal static class DumpException +{ + internal static string ToFormattedstring(this Exception exception) + { + var messages = exception + .GetAllExceptions() + .Where(e => !string.IsNullOrWhiteSpace(e.Message)) + .Select(e => e.GetType().FullName + " : " + e.Message.Trim()); + var sb = new StringBuilder(); + var i = 0; + foreach (var message in messages) + { + sb.Append("".PadLeft(i * 4)); + sb.Append("|---".PadLeft(i++ > 0 ? 4 : 0)); + sb.AppendLine(message); + } + + return sb.ToString(); + } + + private static IEnumerable GetAllExceptions(this Exception exception) + { + yield return exception; + + if (exception is AggregateException aggrEx) + { + foreach (var innerEx in aggrEx.InnerExceptions.SelectMany(e => e.GetAllExceptions())) + { + yield return innerEx; + } + } + else if (exception.InnerException != null) + { + foreach (var innerEx in exception.InnerException.GetAllExceptions()) + { + yield return innerEx; + } + } + } +} + diff --git a/src/Arc4u.Diagnostics/Logging/LoggerWrapperContext.cs b/src/Arc4u.Diagnostics/Logging/LoggerWrapperContext.cs new file mode 100644 index 00000000..a5bd38dd --- /dev/null +++ b/src/Arc4u.Diagnostics/Logging/LoggerWrapperContext.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Arc4u.Diagnostics.Logging; +internal class LoggerWrapperContext +{ + private static Lazy _scopeFactory = default!; + + /// + /// Initializes the specified service provider. + /// + /// The service provider. + public static void Initialize(IServiceProvider serviceProvider) + => _scopeFactory = new Lazy(serviceProvider.GetRequiredService()); + + /// + /// Creates the scope. + /// + /// + /// LoggerContext not initialized + public static IServiceScope CreateScope() + { + if (!_scopeFactory.IsValueCreated) + { + throw new InvalidOperationException("LoggerContext not initialized"); + } + + return _scopeFactory.Value.CreateScope(); + } +} diff --git a/src/Arc4u.Diagnostics/Logging/LoggerWrapperExtensions.cs b/src/Arc4u.Diagnostics/Logging/LoggerWrapperExtensions.cs new file mode 100644 index 00000000..a8cc029e --- /dev/null +++ b/src/Arc4u.Diagnostics/Logging/LoggerWrapperExtensions.cs @@ -0,0 +1,142 @@ +using Arc4u.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class LoggerWrapperExtensions +{ + /// + /// Adds the specified key. + /// + /// + /// The logger. + /// The key. + /// The value. + /// + public static LoggerWrapper Add(this LoggerWrapper logger, string key, object? value) + { + logger.ThrowIfDisposed(); + logger.AdditionalFields[ValidateKey(key)] = value; + return logger; + } + + /// + /// Adds the specified key when condition is true + /// + /// + /// The logger. + /// if set to true [condition]. + /// The key. + /// The value. + /// + public static LoggerWrapper AddIf(this LoggerWrapper logger, bool condition, string key, object? value) + { + logger.ThrowIfDisposed(); + if (condition) + { + logger.AdditionalFields[ValidateKey(key)] = value; + } + return logger; + } + + /// + /// Adds the specified key if doesn't not exist. + /// + /// + /// The logger. + /// The key. + /// The value. + /// + public static LoggerWrapper AddIfNotExist(this LoggerWrapper logger, string key, object? value) + { + logger.ThrowIfDisposed(); + var validKey = ValidateKey(key); + + if (value == null || logger.AdditionalFields.ContainsKey(validKey)) + { + return logger; + } + + logger.AdditionalFields[validKey] = value; + return logger; + } + + /// + /// Adds the specified key or replaces it. + /// + /// + /// The logger. + /// The key. + /// The value. + /// + public static LoggerWrapper AddOrReplace(this LoggerWrapper logger, string key, object? value) + { + logger.ThrowIfDisposed(); + logger.AdditionalFields[ValidateKey(key)] = value; + return logger; + } + + /// + /// Adds the specified key or replaces it when the condition is true. + /// + /// + /// The logger. + /// if set to true [condition]. + /// The key. + /// The value. + /// + public static LoggerWrapper AddOrReplaceIf(this LoggerWrapper logger, bool condition, string key, object? value) + { + logger.ThrowIfDisposed(); + + if (condition) + { + logger.AdditionalFields[ValidateKey(key)] = value; + } + return logger; + } + + /// + /// Adds the stack trace. + /// + /// + /// The logger. + /// + public static LoggerWrapper AddStackTrace(this LoggerWrapper logger) + { + logger.ThrowIfDisposed(); + logger.IncludeStackTrace = true; + return logger; + } + + /// + /// Validates the key. + /// + /// The key. + /// + /// key + /// + private static string ValidateKey(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + return key switch + { + LoggingConstants.ActivityId or + LoggingConstants.Application or + LoggingConstants.Category or + LoggingConstants.Class or + LoggingConstants.Identity or + LoggingConstants.MethodName or + LoggingConstants.ProcessId or + LoggingConstants.Stacktrace or + LoggingConstants.UnwrappedException or + LoggingConstants.SubEventId or + LoggingConstants.ThreadId => + throw new ReservedLoggingKeyException(key), + _ => key, + }; + } +} diff --git a/src/Arc4u.Diagnostics/LoggingConstants.cs b/src/Arc4u.Diagnostics/LoggingConstants.cs index 88bb4bed..266025af 100644 --- a/src/Arc4u.Diagnostics/LoggingConstants.cs +++ b/src/Arc4u.Diagnostics/LoggingConstants.cs @@ -19,4 +19,8 @@ public static class LoggingConstants public const string Stacktrace = "Stacktrace"; public const string Category = "Category"; + + public const string UnwrappedException = "UnwrappedException"; + + public const string SubEventId = "SubEventId"; } diff --git a/src/Arc4u.Diagnostics/MessageProperty.cs b/src/Arc4u.Diagnostics/MessageProperty.cs index 16de0694..0f5001ca 100644 --- a/src/Arc4u.Diagnostics/MessageProperty.cs +++ b/src/Arc4u.Diagnostics/MessageProperty.cs @@ -2,7 +2,7 @@ namespace Arc4u.Diagnostics; public static class MessagePropertyEx { - public static void AddIfNotExist(this IDictionary properties, string key, object value) + public static void AddIfNotExist(this IDictionary properties, string key, object? value) { if (value == null || properties.ContainsKey(key)) { @@ -12,8 +12,10 @@ public static void AddIfNotExist(this IDictionary properties, st properties[key] = value; } - public static void AddOrReplace(this IDictionary properties, string key, object value) + public static void AddOrReplace(this IDictionary properties, string key, object? value) { + ArgumentNullException.ThrowIfNull(value); + properties[key] = value; } } diff --git a/src/Arc4u.UnitTest/Logging/SerilogTests.cs b/src/Arc4u.UnitTest/Logging/SerilogTests.cs index 8e63cb85..2a2b4ced 100644 --- a/src/Arc4u.UnitTest/Logging/SerilogTests.cs +++ b/src/Arc4u.UnitTest/Logging/SerilogTests.cs @@ -1,6 +1,5 @@ using System.Globalization; using Arc4u.Diagnostics; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Core; @@ -8,6 +7,7 @@ using Xunit; using Arc4u.Dependency; using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; namespace Arc4u.UnitTest.Logging; diff --git a/src/Arc4u.sln b/src/Arc4u.sln index 60f3f9a5..cf9fe307 100644 --- a/src/Arc4u.sln +++ b/src/Arc4u.sln @@ -98,6 +98,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arc4u.AspNetCore.Results", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arc4u.Dependency.Tool", "Arc4u.Dependency.Tool\Arc4u.Dependency.Tool.csproj", "{2C83BE21-3C11-46B2-9831-5B84E4912BFB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B9419F5B-9CF3-4247-9929-7AF2362FD8E3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E8E0B4BA-5659-40E6-94C8-CA6D9F339101}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -956,6 +960,50 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CCA589E1-100F-47A6-A34B-FD46FDFDAA2A} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {511B1EB2-F853-4C5C-80A7-0825AECF1BBE} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {B270FC8C-6B28-40FA-BA78-B73F720DB98D} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {705ADAFC-9C5C-4DCB-93C5-B71D49371074} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {C1048831-D985-481B-BA50-2840315EFFEA} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {29DA3E34-4769-457F-B583-8BCFF5958E41} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {531058F7-1DD4-4E51-8727-A2D038B57274} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {1AD8095D-CEF1-486F-912B-CB9C6DCE0A20} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {4AC131B6-9FF2-4F66-A2DE-BE66C3CA366B} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {42B7DC08-23E2-4854-A5DB-71E9FB3BAF6D} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {51D25BBB-E6E5-46AF-81A1-B854F715F2C0} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {150D036C-4B66-4A99-BAA9-28AC0A81E2D7} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {F333A9E4-52F7-473D-AFCE-46A4FCFE844C} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {9B9ABE45-A069-4E97-BAEF-5C273CF4C8AB} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {0F2EEF43-174D-436D-A5B1-DA92D3EF9B25} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {C10B3D6C-E5E4-4365-B5E5-826A2C7A44AE} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {62867DEB-C095-4D34-8C56-3F0C3A0FE3B7} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {39E14B92-11E6-433E-8B5C-082485F16DA9} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {F0D96654-0415-4540-A224-44C2585F825E} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {86290E0E-3450-4CFD-80B3-8EED35E2A121} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {64CBEAB5-41B7-47CE-95E7-62CE3ECFE2FC} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {F0EF3965-11F5-4F8E-8D8C-8A1BC32DFFE0} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {E954455E-AB54-4144-B784-1E68B6548482} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {1959B659-8A69-4D68-A198-C627464C2143} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {5157EBFA-D53F-49A3-BDC6-B4887D4F6E2F} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {FCFE7961-7B48-42BF-BD72-67A1C0858BB1} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {FACC7AF6-8AAF-41B9-A0EF-0E6285EE1CA7} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {F854BCF4-D7DF-46D3-809A-0EF53586EC00} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {F018AC7E-2799-48E3-BAC0-79C94452085A} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {46D4C073-FD32-4C23-B22A-306E69122BF6} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {30D7D397-A29D-4148-AD66-EF26495A5BC0} = {E8E0B4BA-5659-40E6-94C8-CA6D9F339101} + {EA273269-7720-48DE-8866-FC82E2C9490F} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {6ED0D79F-96D4-418A-9C75-69E165FA5682} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {FB64F43E-F22E-4202-9F33-840CB7F44AA1} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {EB7B7062-7366-4262-8686-3DDAA19141B4} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {B16F1734-B286-4898-8B15-EFA0D17DA912} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {06393D15-0DA9-40FA-84A1-9A822B627B52} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {1910732C-CF4C-4411-B29F-F75185DD2F9B} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {2B61C53F-ECFC-471D-AB98-0D23FF2D071A} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {6FD0A0B7-A94C-4DBD-AF62-CE3B5359F088} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {AE2BA9A5-AFCD-4D98-87DC-71D6DCBC2C21} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + {2C83BE21-3C11-46B2-9831-5B84E4912BFB} = {B9419F5B-9CF3-4247-9929-7AF2362FD8E3} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {748E3661-3906-4840-B5DE-843E083BAE5E} EndGlobalSection