Allow for configuration of log file names (#19074)

* Added configuration for the log file name and format.

* Added unit test for LoggingConfiguration.

* Rely on configuration validation to verify supported log file format arguments.

* Fixed unit test failing on build pipeline.
This commit is contained in:
Andy Butland
2025-04-23 12:28:51 +02:00
committed by GitHub
parent 1bddcb8d39
commit 7c98af558d
10 changed files with 236 additions and 6 deletions

View File

@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Core.Configuration.Models;
@@ -13,6 +14,8 @@ public class LoggingSettings
{
internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24);
internal const string StaticDirectory = Constants.SystemDirectories.LogFiles;
internal const string StaticFileNameFormat = LoggingConfiguration.DefaultLogFileNameFormat;
internal const string StaticFileNameFormatArguments = "MachineName";
/// <summary>
/// Gets or sets a value for the maximum age of a log file.
@@ -31,4 +34,25 @@ public class LoggingSettings
/// </value>
[DefaultValue(StaticDirectory)]
public string Directory { get; set; } = StaticDirectory;
/// <summary>
/// Gets or sets the file name format to use for log files.
/// </summary>
/// <value>
/// The file name format.
/// </value>
[DefaultValue(StaticFileNameFormat)]
public string FileNameFormat { get; set; } = StaticFileNameFormat;
/// <summary>
/// Gets or sets the file name format arguments to use for log files.
/// </summary>
/// <value>
/// The file name format arguments as a comma delimited string of accepted values.
/// </value>
/// <remarks>
/// Accepted values for format arguments are: MachineName, EnvironmentName.
/// </remarks>
[DefaultValue(StaticFileNameFormatArguments)]
public string FileNameFormatArguments { get; set; } = StaticFileNameFormatArguments;
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Core.Configuration.Models.Validation;
/// <summary>
/// Validator for configuration representated as <see cref="LoggingSettings" />.
/// </summary>
public class LoggingSettingsValidator : ConfigurationValidatorBase, IValidateOptions<LoggingSettings>
{
/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, LoggingSettings options)
{
if (!ValidateFileNameFormatArgument(options.FileNameFormat, options.FileNameFormatArguments, out var message))
{
return ValidateOptionsResult.Fail(message);
}
return ValidateOptionsResult.Success;
}
private bool ValidateFileNameFormatArgument(string fileNameFormat, string fileNameFormatArguments, out string message)
{
var fileNameFormatArgumentsAsArray = fileNameFormatArguments
.Split([','], StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToArray();
if (fileNameFormatArgumentsAsArray.Any(x => LoggingConfiguration.SupportedFileNameFormatArguments.Contains(x) is false))
{
message = $"The file name arguments '{string.Join(",", fileNameFormatArgumentsAsArray)}' contain one or more values that aren't in the supported list of values '{string.Join(",", LoggingConfiguration.SupportedFileNameFormatArguments)}'.";
return false;
}
try
{
_ = string.Format(fileNameFormat, fileNameFormatArgumentsAsArray);
}
catch (FormatException)
{
message = $"The provided file name format '{fileNameFormat}' could not be used with the provided arguments '{string.Join(",", fileNameFormatArgumentsAsArray)}'.";
return false;
}
message = string.Empty;
return true;
}
}

View File

@@ -43,6 +43,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IValidateOptions<ContentSettings>, ContentSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<GlobalSettings>, GlobalSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<HealthChecksSettings>, HealthChecksSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<LoggingSettings>, LoggingSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<RequestHandlerSettings>, RequestHandlerSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<UnattendedSettings>, UnattendedSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<SecuritySettings>, SecuritySettingsValidator>();

View File

@@ -3,7 +3,17 @@ namespace Umbraco.Cms.Core.Logging;
public interface ILoggingConfiguration
{
/// <summary>
/// Gets the physical path where logs are stored
/// Gets the physical path where logs are stored.
/// </summary>
string LogDirectory { get; }
/// <summary>
/// Gets the file name format for the log files.
/// </summary>
string LogFileNameFormat { get; }
/// <summary>
/// Gets the file name format arguments for the log files.
/// </summary>
string[] GetLogFileNameFormatArguments();
}

View File

@@ -1,9 +1,73 @@
namespace Umbraco.Cms.Core.Logging;
/// <summary>
/// Implements <see cref="ILoggingConfiguration"/> to provide configuration for logging to files.
/// </summary>
public class LoggingConfiguration : ILoggingConfiguration
{
public LoggingConfiguration(string logDirectory) =>
LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory));
/// <summary>
/// The default log file name format.
/// </summary>
public const string DefaultLogFileNameFormat = "UmbracoTraceLog.{0}..json";
/// <summary>
/// The default log file name format arguments.
/// </summary>
public const string DefaultLogFileNameFormatArguments = MachineNameFileFormatArgument;
/// <summary>
/// The collection of supported file name format arguments.
/// </summary>
public static readonly string[] SupportedFileNameFormatArguments =
{
MachineNameFileFormatArgument,
EnvironmentNameFileFormatArgument,
};
private readonly string _logFileNameFormatArguments;
private const string MachineNameFileFormatArgument = "MachineName";
private const string EnvironmentNameFileFormatArgument = "EnvironmentName";
/// <summary>
/// Initializes a new instance of the <see cref="LoggingConfiguration"/> class with the default log file name format and arguments.
/// </summary>
/// <param name="logDirectory">The log file directory.</param>
public LoggingConfiguration(string logDirectory)
: this(logDirectory, DefaultLogFileNameFormat, DefaultLogFileNameFormatArguments)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LoggingConfiguration"/> class.
/// </summary>
/// <param name="logDirectory">The log file directory.</param>
/// <param name="logFileNameFormat">The log file name format.</param>
/// <param name="logFileNameFormatArguments">The log file name format arguments as a comma delimited string.</param>
public LoggingConfiguration(string logDirectory, string logFileNameFormat, string logFileNameFormatArguments)
{
LogDirectory = logDirectory;
LogFileNameFormat = logFileNameFormat;
_logFileNameFormatArguments = logFileNameFormatArguments;
}
/// <inheritdoc/>
public string LogDirectory { get; }
/// <inheritdoc/>
public string LogFileNameFormat { get; }
/// <inheritdoc/>
public string[] GetLogFileNameFormatArguments() => _logFileNameFormatArguments.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Select(GetValue)
.ToArray();
private static string GetValue(string arg) =>
arg switch
{
MachineNameFileFormatArgument => Environment.MachineName,
EnvironmentNameFileFormatArgument => Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
_ => string.Empty,
};
}

View File

@@ -109,7 +109,7 @@ namespace Umbraco.Extensions
.Enrich.FromLogContext(); // allows us to dynamically enrich
logConfig.WriteTo.UmbracoFile(
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory),
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory, loggingConfiguration.LogFileNameFormat, loggingConfiguration.GetLogFileNameFormatArguments()),
fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes,
restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel,
rollingInterval: umbracoFileConfiguration.RollingInterval,

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Infrastructure.Logging.Serilog;
@@ -43,5 +44,8 @@ public class UmbracoFileConfiguration
public int RetainedFileCountLimit { get; set; } = 31;
public string GetPath(string logDirectory) =>
Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json");
GetPath(logDirectory, LoggingConfiguration.DefaultLogFileNameFormat, Environment.MachineName);
public string GetPath(string logDirectory, string fileNameFormat, params string[] fileNameArgs) =>
Path.Combine(logDirectory, string.Format(fileNameFormat, fileNameArgs));
}

View File

@@ -42,7 +42,7 @@ public static class ServiceCollectionExtensions
LoggingSettings loggerSettings = GetLoggerSettings(configuration);
var loggingDir = loggerSettings.GetAbsoluteLoggingPath(hostEnvironment);
ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir);
ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir, loggerSettings.FileNameFormat, loggerSettings.FileNameFormatArguments);
var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration);

View File

@@ -0,0 +1,51 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Configuration.Models.Validation;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validation
{
[TestFixture]
public class LoggingSettingsValidatorTests
{
[Test]
public void Returns_Success_ForValid_Configuration()
{
var validator = new LoggingSettingsValidator();
LoggingSettings options = BuildLoggingSettings();
ValidateOptionsResult result = validator.Validate("settings", options);
Assert.True(result.Succeeded);
}
[Test]
public void Returns_Fail_For_Configuration_With_Invalid_FileNameFormatArguments()
{
var validator = new LoggingSettingsValidator();
LoggingSettings options = BuildLoggingSettings(fileNameFormatArguments: "MachineName,Invalid");
ValidateOptionsResult result = validator.Validate("settings", options);
Assert.False(result.Succeeded);
}
[Test]
public void Returns_Fail_For_Configuration_With_Invalid_FileNameFormat()
{
var validator = new LoggingSettingsValidator();
LoggingSettings options = BuildLoggingSettings(fileNameFormat: "InvalidAsTooManyPlaceholders_{0}_{1}");
ValidateOptionsResult result = validator.Validate("settings", options);
Assert.False(result.Succeeded);
}
private static LoggingSettings BuildLoggingSettings(
string fileNameFormat = LoggingConfiguration.DefaultLogFileNameFormat,
string fileNameFormatArguments = LoggingConfiguration.DefaultLogFileNameFormatArguments) =>
new LoggingSettings
{
FileNameFormat = fileNameFormat,
FileNameFormatArguments = fileNameFormatArguments,
};
}
}

View File

@@ -0,0 +1,25 @@
using NUnit.Framework;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Logging;
[TestFixture]
public class LoggingConfigurationTests
{
[SetUp]
public void SetUp() => Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Production");
[Test]
public void Can_Get_Supported_Log_File_Name_Format_Arguments()
{
var config = new LoggingConfiguration("c:\\logs\\", "UmbracoLogFile_{0}_{1}..json", "MachineName,EnvironmentName");
var result = config.GetLogFileNameFormatArguments();
Assert.AreEqual(2, result.Length);
var expectedMachineName = Environment.MachineName;
var expectedEnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
Assert.AreEqual(expectedMachineName, result[0]);
Assert.AreEqual(expectedEnvironmentName, result[1]);
}
}