Skip to content

Commit 9a978c4

Browse files
authored
Merge pull request #233 from serilog/dev
Merged changes from dev
2 parents 4bda7f8 + 5bc7f49 commit 9a978c4

File tree

10 files changed

+300
-16
lines changed

10 files changed

+300
-16
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ var log = new LoggerConfiguration()
100100
.WriteTo.MSSqlServer(
101101
connectionString: logDB,
102102
tableName: logTable,
103-
columnOptions: opts
103+
columnOptions: options
104104
).CreateLogger();
105105

106106
```
@@ -369,7 +369,16 @@ This column stores the event level (Error, Information, etc.). For backwards-com
369369

370370
### TimeStamp
371371

372-
This column stores the time the log event was sent to Serilog as a SQL `datetime` type. While this may appear to be a good candidate as a clustered primary key, even relatively low-volume logging can emit identical timestamps forcing SQL Server to add a "uniqueifier" value behind the scenes (effectively an auto-incrementing identity-like integer). For frequent timestamp range-searching and sorting, a non-clustered index is better.
372+
This column stores the time the log event was sent to Serilog as a SQL `datetime` (default) or `datetimeoffset` type. If `datetimeoffset` should be used, this can be configured as follows.
373+
374+
```csharp
375+
var columnOptions = new ColumnOptions();
376+
columnOptions.TimeStamp.DataType = SqlDbType.DateTimeOffset;
377+
```
378+
379+
Please be aware that you have to configure the sink for `datetimeoffset` if the used logging database table has a `TimeStamp` column of type `datetimeoffset`. On the other hand you must not configure for `datetimeoffset` if the `TimeStamp` column is of type `datetime`. Failing to configure the data type accordingly can result in log table entries with wrong timezone offsets or no log entries being created at all due to exceptions during logging.
380+
381+
While TimeStamp may appear to be a good candidate as a clustered primary key, even relatively low-volume logging can emit identical timestamps forcing SQL Server to add a "uniqueifier" value behind the scenes (effectively an auto-incrementing identity-like integer). For frequent timestamp range-searching and sorting, a non-clustered index is better.
373382

374383
When the `ConvertToUtc` property is set to `true`, the time stamp is adjusted to the UTC standard. Normally the time stamp value reflects the local time of the machine issuing the log event, including the current timezone information. For example, if the event is written at 07:00 Eastern time, the Eastern timezone is +4:00 relative to UTC, so after UTC conversion the time stamp will be 11:00. Offset is stored as +0:00 but this is _not_ the GMT time zone because UTC does not use offsets (by definition). To state this another way, the timezone is discarded and unrecoverable. UTC is a representation of the date and time _exclusive_ of timezone information. This makes it easy to reference time stamps written from different or changing timezones.
375384

src/Serilog.Sinks.MSSqlServer/Properties/AssemblyInfo.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
[assembly: AssemblyTitle("Serilog.Sinks.MSSqlServer")]
55
[assembly: AssemblyDescription("Serilog sink for MSSqlServer")]
6-
[assembly: AssemblyCopyright("Copyright © Serilog Contributors 2014")]
6+
[assembly: AssemblyCopyright("Copyright © Serilog Contributors 2020")]
77

8-
[assembly: InternalsVisibleTo("Serilog.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c" +
9-
"6fe0fe83ef33c1080bf30690765bc6eb0df26ebfdf8f21670c64265b30db09f73a0dea5b3db4c9" +
10-
"d18dbf6d5a25af5ce9016f281014d79dc3b4201ac646c451830fc7e61a2dfd633d34c39f87b818" +
11-
"94191652df5ac63cc40c77f3542f702bda692e6e8a9158353df189007a49da0f3cfd55eb250066" +
12-
"b19485ec")]
8+
[assembly: InternalsVisibleTo("Serilog.Sinks.MSSqlServer.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c" +
9+
"6fe0fe83ef33c1080bf30690765bc6eb0df26ebfdf8f21670c64265b30db09f73a0dea5b3db4c9" +
10+
"d18dbf6d5a25af5ce9016f281014d79dc3b4201ac646c451830fc7e61a2dfd633d34c39f87b818" +
11+
"94191652df5ac63cc40c77f3542f702bda692e6e8a9158353df189007a49da0f3cfd55eb250066" +
12+
"b19485ec")]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Description>A Serilog sink that writes events to Microsoft SQL Server</Description>
5-
<VersionPrefix>5.1.3</VersionPrefix>
5+
<VersionPrefix>5.1.4</VersionPrefix>
66
<Authors>Michiel van Oudheusden;Serilog Contributors</Authors>
77
<TargetFrameworks>netstandard2.0;net452;netcoreapp2.0;net461</TargetFrameworks>
88
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ public TimeStampColumnOptions() : base()
2020
}
2121

2222
/// <summary>
23-
/// The TimeStamp column only supports the DateTime data type.
23+
/// The TimeStamp column only supports the DateTime and DateTimeOffset data types.
2424
/// </summary>
2525
public new SqlDbType DataType
2626
{
2727
get => base.DataType;
2828
set
2929
{
30-
if (value != SqlDbType.DateTime)
31-
throw new ArgumentException("The Standard Column \"TimeStamp\" only supports the DateTime format.");
30+
if (value != SqlDbType.DateTime && value != SqlDbType.DateTimeOffset)
31+
throw new ArgumentException("The Standard Column \"TimeStamp\" only supports the DateTime and DateTimeOffset formats.");
3232
base.DataType = value;
3333
}
3434
}

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 Serilog Contributors
1+
// Copyright 2020 Serilog Contributors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
1717
using System;
1818
using System.Collections.Generic;
1919
using System.Data;
20-
using System.IO;
2120
using System.Linq;
2221
using System.Text;
2322

@@ -120,7 +119,7 @@ internal KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColu
120119
case StandardColumn.Level:
121120
return new KeyValuePair<string, object>(columnOptions.Level.ColumnName, columnOptions.Level.StoreAsEnum ? (object)logEvent.Level : logEvent.Level.ToString());
122121
case StandardColumn.TimeStamp:
123-
return new KeyValuePair<string, object>(columnOptions.TimeStamp.ColumnName, columnOptions.TimeStamp.ConvertToUtc ? logEvent.Timestamp.ToUniversalTime().DateTime : logEvent.Timestamp.DateTime);
122+
return GetTimeStampStandardColumnNameAndValue(logEvent);
124123
case StandardColumn.Exception:
125124
return new KeyValuePair<string, object>(columnOptions.Exception.ColumnName, logEvent.Exception != null ? logEvent.Exception.ToString() : null);
126125
case StandardColumn.Properties:
@@ -132,6 +131,16 @@ internal KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColu
132131
}
133132
}
134133

134+
private KeyValuePair<string, object> GetTimeStampStandardColumnNameAndValue(LogEvent logEvent)
135+
{
136+
var dateTimeOffset = columnOptions.TimeStamp.ConvertToUtc ? logEvent.Timestamp.ToUniversalTime() : logEvent.Timestamp;
137+
138+
if (columnOptions.TimeStamp.DataType == SqlDbType.DateTimeOffset)
139+
return new KeyValuePair<string, object>(columnOptions.TimeStamp.ColumnName, dateTimeOffset);
140+
141+
return new KeyValuePair<string, object>(columnOptions.TimeStamp.ColumnName, dateTimeOffset.DateTime);
142+
}
143+
135144
private string LogEventToJson(LogEvent logEvent)
136145
{
137146
if (columnOptions.LogEvent.ExcludeAdditionalProperties)

test/Serilog.Sinks.MSSqlServer.Tests/Configuration/Microsoft.Extensions.Configuration/ConfigurationExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
namespace Serilog.Sinks.MSSqlServer.Tests
1111
{
1212
[Collection("LogTest")]
13-
public class ConfigurationExtensions : IDisposable
13+
public sealed class ConfigurationExtensions : IDisposable
1414
{
1515
static string ConnectionStringName = "NamedConnection";
1616
static string ColumnOptionsSection = "CustomColumnNames";

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ public class StringLevelStandardLogColumns
7070
public string Level { get; set; }
7171
}
7272

73+
public class TestTimeStampDateTimeOffsetEntry
74+
{
75+
public DateTimeOffset TimeStamp { get; set; }
76+
}
77+
78+
public class TestTimeStampDateTimeEntry
79+
{
80+
public DateTime TimeStamp { get; set; }
81+
}
82+
7383
public class TestTriggerEntry
7484
{
7585
public Guid Id { get; set; }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Data;
3+
using Xunit;
4+
5+
namespace Serilog.Sinks.MSSqlServer.Tests.Sinks.MSSqlServer.ColumnOptions
6+
{
7+
public class TestTimeStampColumnOptions
8+
{
9+
[Trait("Bugfix", "#187")]
10+
[Fact]
11+
public void CanSetDataTypeDateTime()
12+
{
13+
// arrange
14+
var options = new Serilog.Sinks.MSSqlServer.ColumnOptions();
15+
16+
// act - should not throw
17+
options.TimeStamp.DataType = SqlDbType.DateTime;
18+
}
19+
20+
[Trait("Bugfix", "#187")]
21+
[Fact]
22+
public void CanSetDataTypeDateTimeOffset()
23+
{
24+
// arrange
25+
var options = new Serilog.Sinks.MSSqlServer.ColumnOptions();
26+
27+
// act - should not throw
28+
options.TimeStamp.DataType = SqlDbType.DateTimeOffset;
29+
}
30+
31+
[Trait("Bugfix", "#187")]
32+
[Fact]
33+
public void CannotSetDataTypeNVarChar()
34+
{
35+
// arrange
36+
var options = new Serilog.Sinks.MSSqlServer.ColumnOptions();
37+
38+
// act and assert - should throw
39+
Assert.Throws<ArgumentException>(() => options.TimeStamp.DataType = SqlDbType.NVarChar);
40+
}
41+
}
42+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using Serilog.Events;
2+
using Serilog.Parsing;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Data;
6+
using System.Globalization;
7+
using System.Linq;
8+
using Xunit;
9+
10+
namespace Serilog.Sinks.MSSqlServer.Tests.Sinks.MSSqlServer
11+
{
12+
public class TestMSSqlServerSinkTraits
13+
{
14+
private MSSqlServerSinkTraits traits;
15+
private LogEvent logEvent;
16+
17+
[Trait("Bugfix", "#187")]
18+
[Fact]
19+
public void GetColumnsAndValuesCreatesTimeStampOfTypeDateTimeAccordingToColumnOptions()
20+
{
21+
// arrange
22+
var options = new Serilog.Sinks.MSSqlServer.ColumnOptions();
23+
var testDateTimeOffset = new DateTimeOffset(2020, 1, 1, 9, 0, 0, new TimeSpan(1, 0, 0)); // Timezone +1:00
24+
SetupTest(options, testDateTimeOffset);
25+
26+
// act
27+
var columns = traits.GetColumnsAndValues(logEvent);
28+
29+
// assert
30+
var timeStampColumn = columns.Single(c => c.Key == options.TimeStamp.ColumnName);
31+
Assert.IsType<DateTime>(timeStampColumn.Value);
32+
Assert.Equal(testDateTimeOffset.Hour, ((DateTime)timeStampColumn.Value).Hour);
33+
}
34+
35+
[Trait("Bugfix", "#187")]
36+
[Fact]
37+
public void GetColumnsAndValuesCreatesUtcConvertedTimeStampOfTypeDateTimeAccordingToColumnOptions()
38+
{
39+
// arrange
40+
var options = new Serilog.Sinks.MSSqlServer.ColumnOptions
41+
{
42+
TimeStamp = { ConvertToUtc = true }
43+
};
44+
var testDateTimeOffset = new DateTimeOffset(2020, 1, 1, 9, 0, 0, new TimeSpan(1, 0, 0)); // Timezone +1:00
45+
SetupTest(options, testDateTimeOffset);
46+
47+
// act
48+
var columns = traits.GetColumnsAndValues(logEvent);
49+
50+
// assert
51+
var timeStampColumn = columns.Single(c => c.Key == options.TimeStamp.ColumnName);
52+
Assert.IsType<DateTime>(timeStampColumn.Value);
53+
Assert.Equal(testDateTimeOffset.Hour - 1, ((DateTime)timeStampColumn.Value).Hour);
54+
}
55+
56+
[Trait("Bugfix", "#187")]
57+
[Fact]
58+
public void GetColumnsAndValuesCreatesTimeStampOfTypeDateTimeOffsetAccordingToColumnOptions()
59+
{
60+
// arrange
61+
var options = new Serilog.Sinks.MSSqlServer.ColumnOptions
62+
{
63+
TimeStamp = { DataType = SqlDbType.DateTimeOffset }
64+
};
65+
var testDateTimeOffset = new DateTimeOffset(2020, 1, 1, 9, 0, 0, new TimeSpan(1, 0, 0)); // Timezone +1:00
66+
SetupTest(options, testDateTimeOffset);
67+
68+
// act
69+
var columns = traits.GetColumnsAndValues(logEvent);
70+
71+
// assert
72+
var timeStampColumn = columns.Single(c => c.Key == options.TimeStamp.ColumnName);
73+
Assert.IsType<DateTimeOffset>(timeStampColumn.Value);
74+
var timeStampColumnOffset = (DateTimeOffset)timeStampColumn.Value;
75+
Assert.Equal(testDateTimeOffset.Hour, timeStampColumnOffset.Hour);
76+
Assert.Equal(testDateTimeOffset.Offset, timeStampColumnOffset.Offset);
77+
}
78+
79+
[Trait("Bugfix", "#187")]
80+
[Fact]
81+
public void GetColumnsAndValuesCreatesUtcConvertedTimeStampOfTypeDateTimeOffsetAccordingToColumnOptions()
82+
{
83+
// arrange
84+
var options = new Serilog.Sinks.MSSqlServer.ColumnOptions
85+
{
86+
TimeStamp = { DataType = SqlDbType.DateTimeOffset, ConvertToUtc = true }
87+
};
88+
var testDateTimeOffset = new DateTimeOffset(2020, 1, 1, 9, 0, 0, new TimeSpan(1, 0, 0)); // Timezone +1:00
89+
SetupTest(options, testDateTimeOffset);
90+
91+
// act
92+
var columns = traits.GetColumnsAndValues(logEvent);
93+
94+
// assert
95+
var timeStampColumn = columns.Single(c => c.Key == options.TimeStamp.ColumnName);
96+
Assert.IsType<DateTimeOffset>(timeStampColumn.Value);
97+
var timeStampColumnOffset = (DateTimeOffset)timeStampColumn.Value;
98+
Assert.Equal(testDateTimeOffset.Hour - 1, timeStampColumnOffset.Hour);
99+
Assert.Equal(new TimeSpan(0), timeStampColumnOffset.Offset);
100+
}
101+
102+
private void SetupTest(Serilog.Sinks.MSSqlServer.ColumnOptions options, DateTimeOffset testDateTimeOffset)
103+
{
104+
this.traits = new MSSqlServerSinkTraits("connectionString", "tableName", "schemaName",
105+
options, CultureInfo.InvariantCulture, false);
106+
this.logEvent = new LogEvent(testDateTimeOffset, LogEventLevel.Information, null,
107+
new MessageTemplate(new List<MessageTemplateToken>()), new List<LogEventProperty>());
108+
}
109+
}
110+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System;
2+
using System.Data;
3+
using System.Data.SqlClient;
4+
using System.IO;
5+
using Dapper;
6+
using FluentAssertions;
7+
using Xunit;
8+
9+
namespace Serilog.Sinks.MSSqlServer.Tests
10+
{
11+
[Collection("LogTest")]
12+
public class TestTimeStamp : IDisposable
13+
{
14+
[Trait("Bugfix", "#187")]
15+
[Fact]
16+
public void CanCreateDatabaseWithDateTimeByDefault()
17+
{
18+
// arrange
19+
var loggerConfiguration = new LoggerConfiguration();
20+
Log.Logger = loggerConfiguration.WriteTo.MSSqlServer(
21+
connectionString: DatabaseFixture.LogEventsConnectionString,
22+
tableName: DatabaseFixture.LogTableName,
23+
autoCreateSqlTable: true,
24+
batchPostingLimit: 1,
25+
period: TimeSpan.FromSeconds(10),
26+
columnOptions: new ColumnOptions())
27+
.CreateLogger();
28+
29+
// act
30+
const string loggingInformationMessage = "Logging Information message";
31+
Log.Information(loggingInformationMessage);
32+
Log.CloseAndFlush();
33+
34+
// assert
35+
using (var conn = new SqlConnection(DatabaseFixture.LogEventsConnectionString))
36+
{
37+
var logEvents = conn.Query<TestTimeStampDateTimeEntry>($"SELECT TimeStamp FROM {DatabaseFixture.LogTableName}");
38+
logEvents.Should().NotBeEmpty();
39+
}
40+
}
41+
42+
[Trait("Bugfix", "#187")]
43+
[Fact]
44+
public void CanStoreDateTimeOffsetWithCorrectLocalTimeZone()
45+
{
46+
// arrange
47+
var loggerConfiguration = new LoggerConfiguration();
48+
Log.Logger = loggerConfiguration.WriteTo.MSSqlServer(
49+
connectionString: DatabaseFixture.LogEventsConnectionString,
50+
tableName: DatabaseFixture.LogTableName,
51+
autoCreateSqlTable: true,
52+
batchPostingLimit: 1,
53+
period: TimeSpan.FromSeconds(10),
54+
columnOptions: new ColumnOptions { TimeStamp = { DataType = SqlDbType.DateTimeOffset, ConvertToUtc = false }})
55+
.CreateLogger();
56+
var dateTimeOffsetNow = DateTimeOffset.Now;
57+
58+
// act
59+
const string loggingInformationMessage = "Logging Information message";
60+
Log.Information(loggingInformationMessage);
61+
Log.CloseAndFlush();
62+
63+
// assert
64+
using (var conn = new SqlConnection(DatabaseFixture.LogEventsConnectionString))
65+
{
66+
var logEvents = conn.Query<TestTimeStampDateTimeOffsetEntry>($"SELECT TimeStamp FROM {DatabaseFixture.LogTableName}");
67+
logEvents.Should().Contain(e => e.TimeStamp.Offset == dateTimeOffsetNow.Offset);
68+
}
69+
}
70+
71+
[Trait("Bugfix", "#187")]
72+
[Fact]
73+
public void CanStoreDateTimeOffsetWithUtcTimeZone()
74+
{
75+
// arrange
76+
var loggerConfiguration = new LoggerConfiguration();
77+
Log.Logger = loggerConfiguration.WriteTo.MSSqlServer(
78+
connectionString: DatabaseFixture.LogEventsConnectionString,
79+
tableName: DatabaseFixture.LogTableName,
80+
autoCreateSqlTable: true,
81+
batchPostingLimit: 1,
82+
period: TimeSpan.FromSeconds(10),
83+
columnOptions: new ColumnOptions { TimeStamp = { DataType = SqlDbType.DateTimeOffset, ConvertToUtc = true } })
84+
.CreateLogger();
85+
86+
// act
87+
const string loggingInformationMessage = "Logging Information message";
88+
Log.Information(loggingInformationMessage);
89+
Log.CloseAndFlush();
90+
91+
// assert
92+
using (var conn = new SqlConnection(DatabaseFixture.LogEventsConnectionString))
93+
{
94+
var logEvents = conn.Query<TestTimeStampDateTimeOffsetEntry>($"SELECT TimeStamp FROM {DatabaseFixture.LogTableName}");
95+
logEvents.Should().Contain(e => e.TimeStamp.Offset == new TimeSpan(0));
96+
}
97+
}
98+
99+
public void Dispose()
100+
{
101+
DatabaseFixture.DropTable();
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)