Skip to content

Commit 441f3c5

Browse files
authored
Merge pull request #604 from serilog-mssql/dev
* Implemented #542: Column option ResolveHierarchicalPropertyName to force non-hierarchical handling * Removed unnecessary exception handlers and let Serilog Core do the SelfLog() * Refactoring and performance optimizations in batched and audit sink * Create perftest result on release * Updated issue template * Updated editorconfig * Added specific documentation about when SQL SELECT permission is not required
2 parents 2d9f79d + f7fb888 commit 441f3c5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+799
-368
lines changed

.editorconfig

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,24 @@ csharp_style_var_for_built_in_types = true:warning
5757
csharp_style_var_when_type_is_apparent = true:warning
5858
csharp_using_directive_placement = outside_namespace:warning
5959

60+
# Naming rules for non-public fields
6061
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
6162
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
6263
dotnet_naming_rule.private_members_with_underscore.severity = error
6364
dotnet_naming_symbols.private_fields.applicable_kinds = field
64-
dotnet_naming_symbols.private_fields.applicable_accessibilities = private,protected
65+
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected
6566
dotnet_naming_style.prefix_underscore.capitalization = camel_case
6667
dotnet_naming_style.prefix_underscore.required_prefix = _
68+
69+
# Naming rules for const or static public fields
70+
dotnet_naming_rule.public_fields_pascal_case.symbols = public_constant_static_fields
71+
dotnet_naming_rule.public_fields_pascal_case.style = pascal_case
72+
dotnet_naming_rule.public_fields_pascal_case.severity = error
73+
dotnet_naming_symbols.public_constant_static_fields.applicable_kinds = field
74+
dotnet_naming_symbols.public_constant_static_fields.applicable_accessibilities = public
75+
dotnet_naming_symbols.public_constant_static_fields.required_modifiers = const, static
76+
dotnet_naming_style.pascal_case.capitalization = pascal_case
77+
6778
dotnet_sort_system_directives_first = true
6879
dotnet_style_require_accessibility_modifiers = always:error
80+

.github/ISSUE_TEMPLATE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ If you are opening a feature request, you can ignore this template. Bug reports
88
99
>> List the names and versions of all Serilog packages used in the project:
1010
11-
- Serilog:
12-
- Serilog.Sinks.MSSqlServer:
11+
- Serilog:
12+
- Serilog.Sinks.MSSqlServer:
1313
- (configuration, etc.)
1414

1515
>> Target framework and operating system:
1616
17-
[ ] .NET 6
17+
[ ] .NET 9
18+
[ ] .NET 8
1819
[ ] .NET Framework 4.8
1920
[ ] .NET Framework 4.7
2021
[ ] .NET Framework 4.6
21-
OS:
22+
OS:
2223

2324
>> Provide a *simple* reproduction of your Serilog configuration code:
2425

.github/workflows/release.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ jobs:
3939
run: ./RunPerfTests.ps1 -Filter "*QuickBenchmarks*"
4040
shell: pwsh
4141

42+
- name: Upload perf test results artifact
43+
uses: actions/upload-artifact@v4
44+
with:
45+
name: perftestresults
46+
path: artifacts\perftests
47+
4248
- name: Get last commit message
4349
id: last_commit
4450
if: success() && github.ref == 'refs/heads/main'

Build.ps1

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,17 @@ try
7171
{
7272
Push-Location "$testProjectPath"
7373

74-
echo "build: Testing project in $testProjectPath"
75-
& dotnet test -c Release --collect "XPlat Code Coverage"
76-
if ($LASTEXITCODE -ne 0)
74+
# Run tests for different targets in sequence to avoid database tests
75+
# to fail because of concurrency problems
76+
foreach ($tfm in @( "net462", "net472", "net8.0" ))
7777
{
78-
exit 2
78+
echo "build: Testing project in $testProjectPath for target $tfm"
79+
& dotnet test -c Release --collect "XPlat Code Coverage" --framework "$tfm" --results-directory "./TestResults/$tfm"
80+
if ($LASTEXITCODE -ne 0)
81+
{
82+
exit 2
83+
}
7984
}
80-
8185
}
8286
finally
8387
{

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# 8.1.0
2+
* Implemented #542: Column option `ResolveHierarchicalPropertyName` to force non-hierarchical handling
3+
* Removed unnecessary exception handlers and let Serilog Core do the SelfLog()
4+
* Refactoring and performance optimizations in batched and audit sink
5+
* Create perftest result on release
6+
* Updated issue template
7+
* Updated editorconfig
8+
* Added specific documentation about when SQL SELECT permission is not required
9+
110
# 8.0.0
211
* Updated to .NET 8
312
* Updated nearly all dependencies

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ CREATE TABLE [Logs] (
200200

201201
At a minimum, writing log entries requires SELECT and INSERT permissions for the log table. (SELECT is required because the sink's batching behavior uses bulk inserts which reads the schema before the write operations begin).
202202

203+
If the audit version of the sink is used or the sink option [UseSqlBulkCopy](#usesqlbulkcopy) is set to `true`, only INSERT statements are used and no SELECT permission is required.
204+
203205
SQL permissions are a very complex subject. Here is an example of one possible solution (valid for SQL 2012 or later):
204206

205207
```
@@ -337,6 +339,7 @@ Each Standard Column in the `ColumnOptions.Store` list and any custom columns yo
337339

338340
* `ColumnName`
339341
* `PropertyName`
342+
* `ResolveHierarchicalPropertyName`
340343
* `DataType`
341344
* `AllowNull`
342345
* `DataLength`
@@ -350,7 +353,11 @@ Any valid SQL column name can be used. Standard Columns have default names assig
350353

351354
The optional name of a Serilog property to use as the value for a custom column. If not provided, the property used is the one that has the same name as the specified ColumnName. It applies only to custom columns defined in `AdditionalColumns` and is ignored for standard columns.
352355

353-
PropertyName can contain a simple property name like `SomeProperty` but it can also be used to hierachically reference sub-properties with expressions like `SomeProperty.SomeSubProperty.SomeThirdLevelProperty`. This can be used to easily bind additional columns to specific sub-properties following the paradigm of structured logging. Please be aware that collections are not supported. This means expressions like `SomeProperty.SomeArray[2]` will not work.
356+
PropertyName can contain a simple property name like `SomeProperty` but it can also be used to hierarchically reference sub-properties with expressions like `SomeProperty.SomeSubProperty.SomeThirdLevelProperty`. This can be used to easily bind additional columns to specific sub-properties following the paradigm of structured logging. Please be aware that collections are not supported. This means expressions like `SomeProperty.SomeArray[2]` will not work. Hierarchical property resolution can be disabled using `ResolveHierarchicalPropertyName` in case you need property names containing dots which should not be treated as hierarchical.
357+
358+
### ResolveHierarchicalPropertyName
359+
360+
Controls whether hierarchical sub-property expressions in `PropertyName` are evaluated (see above). The default is `true`. If set to `false` any value is treated as a simple property name and no hierarchical sub-property binding is done.
354361

355362
### DataType
356363

@@ -383,7 +390,7 @@ Numeric types use the default precision and scale. For numeric types, you are re
383390

384391
### AllowNull
385392

386-
Determines whether or not the column can store SQL `NULL` values. The default is true. Some of the other features like `PrimaryKey` have related restrictions, and some of the Standard Columns impose restrictions (for example, the `Id` column never allows nulls).
393+
Determines whether the column can store SQL `NULL` values. The default is `true`. Some of the other features like `PrimaryKey` have related restrictions, and some of the Standard Columns impose restrictions (for example, the `Id` column never allows nulls).
387394

388395
### DataLength
389396

sample/WorkerServiceDemo/Worker.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
2727
};
2828
_logger.LogInformation("{@Structured} {@Scalar}", structured, "Scalar Value");
2929

30+
31+
// Logging a property with dots in its name to AdditionalColumn3
32+
// but treat it as unstructured according to configuration in AdditionalColumns in appsettings.json
33+
_logger.LogInformation("Non-structured property with dot-name to AdditionalColumn3 {@NonstructuredProperty.WithNameContainingDots.Name}",
34+
new Random().Next().ToString());
35+
3036
while (!stoppingToken.IsCancellationRequested)
3137
{
3238
_logger.LogInformation("Worker running at: {time}. CustomProperty1: {CustomProperty1}",

sample/WorkerServiceDemo/appsettings.json

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"columnName": "Timestamp",
2828
"convertToUtc": false
2929
},
30-
"additionalColumns": [
30+
"customColumns": [
3131
{
3232
"columnName": "AdditionalColumn1",
3333
"propertyName": "CustomProperty1",
@@ -37,6 +37,12 @@
3737
"columnName": "AdditionalColumn2",
3838
"propertyName": "Structured.Name",
3939
"dataType": "12"
40+
},
41+
{
42+
"columnName": "AdditionalColumn3",
43+
"propertyName": "NonstructuredProperty.WithNameContainingDots.Name",
44+
"resolveHierarchicalPropertyName": false,
45+
"dataType": "12"
4046
}
4147
]
4248
},
@@ -63,11 +69,22 @@
6369
"columnName": "Timestamp",
6470
"convertToUtc": false
6571
},
66-
"additionalColumns": [
72+
"customColumns": [
6773
{
6874
"columnName": "AdditionalColumn1",
6975
"propertyName": "CustomProperty1",
7076
"dataType": "12"
77+
},
78+
{
79+
"columnName": "AdditionalColumn2",
80+
"propertyName": "Structured.Name",
81+
"dataType": "12"
82+
},
83+
{
84+
"columnName": "AdditionalColumn3",
85+
"propertyName": "NonstructuredProperty.WithNameContainingDots.Name",
86+
"resolveHierarchicalPropertyName": false,
87+
"dataType": "12"
7188
}
7289
]
7390
}

src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/ColumnCollection.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright 2015 Serilog Contributors
2-
//
2+
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
6-
//
6+
//
77
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
8+
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -30,6 +30,12 @@ protected override object GetElementKey(ConfigurationElement element)
3030
{
3131
return ((ColumnConfig)element)?.ColumnName;
3232
}
33+
34+
// For testing
35+
internal void Add(ColumnConfig config)
36+
{
37+
BaseAdd(config);
38+
}
3339
}
3440
}
3541

src/Serilog.Sinks.MSSqlServer/Configuration/Implementations/System.Configuration/ColumnConfig.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright 2015 Serilog Contributors
2-
//
2+
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
6-
//
6+
//
77
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
8+
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -48,6 +48,13 @@ public virtual string PropertyName
4848
set { this[nameof(PropertyName)] = value; }
4949
}
5050

51+
[ConfigurationProperty("ResolveHierarchicalPropertyName")]
52+
public virtual string ResolveHierarchicalPropertyName
53+
{
54+
get { return (string)this[nameof(ResolveHierarchicalPropertyName)]; }
55+
set { this[nameof(ResolveHierarchicalPropertyName)] = value; }
56+
}
57+
5158
[ConfigurationProperty("DataType")]
5259
public string DataType
5360
{
@@ -85,6 +92,8 @@ internal SqlColumn AsSqlColumn()
8592

8693
SetProperty.IfProvidedNotEmpty<string>(this, nameof(PropertyName), (val) => sqlColumn.PropertyName = val);
8794

95+
SetProperty.IfProvided<bool>(this, nameof(ResolveHierarchicalPropertyName), (val) => sqlColumn.ResolveHierarchicalPropertyName = val);
96+
8897
SetProperty.IfProvidedNotEmpty<string>(this, nameof(DataType), (val) => sqlColumn.SetDataTypeFromConfigString(val));
8998

9099
SetProperty.IfProvided<int>(this, nameof(DataLength), (val) => sqlColumn.DataLength = val);

src/Serilog.Sinks.MSSqlServer/Serilog.Sinks.MSSqlServer.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
<PropertyGroup>
44
<Description>A Serilog sink that writes events to Microsoft SQL Server and Azure SQL</Description>
5-
<VersionPrefix>8.0.0</VersionPrefix>
6-
<EnablePackageValidation>false</EnablePackageValidation>
7-
<PackageValidationBaselineVersion>7.0.0</PackageValidationBaselineVersion>
5+
<VersionPrefix>8.1.0</VersionPrefix>
6+
<EnablePackageValidation>true</EnablePackageValidation>
7+
<PackageValidationBaselineVersion>8.0.0</PackageValidationBaselineVersion>
88
<Authors>Michiel van Oudheusden;Christian Kadluba;Serilog Contributors</Authors>
99
<TargetFrameworks>netstandard2.0;net462;net472;net8.0</TargetFrameworks>
1010
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Dependencies/SinkDependencies.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ namespace Serilog.Sinks.MSSqlServer.Dependencies
44
{
55
internal class SinkDependencies
66
{
7-
public IDataTableCreator DataTableCreator { get; set; }
87
public ISqlCommandExecutor SqlDatabaseCreator { get; set; }
98
public ISqlCommandExecutor SqlTableCreator { get; set; }
109
public ISqlBulkBatchWriter SqlBulkBatchWriter { get; set; }

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Dependencies/SinkDependenciesFactory.cs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal static SinkDependencies Create(
2323
var sqlConnectionStringBuilderWrapper = new SqlConnectionStringBuilderWrapper(
2424
connectionString, sinkOptions.EnlistInTransaction);
2525
var sqlConnectionFactory = new SqlConnectionFactory(sqlConnectionStringBuilderWrapper);
26+
var sqlCommandFactory = new SqlCommandFactory();
2627
var dataTableCreator = new DataTableCreator(sinkOptions.TableName, columnOptions);
2728
var sqlCreateTableWriter = new SqlCreateTableWriter(sinkOptions.SchemaName,
2829
sinkOptions.TableName, columnOptions, dataTableCreator);
@@ -47,21 +48,16 @@ internal static SinkDependencies Create(
4748

4849
var sinkDependencies = new SinkDependencies
4950
{
50-
DataTableCreator = dataTableCreator,
5151
SqlDatabaseCreator = new SqlDatabaseCreator(
52-
sqlCreateDatabaseWriter, sqlConnectionFactoryNoDb),
52+
sqlCreateDatabaseWriter, sqlConnectionFactoryNoDb, sqlCommandFactory),
5353
SqlTableCreator = new SqlTableCreator(
54-
sqlCreateTableWriter, sqlConnectionFactory),
55-
SqlBulkBatchWriter = sinkOptions.UseSqlBulkCopy
56-
? (ISqlBulkBatchWriter)new SqlBulkBatchWriter(
57-
sinkOptions.TableName, sinkOptions.SchemaName, columnOptions.DisableTriggers,
58-
sqlConnectionFactory, logEventDataGenerator)
59-
: (ISqlBulkBatchWriter)new SqlInsertStatementWriter(
60-
sinkOptions.TableName, sinkOptions.SchemaName,
61-
sqlConnectionFactory, logEventDataGenerator),
54+
sqlCreateTableWriter, sqlConnectionFactory, sqlCommandFactory),
55+
SqlBulkBatchWriter = new SqlBulkBatchWriter(
56+
sinkOptions.TableName, sinkOptions.SchemaName, columnOptions.DisableTriggers,
57+
dataTableCreator, sqlConnectionFactory, logEventDataGenerator),
6258
SqlLogEventWriter = new SqlInsertStatementWriter(
6359
sinkOptions.TableName, sinkOptions.SchemaName,
64-
sqlConnectionFactory, logEventDataGenerator)
60+
sqlConnectionFactory, sqlCommandFactory, logEventDataGenerator)
6561
};
6662

6763
return sinkDependencies;

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerAuditSink.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
// Copyright 2024 Serilog Contributors
2-
//
1+
// Copyright 2024 Serilog Contributors
2+
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
6-
//
6+
//
77
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
8+
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -30,11 +30,13 @@ public class MSSqlServerAuditSink : ILogEventSink, IDisposable
3030
{
3131
private readonly ISqlLogEventWriter _sqlLogEventWriter;
3232

33+
private bool _disposedValue;
34+
3335
/// <summary>
3436
/// Construct a sink posting to the specified database.
3537
///
3638
/// Note: this is the legacy version of the extension method. Please use the new one using MSSqlServerSinkOptions instead.
37-
///
39+
///
3840
/// </summary>
3941
/// <param name="connectionString">Connection string to access the database.</param>
4042
/// <param name="tableName">Name of the table to store the data in.</param>
@@ -113,7 +115,15 @@ public void Dispose()
113115
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
114116
protected virtual void Dispose(bool disposing)
115117
{
116-
// This class needn't to dispose anything. This is just here for sink interface compatibility.
118+
if (!_disposedValue)
119+
{
120+
if (disposing)
121+
{
122+
_sqlLogEventWriter.Dispose();
123+
}
124+
125+
_disposedValue = true;
126+
}
117127
}
118128

119129
private static void ValidateParameters(MSSqlServerSinkOptions sinkOptions, ColumnOptions columnOptions)
@@ -134,11 +144,6 @@ private static void CheckSinkDependencies(SinkDependencies sinkDependencies)
134144
throw new ArgumentNullException(nameof(sinkDependencies));
135145
}
136146

137-
if (sinkDependencies.DataTableCreator == null)
138-
{
139-
throw new InvalidOperationException("DataTableCreator is not initialized!");
140-
}
141-
142147
if (sinkDependencies.SqlTableCreator == null)
143148
{
144149
throw new InvalidOperationException("SqlTableCreator is not initialized!");

0 commit comments

Comments
 (0)