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

Exception handling #372

Closed
Meteoeoeo opened this issue Feb 18, 2021 · 7 comments
Closed

Exception handling #372

Meteoeoeo opened this issue Feb 18, 2021 · 7 comments
Assignees

Comments

@Meteoeoeo
Copy link

Is it possible to add an exception handling configuration? Currently, if something doesn't work, it affects the process in the application. It would be good to be able to configure it so that any errors would not affect the process, e.g. handling the request.

@thepirat000
Copy link
Owner

Could you elaborate a little bit more about your use case? which extensions are you using? what are you logging? And what exactly means 'any errors'? Errors when generating the audit, errors saving the audit? inherent errors of the audited process?

@Meteoeoeo
Copy link
Author

Meteoeoeo commented Feb 18, 2021

i'm using Audit.Net, Audit.WebApi.Core and Audit.NET.SqlServer.

It would be best if I could set custom error handling on the middleware - eg pass an action, delegate or function, in which I could save the exception to the logs and continue process, or throw it higher and break current process (default behavior).

For Audit.NET.SqlServer I would like log and set another provider when error.

I know that i can use decorator or add some custom provider, but i think it will be nice feature in your library.

@thepirat000
Copy link
Owner

Not sure If I understand you, but you can have a different middleware to do the exception logging task, I don't see why it should be audit middleware responsibility.

Actually the web api audit middleware does log the exception to the audit event.

You should explain a little bit more and add some code examples of what do you expect

@thepirat000 thepirat000 self-assigned this Feb 19, 2021
@Meteoeoeo
Copy link
Author

Probably i described it not enough precisely.
I am using Auditnet, Audit.WebApi.Core and Audit.NET.SqlServer.
For configuration Audit.NET.SqlServer I would like to be able to choose a behavior,
that determines what happens if an exception occurs.
Currently, api always returns Internal Server Error.
For example, when the database doesn't respond.

      Configuration.Setup()
        .UseSqlServer(config => config
        ...
        .SetExceptionBehavior(new ExceptionBehavior)
        ...
   or 
      Configuration.Setup()
        .UseSqlServer(config => config
        ...
        .AddExceptionHandling((exception, auditEvent) => logger.Log(auditEvent.ToJSON(), exception))
        ...

similar for Audit.WebApi.Core and middleware - set some behavior - when some problem occurrs it shouldn't affect the value returned by api.

@thepirat000
Copy link
Owner

thepirat000 commented Feb 25, 2021

I'm not sure if the library should provide an opinionated mechanism to handle exceptions.

Actually I think it's a better idea to write a custom data provider wrapping a real data provider and adding the logic for Fallback.

Furthermore, you could use a framework like Polly that provides patterns for resilience (Timeout, Retry, Fallback, Circuit Breaker and so on...).

Check this generic data provider configurable via Polly Policies:

public class PollyDataProvider : AuditDataProvider
{
    private readonly Policy<object> _policy;
    private readonly AsyncPolicy<object> _asyncPolicy;
    private readonly AuditDataProvider _innerProvider;
    public PollyDataProvider(Policy<object> policy, AsyncPolicy<object> asyncPolicy, AuditDataProvider innerDataProvider)
    {
        _policy = policy;
        _asyncPolicy = asyncPolicy;
        _innerProvider = innerDataProvider;
    }

    public override object InsertEvent(AuditEvent auditEvent)
    {
        var contextData = new Dictionary<string, object>() { { "event", auditEvent } };
        return _policy.Execute(ctx => _innerProvider.InsertEvent(auditEvent), contextData);
    }
    public async override Task<object> InsertEventAsync(AuditEvent auditEvent)
    {
        var contextData = new Dictionary<string, object>() { { "event", auditEvent } };
        return await _asyncPolicy.ExecuteAsync(async ctx => await _innerProvider.InsertEventAsync(auditEvent), contextData);
    }
    public override void ReplaceEvent(object eventId, AuditEvent auditEvent)
    {
        var contextData = new Dictionary<string, object>() { { "id", eventId }, { "event", auditEvent } };
        _policy.Execute(ctx => { _innerProvider.ReplaceEvent(eventId, auditEvent); return null; }, contextData);
    }
    public async override Task ReplaceEventAsync(object eventId, AuditEvent auditEvent)
    {
        var contextData = new Dictionary<string, object>() { { "id", eventId }, { "event", auditEvent } };
        await _asyncPolicy.ExecuteAsync(async (ctx) => { await _innerProvider.ReplaceEventAsync(eventId, auditEvent); return null; }, contextData);
    }
}

Notes:

  • You should supply both policies: one for sync calls and another for async calls.
  • The Policy should be defined with a generic return of type <object>, since that's what the InsertEvent and InsertEventAsync returns.
  • Note the use of contextData dictionary to allow the client obtain the AuditEvent on the policy callbacks/actions.

For example to set up a simple fallback mechanism like the one you are looking for (if a SQL data provider fails, then use the logger):

var sqlDataProvider = new Audit.SqlServer.Providers.SqlDataProvider(config => config
    .ConnectionString("...")
    .TableName("Audit")
    .IdColumnName("Id")
    .JsonColumnName("Data"));

var fallbackPolicy = Policy<object>.Handle<Exception>().Fallback(
    fallbackAction: (context) =>
    {
        var auditEvent = context["event"] as AuditEvent;
        logger.Log(auditEvent.ToJson());
        return -1;
    },
    onFallback: (ex, ctx) => { });

var fallbackAsyncPolicy = Policy<object>.Handle<Exception>().FallbackAsync(
    fallbackAction: (Context context, CancellationToken ct) =>
    {
        var auditEvent = context["event"] as AuditEvent;
        logger.Log(auditEvent.ToJson());
        return Task.FromResult((object)-1);
    },
    onFallbackAsync: async (r, c) => await Task.CompletedTask);

Audit.Core.Configuration.Setup()
    .UseCustomProvider(new PollyDataProvider(fallbackPolicy, fallbackAsyncPolicy, sqlDataProvider))
    .WithCreationPolicy(EventCreationPolicy.InsertOnEnd);

@thepirat000
Copy link
Owner

thepirat000 commented Feb 25, 2021

Or the cheap alternative...

public class LoggerFallbackSqlDataProvider : SqlDataProvider
{
    public LoggerFallbackSqlDataProvider(Action<ISqlServerProviderConfigurator> config) : base(config) { }
    // ...
    public override object InsertEvent(AuditEvent auditEvent)
    {
        try
        {
            return base.InsertEvent(auditEvent);
        }
        catch (Exception)
        {
            logger.Log(auditEvent.ToJson());
            return -1;
        }
    }
    public override Task<object> InsertEventAsync(AuditEvent auditEvent)
    {
        // the same
    }
    //...
}

@thepirat000
Copy link
Owner

Note that starting in version 24, a new Audit.NET.Polly extension is provided to facilitate the configuration of Polly resilience strategies to any Data Provider within Audit.NET.

For example:

Audit.Core.Configuration.Setup()
    .UsePolly(p => p
        .DataProvider(new MongoDataProvider(mongo => mongo...))
        .WithResilience(r => r
            .AddFallback(new()
            {
                ShouldHandle = new PredicateBuilder().Handle<Exception>(),
                OnFallback = arguments =>
                {
                    var auditEvent = arguments.Context.GetAuditEvent();
                    _logger.Log(auditEvent.ToJson());
                    return default;
                },
                FallbackAction = arguments => default
            })));

Or to fallback to a different data provider:

Audit.Core.Configuration.Setup()
    .UsePolly(p => p
        .DataProvider(mongoDataProvider)
        .WithResilience(r => r
            .AddFallback(new()
            {
                ShouldHandle = new PredicateBuilder().Handle<Exception>(),
                FallbackAction = arguments => arguments.FallbackToDataProvider(fileDataProvider)
            })))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants