Adding MaidBotApi

This commit is contained in:
Zeeshaun
2026-03-11 13:21:47 -05:00
parent f3cb730f3b
commit f004c8205a
122 changed files with 4227 additions and 0 deletions
@@ -0,0 +1,14 @@
namespace LCU.Api.Internal.MaidBotApi.Configurations;
public class ApiConfiguration
{
public string AppName { get; set; }
public string SeqUrl { get; set; }
public string SeqApiKey { get; set; }
public string NotificationsApiEmail { get; set; }
public string BatchSchedulerCron { get; set; }
public FeatureFlags FeatureFlags { get; set; }
public string[] CorsAllowedOrigins { get; set; }
public string LogEmailTemplate { get; set; }
public string LogEmailRowTemplate { get; set; }
}
@@ -0,0 +1,7 @@
namespace LCU.Api.Internal.MaidBotApi.Configurations;
public class FeatureFlags
{
public bool LoggingEventConsumer { get; set; }
public bool RunScheduler { get; set; }
}
@@ -0,0 +1,40 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using MassTransit;
using Serilog;
using LCU.Library.Internal.Exceptions;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Api.Internal.MaidBotApi.Services;
namespace LCU.Api.Internal.MaidBotApi.Consumers;
public class LoggingEventConsumer : IConsumer<EventLog>
{
private readonly IServiceScopeFactory _scopeFactory;
public LoggingEventConsumer(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task Consume(ConsumeContext<EventLog> context)
{
try
{
using var scope = _scopeFactory.CreateScope();
await Task.Run(async () => await scope.ServiceProvider.GetRequiredService<IEventLogService>().HandleIncomingEventLog(context.Message));
}
catch (ServiceException serviceException)
{
Log.Fatal(serviceException, $"Handle Incoming EventLog Failure. " +
$"The Handle Incoming EventLog is placed on LCU.MaidBot_error.");
throw;
}
catch (Exception exception)
{
Log.Fatal(exception, $"Unexpected Handle Incoming EventLog Failure. " +
$"The Handle Incoming EventLog is placed on LCU.MaidBot_error.");
throw;
}
}
}
@@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using LCU.Api.Internal.MaidBotApi.Services;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Library.Internal.Exceptions;
namespace LCU.API.Internal.MaidBotApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class EventLogController : ControllerBase
{
private readonly IEventLogService _eventLogService;
public EventLogController(IEventLogService eventLogService)
{
_eventLogService = eventLogService;
}
[HttpGet]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(typeof(EventLog), 400)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get()
{
try
{
var results = await _eventLogService.GetAsync();
if (results == null || !results.Any())
{
return NoContent();
}
return Ok(results);
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpDelete()]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public IActionResult DeleteMany([FromBody] List<EventLog> eventLogs)
{
try
{
_eventLogService.DeleteMany(eventLogs);
return Ok();
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using LCU.Api.Internal.MaidBotApi.Exceptions;
using LCU.Api.Internal.MaidBotApi.Services;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Library.Internal.Exceptions;
namespace LCU.API.Internal.MaidBotApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class LoggingMatrixController : ControllerBase
{
private readonly ILoggingMatrixService _loggingMatrixService;
public LoggingMatrixController(ILoggingMatrixService loggingMatrixService)
{
_loggingMatrixService = loggingMatrixService;
}
[HttpGet(Name = "GetAllLoggingMatrixes")]
[ProducesResponseType(typeof(IEnumerable<LoggingMatrix>), 200)]
[ProducesResponseType(500)]
public async Task<ActionResult> Get()
{
try
{
var result = await _loggingMatrixService.GetAsync();
return Ok(result);
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost]
[ProducesResponseType(typeof(LoggingMatrix), 201)]
[ProducesResponseType(typeof(List<string>), 400)]
[ProducesResponseType(500)]
public async Task<ActionResult> Post([FromBody] LoggingMatrix loggingMatrix)
{
try
{
var result = await _loggingMatrixService.SaveAsync(loggingMatrix);
if (result == null)
{
Log.Error($"Result is null could not create Logging Matrix");
return StatusCode((StatusCodes.Status400BadRequest), "Could not create Logging Matrix.");
}
return StatusCode(StatusCodes.Status201Created, result);
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut]
[ProducesResponseType(typeof(LoggingMatrix), 200)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> Put([FromBody] LoggingMatrix loggingMatrix)
{
try
{
if (loggingMatrix.LoggingMatrixId <= 0)
{
return BadRequest("Invalid ID");
}
var result = await _loggingMatrixService.UpdateAsync(loggingMatrix);
if(result == null)
{
return NotFound($"The LoggingMatrix with LoggingMatrixId: {loggingMatrix.LoggingMatrixId} " +
"could not be found");
}
return Ok(result);
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using LCU.Api.Internal.MaidBotApi.Services;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Library.Internal.Exceptions;
namespace LCU.API.Internal.MaidBotApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class QueueEventLogController : ControllerBase
{
private readonly IQueueEventLogService _queueEventLogService;
public QueueEventLogController(IQueueEventLogService queueEventLogService)
{
_queueEventLogService = queueEventLogService;
}
[HttpPost]
[ProducesResponseType(202)]
[ProducesResponseType(typeof(List<string>), 400)]
[ProducesResponseType(500)]
public async Task<IActionResult> Post([FromBody] EventLog EventLog)
{
try
{
await _queueEventLogService.QueueEventLog(EventLog);
return Accepted();
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,34 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using LCU.Api.Internal.MaidBotApi.EnvironmentView;
namespace LCU.API.Internal.MaidBotApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class StatusController : ControllerBase
{
private readonly IEnvironmentView _environmentView;
public StatusController(IEnvironmentView environmentView)
{
_environmentView = environmentView;
}
[HttpGet()]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
return Ok(_environmentView);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to return status");
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
}
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
using LCU.Shared.Common.NotificationApiLib.Models;
namespace LCU.Api.Internal.MaidBotApi.DataAccesses;
public interface INotificationsApiDao
{
Task SendEmailRequest(EmailRequest emailRequest);
}
@@ -0,0 +1,23 @@
using System;
using System.Threading.Tasks;
using LCU.Api.Internal.MaidBotApi.Configurations;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.Shared.Common.NotificationApiLib.Models;
namespace LCU.Api.Internal.MaidBotApi.DataAccesses;
public class NotificationsApiDao : INotificationsApiDao
{
private readonly ApiConfiguration _apiConfiguration;
private readonly IPostRequest _postRequest;
public NotificationsApiDao(ApiConfiguration apiConfiguration, IPostRequest postRequest)
{
_apiConfiguration = apiConfiguration;
_postRequest = postRequest;
}
public async Task SendEmailRequest(EmailRequest emailRequest)
{
await _postRequest.Async(new Uri(_apiConfiguration.NotificationsApiEmail), emailRequest);
}
}
@@ -0,0 +1,35 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
namespace LCU.Api.Internal.MaidBotApi.EnvironmentView;
public class EnvironmentView : IEnvironmentView
{
public string Application => _env.ApplicationName;
public DateTime CurrentDate => DateTime.Now;
public string BuildVersion => AssembleBuildVersion();
public DateTime BuildDate => AssemblyBuildDate();
public string Environment => _env.EnvironmentName;
private readonly IWebHostEnvironment _env;
public EnvironmentView(IWebHostEnvironment env)
{
_env = env;
}
public static DateTime AssemblyBuildDate()
{
//https://stackoverflow.com/a/804310/266594
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
System.IO.FileInfo fileInfo = new System.IO.FileInfo(assembly.Location);
DateTime lastModified = fileInfo.LastWriteTime;
return lastModified;
}
public static string AssembleBuildVersion()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
return version == null ? "-.-.-" : version.ToString();
}
}
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LCU.Api.Internal.MaidBotApi.EnvironmentView
{
public interface IEnvironmentView
{
string Application { get; }
DateTime CurrentDate { get; }
string BuildVersion { get; }
DateTime BuildDate { get; }
string Environment { get; }
}
}
@@ -0,0 +1,15 @@
using System;
namespace LCU.Api.Internal.MaidBotApi.Exceptions
{
/**
* If an entity cannot be found, that specific error can allow for a 404 or 400 with more detail to be
* returned. So specify that to the API layer.
*/
public class NotFoundException : ApplicationException
{
public NotFoundException(string message) : base(message)
{
}
}
}
@@ -0,0 +1,14 @@
using System;
namespace LCU.Api.Internal.MaidBotApi.Exceptions
{
/**
* Do not want to expose database and related exceptions to the controllers / web layer.
* The lower layers log what happened for developers, then return a general error; unless
* a more specific error can be returned in a way to allow the client to act (such as NotFound).
*/
public class ServerErrorException : Exception
{
public ServerErrorException() : base("A server error occured, and a response could not be produced.") { }
}
}
@@ -0,0 +1,66 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using NCrontab;
using LCU.Api.Internal.MaidBotApi.Services;
using LCU.Api.Internal.MaidBotApi.Configurations;
namespace LCU.API.Internal.MaidBotApi.HostedService;
public class SchedulerHostedService : BackgroundService
{
private readonly ApiConfiguration _apiConfiguration;
private readonly IServiceScopeFactory _scopeFactory;
private readonly CrontabSchedule _schedule;
private DateTime _nextScheduleRun;
public const string DefaultScheduleCronPattern = "*/5 * * * *";
private CancellationToken _cancellationToken;
public SchedulerHostedService(IServiceScopeFactory scopeFactory,
ApiConfiguration apiConfiguration)
{
_scopeFactory = scopeFactory;
_apiConfiguration = apiConfiguration;
var cron = (_apiConfiguration?.BatchSchedulerCron) ?? DefaultScheduleCronPattern;
_schedule = CrontabSchedule.Parse(cron);
_nextScheduleRun = _schedule.GetNextOccurrence(DateTime.Now);
_cancellationToken = new CancellationToken();
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Log.Information("MaidBot Scheduler Hosted Service is starting.");
_cancellationToken = cancellationToken;
try
{
do
{
if (_apiConfiguration.FeatureFlags.RunScheduler && DateTime.Now > _nextScheduleRun)
{
using var scope = _scopeFactory.CreateScope();
await Task.Run(async () => await scope.ServiceProvider.GetRequiredService<ILogDistributionService>().HandleLogDistribution());
_nextScheduleRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, cancellationToken); //5 seconds delay
} while (!cancellationToken.IsCancellationRequested);
}
catch (TaskCanceledException taskCanceledException) when (_cancellationToken.IsCancellationRequested)
{
Log.Information(taskCanceledException, "MaidBot Scheduler Hosted Service was shut down normally.");
}
catch (Exception exception)
{
Log.Error(exception, "Error trying to startup main thread for MaidBot Scheduler Hosted Service");
}
Log.Information("MaidBot Scheduler Hosted Service is stopping.");
}
}
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<AssemblyVersion>1.0.0</AssemblyVersion>
<FileVersion>1.0.0</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DocumentationFile>..\LCU.API.Internal.MaidBotApi\LCU.Api.Internal.MaidBotApi.xml</DocumentationFile>
<NoWarn>1701;1702;1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>..\LCU.API.Internal.MaidBotApi\LCU.Api.Internal.MaidBotApi.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LCU.Library.Internal.Exceptions" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuHttpClient" Version="2.0.1" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransit" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransitModels" Version="1.1.0" />
<PackageReference Include="LCU.Library.Internal.Logging" Version="2.1.0" />
<PackageReference Include="LCU.Shared.Common.NotificationApiLib" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.14" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageReference Include="ncrontab" Version="3.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.Api.Internal.MaidBotLib\LCU.Api.Internal.MaidBotLib.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Templates\LogEmailRowTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\LogEmailTemplate.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>LCU.Api.Internal.MaidBotApi</name>
</assembly>
<members>
<member name="T:LCU.Api.Internal.MaidBotApi.Exceptions.NotFoundException">
If an entity cannot be found, that specific error can allow for a 404 or 400 with more detail to be
returned. So specify that to the API layer.
</member>
<member name="T:LCU.Api.Internal.MaidBotApi.Exceptions.ServerErrorException">
Do not want to expose database and related exceptions to the controllers / web layer.
The lower layers log what happened for developers, then return a general error; unless
a more specific error can be returned in a way to allow the client to act (such as NotFound).
</member>
</members>
</doc>
@@ -0,0 +1,207 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using MassTransit;
using Serilog;
using LCU.API.Internal.MaidBotApi.HostedService;
using LCU.Api.Internal.MaidBotApi.Services;
using LCU.Api.Internal.MaidBotApi.EnvironmentView;
using LCU.Api.Internal.MaidBotLib.Repositories;
using LCU.Api.Internal.MaidBotLib.Persistence;
using LCU.Library.Internal.LcuMassTransitModels;
using LCU.Library.Internal.LcuMassTransit;
using LCU.Api.Internal.MaidBotApi.Consumers;
using LCU.Api.Internal.MaidBotApi.Configurations;
using LCU.Library.Internal.Logging.Models;
using LCU.Library.Internal.Logging;
using LCU.Library.Internal.Logging.Middleware;
using LCU.Library.Internal.LcuHttpClient;
using LCU.Api.Internal.MaidBotApi.DataAccesses;
namespace LCU.API.Internal.MaidBot;
public class Program
{
static string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
public static void Main(string[] args)
{
var builder = BuildBuilder(args);
BuildServicesAndConfigurations(builder);
BuildAppAndRun(builder);
}
private static WebApplicationBuilder BuildBuilder(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
builder.Configuration.AddJsonFile("/app/config/environment-appsettings/appsettings.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/app/config/workload-appsettings/appsettings.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/maidbot-api-seq-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/maidbot-api-connection-configuration-password.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/maidbot-api-connection-configuration-username.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/maidbot-database-connection.json", optional: true, reloadOnChange: true);
return builder;
}
static void BuildServicesAndConfigurations(WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(configuration.GetSection("Connections").GetSection("SQL").GetSection("MaidBotDB").Value);
});
services.AddHttpClient();
services.AddLCUHttpServices();
//Configurations
var apiConfiguration = new ApiConfiguration();
configuration.GetSection("Primary").Bind(apiConfiguration);
services.AddSingleton(apiConfiguration);
//debugging
Console.WriteLine(JsonConvert.SerializeObject(apiConfiguration));
var connectionConfiguration = MassTransitConfigurationSetUp.AddConnectionConfiguration(ref services, configuration);
//debugging
Console.WriteLine(JsonConvert.SerializeObject(connectionConfiguration));
var receiveEndpointConfiguration = new ReceiveEndpointConfiguration();
receiveEndpointConfiguration = MassTransitConfigurationSetUp.GetReceiveEndpointConfiguration(configuration, "ReceiveEndpoint");
services.AddSingleton(receiveEndpointConfiguration);
//debugging
Console.WriteLine(JsonConvert.SerializeObject(receiveEndpointConfiguration));
//EnvironmentView
services.AddScoped<IEnvironmentView, EnvironmentView>();
var loggerConfiguration = new LoggingConfiguration(apiConfiguration.AppName,
apiConfiguration.SeqApiKey,
apiConfiguration.SeqUrl);
var lcuLogging = new LcuLogging(loggerConfiguration);
lcuLogging.UseLcuLogging();
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins(
apiConfiguration.CorsAllowedOrigins
).AllowAnyHeader().AllowAnyMethod();
});
});
AddMaidBotServices(ref services);
AddMaidBotLib(ref services);
AddMaidBotDataAccesses(ref services);
AddMassTransitServices(ref services, connectionConfiguration, receiveEndpointConfiguration, apiConfiguration);
// The Hosted
services.AddHostedService<SchedulerHostedService>();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = $"{apiConfiguration.AppName}", Version = "v1" }); });
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
static void AddMaidBotServices(ref IServiceCollection services)
{
services.AddScoped<ILogDistributionService, LogDistributionService>();
services.AddScoped<IQueueEventLogService, QueueEventLogService>();
services.AddScoped<INotificationsApiService, NotificationsApiService>();
services.AddScoped<ILoggingMatrixService, LoggingMatrixService>();
services.AddScoped<IEventLogService, EventLogService>();
services.AddScoped<IDebugEventLogService, DebugEventLogService>();
}
static void AddMaidBotLib(ref IServiceCollection services)
{
services.AddScoped<ILoggingMatrixRepository, LoggingMatrixRepository>();
services.AddScoped<IEventLogRepository, EventLogRepository>();
services.AddScoped<IDebugEventLogRepository, DebugEventLogRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
}
static void AddMaidBotDataAccesses(ref IServiceCollection services)
{
services.AddScoped<INotificationsApiDao, NotificationsApiDao>();
}
static void AddMassTransitServices(ref IServiceCollection services,
ConnectionConfiguration connectionConfiguration,
ReceiveEndpointConfiguration receiveEndpointConfiguration,
ApiConfiguration apiConfiguration)
{
services.AddMassTransit(x =>
{
x.AddConsumer<LoggingEventConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(connectionConfiguration.HostUrl, "/", host =>
{
host.Username(connectionConfiguration.UserName);
host.Password(connectionConfiguration.Password);
});
cfg.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420));
if (apiConfiguration.FeatureFlags.LoggingEventConsumer)
{
cfg.ReceiveEndpoint(receiveEndpointConfiguration.ReceiveEndpoint, e =>
{
e.ConfigureConsumer<LoggingEventConsumer>(context);
e.SetQuorumQueue();
e.PrefetchCount = receiveEndpointConfiguration.PrefetchLimit;
e.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420));
});
}
});
});
services.AddMassTransitHostedService();
}
static void BuildAppAndRun(WebApplicationBuilder builder)
{
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("v1/swagger.json", "LCU.Api.Internal.MaidBotApi v1"));
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest;
});
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
app.Run();
}
}
@@ -0,0 +1,13 @@
namespace LCU.API.Internal.MaidBot.Security
{
public class AzureActiveDirectoryOptions
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string Instance { get; set; }
public string Domain { get; set; }
public string TenantId { get; set; }
public string ValidAudience { get; set; }
}
}
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace LCU.API.Internal.MaidBot.Security
{
public static class AzureAdServiceCollectionExtensions
{
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
=> builder.AddAzureAdBearer(_ => { });
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureActiveDirectoryOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
builder.AddJwtBearer();
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AzureActiveDirectoryOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureActiveDirectoryOptions> azureOptions)
{
_azureOptions = azureOptions.Value;
}
public void Configure(string name, JwtBearerOptions options)
{
options.Audience = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = _azureOptions.ValidAudience
};
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
}
}
@@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using Serilog;
using LCU.Api.Internal.MaidBotApi.Exceptions;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Api.Internal.MaidBotLib.Repositories;
using LCU.Library.Internal.Exceptions;
namespace LCU.Api.Internal.MaidBotApi.Services;
public class DebugEventLogService : IDebugEventLogService
{
private readonly IDebugEventLogRepository _debugEventLogRepository;
private readonly IUnitOfWork _unitOfWork;
public DebugEventLogService(IUnitOfWork unitOfWork,
IDebugEventLogRepository debugEventLogRepository)
{
_unitOfWork = unitOfWork;
_debugEventLogRepository = debugEventLogRepository;
}
public async Task DebugEventLog(EventLog eventLog)
{
if (!string.IsNullOrEmpty(eventLog.DebugData))
{
var debugEventLog = new DebugEventLog(eventLog);
await SaveAsync(debugEventLog);
}
}
public async Task<DebugEventLog> SaveAsync(DebugEventLog debugEventLog)
{
try
{
var result = await _debugEventLogRepository.AddAsync(debugEventLog);
await _unitOfWork.CompleteAsync();
return result;
}
catch (Exception exception)
{
Log.Error(exception, $"Failure saving DebugEventLog.");
throw new ServiceException($"Failure saving DebugEventLog.");
}
}
}
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Serilog;
using Newtonsoft.Json;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Api.Internal.MaidBotLib.Repositories;
using LCU.Library.Internal.Exceptions;
namespace LCU.Api.Internal.MaidBotApi.Services;
public class EventLogService : IEventLogService
{
private readonly IEventLogRepository _eventLogRepository;
private readonly IDebugEventLogService _debugEventLogService;
private readonly ILoggingMatrixService _loggingMatrixService;
private readonly IUnitOfWork _unitOfWork;
public EventLogService(IEventLogRepository eventLogRepository,
IDebugEventLogService debugEventLogService,
ILoggingMatrixService loggingMatrixService,
IUnitOfWork unitOfWork)
{
_eventLogRepository = eventLogRepository;
_debugEventLogService = debugEventLogService;
_loggingMatrixService = loggingMatrixService;
_unitOfWork = unitOfWork;
}
public async Task HandleIncomingEventLog(EventLog eventLog)
{
var loggingMatrixes = await _loggingMatrixService.GetByApplicationLevelAndGroupAsync(eventLog.Application,
eventLog.Level,
eventLog.Group);
if (loggingMatrixes.Any())
{
await SaveAsync(eventLog);
}
await _debugEventLogService.DebugEventLog(eventLog);
}
public async Task<IEnumerable<EventLog>> GetAsync()
{
try
{
return await _eventLogRepository.ListAsync();
}
catch (Exception exception)
{
var message = "Failure retrieving EventLogs.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}. ");
}
}
public async Task<EventLog> SaveAsync(EventLog eventLog)
{
try
{
var result = await _eventLogRepository.AddAsync(eventLog);
await _unitOfWork.CompleteAsync();
return result;
}
catch (Exception exception)
{
var message = $"Failure saving EventLog. EventLog: { JsonConvert.SerializeObject(eventLog)}";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}.");
}
}
public void DeleteMany(List<EventLog> eventLogs)
{
try
{
_eventLogRepository.RemoveRange(eventLogs);
_unitOfWork.Complete();
}
catch (Exception exception)
{
var message = $"Failure to delete EventLogs.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}.");
}
}
}
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using LCU.Api.Internal.MaidBotLib.Models;
namespace LCU.Api.Internal.MaidBotApi.Services;
public interface IDebugEventLogService
{
Task DebugEventLog(EventLog eventLog);
Task<DebugEventLog> SaveAsync(DebugEventLog debugEventLog);
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Api.Internal.MaidBotLib.Models;
namespace LCU.Api.Internal.MaidBotApi.Services;
public interface IEventLogService
{
Task HandleIncomingEventLog(EventLog eventLog);
Task<IEnumerable<EventLog>> GetAsync();
Task<EventLog> SaveAsync(EventLog eventLog);
void DeleteMany(List<EventLog> eventLogs);
}
@@ -0,0 +1,7 @@
using System.Threading.Tasks;
namespace LCU.Api.Internal.MaidBotApi.Services;
public interface ILogDistributionService
{
Task HandleLogDistribution();
}
@@ -0,0 +1,17 @@
using LCU.Api.Internal.MaidBotLib.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LCU.Api.Internal.MaidBotApi.Services
{
public interface ILoggingMatrixService
{
Task<IEnumerable<LoggingMatrix>> GetAsync();
Task<IEnumerable<LoggingMatrix>> GetActiveAsync();
Task<LoggingMatrix> GetByIdAsync(int id);
Task<IEnumerable<LoggingMatrix>> GetByApplicationLevelAndGroupAsync(string application, string level, string group);
Task<IEnumerable<LoggingMatrix>> GetByApplicationAndLevelAsync(string application, string level);
Task<LoggingMatrix> SaveAsync(LoggingMatrix loggingMatrix);
Task<LoggingMatrix> UpdateAsync(LoggingMatrix loggingMatrix);
}
}
@@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Api.Internal.MaidBotLib.Models;
namespace LCU.Api.Internal.MaidBotApi.Services;
public interface INotificationsApiService
{
Task SendNotificationAsync(LoggingMatrix loggingMatrix, List<EventLog> eventLogs);
}
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
using LCU.Api.Internal.MaidBotLib.Models;
namespace LCU.Api.Internal.MaidBotApi.Services;
public interface IQueueEventLogService
{
Task QueueEventLog(EventLog request);
}
@@ -0,0 +1,59 @@
using Serilog;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LCU.Api.Internal.MaidBotApi.Services;
public class LogDistributionService : ILogDistributionService
{
private readonly IEventLogService _eventLogService;
private readonly ILoggingMatrixService _loggingMatrixService;
private readonly INotificationsApiService _notificationsApiService;
public LogDistributionService(IEventLogService eventLogService,
ILoggingMatrixService loggingMatrixService,
INotificationsApiService notificationsApiService)
{
_eventLogService = eventLogService;
_loggingMatrixService = loggingMatrixService;
_notificationsApiService = notificationsApiService;
}
public async Task HandleLogDistribution()
{
try
{
var eventLogs = await _eventLogService.GetAsync();
if(eventLogs == null || !eventLogs.Any())
{
return;
}
var loggingMatrixs = await _loggingMatrixService.GetActiveAsync();
if (loggingMatrixs == null || !loggingMatrixs.Any())
{
return;
}
foreach (var loggingMatrix in loggingMatrixs)
{
var matrixEventLogs = eventLogs.Where(eventLog => eventLog.Application == loggingMatrix.Application &&
eventLog.Level == loggingMatrix.Level &&
eventLog.Group == loggingMatrix.Group).ToList();
if (!matrixEventLogs.Any())
{
continue;
}
await _notificationsApiService.SendNotificationAsync(loggingMatrix, matrixEventLogs);
}
_eventLogService.DeleteMany(eventLogs.ToList());
}
catch (Exception exception)
{
Log.Error(exception, "Failure to Handle Log Distribution.");
}
}
}
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Serilog;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Api.Internal.MaidBotLib.Repositories;
using LCU.Library.Internal.Exceptions;
namespace LCU.Api.Internal.MaidBotApi.Services;
public class LoggingMatrixService : ILoggingMatrixService
{
private readonly ILoggingMatrixRepository _loggingMatrixRepository;
private readonly IUnitOfWork _unitOfWork;
public LoggingMatrixService(IUnitOfWork unitOfWork,
ILoggingMatrixRepository loggingMatrixRepository)
{
_unitOfWork = unitOfWork;
_loggingMatrixRepository = loggingMatrixRepository;
}
public async Task<IEnumerable<LoggingMatrix>> GetAsync()
{
try
{
return await _loggingMatrixRepository.ListAsync();
}
catch (Exception exception)
{
var message = "Failure retrieving Logging Matrixes.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}. ");
}
}
public async Task<IEnumerable<LoggingMatrix>> GetActiveAsync()
{
try
{
return await _loggingMatrixRepository.ListActiveAsync();
}
catch (Exception exception)
{
var message = "Failure retrieving active Logging Matrixes.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}. ");
}
}
public async Task<IEnumerable<LoggingMatrix>> GetByApplicationLevelAndGroupAsync(string application, string level, string group)
{
try
{
return await _loggingMatrixRepository.RetrieveByApplicationLevelAndGroup(application, level, group);
}
catch (Exception exception)
{
var message = $"Failure retrieving Logging Matrixes with application {application} and level {level}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}. ");
}
}
public async Task<IEnumerable<LoggingMatrix>> GetByApplicationAndLevelAsync(string application, string level)
{
try
{
return await _loggingMatrixRepository.RetrieveByApplciationAndLevel(application, level);
}
catch (Exception exception)
{
var message = $"Failure retrieving Logging Matrixes with application {application} and level {level}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}. ");
}
}
public async Task<LoggingMatrix> GetByIdAsync(int id)
{
LoggingMatrix loggingMatrix = null;
try
{
loggingMatrix = await _loggingMatrixRepository.FindByIdAsync(id);
return loggingMatrix;
}
catch (Exception exception)
{
var message = $"Failure retrieving Logging Matrix with id {id}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}.");
}
}
public async Task<LoggingMatrix> SaveAsync(LoggingMatrix loggingMatrix)
{
try
{
var result = await _loggingMatrixRepository.AddAsync(loggingMatrix);
await _unitOfWork.CompleteAsync();
return result;
}
catch (Exception exception)
{
var message = $"Failure saving Logging Matrix. LoggingMatrix: { JsonConvert.SerializeObject(loggingMatrix)}";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}.");
}
}
public async Task<LoggingMatrix> UpdateAsync(LoggingMatrix loggingMatrix)
{
try
{
var existing = await GetByIdAsync(loggingMatrix.LoggingMatrixId);
if (existing == null)
{
return null;
}
existing.Application = loggingMatrix.Application;
existing.Level = loggingMatrix.Level;
existing.Group = loggingMatrix.Group;
existing.Email = loggingMatrix.Email;
existing.Disabled = loggingMatrix.Disabled;
_loggingMatrixRepository.Update(existing);
await _unitOfWork.CompleteAsync();
return existing;
}
catch (Exception exception)
{
var message = $"Failure updating Logging Matrix. LoggingMatrix: { JsonConvert.SerializeObject(loggingMatrix)}";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}.");
}
}
}
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text;
using System.IO;
using Serilog;
using LCU.Api.Internal.MaidBotApi.DataAccesses;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Library.Internal.Exceptions;
using LCU.Shared.Common.NotificationApiLib.Models;
using LCU.Api.Internal.MaidBotApi.Configurations;
namespace LCU.Api.Internal.MaidBotApi.Services;
public class NotificationsApiService : INotificationsApiService
{
private readonly ApiConfiguration _apiConfiguration;
private readonly INotificationsApiDao _notificationsApiDao;
private const string TableRowsTag = "{TableRows}";
private const string ApplicationTag = "{Application}";
private const string LevelTag = "{Level}";
public NotificationsApiService(ApiConfiguration apiConfiguration,
INotificationsApiDao notificationsApiDao)
{
_apiConfiguration = apiConfiguration;
_notificationsApiDao = notificationsApiDao;
}
public async Task SendNotificationAsync(LoggingMatrix loggingMatrix, List<EventLog> eventLogs)
{
try
{
var emailRequest = new EmailRequest
{
Recipient = new List<string> { loggingMatrix.Email },
Subject = $"{loggingMatrix.Application} - {loggingMatrix.Level}",
Body = CreateEmailBody(loggingMatrix, eventLogs)
};
await _notificationsApiDao.SendEmailRequest(emailRequest);
}
catch (Exception exception)
{
var message = $"Failure to Send Maidbot Email.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public string CreateEmailBody(LoggingMatrix loggingMatrix, List<EventLog> eventLogs)
{
var errorRows = new StringBuilder();
foreach(var eventLog in eventLogs)
{
errorRows.Append(CreateEventLogRow(eventLog));
}
var logEmailTemplateReader = new StreamReader(_apiConfiguration.LogEmailTemplate);
string logEmailTemplateOutput = logEmailTemplateReader.ReadToEnd();
logEmailTemplateOutput = logEmailTemplateOutput.Replace(TableRowsTag, errorRows.ToString());
logEmailTemplateOutput = logEmailTemplateOutput.Replace(ApplicationTag, loggingMatrix.Level);
return logEmailTemplateOutput.Replace(LevelTag, loggingMatrix.Application);
}
public string CreateEventLogRow(EventLog eventLog)
{
var errorRowReader = new StreamReader(_apiConfiguration.LogEmailRowTemplate);
string errorRowOutput = errorRowReader.ReadToEnd();
foreach (var property in eventLog.GetType().GetProperties())
{
string value = property.GetValue(eventLog) == null ? string.Empty :
property.GetValue(eventLog).ToString();
errorRowOutput = errorRowOutput.Replace($"{{{property.Name}}}", value);
}
return errorRowOutput;
}
}
@@ -0,0 +1,36 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MassTransit;
using Serilog;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Library.Internal.Exceptions;
using LCU.Library.Internal.LcuMassTransitModels;
namespace LCU.Api.Internal.MaidBotApi.Services;
public class QueueEventLogService : IQueueEventLogService
{
private readonly ISendEndpoint _sendEndpoint;
public QueueEventLogService(IBusControl busControl,
ReceiveEndpointConfiguration receiveEndpointConfiguration)
{
_sendEndpoint = busControl.GetSendEndpoint(receiveEndpointConfiguration.ReceiveEndpointUri).Result;
}
public async Task QueueEventLog(EventLog request)
{
try
{
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await _sendEndpoint.Send(request, cancellationTokenSource.Token);
}
catch (Exception exception)
{
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Fatal(exception, "Failed to Queue EventLog Request.");
throw new ServiceException("Failed to Queue EventLog Request.");
}
}
}
@@ -0,0 +1,4 @@
<tr id="errorRow" style="height: 22px;">
<td style="max-width: 50px;">{LogTimeStamp}</td>
<td style="max-width: 300px;">{Message}</td>
</tr>
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<style>
#errors {
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
}
#errors td, #errors th {
border: 1px solid #ddd;
padding: 8px;
}
#errors tr:nth-child(even) {
background-color: #f2f2f2;
}
#errors th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4CAF50;
color: white;
}
#emailHeader {
color: #0054a2;
}
#errorTableHeader {
background-color: #0054a2;
color: white;
}
</style>
<meta charset="utf-8">
<title>MaidBot: {Application} Event Logs</title>
</head>
<body>
<h1><span id="emailHeader">MaidBot: {Application} {Level} Event Logs</span></h1>
<table id="errors">
<thead id="errorTableHeader">
<tr style="height: 17px;">
<td style="max-width: 50px;font-weight: bolder;">Timestamp</td>
<td style="max-width: 300px; font-weight: bolder;">Log Details</td>
</tr>
</thead>
<tbody id="errorRows">
{TableRows}
</tbody>
</table>
</body>
</html>
@@ -0,0 +1,38 @@
{
"Primary": {
"AppName": "MaidBot Api",
"SeqUrl": "http://localhost:5080",
"SeqApiKey": "VALUE-IN-KEYVAULT",
"NotificationsApiEmail": "https://localhost:50353/api/Email",
"FeatureFlags": {
"LoggingEventConsumer": true,
"RunScheduler": true
},
"BatchSchedulerCron": "*/5 * * * *",
"CorsAllowedOrigins": [
"http://localhost:3000",
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://qa-api.lcu-internal.com",
"https://uat-api.lcu-internal.com",
"https://api.lcu-internal.com"
],
"LogEmailTemplate": "./Templates/LogEmailTemplate.html",
"LogEmailRowTemplate": "./Templates/LogEmailRowTemplate.html"
},
"ConnectionConfiguration": {
"HostUrl": "localhost",
"UserName": "guest",
"Password": "guest"
},
"ReceiveEndpoint": {
"ReceiveEndpoint": "LCU.MaidBot",
"ReceiveEndpointType": "exchange",
"PrefetchLimit": 1
},
"Connections": {
"SQL": {
"MaidBotDB": "data source=localhost;initial catalog=MaidBot;Integrated Security=SSPI;MultipleActiveResultSets=True;App=EntityFramework"
}
}
}