-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert dotted property names into nested values when Serilog v4's ex…
…perimental AppContext switch is set
- Loading branch information
1 parent
33bd153
commit 5df779d
Showing
6 changed files
with
332 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/PreserveDottedPropertyNames.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Copyright © Serilog Contributors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
using System.Collections.Generic; | ||
using Serilog.Events; | ||
|
||
namespace Serilog.Sinks.Seq.Conventions; | ||
|
||
/// <summary> | ||
/// Maintains verbatim processing of property names. A property named <c>"a.b"</c> will be transmitted to Seq as a | ||
/// scalar value with name <c>"a.b"</c>. | ||
/// </summary> | ||
class PreserveDottedPropertyNames: IDottedPropertyNameConvention | ||
{ | ||
/// <inheritdoc /> | ||
public IReadOnlyDictionary<string, LogEventPropertyValue> ProcessDottedPropertyNames(IReadOnlyDictionary<string, LogEventPropertyValue> maybeDotted) | ||
{ | ||
return maybeDotted; | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
src/Serilog.Sinks.Seq/Sinks/Seq/Conventions/UnflattenDottedPropertyNames.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
// Copyright © Serilog Contributors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using Serilog.Events; | ||
|
||
namespace Serilog.Sinks.Seq.Conventions; | ||
|
||
/// <summary> | ||
/// Experimental. Unflatten property names. A property with name <c>"a.b"</c> will be transmitted to Seq as | ||
/// a structure with name <c>"a"</c>, and one member <c>"b"</c>. | ||
/// </summary> | ||
/// <remarks>This behavior is enabled when the <c>Serilog.Parsing.MessageTemplateParser.AcceptDottedPropertyNames</c> | ||
/// <see cref="AppContext"/> switch is set to value <c langword="true"/>.</remarks> | ||
class UnflattenDottedPropertyNames: IDottedPropertyNameConvention | ||
{ | ||
const int MaxDepth = 10; | ||
|
||
/// <inheritdoc /> | ||
public IReadOnlyDictionary<string, LogEventPropertyValue> ProcessDottedPropertyNames(IReadOnlyDictionary<string, LogEventPropertyValue> maybeDotted) | ||
{ | ||
return DottedToNestedRecursive(maybeDotted, 0); | ||
} | ||
|
||
static IReadOnlyDictionary<string, LogEventPropertyValue> DottedToNestedRecursive(IReadOnlyDictionary<string, LogEventPropertyValue> maybeDotted, int depth) | ||
{ | ||
if (depth == MaxDepth) | ||
return maybeDotted; | ||
|
||
// Assume that the majority of entries will be bare or have unique prefixes. | ||
var result = new Dictionary<string, LogEventPropertyValue>(maybeDotted.Count); | ||
|
||
// Sorted for determinism. | ||
var dotted = new SortedDictionary<string, LogEventPropertyValue>(StringComparer.Ordinal); | ||
|
||
// First - give priority to bare names, since these would otherwise be claimed by the parents of further nested | ||
// layers and we'd have nowhere to put them when resolving conflicts. (Dotted entries that conflict can keep their dotted keys). | ||
|
||
foreach (var kv in maybeDotted) | ||
{ | ||
if (IsDottedIdentifier(kv.Key)) | ||
{ | ||
// Stash for processing in the next stage. | ||
dotted.Add(kv.Key, kv.Value); | ||
} | ||
else | ||
{ | ||
result.Add(kv.Key, kv.Value); | ||
} | ||
} | ||
|
||
// Then - for dotted keys with a prefix not already present in the result, convert to structured data and add to | ||
// the result. Any set of dotted names that collide with a preexisting key will be left as-is. | ||
|
||
string? prefix = null; | ||
Dictionary<string, LogEventPropertyValue>? nested = null; | ||
foreach (var kv in dotted) | ||
{ | ||
var (newPrefix, rem) = TakeFirstIdentifier(kv.Key); | ||
|
||
if (prefix != null && prefix != newPrefix) | ||
{ | ||
result.Add(prefix, MakeStructureValue(DottedToNestedRecursive(nested!, depth + 1))); | ||
prefix = null; | ||
nested = null; | ||
} | ||
|
||
if (nested != null && !nested.ContainsKey(rem)) | ||
{ | ||
prefix = newPrefix; | ||
nested.Add(rem, kv.Value); | ||
} | ||
else if (nested == null && !result.ContainsKey(newPrefix)) | ||
{ | ||
prefix = newPrefix; | ||
nested = new () { { rem, kv.Value } }; | ||
} | ||
else | ||
{ | ||
result.Add(kv.Key, kv.Value); | ||
} | ||
} | ||
|
||
if (prefix != null) | ||
{ | ||
result[prefix] = MakeStructureValue(DottedToNestedRecursive(nested!, depth + 1)); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
static LogEventPropertyValue MakeStructureValue(IReadOnlyDictionary<string,LogEventPropertyValue> properties) | ||
{ | ||
return new StructureValue(properties.Select(kv => new LogEventProperty(kv.Key, kv.Value)), typeTag: null); | ||
} | ||
|
||
internal static bool IsDottedIdentifier(string key) => | ||
key.Contains('.') && | ||
!key.StartsWith(".", StringComparison.Ordinal) && | ||
!key.EndsWith(".", StringComparison.Ordinal) && | ||
key.Split('.').All(IsIdentifier); | ||
|
||
static bool IsIdentifier(string s) => s.Length != 0 && | ||
!char.IsDigit(s[0]) && | ||
s.All(ch => char.IsLetter(ch) || char.IsDigit(ch) || ch == '_'); | ||
|
||
static (string, string) TakeFirstIdentifier(string dottedIdentifier) | ||
{ | ||
// We can do this simplistically because keys in `dotted` conform to `IsDottedName`. | ||
Debug.Assert(IsDottedIdentifier(dottedIdentifier)); | ||
|
||
var firstDot = dottedIdentifier.IndexOf('.'); | ||
var prefix = dottedIdentifier.Substring(0, firstDot); | ||
var rem = dottedIdentifier.Substring(firstDot + 1); | ||
return (prefix, rem); | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
src/Serilog.Sinks.Seq/Sinks/Seq/IDottedPropertyNameConvention.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Copyright © Serilog Contributors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
using System.Collections.Generic; | ||
using Serilog.Events; | ||
|
||
namespace Serilog.Sinks.Seq; | ||
|
||
/// <summary> | ||
/// Enables switching between the experimental "unflattening" behavior applied to dotted property names, and the | ||
/// regular verbatim property name handling. | ||
/// </summary> | ||
interface IDottedPropertyNameConvention | ||
{ | ||
/// <summary> | ||
/// Convert the properties in <paramref name="maybeDotted"/> into the form specified by the current property | ||
/// name processing convention. | ||
/// </summary> | ||
/// <param name="maybeDotted">The properties associated with a log event.</param> | ||
/// <returns>The processed properties.</returns> | ||
IReadOnlyDictionary<string, LogEventPropertyValue> ProcessDottedPropertyNames(IReadOnlyDictionary<string, LogEventPropertyValue> maybeDotted); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
test/Serilog.Sinks.Seq.Tests/Conventions/UnflattenDottedPropertyNamesTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Serilog.Events; | ||
using Serilog.Sinks.Seq.Conventions; | ||
using Xunit; | ||
|
||
namespace Serilog.Sinks.Seq.Tests.Conventions; | ||
|
||
public class UnflattenDottedPropertyNamesTests | ||
{ | ||
[Fact] | ||
public void DottedToNestedWorks() | ||
{ | ||
var someDotted = new Dictionary<string, LogEventPropertyValue> | ||
{ | ||
["dotnet.ilogger.category"] = new ScalarValue("Test.App"), | ||
["environment.name"] = new ScalarValue("Production"), | ||
["environment.region"] = new ScalarValue("us-west-2"), | ||
["environment.beverage.name"] = new ScalarValue("coffee"), | ||
["environment.domains"] = new StructureValue( | ||
[ | ||
new LogEventProperty("example.com", new ScalarValue(42)), | ||
new LogEventProperty("datalust.co", new ScalarValue(43)) | ||
]), | ||
["scope"] = new StructureValue([]), | ||
["scope.name"] = new ScalarValue("Gerald"), | ||
["vegetable"] = new ScalarValue("Potato"), | ||
["Scope"] = new ScalarValue("Periscope"), | ||
[".gitattributes"] = new ScalarValue("Text") | ||
}; | ||
|
||
var expected = new Dictionary<string, LogEventPropertyValue> | ||
{ | ||
["dotnet"] = new StructureValue( | ||
[ | ||
new LogEventProperty("ilogger", new StructureValue( | ||
[ | ||
new LogEventProperty("category", new ScalarValue("Test App")) | ||
])) | ||
]), | ||
["environment"] = new StructureValue( | ||
[ | ||
new LogEventProperty("name", new ScalarValue("Production")), | ||
new LogEventProperty("region", new ScalarValue("us-west-2")), | ||
new LogEventProperty("beverage", new StructureValue( | ||
[ | ||
new LogEventProperty("name", new ScalarValue("coffee")) | ||
])), | ||
new LogEventProperty("domains", new StructureValue( | ||
[ | ||
new LogEventProperty("example.com", new ScalarValue(42)), | ||
new LogEventProperty("datalust.co", new ScalarValue(43)) | ||
])) | ||
]), | ||
["scope"] = new StructureValue([]), | ||
["scope.name"] = new ScalarValue("Gerald"), | ||
["vegetable"] = new ScalarValue("Potato"), | ||
["Scope"] = new ScalarValue("Periscope"), | ||
[".gitattributes"] = new ScalarValue("Text") | ||
}; | ||
|
||
var actual = new UnflattenDottedPropertyNames().ProcessDottedPropertyNames(someDotted); | ||
|
||
Assert.Equal(expected.Count, actual.Count); | ||
foreach (var expectedProperty in expected) | ||
{ | ||
Assert.True(actual.TryGetValue(expectedProperty.Key, out var actualProperty)); | ||
AssertEquivalentValue(expectedProperty.Value, expectedProperty.Value); | ||
} | ||
} | ||
|
||
[Theory] | ||
[InlineData("", false)] | ||
[InlineData(".", false)] | ||
[InlineData("..", false)] | ||
[InlineData(".a", false)] | ||
[InlineData("a.", false)] | ||
[InlineData("a..b", false)] | ||
[InlineData("a.b..c", false)] | ||
[InlineData("a.b.", false)] | ||
[InlineData("a. .b", false)] | ||
[InlineData("1.0", false)] | ||
[InlineData("1", false)] | ||
[InlineData("a", false)] | ||
[InlineData("abc", false)] | ||
[InlineData("a.b", true)] | ||
[InlineData("a1.bc._._xd.e_", true)] | ||
public void OnlyProcessesValidDottedNames(string key, bool isValid) | ||
{ | ||
var actual = UnflattenDottedPropertyNames.IsDottedIdentifier(key); | ||
Assert.Equal(isValid, actual); | ||
} | ||
|
||
static void AssertEquivalentValue(LogEventPropertyValue expected, LogEventPropertyValue actual) | ||
{ | ||
switch (expected, actual) | ||
{ | ||
case (ScalarValue expectedScalar, ScalarValue actualScalar): | ||
{ | ||
Assert.Equal(expectedScalar.Value, actualScalar.Value); | ||
break; | ||
} | ||
case (StructureValue expectedStructure, StructureValue actualStructure): | ||
{ | ||
Assert.Equal(expectedStructure.TypeTag, actualStructure.TypeTag); | ||
Assert.Equal(expectedStructure.Properties.Count, actualStructure.Properties.Count); | ||
var actualProperties = actualStructure.Properties.ToDictionary(p => p.Name, p => p.Value); | ||
foreach (var expectedProperty in expectedStructure.Properties) | ||
{ | ||
var actualValue = Assert.Contains(expectedProperty.Name, actualProperties); | ||
AssertEquivalentValue(expectedProperty.Value, actualValue); | ||
} | ||
break; | ||
} | ||
default: | ||
{ | ||
Assert.Equal(expected, actual); | ||
break; | ||
} | ||
} | ||
} | ||
} |