Skip to content

Commit

Permalink
Replace regex & joins with StringCleaner & StringBuilding
Browse files Browse the repository at this point in the history
  • Loading branch information
Nickztar committed Jun 26, 2024
1 parent 2d8734c commit 4b6417f
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 110 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Serilog.Sinks.LogtailSys\Serilog.Sinks.LogtailSys.csproj" />
</ItemGroup>

</Project>
139 changes: 139 additions & 0 deletions Serilog.Sinks.LogtailSys.Tests/StringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System.Text.RegularExpressions;
using FluentAssertions;
using Serilog.Sinks.Logtail;

namespace Serilog.Sinks.LogtailSys.Tests;

public partial class StringTests
{
[Theory]
[InlineData("hello world", '"', "hello world")]
[InlineData("\"hello world", '"', "hello world")]
[InlineData("hello world\"", '"', "hello world")]
[InlineData("\"hello world\"", '"', "hello world")]
[InlineData("\"hell\"o\" world\"", '"', "hell\"o\" world")]
[InlineData("\"hell\\\"o\\\" world\"", '"', "hell\"o\" world")]
[InlineData("", '"', "")]
[InlineData("\"", '"', "")]
[InlineData("\"\"", '"', "")]
public void CanTrimString(string source, char trimChar, string expected)
{
var cleaned = new StringCleaner(source)
.WithTrimed(trimChar)
.WithUnescapeQuotes()
.Build();
cleaned.Should().Be(expected);
var previousCleaned = source.TrimAndUnescapeQuotes();
cleaned.Should().Be(previousCleaned, because: "Should match previous");
}

[Theory]
[InlineData("Hello world", "Hello world", '"', '\\', ']')]
[InlineData("[]Hello world", @"[\]Hello world", '"', '\\', ']')]
[InlineData("[]H\"e\"llo world", "[\\]H\\\"e\\\"llo world", '"', '\\', ']')]
public void CanEscapeString(string source, string expected, params char[] charsToReplace)
{
var cleaned = new StringCleaner(source)
.WithEscapedChars(charsToReplace)
.Build();
cleaned.Should().Be(expected);
var previousCleaned = PropertyValueRx().Replace(source, match => $@"\{match}");
cleaned.Should().Be(previousCleaned, because: "Should match previous");
}

[Fact]
public void CanGetMaxLength()
{
for (var i = 0; i < 100; i++)
{
var str = new string('*', i) + " ";
var strCopy = new string('*', i) + " ";
var cleaned = new StringCleaner(str)
.WithMaxLength(32)
.Build();
var previous = strCopy.WithMaxLength(32);

var expectedLen = str.Length switch
{
<= 32 => str.Length,
_ => 32
};
cleaned.Should().Be(previous, because: "Should match previous");
cleaned.Should().HaveLength(expectedLen);
}
}

[Theory]
[InlineData("", "")]
[InlineData("Hej!", "Hej!")]
[InlineData("Hej!^", "Hej!^")]
[InlineData("Hej!^~", "Hej!^~")]
[InlineData("Hej!^~-", "Hej!^~-")]
[InlineData("Hej!^@", "Hej!^@")]
[InlineData("Hej 💦", "Hej")]
[InlineData("Hej ", "Hej")]
public void AsPrintableAscii(string source, string expected)
{
var cleaned = new StringCleaner(source)
.WithAsciiPrintable()
.Build();
var previousClean = source.AsPrintableAscii();

cleaned.Should().Be(expected);
cleaned.Should().Be(previousClean, because: "Should match previous");
}

[GeneratedRegex("[\\]\\\\\"]")]
private static partial Regex PropertyValueRx();
}

public static partial class StringExtensions
{
/// <summary>
/// Truncates a string so that it is no longer than the specified number of characters.
/// If the truncated string ends with a space, it will be removed
/// </summary>
/// <param name="source">String to be truncated</param>
/// <param name="maxLength">Maximum string length before truncation will occur</param>
/// <returns>Original string, or a truncated to the specified length if too long</returns>
public static string WithMaxLength(this string source, int maxLength)
{
if (string.IsNullOrEmpty(source))
return source;
return source.Length > maxLength
? source[..maxLength].TrimEnd()
: source;
}

public static string AsPrintableAscii(this string source)
{
if (string.IsNullOrEmpty(source))
return source;
return PrintableAsciiRx().Replace(source, string.Empty);
}

/// <summary>
/// Remove any surrounding quotes, and unescape all others
/// </summary>
/// <param name="source">String to be processed</param>
/// <returns>The string, with surrounding quotes removed and all others unescapes</returns>
public static string TrimAndUnescapeQuotes(this string source)
{
if (string.IsNullOrEmpty(source))
return source;

return source
.Trim('"')
.Replace(@"\""", @"""");
}

public static int ToInt(this string source)
=> Convert.ToInt32(source);

[GeneratedRegex(@"[^\u0021-\u007E]", RegexOptions.Compiled | RegexOptions.Singleline)]
private static partial Regex PrintableAsciiRx();


[GeneratedRegex("[=\\\"\\]]")]
private static partial Regex PropertyKeyRx();
}
6 changes: 6 additions & 0 deletions Serilog.Sinks.LogtailSys.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.LogtailSys",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.LogtailSys.Example", "Serilog.Sinks.LogtailSys.Example\Serilog.Sinks.LogtailSys.Example.csproj", "{E01C60E8-9A08-4085-9F76-5068E98CA229}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.LogtailSys.Tests", "Serilog.Sinks.LogtailSys.Tests\Serilog.Sinks.LogtailSys.Tests.csproj", "{CD0E3355-D7EB-485E-AD0B-782380DC7532}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,6 +23,10 @@ Global
{E01C60E8-9A08-4085-9F76-5068E98CA229}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E01C60E8-9A08-4085-9F76-5068E98CA229}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E01C60E8-9A08-4085-9F76-5068E98CA229}.Release|Any CPU.Build.0 = Release|Any CPU
{CD0E3355-D7EB-485E-AD0B-782380DC7532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD0E3355-D7EB-485E-AD0B-782380DC7532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD0E3355-D7EB-485E-AD0B-782380DC7532}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD0E3355-D7EB-485E-AD0B-782380DC7532}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
89 changes: 43 additions & 46 deletions Serilog.Sinks.LogtailSys/Formatters/LogtailFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Serilog.Events;
using Serilog.Formatting;
Expand Down Expand Up @@ -57,14 +58,16 @@ public LogtailFormatter(
this.applicationName = applicationName ?? ProcessName;

// Conform to the RFC
this.applicationName = this.applicationName
.AsPrintableAscii()
.WithMaxLength(48);
this.applicationName = new StringCleaner(this.applicationName)
.WithAsciiPrintable()
.WithMaxLength(48)
.Build();

// Conform to the RFC
this.messageIdPropertyName = (messageIdPropertyName ?? DefaultMessageIdPropertyName)
.AsPrintableAscii()
.WithMaxLength(32);
this.messageIdPropertyName = new StringCleaner(messageIdPropertyName ?? DefaultMessageIdPropertyName)
.WithAsciiPrintable()
.WithMaxLength(32)
.Build();

this.tokenKey = tokenKey;
this.token = token;
Expand Down Expand Up @@ -92,49 +95,51 @@ private string GetMessageId(LogEvent logEvent)
{
var hasMsgId = logEvent.Properties.TryGetValue(messageIdPropertyName, out var propertyValue);

if (!hasMsgId)
if (!hasMsgId || propertyValue == null)
return NILVALUE;

var result = propertyValue?
.ToString()
.TrimAndUnescapeQuotes();
var result = new StringCleaner(propertyValue.ToString())
.WithTrimed('"')
.WithUnescapeQuotes()
.WithAsciiPrintable()
.WithMaxLength(32)
.Build();

// Conform to the RFC's restrictions
result = result?
.AsPrintableAscii()
.WithMaxLength(32);

return result is { Length: >= 1 }
? result
: NILVALUE;
}

private string RenderStructuredData(LogEvent logEvent)
{
var tokenPart = $"{tokenKey}=\"{token}\"";
var structuredDataKvps = string.Join(" ", logEvent.Properties.Select(t => $"""
{RenderPropertyKey(t.Key)}="{RenderPropertyValue(t.Value)}"
"""));
var structuredData = string.IsNullOrEmpty(structuredDataKvps) ? $"[{tokenPart}]" : $"[{tokenPart}][{dataName} {structuredDataKvps}]";

return structuredData;
var builder = new StringBuilder();
builder.Append($"{tokenKey}=\"{token}\"");
if (logEvent.Properties.Count is 0)
return builder.ToString();

builder.Append($"[{dataName} ");
foreach (var property in logEvent.Properties)
{
var kvp = $"""
{RenderPropertyKey(property.Key)}="{RenderPropertyValue(property.Value)}"
""";
builder.Append(kvp);
}
builder.Append(']');
return builder.ToString();
}

private static string RenderPropertyKey(string propertyKey)
{
// Conform to the RFC's restrictions
var result = propertyKey.AsPrintableAscii();

// Also remove any '=', ']', and '"", as these are also not permitted in structured data parameter names
// Unescaped regex pattern: [=\"\]]

#if NET7_0
result = PropertyKeyRx().Replace(result, string.Empty);
#else
result = Regex.Replace(result, "[=\\\"\\]]", string.Empty);
#endif

return result.WithMaxLength(32);
return new StringCleaner(propertyKey)
.WithAsciiPrintable()
.WithEscapedChars('=', '"', ']')
.WithMaxLength(32)
.Build();
}

/// <summary>
Expand All @@ -145,23 +150,15 @@ private static string RenderPropertyKey(string propertyKey)
private static string RenderPropertyValue(LogEventPropertyValue propertyValue)
{
// Trim surrounding quotes, and unescape all others
var result = propertyValue
.ToString()
.TrimAndUnescapeQuotes();

// Use a backslash to escape backslashes, double quotes and closing square brackets
#if NET7_0
return PropertyValueRx().Replace(result, match => $@"\{match}");
#else
return Regex.Replace(result, @"[\]\\""]", match => $@"\{match}");
#endif
return new StringCleaner(propertyValue
.ToString())
.WithTrimed('"')
.WithUnescapeQuotes()
.WithEscapedChars('"', '\\', ']')
.Build();

}

#if NET7_0
[GeneratedRegex("[\\]\\\\\"]")]
private static partial Regex PropertyValueRx();
[GeneratedRegex("[=\\\"\\]]")]
private static partial Regex PropertyKeyRx();
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ private static IPEndPoint ResolveIP(string host, int port)
{
var addr = Dns.GetHostAddresses(host)
.First(x => x.AddressFamily is AddressFamily.InterNetwork or AddressFamily.InterNetworkV6);

return new IPEndPoint(addr, port);
}
}
Expand Down
6 changes: 3 additions & 3 deletions Serilog.Sinks.LogtailSys/Serilog.Sinks.LogtailSys.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.1;netstandard2.0;net7.0</TargetFrameworks>
<TargetFrameworks>netstandard2.1;netstandard2.0;net8.0</TargetFrameworks>
<Description>Fully-featured Serilog sink that logs events to logtail using UDP (rSyslog).</Description>
<Authors>Nicholas Brostrom (Nickztar)</Authors>
<Company>Effectsoft AB</Company>
Expand All @@ -23,8 +23,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.PeriodicBatching" Version="3.1.0" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.PeriodicBatching" Version="4.1.1" />
</ItemGroup>


Expand Down
Loading

0 comments on commit 4b6417f

Please sign in to comment.