Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Truncate additional columns, allow varchar on standard columns #490

Merged
merged 4 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ The content of this column is rendered as JSON by default or with a custom IText

## Custom Property Columns

By default, any log event properties you include in your log statements will be saved to the XML `Properties` column or the JSON `LogEvent` column. But they can also be stored in their own individual columns via the `AdditionalColumns` collection. This adds overhead to write operations but is very useful for frequently-queried properties. Only `ColumnName` is required; the default configuration is `varchar(max)`.
By default, any log event properties you include in your log statements will be saved to the XML `Properties` column or the JSON `LogEvent` column. But they can also be stored in their own individual columns via the `AdditionalColumns` collection. This adds overhead to write operations but is very useful for frequently-queried properties. Only `ColumnName` is required; the default configuration is `varchar(max)`. If you specify a DataLength on a column of character data types (NVarChar, VarChar, Char, NChar) the string will be automatically truncated to the datalength to fit in the column.

```csharp
var columnOptions = new ColumnOptions
Expand Down
5 changes: 5 additions & 0 deletions src/Serilog.Sinks.MSSqlServer/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ namespace Serilog.Sinks.MSSqlServer.Extensions
{
internal static class StringExtensions
{
public static string TruncateOutput(this string value, int dataLength) =>
dataLength < 0
? value // No need to truncate if length set to maximum
: value.Truncate(dataLength, "...");

public static string Truncate(this string value, int maxLength, string suffix)
{
if (value == null) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public ExceptionColumnOptions() : base()
get => base.DataType;
set
{
if (value != SqlDbType.NVarChar)
throw new ArgumentException("The Standard Column \"Exception\" must be NVarChar.");
if (!SqlDataTypes.VariableCharacterColumnTypes.Contains(value))
throw new ArgumentException("The Standard Column \"Exception\" must be NVarChar or VarChar.");
base.DataType = value;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public LevelColumnOptions() : base()
get => base.DataType;
set
{
if (value != SqlDbType.NVarChar && value != SqlDbType.TinyInt)
throw new ArgumentException("The Standard Column \"Level\" must be of data type NVarChar or TinyInt.");
if (!SqlDataTypes.VariableCharacterColumnTypes.Contains(value) && value != SqlDbType.TinyInt)
throw new ArgumentException("The Standard Column \"Level\" must be of data type NVarChar, VarChar or TinyInt.");
base.DataType = value;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public LogEventColumnOptions() : base()
get => base.DataType;
set
{
if (value != SqlDbType.NVarChar)
throw new ArgumentException("The Standard Column \"LogEvent\" must be NVarChar.");
if (!SqlDataTypes.VariableCharacterColumnTypes.Contains(value))
throw new ArgumentException("The Standard Column \"LogEvent\" must be NVarChar or VarChar.");
base.DataType = value;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public MessageColumnOptions() : base()
get => base.DataType;
set
{
if (value != SqlDbType.NVarChar)
throw new ArgumentException("The Standard Column \"Message\" must be NVarChar.");
if (!SqlDataTypes.VariableCharacterColumnTypes.Contains(value))
throw new ArgumentException("The Standard Column \"Message\" must be NVarChar or VarChar.");
base.DataType = value;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public MessageTemplateColumnOptions() : base()
get => base.DataType;
set
{
if (value != SqlDbType.NVarChar)
throw new ArgumentException("The Standard Column \"MessageTemplate\" must be NVarChar.");
if (!SqlDataTypes.VariableCharacterColumnTypes.Contains(value))
throw new ArgumentException("The Standard Column \"MessageTemplate\" must be NVarChar or VarChar.");
base.DataType = value;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer.Extensions;

namespace Serilog.Sinks.MSSqlServer.Output
{
Expand Down Expand Up @@ -45,6 +46,11 @@ public KeyValuePair<string, object> GetAdditionalColumnNameAndValue(SqlColumn ad
var columnType = additionalColumn.AsDataColumn().DataType;
if (columnType.IsAssignableFrom(scalarValue.Value.GetType()))
{
if (SqlDataTypes.DataLengthRequired.Contains(additionalColumn.DataType))
{
return new KeyValuePair<string, object>(columnName, scalarValue.Value.ToString().TruncateOutput(additionalColumn.DataLength));

}
return new KeyValuePair<string, object>(columnName, scalarValue.Value);
}

Expand All @@ -54,6 +60,9 @@ public KeyValuePair<string, object> GetAdditionalColumnNameAndValue(SqlColumn ad
}
else
{
if (additionalColumn.AllowNull) {
return new KeyValuePair<string, object>(columnName, DBNull.Value);
}
return new KeyValuePair<string, object>(columnName, property.Value.ToString());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ public KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColumn
switch (column)
{
case StandardColumn.Message:
return new KeyValuePair<string, object>(_columnOptions.Message.ColumnName, TruncateOutput(logEvent.RenderMessage(_formatProvider), _columnOptions.Message.DataLength));
return new KeyValuePair<string, object>(_columnOptions.Message.ColumnName, logEvent.RenderMessage(_formatProvider).TruncateOutput(_columnOptions.Message.DataLength));
case StandardColumn.MessageTemplate:
return new KeyValuePair<string, object>(_columnOptions.MessageTemplate.ColumnName, TruncateOutput(logEvent.MessageTemplate.Text, _columnOptions.MessageTemplate.DataLength));
return new KeyValuePair<string, object>(_columnOptions.MessageTemplate.ColumnName, logEvent.MessageTemplate.Text.TruncateOutput(_columnOptions.MessageTemplate.DataLength));
case StandardColumn.Level:
return new KeyValuePair<string, object>(_columnOptions.Level.ColumnName, _columnOptions.Level.StoreAsEnum ? (object)logEvent.Level : logEvent.Level.ToString());
case StandardColumn.TimeStamp:
return GetTimeStampStandardColumnNameAndValue(logEvent);
case StandardColumn.Exception:
return new KeyValuePair<string, object>(_columnOptions.Exception.ColumnName, TruncateOutput(logEvent.Exception?.ToString(), _columnOptions.Exception.DataLength));
return new KeyValuePair<string, object>(_columnOptions.Exception.ColumnName, logEvent.Exception?.ToString().TruncateOutput(_columnOptions.Exception.DataLength));
case StandardColumn.Properties:
return new KeyValuePair<string, object>(_columnOptions.Properties.ColumnName, ConvertPropertiesToXmlStructure(logEvent.Properties));
case StandardColumn.LogEvent:
Expand All @@ -63,11 +63,6 @@ public KeyValuePair<string, object> GetStandardColumnNameAndValue(StandardColumn
}
}

private static string TruncateOutput(string value, int dataLength) =>
dataLength < 0
? value // No need to truncate if length set to maximum
: value.Truncate(dataLength, "...");

private KeyValuePair<string, object> GetTimeStampStandardColumnNameAndValue(LogEvent logEvent)
{
var dateTimeOffset = _columnOptions.TimeStamp.ConvertToUtc ? logEvent.Timestamp.ToUniversalTime() : logEvent.Timestamp;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;

namespace Serilog.Sinks.MSSqlServer
Expand Down Expand Up @@ -51,6 +52,14 @@ public static class SqlDataTypes
// not supported by enum: numeric, FILESTREAM, rowversion
};

/// <summary>
/// SQL column types for supported strings
/// </summary>
public static readonly ReadOnlyCollection<SqlDbType> VariableCharacterColumnTypes = new ReadOnlyCollection<SqlDbType>(new List<SqlDbType> {
SqlDbType.NVarChar,
SqlDbType.VarChar
});

/// <summary>
/// The SQL column types which require a non-zero DataLength property.
/// </summary>
Expand Down
7 changes: 5 additions & 2 deletions test/Serilog.Sinks.MSSqlServer.Tests/Misc/SqlTypesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,12 @@ public void AuditLogCharacterDataSqlTypes()
Log.Information("NVarChar {NVarChar}", twentyChars);
Log.Information("VarChar {VarChar}", twentyChars);

// should throw truncation exception
// should truncate but not throw

Assert.Throws<AggregateException>(() => Log.Information("Char {Char}", thirtyChars));
Log.Information("Char {Char}", thirtyChars);
Log.Information("NChar {NChar}", thirtyChars);
Log.Information("NVarChar {NVarChar}", thirtyChars);
Log.Information("VarChar {VarChar}", thirtyChars);

Log.CloseAndFlush();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public void GetAdditionalColumnNameAndValueReturnsNullForNotFoundHierachicalProp
}

[Fact]
public void GetAdditionalColumnNameAndValueConvertsValueTypeToStringIfConversionToColumnTypeFails()
public void GetAdditionalColumnNameAndValueUsesNullIfConversionToColumnTypeFails()
{
// Arrange
const string columnName = "AdditionalProperty1";
Expand All @@ -131,8 +131,8 @@ public void GetAdditionalColumnNameAndValueConvertsValueTypeToStringIfConversion
// Assert
_columnSimplePropertyValueResolver.Verify(r => r.GetPropertyValueForColumn(additionalColumn, properties), Times.Once);
Assert.Equal(columnName, result.Key);
Assert.IsType<string>(result.Value);
Assert.Equal("1", result.Value); // Cannot convert int to SqlDbType.DateTimeOffset so returns string
Assert.IsType<DBNull>(result.Value);
Assert.Equal(DBNull.Value, result.Value); // Cannot convert int to SqlDbType.DateTimeOffset so returns null
}

[Fact]
Expand Down Expand Up @@ -180,5 +180,27 @@ public void GetAdditionalColumnNameAndValueConvertsNullValueForNullable()
Assert.IsType<DBNull>(result.Value);
Assert.Equal(DBNull.Value, result.Value);
}

[Fact]
public void GetAdditionalColumnNameAndValueReturnsTruncatedForCharacterTypesWithDataLength()
{
// Arrange
const string columnName = "AdditionalProperty1";
const string propertyValue = "Additional Property Value";
var additionalColumn = new SqlColumn(columnName, SqlDbType.NVarChar);
additionalColumn.DataLength = 10;
var properties = new Dictionary<string, LogEventPropertyValue>();
_columnSimplePropertyValueResolver.Setup(r => r.GetPropertyValueForColumn(
It.IsAny<SqlColumn>(), It.IsAny<IReadOnlyDictionary<string, LogEventPropertyValue>>()))
.Returns(new KeyValuePair<string, LogEventPropertyValue>(columnName, new ScalarValue(propertyValue)));

// Act
var result = _sut.GetAdditionalColumnNameAndValue(additionalColumn, properties);

// Assert
_columnSimplePropertyValueResolver.Verify(r => r.GetPropertyValueForColumn(additionalColumn, properties), Times.Once);
Assert.Equal(columnName, result.Key);
Assert.Equal("Additio...", result.Value);
}
}
}