Skip to content

Commit 91cb3c9

Browse files
committed
initial implementation
1 parent 993a91e commit 91cb3c9

File tree

6 files changed

+144
-55
lines changed

6 files changed

+144
-55
lines changed

README.md

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ var log = new LoggerConfiguration()
8484

8585
## Table definition
8686

87-
You'll need to create a table like this in your database:
87+
You'll need to create a table like this in your database. Many other variations are possible. In particular, give careful consideration to whether you need the Id column (discussed in the next section). The table definition shown here is the default configuration.
8888

8989
```
9090
CREATE TABLE [Logs] (
@@ -95,8 +95,7 @@ CREATE TABLE [Logs] (
9595
[Level] nvarchar(128) NULL,
9696
[TimeStamp] datetimeoffset(7) NOT NULL, -- use datetime for SQL Server pre-2008
9797
[Exception] nvarchar(max) NULL,
98-
[Properties] xml NULL,
99-
[LogEvent] nvarchar(max) NULL
98+
[Properties] xml NULL
10099
101100
CONSTRAINT [PK_Logs]
102101
PRIMARY KEY CLUSTERED ([Id] ASC)
@@ -107,23 +106,76 @@ CREATE TABLE [Logs] (
107106
) ON [PRIMARY];
108107
```
109108

110-
**Remember to grant the necessary permissions for the sink to be able to write to the log table.**
109+
If you don't plan to use a column, you can specify which columns to exclude in the `columnOptions.Store` parameter (see below).
111110

112-
If you don't plan on using one or more columns, you can specify which columns to include in the *columnOptions.Store* parameter (see below).
113-
114-
The Level column should be defined as a TinyInt if the *columnOptions.Level.StoreAsEnum* is set to true.
111+
The Level column should be defined as a `tinyint` when `columnOptions.Level.StoreAsEnum` is set to `true`.
115112

116113

117114
### Automatic table creation
118115

119-
If you set the `autoCreateSqlTable` option to `true`, the sink will create a table for you in the database specified in the connection string. Make sure that the user associated with this connection string has enough rights to make schema changes.
116+
If you set the `autoCreateSqlTable` option to `true`, the sink will create a table for you in the database specified in the connection string. Make sure that the user associated with this connection string has enough rights to make schema changes; see below.
117+
118+
119+
### Permissions
120+
121+
SQL permissions are a very complex subject. Here is an example of one possible solution (valid for SQL 2012 or later):
122+
123+
```
124+
CREATE ROLE [SerilogAutoCreate];
125+
GRANT SELECT ON sys.tables TO [SerilogAutoCreate];
126+
GRANT SELECT ON sys.schemas TO [SerilogAutoCreate];
127+
GRANT ALTER ON SCHEMA::[dbo] TO [SerilogAutoCreate]
128+
GRANT CREATE TABLE ON DATABASE::[SerilogTest] TO [SerilogAutoCreate];
129+
130+
CREATE ROLE [SerilogWriter];
131+
GRANT SELECT TO [SerilogWriter];
132+
GRANT INSERT TO [SerilogWriter];
133+
134+
CREATE LOGIN [Serilog] WITH PASSWORD = 'password';
135+
136+
CREATE USER [Serilog] FOR LOGIN [Serilog] WITH DEFAULT_SCHEMA = dbo;
137+
GRANT CONNECT TO [Serilog];
138+
139+
ALTER ROLE [SerilogAutoCreate] ADD MEMBER [Serilog];
140+
ALTER ROLE [SerilogWriter] ADD MEMBER [Serilog];
141+
```
142+
143+
This creates a SQL login named `Serilog`, a database user named `Serilog`, and assigned to that user are the roles `SerilogAutoCreate` and `SerilogWriter`. As the name suggests, the SerilogAutoCreate role is not needed if you create the database ahead of time, which is the recommended course of action if you're concerned about security at this level.
144+
145+
Also, ideally the SerilogWriter role would be restricted to the log table only, and that table has to already exist for table-specific GRANT statements to execute, so that's another reason that you probably don't want to use auto-create. Table-level restrictions would look like this (assuming you name your log table SecuredLog, of course):
146+
147+
```
148+
GRANT SELECT ON [dbo].[SecuredLog] TO [SerilogWriter];
149+
GRANT SELECT ON [dbo].[SecuredLog] TO [SerilogWriter];
150+
```
151+
152+
There are many possible variations. For example, you could also create a new schema that was specific to the log(s) and restrict access that way.
153+
154+
155+
## Id Column Options
156+
157+
Previous versions of this sink assumed the Id column is always present as an `int` `IDENTITY` primary key with a clustered index. Other configurations are available, however this is still the default strictly for backwards-compatibility reasons.
158+
159+
You should consider your anticipated logging volume and query requirements carefully. The default setting is not especially useful in real-world query scenarios since a clustered index is primarily of use when the key is used for sorting or range searches, which will rarely be the case for the Id column.
160+
161+
### No Id Column
162+
163+
If you eliminate the Id column completely, the log table is stored as an unindexed heap. This is the ideal write-speed scenario for logging, however any non-clustered indexes you add will degrade write performance. One way to mitigate this is to keep the non-clustered indexes offline and use batch reindexing on a scheduled basis. If you create your table ahead of time, simply omit the Id column and the constraint shown in the previous section.
164+
165+
### Unclustered Id Column
166+
167+
You can also retain the Id column as an `IDENTITY` primary key, but without a clustered index. The log is still stored as an unindexed heap, but writes with non-clustered indexes are slightly faster. The non-clustered indexes will reference the Id primary key. However, read performance will be slightly degraded since it requires two reads (the covering non-clustered index, then dereferencing the heap row from the Id). To create this type of table ahead of time, change the constraint in the previous section to `NONCLUSTERED` and leave out the `WITH` clause.
168+
169+
### Bigint Data Type
170+
171+
For very large log tables, you may wish to create the Id column with the `bigint` datatype. This 8-byte integer will permit a maximum identity value of 9,223,372,036,854,775,807. The only change to the table syntax in the previous section is the datatype where `[Id]` is defined.
120172

121173

122174
## Standard columns
123175

124-
The "standard columns" used by this sink (apart from obvious required columns like Id) are described by the StandardColumn enumeration and controlled through code by the `columnOptions.Store` collection.
176+
The "standard columns" used by this sink are described by the `StandardColumn` enumeration and controlled through code by the `columnOptions.Store` collection. By default (and consistent with the SQL command to create a table, above) these columns are included:
125177

126-
By default (and consistent with the SQL command to create a table, above) these columns are included:
178+
- `StandardColumn.Id`
127179
- `StandardColumn.Message`
128180
- `StandardColumn.MessageTemplate`
129181
- `StandardColumn.Level`
@@ -141,10 +193,9 @@ columnOptions.Store.Remove(StandardColumn.Properties);
141193
columnOptions.Store.Add(StandardColumn.LogEvent);
142194
```
143195

144-
You can also store your own log event properties as additional columns; see below.
145-
196+
You can also store your own log event properties in additional custom columns; see below.
146197

147-
### Saving properties in additional columns
198+
### Saving properties in custom columns
148199

149200
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.
150201

@@ -153,25 +204,30 @@ var columnOptions = new ColumnOptions
153204
{
154205
AdditionalDataColumns = new Collection<DataColumn>
155206
{
156-
new DataColumn {DataType = typeof (string), ColumnName = "User"},
157-
new DataColumn {DataType = typeof (string), ColumnName = "Other"},
207+
new DataColumn {DataType = "nvarchar", ColumnName = "UserName", DataLength = 64},
208+
new DataColumn {DataType = "varchar", ColumnName = "RequestUri", DataLength = -1, AllowNull = false},
158209
}
159210
};
160211

161212
var log = new LoggerConfiguration()
162-
.WriteTo.MSSqlServer(@"Server=.\SQLEXPRESS;Database=LogEvents;Trusted_Connection=True;", "Logs", columnOptions: columnOptions)
213+
.WriteTo.MSSqlServer(@"Server=...", "Logs", columnOptions: columnOptions)
163214
.CreateLogger();
164215
```
165216

166-
The log event properties `User` and `Other` will now be placed in the corresponding column upon logging. The property name must match a column name in your table. Be sure to include them in the table definition.
217+
The log event properties `UserName` and `RequestUri` will be written to the corresponding columns whenever those values (with the exact same property name) occur in a log entry. Be sure to include them in the table definition if you create your table ahead of time.
167218

219+
Variable-length data types like `varchar` require a `DataLength` property. Use -1 to specify SQL's `MAX` length.
168220

169-
#### Excluding redundant items from the Properties column
221+
**Standard column names are reserved. Even if you exclude a standard column, never create a custom column by the same name.**
222+
223+
224+
#### Excluding redundant Properties or LogEvent data
170225

171226
By default, additional properties will still be included in the data saved to the XML Properties or JSON LogEvent column (assuming one or both are enabled via the `columnOptions.Store` parameter). This is consistent with the idea behind structured logging, and makes it easier to convert the log data to another (e.g. NoSQL) storage platform later if desired.
172227

173-
However, if necessary, then 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.
228+
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.
174229

230+
The standard columns are always excluded from the Properties and LogEvent columns.
175231

176232
### Columns defined by AppSettings (.NET Framework)
177233

@@ -219,7 +275,7 @@ The equivalent of adding custom columns as shown in the .NET Framework example a
219275
}
220276
```
221277

222-
As the name suggests, `columnOptionSection` is an entire configuration section in its own right. All possible entries and some sample values are shown below. All properties and subsections are optional.
278+
As the name suggests, `columnOptionSection` is an entire configuration section in its own right. All possible entries and some sample values are shown below. All properties and subsections are optional. It is not currently possible to specify a `PropertiesFilter` predicate in configuration.
223279

224280
```json
225281
"columnOptionsSection": {
@@ -230,7 +286,7 @@ As the name suggests, `columnOptionSection` is an entire configuration section i
230286
{ "ColumnName": "Release", "DataType": "varchar", "DataLength": 32 }
231287
],
232288
"disableTriggers": true,
233-
"id": { "columnName": "Id" },
289+
"id": { "columnName": "Id", "bigint": true, "clusteredIndex": true },
234290
"level": { "columnName": "Level", "storeAsEnum": false },
235291
"properties": {
236292
"columnName": "Properties",
@@ -256,11 +312,13 @@ As the name suggests, `columnOptionSection` is an entire configuration section i
256312
```
257313

258314

259-
### Options for serialization of the log event data
315+
### Options for serialization of event data
316+
317+
Typically you will choose either XML or JSON serialization, but not both.
260318

261319
#### JSON (LogEvent column)
262320

263-
The log event JSON 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.
321+
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.
264322

265323
#### XML (Properties column)
266324

@@ -276,7 +334,7 @@ If `OmitDictionaryContainerElement`, `OmitSequenceContainerElement` or `OmitStru
276334

277335
If `OmitElementIfEmpty` is set then if a property is empty, it will not be serialized.
278336

279-
##### Querying the properties XML
337+
##### Querying the Properties XML data
280338

281339
Extracting and querying the properties data directly can be helpful when looking for specific log sequences.
282340

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,15 @@ private static ColumnOptions ConfigureColumnOptions(ColumnOptions columnOptions,
216216

217217
SetIfProvided<bool>((val) => { opts.DisableTriggers = val; }, config["disableTriggers"]);
218218

219-
SetIfProvided<string>((val) => { opts.Id.ColumnName = val; }, config.GetSection("id")["columnName"]);
219+
var section = config.GetSection("id");
220+
if (section.GetChildren().Any())
221+
{
222+
SetIfProvided<string>((val) => { opts.Id.ColumnName = val; }, section["columnName"]);
223+
SetIfProvided<bool>((val) => { opts.Id.BigInt = val; }, section["bigInt"]);
224+
SetIfProvided<bool>((val) => { opts.Id.ClusteredIndex = val; }, section["clusteredIndex"]);
225+
}
220226

221-
var section = config.GetSection("level");
227+
section = config.GetSection("level");
222228
if(section.GetChildren().Any())
223229
{
224230
SetIfProvided<string>((val) => { opts.Level.ColumnName = val; }, section["columnName"]);

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ public class ColumnOptions
1717
/// </summary>
1818
public ColumnOptions()
1919
{
20-
Id = new IdColumnOptions();
20+
Id = new IdColumnOptions
21+
{
22+
// Defaults for backwards compatibility only; not recommended, see docs
23+
BigInt = false,
24+
ClusteredIndex = true
25+
};
2126

2227
Level = new LevelColumnOptions();
2328

2429
Properties = new PropertiesColumnOptions();
2530

2631
Store = new Collection<StandardColumn>
2732
{
33+
StandardColumn.Id,
2834
StandardColumn.Message,
2935
StandardColumn.MessageTemplate,
3036
StandardColumn.Level,
@@ -116,7 +122,19 @@ public ICollection<StandardColumn> Store
116122
/// <summary>
117123
/// Options for the Id column.
118124
/// </summary>
119-
public class IdColumnOptions : CommonColumnOptions { }
125+
public class IdColumnOptions : CommonColumnOptions
126+
{
127+
/// <summary>
128+
/// If true, stores as bigint (max value 2^64-1) instead of the default int.
129+
/// </summary>
130+
public bool BigInt { get; set; }
131+
132+
/// <summary>
133+
/// If false, the log table will be stored as a heap structure. This will improve
134+
/// write performance at the cost of query performance. See documentation for details.
135+
/// </summary>
136+
public bool ClusteredIndex { get; set; }
137+
}
120138

121139
/// <summary>
122140
/// Options for the Level column.

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

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ public MSSqlServerSinkTraits(string connectionString, string tableName, string s
5151
ColumnOptions = columnOptions ?? new ColumnOptions();
5252
FormatProvider = formatProvider;
5353

54-
ExcludedColumnNames = new List<string>(ColumnOptions.Store.Count + 1);
55-
ExcludedColumnNames.Add(ColumnOptions.Id.ColumnName ?? "Id");
54+
ExcludedColumnNames = new List<string>(ColumnOptions.Store.Count);
55+
if (ColumnOptions.Store.Contains(StandardColumn.Id)) ExcludedColumnNames.Add(ColumnOptions.Id.ColumnName ?? "Id");
5656
if (ColumnOptions.Store.Contains(StandardColumn.Message)) ExcludedColumnNames.Add(ColumnOptions.Message.ColumnName ?? "Message");
5757
if (ColumnOptions.Store.Contains(StandardColumn.MessageTemplate)) ExcludedColumnNames.Add(ColumnOptions.MessageTemplate.ColumnName ?? "MessageTemplate");
5858
if (ColumnOptions.Store.Contains(StandardColumn.Level)) ExcludedColumnNames.Add(ColumnOptions.Level.ColumnName ?? "Level");
@@ -93,7 +93,9 @@ public IEnumerable<KeyValuePair<string, object>> GetColumnsAndValues(LogEvent lo
9393
{
9494
foreach (var column in ColumnOptions.Store)
9595
{
96-
yield return GetStandardColumnNameAndValue(column, logEvent);
96+
// never write to Id since it will be auto-incrementing (IDENTITY)
97+
if(column != StandardColumn.Id)
98+
yield return GetStandardColumnNameAndValue(column, logEvent);
9799
}
98100

99101
if (ColumnOptions.AdditionalDataColumns != null)
@@ -254,18 +256,26 @@ private DataTable CreateDataTable()
254256
{
255257
var eventsTable = new DataTable(TableName);
256258

257-
var id = new DataColumn
258-
{
259-
DataType = typeof(Int32),
260-
ColumnName = !string.IsNullOrWhiteSpace(ColumnOptions.Id.ColumnName) ? ColumnOptions.Id.ColumnName : "Id",
261-
AutoIncrement = true
262-
};
263-
eventsTable.Columns.Add(id);
264-
265259
foreach (var standardColumn in ColumnOptions.Store)
266260
{
267261
switch (standardColumn)
268262
{
263+
case StandardColumn.Id:
264+
var id = new DataColumn
265+
{
266+
DataType = ColumnOptions.Id.BigInt ? typeof(long) : typeof(int),
267+
ColumnName = ColumnOptions.Id.ColumnName ?? StandardColumn.Id.ToString(),
268+
AutoIncrement = true
269+
};
270+
eventsTable.Columns.Add(id);
271+
id.ExtendedProperties.Add("clusteredIndex", false);
272+
if(ColumnOptions.Id.ClusteredIndex)
273+
{
274+
id.ExtendedProperties["clusteredIndex"] = true;
275+
eventsTable.PrimaryKey = new DataColumn[] { id };
276+
}
277+
break;
278+
269279
case StandardColumn.Level:
270280
eventsTable.Columns.Add(new DataColumn
271281
{
@@ -329,11 +339,6 @@ private DataTable CreateDataTable()
329339
eventsTable.Columns.AddRange(ColumnOptions.AdditionalDataColumns.ToArray());
330340
}
331341

332-
// Create an array for DataColumn objects.
333-
var keys = new DataColumn[1];
334-
keys[0] = id;
335-
eventsTable.PrimaryKey = keys;
336-
337342
return eventsTable;
338343
}
339344

0 commit comments

Comments
 (0)