Skip to content

Commit

Permalink
Merge pull request serilog#826 from nblumhardt/f-audit
Browse files Browse the repository at this point in the history
Audit-style logging
  • Loading branch information
nblumhardt authored Aug 15, 2016
2 parents 2d88aa6 + 3089b5f commit ffb7ce4
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 38 deletions.
106 changes: 106 additions & 0 deletions src/Serilog/Configuration/LoggerAuditSinkConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2016 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using Serilog.Core;
using Serilog.Events;

namespace Serilog.Configuration
{
/// <summary>
/// Controls sink configuration.
/// </summary>
public class LoggerAuditSinkConfiguration
{
LoggerSinkConfiguration _sinkConfiguration;

internal LoggerAuditSinkConfiguration(LoggerConfiguration loggerConfiguration, Action<ILogEventSink> addSink, Action<LoggerConfiguration> applyInheritedConfiguration)
{
_sinkConfiguration = new LoggerSinkConfiguration(loggerConfiguration, addSink, applyInheritedConfiguration);
}

/// <summary>
/// Audit log events to the specified <see cref="ILogEventSink"/>.
/// </summary>
/// <param name="logEventSink">The sink.</param>
/// <param name="restrictedToMinimumLevel">The minimum level for
/// events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified.</param>
/// <param name="levelSwitch">A switch allowing the pass-through minimum level
/// to be changed at runtime.</param>
/// <returns>Configuration object allowing method chaining.</returns>
public LoggerConfiguration Sink(
ILogEventSink logEventSink,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
// ReSharper disable once MethodOverloadWithOptionalParameter
LoggingLevelSwitch levelSwitch = null)
{
return _sinkConfiguration.Sink(logEventSink, restrictedToMinimumLevel, levelSwitch);
}

/// <summary>
/// Audit log events to the specified <see cref="ILogEventSink"/>.
/// </summary>
/// <typeparam name="TSink">The sink.</typeparam>
/// <param name="restrictedToMinimumLevel">The minimum level for
/// events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified.</param>
/// <param name="levelSwitch">A switch allowing the pass-through minimum level
/// to be changed at runtime.</param>
/// <returns>Configuration object allowing method chaining.</returns>
public LoggerConfiguration Sink<TSink>(
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
LoggingLevelSwitch levelSwitch = null)
where TSink : ILogEventSink, new()
{
return _sinkConfiguration.Sink<TSink>(restrictedToMinimumLevel, levelSwitch);
}

/// <summary>
/// Audit log events to a sub-logger, where further processing may occur. Events through
/// the sub-logger will be constrained by filters and enriched by enrichers that are
/// active in the parent. A sub-logger cannot be used to log at a more verbose level, but
/// a less verbose level is possible.
/// </summary>
/// <param name="configureLogger">An action that configures the sub-logger.</param>
/// <param name="restrictedToMinimumLevel">The minimum level for
/// events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified.</param>
/// <param name="levelSwitch">A switch allowing the pass-through minimum level
/// to be changed at runtime.</param>
/// <returns>Configuration object allowing method chaining.</returns>
public LoggerConfiguration Logger(
Action<LoggerConfiguration> configureLogger,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
LoggingLevelSwitch levelSwitch = null)
{
return _sinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch);
}

/// <summary>
/// Audit log events to a sub-logger, where further processing may occur. Events through
/// the sub-logger will be constrained by filters and enriched by enrichers that are
/// active in the parent. A sub-logger cannot be used to log at a more verbose level, but
/// a less verbose level is possible.
/// </summary>
/// <param name="logger">The sub-logger. This will <em>not</em> be shut down automatically when the
/// parent logger is disposed.</param>
/// <param name="restrictedToMinimumLevel">The minimum level for
/// events passed through the sink.</param>
/// <returns>Configuration object allowing method chaining.</returns>
public LoggerConfiguration Logger(
ILogger logger,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum)
{
return _sinkConfiguration.Logger(logger, restrictedToMinimumLevel);
}
}
}
7 changes: 1 addition & 6 deletions src/Serilog/Configuration/LoggerSinkConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

using System;
using System.ComponentModel;

using Serilog.Core;
using Serilog.Core.Sinks;
using Serilog.Debugging;
Expand All @@ -31,8 +30,6 @@ public class LoggerSinkConfiguration
readonly Action<ILogEventSink> _addSink;
readonly Action<LoggerConfiguration> _applyInheritedConfiguration;

const string DefaultOutputTemplate = "{Timestamp} [{Level}] {Message}{NewLine}{Exception}";

internal LoggerSinkConfiguration(LoggerConfiguration loggerConfiguration, Action<ILogEventSink> addSink, Action<LoggerConfiguration> applyInheritedConfiguration)
{
if (loggerConfiguration == null) throw new ArgumentNullException(nameof(loggerConfiguration));
Expand All @@ -51,7 +48,7 @@ internal LoggerSinkConfiguration(LoggerConfiguration loggerConfiguration, Action
/// events passed through the sink.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>Provided for binary compatibility for earlier versions,
/// should be removed in 2.0. Not marked obsolete because warnings
/// should be removed in 3.0. Not marked obsolete because warnings
/// would be syntactically annoying to avoid.</remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public LoggerConfiguration Sink(
Expand Down Expand Up @@ -110,8 +107,6 @@ public LoggerConfiguration Sink<TSink>(
return Sink(new TSink(), restrictedToMinimumLevel, levelSwitch);
}



/// <summary>
/// Write log events to a sub-logger, where further processing may occur. Events through
/// the sub-logger will be constrained by filters and enriched by enrichers that are
Expand Down
54 changes: 54 additions & 0 deletions src/Serilog/Core/Sinks/AggregateSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2016 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using Serilog.Debugging;
using Serilog.Events;

namespace Serilog.Core.Sinks
{
class AggregateSink : ILogEventSink
{
readonly ILogEventSink[] _sinks;

public AggregateSink(IEnumerable<ILogEventSink> sinks)
{
if (sinks == null) throw new ArgumentNullException(nameof(sinks));
_sinks = sinks.ToArray();
}

public void Emit(LogEvent logEvent)
{
List<Exception> exceptions = null;
foreach (var sink in _sinks)
{
try
{
sink.Emit(logEvent);
}
catch (Exception ex)
{
SelfLog.WriteLine("Caught exception while emitting to sink {0}: {1}", sink, ex);
exceptions = exceptions ?? new List<Exception>();
exceptions.Add(ex);
}
}

if (exceptions != null)
throw new AggregateException("Failed to emit a log event.", exceptions);
}
}
}
9 changes: 7 additions & 2 deletions src/Serilog/Core/Sinks/FilteringSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ namespace Serilog.Core.Sinks
class FilteringSink : ILogEventSink
{
readonly ILogEventSink _sink;
readonly bool _propagateExceptions;
readonly ILogEventFilter[] _filters;

public FilteringSink(ILogEventSink sink, IEnumerable<ILogEventFilter> filters)
public FilteringSink(ILogEventSink sink, IEnumerable<ILogEventFilter> filters, bool propagateExceptions)
{
if (sink == null) throw new ArgumentNullException(nameof(sink));
if (filters == null) throw new ArgumentNullException(nameof(filters));
_sink = sink;
_propagateExceptions = propagateExceptions;
_filters = filters.ToArray();
}

Expand All @@ -47,7 +49,10 @@ public void Emit(LogEvent logEvent)
}
catch (Exception ex)
{
SelfLog.WriteLine("Caught exception {0} while applying filters.", ex);
SelfLog.WriteLine("Caught exception while applying filters: {0}", ex);

if (_propagateExceptions)
throw;
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/Serilog/Core/Sinks/SafeAggregateSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Serilog.Debugging;
using Serilog.Events;

namespace Serilog.Core.Sinks
{
class SafeAggregateSink : ILogEventSink
{
readonly IEnumerable<ILogEventSink> _sinks;
readonly ILogEventSink[] _sinks;

public SafeAggregateSink(IEnumerable<ILogEventSink> sinks)
{
if (sinks == null) throw new ArgumentNullException(nameof(sinks));
_sinks = sinks;
_sinks = sinks.ToArray();
}

public void Emit(LogEvent logEvent)
Expand All @@ -39,10 +40,9 @@ public void Emit(LogEvent logEvent)
}
catch (Exception ex)
{
SelfLog.WriteLine("Caught exception {0} while emitting to sink {1}.", ex, sink);
SelfLog.WriteLine("Caught exception while emitting to sink {0}: {1}", sink, ex);
}
}
}
}
}

48 changes: 33 additions & 15 deletions src/Serilog/LoggerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace Serilog
public class LoggerConfiguration
{
readonly List<ILogEventSink> _logEventSinks = new List<ILogEventSink>();
readonly List<ILogEventSink> _auditSinks = new List<ILogEventSink>();
readonly List<ILogEventEnricher> _enrichers = new List<ILogEventEnricher>();
readonly List<ILogEventFilter> _filters = new List<ILogEventFilter>();
readonly List<Type> _additionalScalarTypes = new List<Type>();
Expand All @@ -40,22 +41,31 @@ public class LoggerConfiguration
int _maximumDestructuringDepth = 10;
bool _loggerCreated;

void ApplyInheritedConfiguration(LoggerConfiguration child)
{
if (_levelSwitch != null)
child.MinimumLevel.ControlledBy(_levelSwitch);
else
child.MinimumLevel.Is(_minimumLevel);
}

/// <summary>
/// Configures the sinks that log events will be emitted to.
/// </summary>
public LoggerSinkConfiguration WriteTo
{
get
{
return new LoggerSinkConfiguration(this, s => _logEventSinks.Add(s), child =>
{
if (_levelSwitch != null)
child.MinimumLevel.ControlledBy(_levelSwitch);
else
child.MinimumLevel.Is(_minimumLevel);
});
}
}
public LoggerSinkConfiguration WriteTo => new LoggerSinkConfiguration(this, s => _logEventSinks.Add(s), ApplyInheritedConfiguration);

/// <summary>
/// Configures sinks for auditing, instead of regular (safe) logging. When auditing is used,
/// exceptions from sinks and any intermediate filters propagate back to the caller. Most callers
/// should use <see cref="WriteTo"/> instead.
/// </summary>
/// <remarks>
/// Not all sinks are compatible with transactional auditing requirements (many will use asynchronous
/// batching to improve write throughput and latency). Sinks need to opt-in to auditing support by
/// extending <see cref="LoggerAuditSinkConfiguration"/>, though the generic <see cref="LoggerAuditSinkConfiguration.Sink"/>
/// method allows any sink class to be adapted for auditing.
/// </remarks>
public LoggerAuditSinkConfiguration AuditTo => new LoggerAuditSinkConfiguration(this, s => _auditSinks.Add(s), ApplyInheritedConfiguration);

/// <summary>
/// Configures the minimum level at which events will be passed to sinks. If
Expand Down Expand Up @@ -129,10 +139,18 @@ public Logger CreateLogger()

ILogEventSink sink = new SafeAggregateSink(_logEventSinks);

var auditing = _auditSinks.Any();
if (auditing)
sink = new AggregateSink(new[] { sink }.Concat(_auditSinks));

if (_filters.Any())
sink = new FilteringSink(sink, _filters);
{
// A throwing filter could drop an auditable event, so exceptions in filters must be propagated
// if auditing is used.
sink = new FilteringSink(sink, _filters, auditing);
}

var converter = new PropertyValueConverter(_maximumDestructuringDepth, _additionalScalarTypes, _additionalDestructuringPolicies);
var converter = new PropertyValueConverter(_maximumDestructuringDepth, _additionalScalarTypes, _additionalDestructuringPolicies, auditing);
var processor = new MessageTemplateProcessor(converter);

ILogEventEnricher enricher;
Expand Down
Loading

0 comments on commit ffb7ce4

Please sign in to comment.