Skip to content

Commit 101c4ba

Browse files
committed
fixes #90
1 parent b7ab58b commit 101c4ba

File tree

14 files changed

+285
-63
lines changed

14 files changed

+285
-63
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ You can also store your own log event properties in additional custom columns; s
204204

205205
### Saving properties in custom columns
206206

207-
By default any log event properties you include in your log statements will be saved to the Properties column (and/or LogEvent column, per columnOption.Store). But they can also be stored in their own columns via the AdditionalDataColumns setting.
207+
By default any log event properties you include in your log statements will be saved to the XML Properties column and/or the JSON LogEvent column (per columnOption.Store configuration). But they can also be stored in their own columns via the AdditionalDataColumns setting.
208208

209209
```csharp
210210
var columnOptions = new ColumnOptions
@@ -234,7 +234,7 @@ By default, additional properties will still be included in the data saved to th
234234

235235
However, if necessary, the properties being saved in their own columns can be excluded from the data. Use the `columnOptions.Properties.ExcludeAdditionalProperties` parameter in the sink configuration to exclude the redundant properties from the XML, or `columnOptions.LogEvent.ExcludeAdditionalProperties` if you've added the JSON LogEvent column.
236236

237-
The standard columns are always excluded from the Properties and LogEvent columns.
237+
The standard columns are always excluded from the XML Properties column and can be excluded from the JSON LogEvent column with `columnOptions.LogEvent.ExludeStandardColumns` (this defaults to `false` for backwards-compatibility reasons).
238238

239239
### Columns defined by AppSettings (.NET Framework)
240240

@@ -311,7 +311,11 @@ As the name suggests, `columnOptionSection` is an entire configuration section i
311311
"usePropertyKeyAsElementName": false
312312
},
313313
"timeStamp": { "columnName": "Timestamp", "convertToUtc": true },
314-
"logEvent": { "columnName": "LogEvent", "excludeAdditionalProperties": true },
314+
"logEvent": {
315+
"columnName": "LogEvent",
316+
"excludeAdditionalProperties": true,
317+
"excludeStandardColumns": true
318+
},
315319
"message": { "columnName": "Message" },
316320
"exception": { "columnName": "Exception" },
317321
"messageTemplate": { "columnName": "MessageTemplate" }
@@ -325,7 +329,7 @@ Typically you will choose either XML or JSON serialization, but not both.
325329

326330
#### JSON (LogEvent column)
327331

328-
Event data items can be stored to the LogEvent column. This can be enabled by adding the LogEvent column to the `columnOptions.Store` collection. Use the `columnOptions.LogEvent.ExcludeAdditionalProperties` parameter to exclude redundant properties from the JSON. This is analogue to excluding redundant items from XML in the Properties column.
332+
Event data items can be stored as JSON in the LogEvent column. This can be enabled by adding the LogEvent column to the `columnOptions.Store` collection. Use the `columnOptions.LogEvent` parameters `ExcludeAdditionalProperties` and `ExcludeStandardColumns` to exclude redundant properties from the JSON. This is analogous to excluding redundant items from XML in the Properties column.
329333

330334
#### XML (Properties column)
331335

src/Serilog.Sinks.MSSqlServer/Configuration/Microsoft.Extensions.Configuration/LoggerConfigurationMSSqlServerExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ private static ColumnOptions ConfigureColumnOptions(ColumnOptions columnOptions,
262262
{
263263
SetIfProvided<string>((val) => { opts.LogEvent.ColumnName = val; }, section["columnName"]);
264264
SetIfProvided<bool>((val) => { opts.LogEvent.ExcludeAdditionalProperties = val; }, section["excludeAdditionalProperties"]);
265+
SetIfProvided<bool>((val) => { opts.LogEvent.ExcludeStandardColumns = val; }, section["ExcludeStandardColumns"]);
265266
}
266267

267268
SetIfProvided<string>((val) => { opts.Message.ColumnName = val; }, config.GetSection("message")["columnName"]);

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/ColumnOptions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ public ColumnOptions()
4343
MessageTemplate = new MessageTemplateColumnOptions();
4444
TimeStamp = new TimeStampColumnOptions();
4545
Exception = new ExceptionColumnOptions();
46-
LogEvent = new LogEventColumnOptions();
46+
47+
LogEvent = new LogEventColumnOptions
48+
{
49+
// these defaults maintain backwards compatibility
50+
ExcludeAdditionalProperties = false,
51+
ExcludeStandardColumns = false
52+
};
4753
}
4854

4955
/// <summary>
@@ -265,6 +271,11 @@ public class LogEventColumnOptions : CommonColumnOptions
265271
/// Exclude properties from the LogEvent column if they are being saved to additional columns.
266272
/// </summary>
267273
public bool ExcludeAdditionalProperties { get; set; }
274+
275+
/// <summary>
276+
/// Whether to include Standard Columns in the LogEvent column (for backwards compatibility).
277+
/// </summary>
278+
public bool ExcludeStandardColumns { get; set; }
268279
}
269280

270281
/// <summary>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2018 Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Serilog.Events;
16+
using Serilog.Formatting;
17+
using Serilog.Formatting.Json;
18+
using Serilog.Parsing;
19+
using System;
20+
using System.Collections.Generic;
21+
using System.IO;
22+
using System.Linq;
23+
24+
namespace Serilog.Sinks.MSSqlServer
25+
{
26+
/// <summary>
27+
/// Custom JSON formatter to generate content for the LogEvent Standard Column.
28+
/// </summary>
29+
internal class JsonLogEventFormatter : ITextFormatter
30+
{
31+
static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter(typeTagName: null);
32+
33+
MSSqlServerSinkTraits traits;
34+
35+
/// <summary>
36+
/// Constructor. A reference to the parent Traits object is used so that JSON
37+
/// can serialize Standard Column values exactly the way they would be written
38+
/// to discrete SQL columns.
39+
/// </summary>
40+
public JsonLogEventFormatter(MSSqlServerSinkTraits parent)
41+
{
42+
traits = parent;
43+
}
44+
45+
/// <summary>
46+
/// Format the log event into the output while respecting the LogEvent column settings.
47+
/// </summary>
48+
public void Format(LogEvent logEvent, TextWriter output)
49+
{
50+
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
51+
if (output == null) throw new ArgumentNullException(nameof(output));
52+
53+
output.Write("{");
54+
55+
if (traits.ColumnOptions.LogEvent.ExcludeStandardColumns == false)
56+
{
57+
// The XML Properties column has never included the Standard Columns, but prior
58+
// to adding this sink-specific JSON formatter, the LogEvent JSON column relied
59+
// upon the general-purpose JsonFormatter in the main Serilog project which does
60+
// write some log event data considered Standard Columns by this sink. In order
61+
// to minimze breaking changes, the LogEvent column behavior slightly deviates
62+
// from the XML behavior by adding the ExcludeStandardColumns flag to control
63+
// whether Standard Columns are written (specifically, the subset of Standard
64+
// columns that were output by the external JsonFormatter class).
65+
66+
string precedingDelimiter = "";
67+
var store = traits.ColumnOptions.Store;
68+
69+
WriteIfPresent(StandardColumn.TimeStamp);
70+
WriteIfPresent(StandardColumn.Level);
71+
WriteIfPresent(StandardColumn.Message);
72+
WriteIfPresent(StandardColumn.MessageTemplate);
73+
if(logEvent.Exception != null) WriteIfPresent(StandardColumn.Exception);
74+
75+
void WriteIfPresent(StandardColumn col)
76+
{
77+
if(store.Contains(col))
78+
{
79+
output.Write(precedingDelimiter);
80+
precedingDelimiter = ",";
81+
var colData = traits.GetStandardColumnNameAndValue(col, logEvent);
82+
JsonValueFormatter.WriteQuotedJsonString(colData.Key, output);
83+
output.Write(":");
84+
string value = (col != StandardColumn.TimeStamp) ? (string)(colData.Value ?? string.Empty) : ((DateTime)colData.Value).ToString("o");
85+
JsonValueFormatter.WriteQuotedJsonString(value, output);
86+
}
87+
}
88+
}
89+
90+
if (logEvent.Properties.Count != 0)
91+
WriteProperties(logEvent.Properties, output);
92+
93+
var tokensWithFormat = logEvent.MessageTemplate.Tokens
94+
.OfType<PropertyToken>()
95+
.Where(pt => pt.Format != null)
96+
.GroupBy(pt => pt.PropertyName)
97+
.ToArray();
98+
99+
if (tokensWithFormat.Length != 0)
100+
{
101+
WriteRenderings(tokensWithFormat, logEvent.Properties, output);
102+
}
103+
104+
output.Write("}");
105+
}
106+
107+
static void WriteProperties(IReadOnlyDictionary<string, LogEventPropertyValue> properties, TextWriter output)
108+
{
109+
output.Write(",\"Properties\":{");
110+
111+
string precedingDelimiter = "";
112+
foreach (var property in properties)
113+
{
114+
output.Write(precedingDelimiter);
115+
precedingDelimiter = ",";
116+
JsonValueFormatter.WriteQuotedJsonString(property.Key, output);
117+
output.Write(':');
118+
ValueFormatter.Format(property.Value, output);
119+
}
120+
121+
output.Write('}');
122+
}
123+
124+
static void WriteRenderings(IEnumerable<IGrouping<string, PropertyToken>> tokensWithFormat, IReadOnlyDictionary<string, LogEventPropertyValue> properties, TextWriter output)
125+
{
126+
output.Write(",\"Renderings\":{");
127+
128+
string precedingDelimiter = "";
129+
foreach (var ptoken in tokensWithFormat)
130+
{
131+
output.Write(precedingDelimiter);
132+
precedingDelimiter = ",";
133+
134+
JsonValueFormatter.WriteQuotedJsonString(ptoken.Key, output);
135+
output.Write(":[");
136+
137+
var fdelim = "";
138+
foreach (var format in ptoken)
139+
{
140+
output.Write(fdelim);
141+
fdelim = ",";
142+
143+
output.Write("{\"Format\":");
144+
JsonValueFormatter.WriteQuotedJsonString(format.Format, output);
145+
146+
output.Write(",\"Rendering\":");
147+
var sw = new StringWriter();
148+
format.Render(properties, sw);
149+
JsonValueFormatter.WriteQuotedJsonString(sw.ToString(), output);
150+
output.Write('}');
151+
}
152+
153+
output.Write(']');
154+
}
155+
156+
output.Write('}');
157+
}
158+
}
159+
}

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSinkTraits.cs

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
using Serilog.Debugging;
1616
using Serilog.Events;
17-
using Serilog.Formatting.Json;
1817

1918
using System;
2019
using System.Collections.Generic;
@@ -32,10 +31,10 @@ internal sealed class MSSqlServerSinkTraits : IDisposable
3231
public string SchemaName { get; }
3332
public ColumnOptions ColumnOptions { get; }
3433
public IFormatProvider FormatProvider { get; }
35-
public JsonFormatter JsonFormatter { get; }
34+
public JsonLogEventFormatter jsonLogEventFormatter { get; }
3635
public ISet<string> AdditionalDataColumnNames { get; }
3736
public DataTable EventTable { get; }
38-
public List<string> ExcludedColumnNames { get; }
37+
public ISet<string> StandardDataColumnNames { get; }
3938

4039
public MSSqlServerSinkTraits(string connectionString, string tableName, string schemaName, ColumnOptions columnOptions, IFormatProvider formatProvider, bool autoCreateSqlTable)
4140
{
@@ -51,21 +50,23 @@ public MSSqlServerSinkTraits(string connectionString, string tableName, string s
5150
ColumnOptions = columnOptions ?? new ColumnOptions();
5251
FormatProvider = formatProvider;
5352

54-
ExcludedColumnNames = new List<string>(ColumnOptions.Store.Count);
55-
if (ColumnOptions.Store.Contains(StandardColumn.Id)) ExcludedColumnNames.Add(ColumnOptions.Id.ColumnName ?? "Id");
56-
if (ColumnOptions.Store.Contains(StandardColumn.Message)) ExcludedColumnNames.Add(ColumnOptions.Message.ColumnName ?? "Message");
57-
if (ColumnOptions.Store.Contains(StandardColumn.MessageTemplate)) ExcludedColumnNames.Add(ColumnOptions.MessageTemplate.ColumnName ?? "MessageTemplate");
58-
if (ColumnOptions.Store.Contains(StandardColumn.Level)) ExcludedColumnNames.Add(ColumnOptions.Level.ColumnName ?? "Level");
59-
if (ColumnOptions.Store.Contains(StandardColumn.TimeStamp)) ExcludedColumnNames.Add(ColumnOptions.TimeStamp.ColumnName ?? "TimeStamp");
60-
if (ColumnOptions.Store.Contains(StandardColumn.Exception)) ExcludedColumnNames.Add(ColumnOptions.Exception.ColumnName ?? "Exception");
61-
if (ColumnOptions.Store.Contains(StandardColumn.Properties)) ExcludedColumnNames.Add(ColumnOptions.Properties.ColumnName ?? "Properties");
62-
if (ColumnOptions.Store.Contains(StandardColumn.LogEvent)) ExcludedColumnNames.Add(ColumnOptions.LogEvent.ColumnName ?? "LogEvent");
63-
53+
StandardDataColumnNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
54+
if (ColumnOptions.Store.Contains(StandardColumn.Id)) StandardDataColumnNames.Add(ColumnOptions.Id.ColumnName ?? "Id");
55+
if (ColumnOptions.Store.Contains(StandardColumn.Message)) StandardDataColumnNames.Add(ColumnOptions.Message.ColumnName ?? "Message");
56+
if (ColumnOptions.Store.Contains(StandardColumn.MessageTemplate)) StandardDataColumnNames.Add(ColumnOptions.MessageTemplate.ColumnName ?? "MessageTemplate");
57+
if (ColumnOptions.Store.Contains(StandardColumn.Level)) StandardDataColumnNames.Add(ColumnOptions.Level.ColumnName ?? "Level");
58+
if (ColumnOptions.Store.Contains(StandardColumn.TimeStamp)) StandardDataColumnNames.Add(ColumnOptions.TimeStamp.ColumnName ?? "TimeStamp");
59+
if (ColumnOptions.Store.Contains(StandardColumn.Exception)) StandardDataColumnNames.Add(ColumnOptions.Exception.ColumnName ?? "Exception");
60+
if (ColumnOptions.Store.Contains(StandardColumn.Properties)) StandardDataColumnNames.Add(ColumnOptions.Properties.ColumnName ?? "Properties");
61+
if (ColumnOptions.Store.Contains(StandardColumn.LogEvent)) StandardDataColumnNames.Add(ColumnOptions.LogEvent.ColumnName ?? "LogEvent");
62+
63+
AdditionalDataColumnNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
6464
if (ColumnOptions.AdditionalDataColumns != null)
65-
AdditionalDataColumnNames = new HashSet<string>(ColumnOptions.AdditionalDataColumns.Select(c => c.ColumnName), StringComparer.OrdinalIgnoreCase);
65+
foreach (var col in ColumnOptions.AdditionalDataColumns)
66+
AdditionalDataColumnNames.Add(col.ColumnName);
6667

6768
if (ColumnOptions.Store.Contains(StandardColumn.LogEvent))
68-
JsonFormatter = new JsonFormatter(formatProvider: formatProvider);
69+
jsonLogEventFormatter = new JsonLogEventFormatter(this);
6970

7071
EventTable = CreateDataTable();
7172

@@ -110,7 +111,7 @@ public void Dispose()
110111
EventTable.Dispose();
111112
}
112113

113-
private KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColumn column, LogEvent logEvent)
114+
internal KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColumn column, LogEvent logEvent)
114115
{
115116
switch (column)
116117
{
@@ -143,7 +144,7 @@ private string LogEventToJson(LogEvent logEvent)
143144

144145
var sb = new StringBuilder();
145146
using (var writer = new System.IO.StringWriter(sb))
146-
JsonFormatter.Format(logEvent, writer);
147+
jsonLogEventFormatter.Format(logEvent, writer);
147148
return sb.ToString();
148149
}
149150

@@ -203,7 +204,7 @@ private IEnumerable<KeyValuePair<string, object>> ConvertPropertiesToColumn(IRea
203204
{
204205
foreach (var property in properties)
205206
{
206-
if (!EventTable.Columns.Contains(property.Key) || ExcludedColumnNames.Contains(property.Key))
207+
if (!EventTable.Columns.Contains(property.Key) || StandardDataColumnNames.Contains(property.Key))
207208
continue;
208209

209210
var columnName = property.Key;

test/Serilog.Sinks.MSSqlServer.Tests/CustomStandardColumnNames.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@ public void CustomIdColumn()
4141
}
4242
}
4343

44-
internal class IdentityQuery
45-
{
46-
public int IsIdentity { get; set; }
47-
}
48-
4944
[Fact]
5045
public void DefaultIdColumn()
5146
{
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
3+
namespace Serilog.Sinks.MSSqlServer.Tests
4+
{
5+
public class IdentityQuery
6+
{
7+
public int IsIdentity { get; set; }
8+
}
9+
10+
public class LogEventColumn
11+
{
12+
public string LogEvent { get; set; }
13+
}
14+
15+
public class SysObjectQuery
16+
{
17+
public int IndexType { get; set; }
18+
}
19+
20+
public class PropertiesColumns
21+
{
22+
public string Properties { get; set; }
23+
}
24+
25+
public class EnumLevelStandardLogColumns
26+
{
27+
public string Message { get; set; }
28+
public byte Level { get; set; }
29+
}
30+
31+
public class StringLevelStandardLogColumns
32+
{
33+
public string Message { get; set; }
34+
public string Level { get; set; }
35+
}
36+
37+
public class TestTriggerEntry
38+
{
39+
public Guid Id { get; set; }
40+
public string Data { get; set; }
41+
}
42+
}

0 commit comments

Comments
 (0)