Skip to content

Commit

Permalink
feat: error events (#135)
Browse files Browse the repository at this point in the history
* feat: add errorevent mechanism

* feat: implement errorevent in apiclient

* feat: implement errorevents in fetchfeaturetogglestask

* chore: more fetchtoggles task tests for IO

* feat: implement errorevents in default unleash

* feat: implement error events in cachedfilesloader

* chore: eventconfig can be null

* chore: docs + rename file

* chore: test result, dont raise for impression evts

* chore: add events handling to sample app
  • Loading branch information
daveleek authored May 22, 2023
1 parent 81d8afb commit bc857fb
Show file tree
Hide file tree
Showing 16 changed files with 330 additions and 32 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Refer to the [Unleash context](#unleash-context) section for more information ab

Currently supported events:
- [Impression data events](https://docs.getunleash.io/advanced/impression-data#impression-event-data)
- Error events

```csharp

Expand All @@ -181,10 +182,11 @@ var settings = new UnleashSettings()

var unleash = new DefaultUnleash(settings);

// Set up handling of impression events
// Set up handling of impression and error events
unleash.ConfigureEvents(cfg =>
{
cfg.ImpressionEvent = evt => { Console.WriteLine($"{evt.FeatureName}: {evt.Enabled}"); };
cfg.ErrorEvent = evt => { /* Handling code here */ Console.WriteLine($"{evt.ErrorType} occured."); };
});

```
Expand Down
12 changes: 9 additions & 3 deletions samples/WebApplication/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,20 @@ public Startup(IConfiguration configuration)
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IUnleash>(c => new DefaultUnleash(new UnleashSettings()
var unleash = new DefaultUnleash(new UnleashSettings()
{
UnleashApi = new Uri("http://web:4242"),
UnleashApi = new Uri("http://localhost:4242/api"),
AppName = "variant-sample",
InstanceTag = "instance 1",
SendMetricsInterval = TimeSpan.FromSeconds(10),
FetchTogglesInterval = TimeSpan.FromSeconds(10),
}));
});
unleash.ConfigureEvents(evtCfg =>
{
evtCfg.ImpressionEvent = evt => { Console.WriteLine(evt.FeatureName); };
evtCfg.ErrorEvent = evt => { Console.WriteLine(evt.ErrorType + "-" + evt.Error?.Message); };
});
services.AddSingleton<IUnleash>(c => unleash);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down
6 changes: 6 additions & 0 deletions src/Unleash/Communication/UnleashApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Unleash.Events;
using Unleash.Internal;
using Unleash.Logging;
using Unleash.Metrics;
Expand All @@ -18,17 +19,20 @@ internal class UnleashApiClient : IUnleashApiClient
private readonly HttpClient httpClient;
private readonly IJsonSerializer jsonSerializer;
private readonly UnleashApiClientRequestHeaders clientRequestHeaders;
private readonly EventCallbackConfig eventConfig;
private readonly string projectId;

public UnleashApiClient(
HttpClient httpClient,
IJsonSerializer jsonSerializer,
UnleashApiClientRequestHeaders clientRequestHeaders,
EventCallbackConfig eventConfig,
string projectId = null)
{
this.httpClient = httpClient;
this.jsonSerializer = jsonSerializer;
this.clientRequestHeaders = clientRequestHeaders;
this.eventConfig = eventConfig;
this.projectId = projectId;
}

Expand All @@ -51,6 +55,7 @@ public async Task<FetchTogglesResult> FetchToggles(string etag, CancellationToke
{
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Logger.Trace($"UNLEASH: Error {response.StatusCode} from server in '{nameof(FetchToggles)}': " + error);
eventConfig?.RaiseError(new ErrorEvent() { ErrorType = ErrorType.Client, StatusCode = response.StatusCode, Resource = resourceUri });

return new FetchTogglesResult
{
Expand Down Expand Up @@ -139,6 +144,7 @@ private async Task<bool> Post(string resourceUri, Stream stream, CancellationTok

var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Logger.Trace($"UNLEASH: Error {response.StatusCode} from request '{resourceUri}' in '{nameof(UnleashApiClient)}': " + error);
eventConfig?.RaiseError(new ErrorEvent() { Resource = resourceUri, ErrorType = ErrorType.Client, StatusCode = response.StatusCode });

return false;
}
Expand Down
9 changes: 4 additions & 5 deletions src/Unleash/DefaultUnleash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Unleash
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Unleash.Events;
using Unleash.Variants;

/// <inheritdoc />
Expand Down Expand Up @@ -64,7 +65,7 @@ public DefaultUnleash(UnleashSettings settings, bool overrideDefaultStrategies,
strategies = SelectStrategies(strategies, overrideDefaultStrategies);
strategyMap = BuildStrategyMap(strategies);

services = new UnleashServices(settings, strategyMap);
services = new UnleashServices(settings, EventConfig, strategyMap);

Logger.Info($"UNLEASH: Unleash instance number { currentInstanceNo } is initialized and configured with: {settings}");

Expand All @@ -78,7 +79,7 @@ public DefaultUnleash(UnleashSettings settings, bool overrideDefaultStrategies,
/// <inheritdoc />
public ICollection<FeatureToggle> FeatureToggles => services.ToggleCollection.Instance.Features;

private EventCallbackConfig EventConfig { get; set; }
private EventCallbackConfig EventConfig { get; } = new EventCallbackConfig();

/// <inheritdoc />
public bool IsEnabled(string toggleName)
Expand Down Expand Up @@ -253,9 +254,7 @@ public void ConfigureEvents(Action<EventCallbackConfig> callback)

try
{
var evtConfig = new EventCallbackConfig();
callback(evtConfig);
EventConfig = evtConfig;
callback(EventConfig);
}
catch (Exception ex)
{
Expand Down
15 changes: 15 additions & 0 deletions src/Unleash/Events/ErrorEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;

namespace Unleash.Events
{
public class ErrorEvent
{
public ErrorType ErrorType { get; set; }
public Exception Error { get; set; }
public HttpStatusCode? StatusCode { get; internal set; }
public string Resource { get; internal set; }
}
}
15 changes: 15 additions & 0 deletions src/Unleash/Events/ErrorType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Unleash.Events
{
public enum ErrorType
{
Client,
TogglesBackup,
Bootstrap,
ImpressionEvent,
FileCache
}
}
16 changes: 15 additions & 1 deletion src/Unleash/Internal/CachedFilesLoader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using Unleash.Events;
using Unleash.Logging;
using Unleash.Scheduling;
using Unleash.Serialization;
Expand All @@ -11,15 +12,24 @@ internal class CachedFilesLoader
private readonly IJsonSerializer jsonSerializer;
private readonly IFileSystem fileSystem;
private readonly IToggleBootstrapProvider toggleBootstrapProvider;
private readonly EventCallbackConfig eventConfig;
private readonly string toggleFile;
private readonly string etagFile;
private readonly bool bootstrapOverride;

public CachedFilesLoader(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IToggleBootstrapProvider toggleBootstrapProvider, string toggleFile, string etagFile, bool bootstrapOverride = true)
public CachedFilesLoader(
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IToggleBootstrapProvider toggleBootstrapProvider,
EventCallbackConfig eventConfig,
string toggleFile,
string etagFile,
bool bootstrapOverride = true)
{
this.jsonSerializer = jsonSerializer;
this.fileSystem = fileSystem;
this.toggleBootstrapProvider = toggleBootstrapProvider;
this.eventConfig = eventConfig;
this.toggleFile = toggleFile;
this.etagFile = etagFile;
this.bootstrapOverride = bootstrapOverride;
Expand All @@ -40,6 +50,7 @@ public CachedFilesResult EnsureExistsAndLoad()
catch (IOException ex)
{
Logger.ErrorException($"UNLEASH: Unhandled exception when writing to ETag file '{etagFile}'.", ex);
eventConfig?.RaiseError(new ErrorEvent() { Error = ex, ErrorType = ErrorType.FileCache });
}
}
else
Expand All @@ -51,6 +62,7 @@ public CachedFilesResult EnsureExistsAndLoad()
catch (IOException ex)
{
Logger.ErrorException($"UNLEASH: Unhandled exception when reading from ETag file '{etagFile}'.", ex);
eventConfig?.RaiseError(new ErrorEvent() { Error = ex, ErrorType = ErrorType.FileCache });
}
}

Expand All @@ -65,6 +77,7 @@ public CachedFilesResult EnsureExistsAndLoad()
catch (IOException ex)
{
Logger.ErrorException($"UNLEASH: Unhandled exception when writing to toggle file '{toggleFile}'.", ex);
eventConfig?.RaiseError(new ErrorEvent() { Error = ex, ErrorType = ErrorType.FileCache });
}
}
else
Expand All @@ -79,6 +92,7 @@ public CachedFilesResult EnsureExistsAndLoad()
catch (IOException ex)
{
Logger.ErrorException($"UNLEASH: Unhandled exception when reading from toggle file '{toggleFile}'.", ex);
eventConfig?.RaiseError(new ErrorEvent() { Error = ex, ErrorType = ErrorType.FileCache });
}
}

Expand Down
12 changes: 10 additions & 2 deletions src/Unleash/Internal/EventCallbackConfig.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
using Unleash.Events;

namespace Unleash.Internal
{
public class EventCallbackConfig
{
public Action<ImpressionEvent> ImpressionEvent { get; set; }
public Action<ErrorEvent> ErrorEvent { get; set; }

public void RaiseError(ErrorEvent evt)
{
if (ErrorEvent != null)
{
ErrorEvent(evt);
}
}
}
}
11 changes: 6 additions & 5 deletions src/Unleash/Internal/UnleashServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal class UnleashServices : IDisposable
internal ThreadSafeMetricsBucket MetricsBucket { get; }
internal FetchFeatureTogglesTask FetchFeatureTogglesTask { get; }

public UnleashServices(UnleashSettings settings, Dictionary<string, IStrategy> strategyMap)
public UnleashServices(UnleashSettings settings, EventCallbackConfig eventConfig, Dictionary<string, IStrategy> strategyMap)
{
var fileSystem = settings.FileSystem ?? new FileSystem(settings.Encoding);

Expand All @@ -36,7 +36,7 @@ public UnleashServices(UnleashSettings settings, Dictionary<string, IStrategy> s
CancellationToken = cancellationTokenSource.Token;
ContextProvider = settings.UnleashContextProvider;

var loader = new CachedFilesLoader(settings.JsonSerializer, fileSystem, settings.ToggleBootstrapProvider, backupFile, etagBackupFile, settings.BootstrapOverride);
var loader = new CachedFilesLoader(settings.JsonSerializer, fileSystem, settings.ToggleBootstrapProvider, eventConfig, backupFile, etagBackupFile, settings.BootstrapOverride);
var cachedFilesResult = loader.EnsureExistsAndLoad();

ToggleCollection = new ThreadSafeToggleCollection
Expand All @@ -63,7 +63,7 @@ public UnleashServices(UnleashSettings settings, Dictionary<string, IStrategy> s
CustomHttpHeaders = settings.CustomHttpHeaders,
CustomHttpHeaderProvider = settings.UnleashCustomHttpHeaderProvider,
SupportedSpecVersion = supportedSpecVersion
}, settings.ProjectId);
}, eventConfig, settings.ProjectId);
}
else
{
Expand All @@ -77,9 +77,10 @@ public UnleashServices(UnleashSettings settings, Dictionary<string, IStrategy> s

var fetchFeatureTogglesTask = new FetchFeatureTogglesTask(
apiClient,
ToggleCollection,
ToggleCollection,
settings.JsonSerializer,
fileSystem,
fileSystem,
eventConfig,
backupFile,
etagBackupFile)
{
Expand Down
19 changes: 18 additions & 1 deletion src/Unleash/Scheduling/FetchFeatureTogglesTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Unleash.Internal;
using Unleash.Serialization;
using Unleash.Logging;
using Unleash.Events;
using System.Net.Http;

namespace Unleash.Scheduling
{
Expand All @@ -16,6 +18,7 @@ internal class FetchFeatureTogglesTask : IUnleashScheduledTask
private readonly string etagFile;

private readonly IFileSystem fileSystem;
private readonly EventCallbackConfig eventConfig;
private readonly IUnleashApiClient apiClient;
private readonly IJsonSerializer jsonSerializer;
private readonly ThreadSafeToggleCollection toggleCollection;
Expand All @@ -28,20 +31,32 @@ public FetchFeatureTogglesTask(
ThreadSafeToggleCollection toggleCollection,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
EventCallbackConfig eventConfig,
string toggleFile,
string etagFile)
{
this.apiClient = apiClient;
this.toggleCollection = toggleCollection;
this.jsonSerializer = jsonSerializer;
this.fileSystem = fileSystem;
this.eventConfig = eventConfig;
this.toggleFile = toggleFile;
this.etagFile = etagFile;
}

public async Task ExecuteAsync(CancellationToken cancellationToken)
{
var result = await apiClient.FetchToggles(Etag, cancellationToken).ConfigureAwait(false);
FetchTogglesResult result;
try
{
result = await apiClient.FetchToggles(Etag, cancellationToken).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
Logger.ErrorException($"UNLEASH: Unhandled exception when fetching toggles.", ex);
eventConfig?.RaiseError(new ErrorEvent() { ErrorType = ErrorType.Client, Error = ex });
return;
}

if (!result.HasChanged)
return;
Expand All @@ -64,6 +79,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken)
catch (IOException ex)
{
Logger.ErrorException($"UNLEASH: Unhandled exception when writing to toggle file '{toggleFile}'.", ex);
eventConfig?.RaiseError(new ErrorEvent() { ErrorType = ErrorType.TogglesBackup, Error = ex });
}

Etag = result.Etag;
Expand All @@ -75,6 +91,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken)
catch (IOException ex)
{
Logger.ErrorException($"UNLEASH: Unhandled exception when writing to ETag file '{etagFile}'.", ex);
eventConfig?.RaiseError(new ErrorEvent() { ErrorType = ErrorType.TogglesBackup, Error = ex });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using NUnit.Framework;
using NUnit.Framework.Internal;
using Unleash.Communication;
using Unleash.Events;
using Unleash.Internal;
using Unleash.Serialization;

namespace Unleash.Tests.Communication
Expand Down Expand Up @@ -32,7 +34,7 @@ private static IUnleashApiClient CreateApiClient()
};

var httpClient = httpClientFactory.Create(apiUri);
var client = new UnleashApiClient(httpClient, jsonSerializer, requestHeaders);
var client = new UnleashApiClient(httpClient, jsonSerializer, requestHeaders, new EventCallbackConfig());
return client;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using FluentAssertions;
using NUnit.Framework;
using Unleash.Communication;
using Unleash.Events;
using Unleash.Internal;
using Unleash.Serialization;
using Unleash.Tests.Mock;

Expand Down Expand Up @@ -36,7 +38,7 @@ private UnleashApiClient NewTestableClient(string project, MockHttpMessageHandle
Timeout = TimeSpan.FromSeconds(5)
};

return new UnleashApiClient(httpClient, jsonSerializer, requestHeaders, project);
return new UnleashApiClient(httpClient, jsonSerializer, requestHeaders, new EventCallbackConfig(), project);
}

[Test]
Expand Down
Loading

0 comments on commit bc857fb

Please sign in to comment.