Skip to content

Commit bcc396e

Browse files
authored
Merge pull request #141 from MV10/issue_69
Make Id optional, allow nonclustered PK, allow bigint Id
2 parents 993a91e + 9f041ee commit bcc396e

File tree

6 files changed

+153
-59
lines changed

6 files changed

+153
-59
lines changed

README.md

Lines changed: 89 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,83 @@ 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+
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).
122+
123+
SQL permissions are a very complex subject. Here is an example of one possible solution (valid for SQL 2012 or later):
124+
125+
```
126+
CREATE ROLE [SerilogAutoCreate];
127+
GRANT SELECT ON sys.tables TO [SerilogAutoCreate];
128+
GRANT SELECT ON sys.schemas TO [SerilogAutoCreate];
129+
GRANT ALTER ON SCHEMA::[dbo] TO [SerilogAutoCreate]
130+
GRANT CREATE TABLE ON DATABASE::[SerilogTest] TO [SerilogAutoCreate];
131+
132+
CREATE ROLE [SerilogWriter];
133+
GRANT SELECT TO [SerilogWriter];
134+
GRANT INSERT TO [SerilogWriter];
135+
136+
CREATE LOGIN [Serilog] WITH PASSWORD = 'password';
137+
138+
CREATE USER [Serilog] FOR LOGIN [Serilog] WITH DEFAULT_SCHEMA = dbo;
139+
GRANT CONNECT TO [Serilog];
140+
141+
ALTER ROLE [SerilogAutoCreate] ADD MEMBER [Serilog];
142+
ALTER ROLE [SerilogWriter] ADD MEMBER [Serilog];
143+
```
144+
145+
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.
146+
147+
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):
148+
149+
```
150+
GRANT SELECT ON [dbo].[SecuredLog] TO [SerilogWriter];
151+
GRANT SELECT ON [dbo].[SecuredLog] TO [SerilogWriter];
152+
```
153+
154+
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.
155+
156+
157+
## Id Column Options
158+
159+
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 for backwards-compatibility reasons.
160+
161+
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.
162+
163+
### No Id Column
164+
165+
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 slightly 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.
166+
167+
### Unclustered Id Column
168+
169+
You can also retain the Id column as an `IDENTITY` primary key, but using a non-clustered index. The log is still stored as an unindexed heap, but writes with non-clustered indexes are slightly faster. Non-clustered indexes on other columns 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.
120170

171+
### Bigint Data Type
172+
173+
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. This will slightly degrade both read and write performance.
174+
175+
## Batch Size and Performance
176+
177+
This is a "periodic batching sink." This means the sink will queue a certain number of log events before they're actually written to SQL Server as a bulk insert operation. There is also a timeout so that the batch is always written even if it has not been filled. By default, the batch size is 50 and the timeout is 5 seconds. You can change these through configuration.
178+
179+
Consider increasing the batch size in high-volume logging environments. In one test of a loop writing a single log entry to a local server instance (no network traffic), the default batch achieved around 14,000 rows per second. Increasing the batch size to 1000 rows increased write speed to nearly 43,000 rows per second. However, you should also consider the risk-factor. If the server crashes or the connection goes down, you may lose an entire batch of log entries. You can mitigate this by reducing the timeout. Run performance tests to find the optimal batch size for your production log content, network setup, and server configuration.
121180

122181
## Standard columns
123182

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.
183+
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:
125184

126-
By default (and consistent with the SQL command to create a table, above) these columns are included:
185+
- `StandardColumn.Id`
127186
- `StandardColumn.Message`
128187
- `StandardColumn.MessageTemplate`
129188
- `StandardColumn.Level`
@@ -141,10 +200,9 @@ columnOptions.Store.Remove(StandardColumn.Properties);
141200
columnOptions.Store.Add(StandardColumn.LogEvent);
142201
```
143202

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

146-
147-
### Saving properties in additional columns
205+
### Saving properties in custom columns
148206

149207
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.
150208

@@ -153,25 +211,30 @@ var columnOptions = new ColumnOptions
153211
{
154212
AdditionalDataColumns = new Collection<DataColumn>
155213
{
156-
new DataColumn {DataType = typeof (string), ColumnName = "User"},
157-
new DataColumn {DataType = typeof (string), ColumnName = "Other"},
214+
new DataColumn {DataType = typeof(string), ColumnName = "UserName", DataLength = 64},
215+
new DataColumn {DataType = typeof(string), ColumnName = "RequestUri", DataLength = -1, AllowNull = false},
158216
}
159217
};
160218

161219
var log = new LoggerConfiguration()
162-
.WriteTo.MSSqlServer(@"Server=.\SQLEXPRESS;Database=LogEvents;Trusted_Connection=True;", "Logs", columnOptions: columnOptions)
220+
.WriteTo.MSSqlServer(@"Server=...", "Logs", columnOptions: columnOptions)
163221
.CreateLogger();
164222
```
165223

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.
224+
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.
225+
226+
When configuring through code, set the `DataType` property to a .NET type. When configuring through XML, JSON or other settings packages, specify a SQL data type. It will be internally converted to an equivalent .NET type. Variable-length data types like `string` and `varchar` require a `DataLength` property. Use -1 to specify SQL's `MAX` length.
167227

228+
**Standard column names are reserved. Even if you exclude a standard column, never create a custom column by the same name.**
168229

169-
#### Excluding redundant items from the Properties column
230+
231+
#### Excluding redundant Properties or LogEvent data
170232

171233
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.
172234

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.
235+
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.
174236

237+
The standard columns are always excluded from the Properties and LogEvent columns.
175238

176239
### Columns defined by AppSettings (.NET Framework)
177240

@@ -219,7 +282,7 @@ The equivalent of adding custom columns as shown in the .NET Framework example a
219282
}
220283
```
221284

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.
285+
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.
223286

224287
```json
225288
"columnOptionsSection": {
@@ -230,7 +293,7 @@ As the name suggests, `columnOptionSection` is an entire configuration section i
230293
{ "ColumnName": "Release", "DataType": "varchar", "DataLength": 32 }
231294
],
232295
"disableTriggers": true,
233-
"id": { "columnName": "Id" },
296+
"id": { "columnName": "Id", "bigint": true, "nonClusteredIndex": true },
234297
"level": { "columnName": "Level", "storeAsEnum": false },
235298
"properties": {
236299
"columnName": "Properties",
@@ -256,11 +319,13 @@ As the name suggests, `columnOptionSection` is an entire configuration section i
256319
```
257320

258321

259-
### Options for serialization of the log event data
322+
### Options for serialization of event data
323+
324+
Typically you will choose either XML or JSON serialization, but not both.
260325

261326
#### JSON (LogEvent column)
262327

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.
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.
264329

265330
#### XML (Properties column)
266331

@@ -276,7 +341,7 @@ If `OmitDictionaryContainerElement`, `OmitSequenceContainerElement` or `OmitStru
276341

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

279-
##### Querying the properties XML
344+
##### Querying the Properties XML data
280345

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

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

Lines changed: 11 additions & 5 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.NonClusteredIndex = val; }, section["nonClusteredIndex"]);
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"]);
@@ -258,11 +264,11 @@ private static ColumnOptions ConfigureColumnOptions(ColumnOptions columnOptions,
258264
SetIfProvided<bool>((val) => { opts.LogEvent.ExcludeAdditionalProperties = val; }, section["excludeAdditionalProperties"]);
259265
}
260266

261-
SetIfProvided<string>((val) => { opts.Id.ColumnName = val; }, config.GetSection("message")["columnName"]);
267+
SetIfProvided<string>((val) => { opts.Message.ColumnName = val; }, config.GetSection("message")["columnName"]);
262268

263-
SetIfProvided<string>((val) => { opts.Id.ColumnName = val; }, config.GetSection("exception")["columnName"]);
269+
SetIfProvided<string>((val) => { opts.Exception.ColumnName = val; }, config.GetSection("exception")["columnName"]);
264270

265-
SetIfProvided<string>((val) => { opts.Id.ColumnName = val; }, config.GetSection("messageTemplate")["columnName"]);
271+
SetIfProvided<string>((val) => { opts.MessageTemplate.ColumnName = val; }, config.GetSection("messageTemplate")["columnName"]);
266272

267273
return opts;
268274

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

Lines changed: 22 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, see docs
23+
BigInt = false,
24+
NonClusteredIndex = false
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,13 @@ 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+
}
120132

121133
/// <summary>
122134
/// Options for the Level column.
@@ -223,6 +235,14 @@ public class CommonColumnOptions
223235
/// The name of the column in the database.
224236
/// </summary>
225237
public string ColumnName { get; set; }
238+
239+
/// <summary>
240+
/// Currently only implemented for the optional Id IDENTITY PK column. Defaults to false.
241+
/// If true, the log table will be stored as a heap structure. This can improve
242+
/// write performance at the cost of query performance. See documentation for details.
243+
/// </summary>
244+
public bool NonClusteredIndex { get; set; }
245+
226246
}
227247

228248
/// <summary>

0 commit comments

Comments
 (0)