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

(feat) Adding Chaos Engineering #136

Merged
merged 3 commits into from
Aug 25, 2024
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
17 changes: 17 additions & 0 deletions src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Http;

namespace Mockaco.Chaos.Strategies;

internal class ChaosStrategyBehavior : IChaosStrategy
{
public Task Response(HttpResponse httpResponse)
{
httpResponse.StatusCode = (int)HttpStatusCode.ServiceUnavailable;

var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.ServiceUnavailable}");

return httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length, default);
}
}
17 changes: 17 additions & 0 deletions src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Http;

namespace Mockaco.Chaos.Strategies;

internal class ChaosStrategyException : IChaosStrategy
{
public Task Response(HttpResponse httpResponse)
{
httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError;

var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.InternalServerError}");

return httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length);
}
}
23 changes: 23 additions & 0 deletions src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyLatency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Mockaco.Chaos.Strategies;

internal class ChaosStrategyLatency : IChaosStrategy
{
private readonly ILogger<ChaosStrategyLatency> _logger;
private readonly IOptions<ChaosOptions> _options;

public ChaosStrategyLatency(ILogger<ChaosStrategyLatency> logger, IOptions<ChaosOptions> options)
{
_logger = logger;
_options = options;
}
public Task Response(HttpResponse httpResponse)
{
var responseDelay = new Random().Next(_options.Value.MinimumLatencyTime, _options.Value.MaximumLatencyTime);
_logger.LogInformation($"Chaos Latency (ms): {responseDelay}");
return Task.Delay(responseDelay);
}
}
17 changes: 17 additions & 0 deletions src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Http;

namespace Mockaco.Chaos.Strategies;

internal class ChaosStrategyResult : IChaosStrategy
{
public Task Response(HttpResponse httpResponse)
{
httpResponse.StatusCode = (int)HttpStatusCode.BadRequest;

var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.BadRequest}");

return httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length);
}
}
27 changes: 27 additions & 0 deletions src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyTimeout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace Mockaco.Chaos.Strategies;

internal class ChaosStrategyTimeout : IChaosStrategy
{
private readonly IOptions<ChaosOptions> _options;

public ChaosStrategyTimeout(IOptions<ChaosOptions> options)
{
_options = options;
}

public async Task Response(HttpResponse httpResponse)
{
await Task.Delay(_options.Value.TimeBeforeTimeout);

httpResponse.StatusCode = (int)HttpStatusCode.RequestTimeout;

var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.RequestTimeout}");

await httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length, default);
}
}
8 changes: 8 additions & 0 deletions src/Mockaco.AspNetCore/Chaos/Strategies/IChaosStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Http;

namespace Mockaco.Chaos.Strategies;

internal interface IChaosStrategy
{
Task Response(HttpResponse httpResponse);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public static IApplicationBuilder UseMockaco(this IApplicationBuilder app, Actio

var options = app.ApplicationServices.GetRequiredService<IOptions<MockacoOptions>>().Value;

var optionsChaos = app.ApplicationServices.GetRequiredService<IOptions<ChaosOptions>>().Value;

app.UseEndpoints(endpoints =>
{
endpoints.Map($"/{options.VerificationEndpointPrefix ?? options.MockacoEndpoint}/{options.VerificationEndpointName}", VerifyerExtensions.Verify);
Expand All @@ -36,6 +38,7 @@ public static IApplicationBuilder UseMockaco(this IApplicationBuilder app, Actio
app
.UseMiddleware<RequestMatchingMiddleware>()
.UseMiddleware<ResponseDelayMiddleware>()
.UseMiddleware<ChaosMiddleware>()
.UseMiddleware<ResponseMockingMiddleware>()
.UseMiddleware<CallbackMiddleware>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Mockaco;
using Mockaco.Chaos.Strategies;
using Mockaco.HealthChecks;
using Mockaco.Settings;

Expand All @@ -15,6 +16,7 @@ public static IServiceCollection AddMockaco(this IServiceCollection services) =>
public static IServiceCollection AddMockaco(this IServiceCollection services, Action<MockacoOptions> config) =>
services
.AddOptions<MockacoOptions>().Configure(config).Services
.AddOptions<ChaosOptions>().Configure<IOptions<MockacoOptions>>((options, parent) => options = parent.Value.Chaos).Services
.AddOptions<TemplateFileProviderOptions>()
.Configure<IOptions<MockacoOptions>>((options, parent) => options = parent.Value.TemplateFileProvider)
.Services
Expand All @@ -30,6 +32,7 @@ private static IServiceCollection AddConfiguration(this IServiceCollection servi
services
.AddOptions()
.Configure<MockacoOptions>(config)
.Configure<ChaosOptions>(config.GetSection("Chaos"))
.Configure<TemplateFileProviderOptions>(config.GetSection("TemplateFileProvider"));

private static IServiceCollection AddCommonServices(this IServiceCollection services)
Expand All @@ -38,6 +41,7 @@ private static IServiceCollection AddCommonServices(this IServiceCollection serv
.AddMemoryCache()
.AddHttpClient()
.AddInternalServices()
.AddChaosServices()
.AddHostedService<MockProviderWarmUp>();

services
Expand Down Expand Up @@ -81,5 +85,13 @@ private static IServiceCollection AddInternalServices(this IServiceCollection se
.AddTransient<ITemplateTransformer, TemplateTransformer>()

.AddTemplatesGenerating();

private static IServiceCollection AddChaosServices(this IServiceCollection services) =>
services
.AddTransient<IChaosStrategy, ChaosStrategyBehavior>()
.AddTransient<IChaosStrategy, ChaosStrategyException>()
.AddTransient<IChaosStrategy, ChaosStrategyLatency>()
.AddTransient<IChaosStrategy, ChaosStrategyResult>()
.AddTransient<IChaosStrategy, ChaosStrategyTimeout>();
}
}
5 changes: 5 additions & 0 deletions src/Mockaco.AspNetCore/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@ public static async Task<bool> AllAsync<TSource>(this IEnumerable<Task<TSource>>

return true;
}

public static T Random<T>(this IEnumerable<T> enumerable) {
int index = new Random().Next(0, enumerable.Count());
return enumerable.ElementAt(index);
}
}
}
73 changes: 73 additions & 0 deletions src/Mockaco.AspNetCore/Middlewares/ChaosMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Mockaco.Chaos.Strategies;

namespace Mockaco;

internal class ChaosMiddleware
{
private readonly RequestDelegate _next;
private readonly IEnumerable<IChaosStrategy> _strategies;
private readonly ILogger<ChaosMiddleware> _logger;
private readonly IOptions<ChaosOptions> _options;

private List<int> ErrorList { get; set; }
private int Counter { get; set; }

public ChaosMiddleware(
RequestDelegate next,
IEnumerable<IChaosStrategy> strategies,
ILogger<ChaosMiddleware> logger,
IOptions<ChaosOptions> options)
{
_next = next;
_strategies = strategies;
_logger = logger;
_options = options;
}

public async Task Invoke(HttpContext httpContext)
{
if (!_options.Value.Enabled)
{
return;
}

Counter++;
if (Counter > 100)
Counter = 1;

if (Counter == 1)
ErrorList = GenerateErrorList(_options.Value.ChaosRate);

if (ErrorList.Contains(Counter))
{
var selected = _strategies.Random();
_logger.LogInformation($"Chaos: {selected?.ToString()}");
if (selected != null) await selected.Response(httpContext.Response);
}

if (httpContext.Response.StatusCode != (int)HttpStatusCode.OK)
return;


await _next(httpContext);
}

private List<int> GenerateErrorList(int rate)
{
var list = new List<int>();
while (list.Count < rate)
{
var item = new Random().Next(1, 100);
if (!list.Contains(item))
{
list.Add(item);
}
}

return list.OrderBy(x => x).ToList();
}
}
19 changes: 19 additions & 0 deletions src/Mockaco.AspNetCore/Options/ChaosOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Mockaco;

public class ChaosOptions
{
public bool Enabled { get; set; }
public int ChaosRate { get; set; }
public int MinimumLatencyTime { get; set; }
public int MaximumLatencyTime { get; set; }
public int TimeBeforeTimeout { get; set; }

public ChaosOptions()
{
Enabled = true;
ChaosRate = 10;
MinimumLatencyTime = 500;
MaximumLatencyTime = 3000;
TimeBeforeTimeout = 10000;
}
}
3 changes: 3 additions & 0 deletions src/Mockaco.AspNetCore/Options/MockacoOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class MockacoOptions

// Deprecated (use MockacoEndpoint instead)
public string VerificationEndpointPrefix { get; set; }

public ChaosOptions Chaos { get; set; }

public string VerificationEndpointName { get; set; }

Expand All @@ -38,6 +40,7 @@ public MockacoOptions()
MockacoEndpoint = "_mockaco";
VerificationEndpointName = "verification";
TemplateFileProvider = new();
Chaos = new ChaosOptions();
}
}
}
14 changes: 14 additions & 0 deletions src/Mockaco.AspNetCore/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Bogus.PhoneNumberExtensions
Microsoft.AspNetCore.Builder.MockacoApplicationBuilder
Microsoft.Extensions.DependencyInjection.MockacoServiceCollection
Mockaco.ChaosOptions
Mockaco.ChaosOptions.ChaosOptions() -> void
Mockaco.ChaosOptions.ChaosRate.get -> int
Mockaco.ChaosOptions.ChaosRate.set -> void
Mockaco.ChaosOptions.Enabled.get -> bool
Mockaco.ChaosOptions.Enabled.set -> void
Mockaco.ChaosOptions.MaximumLatencyTime.get -> int
Mockaco.ChaosOptions.MaximumLatencyTime.set -> void
Mockaco.ChaosOptions.MinimumLatencyTime.get -> int
Mockaco.ChaosOptions.MinimumLatencyTime.set -> void
Mockaco.ChaosOptions.TimeBeforeTimeout.get -> int
Mockaco.ChaosOptions.TimeBeforeTimeout.set -> void
Mockaco.HealthChecks.StartupHealthCheck
Mockaco.HealthChecks.StartupHealthCheck.CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>
Mockaco.HealthChecks.StartupHealthCheck.StartupCompleted.get -> bool
Expand Down Expand Up @@ -42,6 +54,8 @@ Mockaco.Mock.RawTemplate.set -> void
Mockaco.Mock.Route.get -> string
Mockaco.Mock.Route.set -> void
Mockaco.MockacoOptions
Mockaco.MockacoOptions.Chaos.get -> Mockaco.ChaosOptions
Mockaco.MockacoOptions.Chaos.set -> void
Mockaco.MockacoOptions.DefaultHttpContentType.get -> string
Mockaco.MockacoOptions.DefaultHttpContentType.set -> void
Mockaco.MockacoOptions.DefaultHttpStatusCode.get -> System.Net.HttpStatusCode
Expand Down
9 changes: 8 additions & 1 deletion src/Mockaco/Settings/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
"Imports": [],
"MatchedRoutesCacheDuration": 60,
"MockacoEndpoint": "_mockaco",
"VerificationEndpointName": "verification"
"VerificationEndpointName": "verification",
"Chaos": {
"Enabled": false,
"ChaosRate": 20,
"MinimumLatencyTime": 500,
"MaximumLatencyTime": 3000,
"TimeBeforeTimeout": 10000
}
},
"AllowedHosts": "*",
"Serilog": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Mockaco.Tests.Extensions;

public class EnumerableExtensionTests
{
[Theory]
[MemberData(nameof(Data))]
public void Select_Random_IEnumerables(List<object> source)
{
IEnumerable<object> enummerables = source;

object selected = enummerables.Random();

Assert.Contains(selected, source);
}

public static IEnumerable<object[]> Data()
{
yield return new object[] { new List<object> { 1, 2, 3 } };
yield return new object[] { new List<object> { "a", "b", "c" } };
yield return new object[] { new List<object> { "a1", "c2" } };
}
}
Loading
Loading