Skip to content

Commit 5b3b7fa

Browse files
authored
Merge pull request #51 from nblumhardt/json-safe-string
Introduce `JsonSafeString`
2 parents 5fe11a2 + c4c34f8 commit 5b3b7fa

File tree

13 files changed

+122
-99
lines changed

13 files changed

+122
-99
lines changed

Build.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ foreach ($src in ls src/*) {
2121
echo "build: Packaging project in $src"
2222

2323
if ($suffix) {
24-
& dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --include-source
24+
& dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix
2525
} else {
26-
& dotnet pack -c Release -o ..\..\artifacts --include-source
26+
& dotnet pack -c Release -o ..\..\artifacts
2727
}
2828

2929
if($LASTEXITCODE -ne 0) { exit 1 }

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ In `appsettings.json` add a `"Seq"` property to `"Logging"` to configure the ser
105105
The logging provider will dynamically adjust the default logging level up or down based on the level associated with an API key in Seq. For further information see
106106
the [Seq documentation](http://docs.datalust.co/docs/using-serilog#dynamic-level-control).
107107

108+
### Including literal JSON strings in log events
109+
110+
The logging provider ships with a `JsonSafeString` type that can be used to communicate to the logger that a string contains valid JSON, which can be safely included in a log event as structured data.
111+
112+
```csharp
113+
var json = "{\"A\": 42}";
114+
_logger.LogInformation("The answer is {Answer}", new JsonSafeString(json));
115+
```
116+
108117
### Troubleshooting
109118

110119
> Nothing showed up, what can I do?

seq-extensions-logging.sln

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
2-
Microsoft Visual Studio Solution File, Format Version 12.00
1+
Microsoft Visual Studio Solution File, Format Version 12.00
32
# Visual Studio Version 16
43
VisualStudioVersion = 16.0.29326.143
54
MinimumVisualStudioVersion = 10.0.40219.1

seq-extensions-logging.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
44
<s:Boolean x:Key="/Default/UserDictionary/Words/=Datalust/@EntryIndexedValue">True</s:Boolean>
55
<s:Boolean x:Key="/Default/UserDictionary/Words/=Enricher/@EntryIndexedValue">True</s:Boolean>
6+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Formattable/@EntryIndexedValue">True</s:Boolean>
67
<s:Boolean x:Key="/Default/UserDictionary/Words/=Serilog/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

src/Seq.Extensions.Logging/Seq.Extensions.Logging.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
<PackageIcon>icon.png</PackageIcon>
1515
<PackageProjectUrl>https://github.com/datalust/seq-extensions-logging</PackageProjectUrl>
1616
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
17+
<LangVersion>latest</LangVersion>
1718
<RootNamespace />
1819
</PropertyGroup>
1920

21+
<ItemGroup>
22+
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="All" />
23+
</ItemGroup>
24+
2025
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == 'netstandard2.1' ">
2126
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.22" />
2227
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.22" />
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#nullable enable
2+
3+
namespace Seq.Extensions.Logging
4+
{
5+
/// <summary>
6+
/// A wrapper type that marks a string as containing valid JSON that's safe to include as-is in log events.
7+
/// </summary>
8+
public readonly struct JsonSafeString
9+
{
10+
string? Json { get; }
11+
12+
/// <summary>
13+
/// Construct a <see cref="JsonSafeString"/> with well-formed JSON. The JSON is not validated: it must be
14+
/// externally validated or proven to be valid JSON before calling this constructor.
15+
/// </summary>
16+
/// <param name="json">A well-formed JSON string that can be included directly in a log event.</param>
17+
public JsonSafeString(string json)
18+
{
19+
Json = json;
20+
}
21+
22+
/// <inheritdoc />
23+
public override string ToString()
24+
{
25+
// As this type is a struct, it's impossible to enforce that `Json` is non-null. We use "<null>" instead
26+
// of a literal JSON null, so that valid null values can be distinguished from invalid values.
27+
return Json ?? "\"<null>\"";
28+
}
29+
}
30+
}

src/Seq.Extensions.Logging/Seq/Extensions/Logging/SelfLog.cs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,6 @@ public static class SelfLog
2525
{
2626
static Action<string> _output;
2727

28-
/// <summary>
29-
/// The output mechanism for self-log messages.
30-
/// </summary>
31-
/// <example>
32-
/// SelfLog.Out = Console.Error;
33-
/// </example>
34-
// ReSharper disable once MemberCanBePrivate.Global, UnusedAutoPropertyAccessor.Global
35-
[Obsolete("Use SelfLog.Enable(value) and SelfLog.Disable() instead")]
36-
public static TextWriter Out
37-
{
38-
set
39-
{
40-
if (value != null)
41-
Enable(value);
42-
else
43-
Disable();
44-
}
45-
}
46-
4728
/// <summary>
4829
/// Set the output mechanism for self-log messages.
4930
/// </summary>
@@ -68,8 +49,7 @@ public static void Enable(TextWriter output)
6849
/// // ReSharper disable once MemberCanBePrivate.Global
6950
public static void Enable(Action<string> output)
7051
{
71-
if (output == null) throw new ArgumentNullException(nameof(output));
72-
_output = output;
52+
_output = output ?? throw new ArgumentNullException(nameof(output));
7353
}
7454

7555
/// <summary>

src/Seq.Extensions.Logging/Serilog/Formatting/Compact/CompactJsonFormatter.cs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,8 @@
2222

2323
namespace Serilog.Formatting.Compact
2424
{
25-
class CompactJsonFormatter
25+
static class CompactJsonFormatter
2626
{
27-
readonly JsonValueFormatter _valueFormatter;
28-
29-
/// <summary>
30-
/// Construct a <see cref="CompactJsonFormatter"/>, optionally supplying a formatter for
31-
/// <see cref="LogEventPropertyValue"/>s on the event.
32-
/// </summary>
33-
/// <param name="valueFormatter">A value formatter, or null.</param>
34-
public CompactJsonFormatter(JsonValueFormatter valueFormatter = null)
35-
{
36-
_valueFormatter = valueFormatter ?? new JsonValueFormatter(typeTagName: "$type");
37-
}
38-
39-
/// <summary>
40-
/// Format the log event into the output. Subsequent events will be newline-delimited.
41-
/// </summary>
42-
/// <param name="logEvent">The event to format.</param>
43-
/// <param name="output">The output.</param>
44-
public void Format(LogEvent logEvent, TextWriter output)
45-
{
46-
FormatEvent(logEvent, output, _valueFormatter);
47-
output.WriteLine();
48-
}
49-
5027
/// <summary>
5128
/// Format the log event into the output.
5229
/// </summary>

src/Seq.Extensions.Logging/Serilog/Formatting/Json/JsonValueFormatter.cs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
using System;
1616
using System.Globalization;
1717
using System.IO;
18+
using Seq.Extensions.Logging;
1819
using Serilog.Data;
1920
using Serilog.Events;
21+
// ReSharper disable ForCanBeConvertedToForeach
2022

2123
namespace Serilog.Formatting.Json
2224
{
@@ -152,7 +154,7 @@ protected override bool VisitDictionaryValue(TextWriter state, DictionaryValue d
152154
/// </summary>
153155
/// <param name="value">The value to write.</param>
154156
/// <param name="output">The output</param>
155-
protected virtual void FormatLiteralValue(object value, TextWriter output)
157+
static void FormatLiteralValue(object value, TextWriter output)
156158
{
157159
if (value == null)
158160
{
@@ -163,40 +165,38 @@ protected virtual void FormatLiteralValue(object value, TextWriter output)
163165
// Although the linear switch-on-type has apparently worse algorithmic performance than the O(1)
164166
// dictionary lookup alternative, in practice, it's much to make a few equality comparisons
165167
// than the hash/bucket dictionary lookup, and since most data will be string (one comparison),
166-
// numeric (a handful) or an object (two comparsions) the real-world performance of the code
168+
// numeric (a handful) or an object (two comparisons) the real-world performance of the code
167169
// as written is as fast or faster.
168170

169-
var str = value as string;
170-
if (str != null)
171+
if (value is string str)
171172
{
172173
FormatStringValue(str, output);
173174
return;
174175
}
175176

176177
if (value is ValueType)
177178
{
178-
if (value is int || value is uint || value is long || value is ulong || value is decimal ||
179-
value is byte || value is sbyte || value is short || value is ushort)
179+
if (value is int or uint or long or ulong or decimal or byte or sbyte or short or ushort)
180180
{
181181
FormatExactNumericValue((IFormattable)value, output);
182182
return;
183183
}
184184

185-
if (value is double)
185+
if (value is double d)
186186
{
187-
FormatDoubleValue((double)value, output);
187+
FormatDoubleValue(d, output);
188188
return;
189189
}
190190

191-
if (value is float)
191+
if (value is float f)
192192
{
193-
FormatFloatValue((float)value, output);
193+
FormatFloatValue(f, output);
194194
return;
195195
}
196196

197-
if (value is bool)
197+
if (value is bool b)
198198
{
199-
FormatBooleanValue((bool)value, output);
199+
FormatBooleanValue(b, output);
200200
return;
201201
}
202202

@@ -206,22 +206,33 @@ protected virtual void FormatLiteralValue(object value, TextWriter output)
206206
return;
207207
}
208208

209-
if (value is DateTime || value is DateTimeOffset)
209+
if (value is DateTime or DateTimeOffset)
210210
{
211211
FormatDateTimeValue((IFormattable)value, output);
212212
return;
213213
}
214214

215-
if (value is TimeSpan)
215+
if (value is TimeSpan span)
216216
{
217-
FormatTimeSpanValue((TimeSpan)value, output);
217+
FormatTimeSpanValue(span, output);
218218
return;
219219
}
220220
}
221221

222+
if (value is JsonSafeString jss)
223+
{
224+
FormatJsonSafeString(jss, output);
225+
return;
226+
}
227+
222228
FormatLiteralObjectValue(value, output);
223229
}
224230

231+
static void FormatJsonSafeString(JsonSafeString jss, TextWriter output)
232+
{
233+
output.Write(jss.ToString());
234+
}
235+
225236
static void FormatBooleanValue(bool value, TextWriter output)
226237
{
227238
output.Write(value ? "true" : "false");

src/Seq.Extensions.Logging/Serilog/Parameters/PropertyValueConverter.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ namespace Serilog.Parameters
3232
// writing a log event (roughly) in control of the cost of recording that event.
3333
partial class PropertyValueConverter : ILogEventPropertyFactory, ILogEventPropertyValueFactory
3434
{
35-
static readonly HashSet<Type> BuiltInScalarTypes = new HashSet<Type>
35+
readonly HashSet<Type> _builtInScalarTypes = new()
3636
{
3737
typeof(bool),
3838
typeof(char),
@@ -43,6 +43,11 @@ partial class PropertyValueConverter : ILogEventPropertyFactory, ILogEventProper
4343
typeof(Guid), typeof(Uri)
4444
};
4545

46+
static IEnumerable<Type> SeqExtensionsLoggingScalarTypes() => new[]
47+
{
48+
typeof(JsonSafeString)
49+
};
50+
4651
readonly IDestructuringPolicy[] _destructuringPolicies;
4752
readonly IScalarConversionPolicy[] _scalarConversionPolicies;
4853
readonly int _maximumDestructuringDepth;
@@ -62,7 +67,7 @@ public PropertyValueConverter(
6267

6368
_scalarConversionPolicies = new IScalarConversionPolicy[]
6469
{
65-
new SimpleScalarConversionPolicy(BuiltInScalarTypes),
70+
new SimpleScalarConversionPolicy(_builtInScalarTypes.Concat(SeqExtensionsLoggingScalarTypes())),
6671
new NullableScalarConversionPolicy(),
6772
new EnumScalarConversionPolicy(),
6873
new ByteArrayScalarConversionPolicy(),
@@ -220,7 +225,7 @@ bool IsValueTypeDictionary(Type valueType)
220225

221226
bool IsValidDictionaryKeyType(Type valueType)
222227
{
223-
return BuiltInScalarTypes.Contains(valueType) ||
228+
return _builtInScalarTypes.Contains(valueType) ||
224229
valueType.GetTypeInfo().IsEnum;
225230
}
226231

@@ -247,14 +252,14 @@ IEnumerable<LogEventProperty> GetProperties(object value, ILogEventPropertyValue
247252
if (_propagateExceptions)
248253
throw;
249254

250-
propValue = "The property accessor threw an exception: " + ex.InnerException.GetType().Name;
255+
propValue = "The property accessor threw an exception: " + ex.InnerException?.GetType().Name;
251256
}
252257
yield return new LogEventProperty(prop.Name, recursive.CreatePropertyValue(propValue, true));
253258
}
254259
}
255260

256261
[MethodImpl(MethodImplOptions.AggressiveInlining)]
257-
internal static bool IsCompilerGeneratedType(Type type)
262+
static bool IsCompilerGeneratedType(Type type)
258263
{
259264
var typeInfo = type.GetTypeInfo();
260265
var typeName = type.Name;
@@ -266,4 +271,3 @@ internal static bool IsCompilerGeneratedType(Type type)
266271
}
267272
}
268273
}
269-

0 commit comments

Comments
 (0)