Skip to content

Commit

Permalink
Merge pull request #73 from ejball/omit-default-fields
Browse files Browse the repository at this point in the history
Allow omitting default fields.
  • Loading branch information
mattwcole authored Feb 4, 2023
2 parents 2c58eb6 + 1b5af2c commit c5d197d
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 54 deletions.
36 changes: 36 additions & 0 deletions src/Gelf.Extensions.Logging/GelfLogContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using Microsoft.Extensions.Logging;

namespace Gelf.Extensions.Logging
{
public class GelfLogContext
{
/// <summary>
/// The logger name.
/// </summary>
public string LoggerName { get; }

/// <summary>
/// The logging level.
/// </summary>
public LogLevel LogLevel { get; }

/// <summary>
/// The event ID.
/// </summary>
public EventId EventId { get; }

/// <summary>
/// The exception, if any.
/// </summary>
public Exception? Exception { get; }

internal GelfLogContext(string loggerName, LogLevel logLevel, EventId eventId, Exception? exception)
{
LoggerName = loggerName;
LogLevel = logLevel;
EventId = eventId;
Exception = exception;
}
}
}
85 changes: 48 additions & 37 deletions src/Gelf.Extensions.Logging/GelfLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,7 @@ namespace Gelf.Extensions.Logging
public class GelfLogger : ILogger
{
private static readonly Regex AdditionalFieldKeyRegex = new(@"^[\w\.\-]*$", RegexOptions.Compiled);
private static readonly HashSet<string> ReservedAdditionalFieldKeys = new()
{
"id",
"logger",
"exception",
"event_id",
"event_name",
"message_template"
};
private static readonly HashSet<string> ReservedAdditionalFieldKeys = new() { "id" };

private readonly string _name;
private readonly GelfMessageProcessor _messageProcessor;
Expand All @@ -45,19 +37,11 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
{
ShortMessage = formatter(state, exception),
Host = Options.LogSource,
Logger = _name,
Exception = exception?.ToString(),
Level = GetLevel(logLevel),
Timestamp = GetTimestamp(),
AdditionalFields = GetAdditionalFields(logLevel, eventId, state, exception).ToArray()
};

if (eventId != default)
{
message.EventId = eventId.Id;
message.EventName = eventId.Name;
}

_messageProcessor.SendMessage(message);
}

Expand Down Expand Up @@ -92,35 +76,30 @@ private static double GetTimestamp()
private IEnumerable<KeyValuePair<string, object>> GetAdditionalFields<TState>(
LogLevel logLevel, EventId eventId, TState state, Exception? exception)
{
var logContext = new GelfLogContext(_name, logLevel, eventId, exception);

var additionalFields = Options.AdditionalFields
.Concat(GetFactoryAdditionalFields(logLevel, eventId, exception))
.Concat(GetFactoryAdditionalFields(logContext))
.Concat(GetScopeAdditionalFields())
.Concat(GetStateAdditionalFields(state));
.Concat(GetStateAdditionalFields(state))
.Concat(GetDefaultAdditionalFields(logContext));

foreach (var field in additionalFields)
{
if (field.Key != "{OriginalFormat}")
if (AdditionalFieldKeyRegex.IsMatch(field.Key) && !ReservedAdditionalFieldKeys.Contains(field.Key))
{
if (AdditionalFieldKeyRegex.IsMatch(field.Key) && !ReservedAdditionalFieldKeys.Contains(field.Key))
{
yield return field;
}
else
{
Debug.Fail($"GELF message has additional field with invalid key \"{field.Key}\".");
}
yield return field;
}
else if (Options.IncludeMessageTemplates)
else
{
yield return new KeyValuePair<string, object>("message_template", field.Value);
Debug.Fail($"GELF message has additional field with invalid key \"{field.Key}\".");
}
}
}

private IEnumerable<KeyValuePair<string, object>> GetFactoryAdditionalFields(
LogLevel logLevel, EventId eventId, Exception? exception)
private IEnumerable<KeyValuePair<string, object>> GetFactoryAdditionalFields(GelfLogContext logContext)
{
return Options.AdditionalFieldsFactory?.Invoke(logLevel, eventId, exception) ??
return Options.AdditionalFieldsFactory?.Invoke(logContext) ??
Enumerable.Empty<KeyValuePair<string, object>>();
}

Expand Down Expand Up @@ -185,11 +164,43 @@ private IEnumerable<KeyValuePair<string, object>> GetScopeAdditionalFields()
return additionalFields;
}

private static IEnumerable<KeyValuePair<string, object>> GetStateAdditionalFields<TState>(TState state)
private IEnumerable<KeyValuePair<string, object>> GetStateAdditionalFields<TState>(TState state)
{
return state is IEnumerable<KeyValuePair<string, object>> additionalFields
? additionalFields
: Enumerable.Empty<KeyValuePair<string, object>>();
var additionalFields = state as IEnumerable<KeyValuePair<string, object>>
?? Enumerable.Empty<KeyValuePair<string, object>>();

foreach (var field in additionalFields)
{
if (field.Key != "{OriginalFormat}")
{
yield return field;
}
else if (Options.IncludeMessageTemplates)
{
yield return new KeyValuePair<string, object>("message_template", field.Value);
}
}
}

private IEnumerable<KeyValuePair<string, object>> GetDefaultAdditionalFields(GelfLogContext logContext)
{
if (!Options.IncludeDefaultFields)
{
yield break;
}

yield return new KeyValuePair<string, object>("logger", logContext.LoggerName);

if (logContext.Exception != null)
{
yield return new KeyValuePair<string, object>("exception", logContext.Exception);
}

if (logContext.EventId != default)
{
yield return new KeyValuePair<string, object>("event_id", logContext.EventId.Id);
yield return new KeyValuePair<string, object>("event_name", logContext.EventId.Name);
}
}
}
}
7 changes: 6 additions & 1 deletion src/Gelf.Extensions.Logging/GelfLoggerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class GelfLoggerOptions
/// <summary>
/// Additional fields computed based on raw log data.
/// </summary>
public Func<LogLevel, EventId, Exception?, Dictionary<string, object>>? AdditionalFieldsFactory { get; set; }
public Func<GelfLogContext, Dictionary<string, object>>? AdditionalFieldsFactory { get; set; }

/// <summary>
/// Headers used when sending logs via HTTP(S).
Expand All @@ -70,5 +70,10 @@ public class GelfLoggerOptions
/// Include a field with the original message template before structured log parameters are replaced.
/// </summary>
public bool IncludeMessageTemplates { get; set; }

/// <summary>
/// Include default fields (logger, exception, event_id, event_name).
/// </summary>
public bool IncludeDefaultFields { get; set; } = true;
}
}
8 changes: 0 additions & 8 deletions src/Gelf.Extensions.Logging/GelfMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ public class GelfMessage

public SyslogSeverity Level { get; set; }

public string? Logger { get; set; }

public string? Exception { get; set; }

public int? EventId { get; set; }

public string? EventName { get; set; }

public IReadOnlyCollection<KeyValuePair<string, object>> AdditionalFields { get; set; } =
Array.Empty<KeyValuePair<string, object>>();
}
Expand Down
4 changes: 0 additions & 4 deletions src/Gelf.Extensions.Logging/GelfMessageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ public static string ToJson(this GelfMessage message)
jsonWriter.WriteStringUnlessNull("short_message", message.ShortMessage);
jsonWriter.WriteNumber("timestamp", message.Timestamp);
jsonWriter.WriteNumber("level", (int) message.Level);
jsonWriter.WriteStringUnlessNull("_logger", message.Logger);
jsonWriter.WriteStringUnlessNull("_exception", message.Exception);
jsonWriter.WriteNumberUnlessNull("_event_id", message.EventId);
jsonWriter.WriteStringUnlessNull("_event_name", message.EventName);

foreach (var field in message.AdditionalFields)
{
Expand Down
102 changes: 98 additions & 4 deletions test/Gelf.Extensions.Logging.Tests/GelfLoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,86 @@ public async Task Includes_event_IDs_on_messages()
Assert.Equal("foo", message.event_name);
}

[Fact]
public async Task Omits_default_fields_via_option()
{
var options = LoggerFixture.LoggerOptions;
options.IncludeDefaultFields = false;
var messageText = Faker.Lorem.Sentence();
var exception = new Exception("Something went wrong!");

using var loggerFactory = LoggerFixture.CreateLoggerFactory(options);
var sut = loggerFactory.CreateLogger(nameof(GelfLoggerTests));
sut.LogError(new EventId(197, "foo"), exception, messageText);

var message = await GraylogFixture.WaitForMessageAsync();

Assert.Equal(messageText, message.message);
Assert.Throws<RuntimeBinderException>(() => message.logger);
Assert.Throws<RuntimeBinderException>(() => message.exception);
Assert.Throws<RuntimeBinderException>(() => message.event_id);
Assert.Throws<RuntimeBinderException>(() => message.event_name);
}

[Fact]
public async Task Renames_optional_fields_via_option()
{
var options = LoggerFixture.LoggerOptions;
options.IncludeDefaultFields = false;
options.AdditionalFieldsFactory = logContext => new Dictionary<string, object>
{
["Logger"] = logContext.LoggerName,
["Exception"] = logContext.Exception?.ToString(),
["EventId"] = logContext.EventId.Id,
["EventName"] = logContext.EventId.Name,
};
var messageText = Faker.Lorem.Sentence();
var exception = new Exception("Something went wrong!");

using var loggerFactory = LoggerFixture.CreateLoggerFactory(options);
var sut = loggerFactory.CreateLogger(nameof(GelfLoggerTests));
sut.LogError(new EventId(197, "foo"), exception, messageText);

var message = await GraylogFixture.WaitForMessageAsync();

Assert.Equal(messageText, message.message);
Assert.Throws<RuntimeBinderException>(() => message.logger);
Assert.Throws<RuntimeBinderException>(() => message.exception);
Assert.Throws<RuntimeBinderException>(() => message.event_id);
Assert.Throws<RuntimeBinderException>(() => message.event_name);
Assert.Equal(nameof(GelfLoggerTests), message.Logger);
Assert.Equal(exception.ToString(), message.Exception);
Assert.Equal(197, message.EventId);
Assert.Equal("foo", message.EventName);
}

[Fact]
public async Task Allows_default_fields_when_omitted()
{
var options = LoggerFixture.LoggerOptions;
options.IncludeDefaultFields = false;
options.AdditionalFields.Add("logger", "n/a");
options.AdditionalFields.Add("exception", "n/a");
options.AdditionalFields.Add("event_id", 0);
options.AdditionalFields.Add("event_name", "n/a");
options.AdditionalFields.Add("message_template", "n/a");
var messageText = Faker.Lorem.Sentence();
var exception = new Exception("Something went wrong!");

using var loggerFactory = LoggerFixture.CreateLoggerFactory(options);
var sut = loggerFactory.CreateLogger(nameof(GelfLoggerTests));
sut.LogError(new EventId(197, "foo"), exception, messageText);

var message = await GraylogFixture.WaitForMessageAsync();

Assert.Equal(messageText, message.message);
Assert.Equal("n/a", message.logger);
Assert.Equal("n/a", message.exception);
Assert.Equal(0, message.event_id);
Assert.Equal("n/a", message.event_name);
Assert.Equal("n/a", message.message_template);
}

[Fact]
public async Task Sends_message_with_additional_fields_from_options()
{
Expand Down Expand Up @@ -196,6 +276,20 @@ public async Task Uses_structured_log_fields_when_keys_duplicated_in_scope()
Assert.Equal("structured", message.foo);
}

[Fact]
public async Task Uses_default_log_fields_when_keys_duplicated()
{
var sut = LoggerFixture.CreateLogger<GelfLoggerTests>();
using (sut.BeginScope(("logger", "scope")))
{
sut.LogDebug("Structured log line with {logger}", "structured");
}

var message = await GraylogFixture.WaitForMessageAsync();

Assert.Equal(typeof(GelfLoggerTests).FullName, message.logger);
}

[Fact]
public async Task Ignores_null_values_in_additional_fields()
{
Expand Down Expand Up @@ -244,12 +338,12 @@ public async Task Sends_message_templates_when_enabled()
public async Task Sends_message_with_additional_fields_from_factory()
{
var options = LoggerFixture.LoggerOptions;
options.AdditionalFieldsFactory = (originalLogLevel, originalEvent, originalException) =>
options.AdditionalFieldsFactory = logContext =>
new Dictionary<string, object>
{
["log_level"] = originalLogLevel.ToString(),
["exception_type"] = originalException?.GetType().ToString(),
["custom_event_name"] = originalEvent.Name
["log_level"] = logContext.LogLevel.ToString(),
["exception_type"] = logContext.Exception?.GetType().ToString(),
["custom_event_name"] = logContext.EventId.Name
};

using var loggerFactory = LoggerFixture.CreateLoggerFactory(options);
Expand Down

0 comments on commit c5d197d

Please sign in to comment.