From 9556da873060ce095f352bcf948e70732530c5e4 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 26 Feb 2025 12:53:40 -0600 Subject: [PATCH] Feature/aspire and logging updates (#1842) * Updated Aspire and deps * Improved logging around resource deletion. * pr feedback --- .../Exceptionless.AppHost.csproj | 6 +- .../Extensions/ElasticsearchExtensions.cs | 5 +- .../Extensions/VolumeNameGenerator.cs | 65 ----- .../Exceptionless.Core.csproj | 4 +- src/Exceptionless.Core/Jobs/EventPostsJob.cs | 269 +++++++++--------- .../RemoveBotEventsWorkItemHandler.cs | 3 +- .../Controllers/EventController.cs | 13 + .../Controllers/OrganizationController.cs | 2 +- .../Controllers/StackController.cs | 16 +- .../Exceptionless.Web.csproj | 4 +- .../Extensions/LoggerExtensions.cs | 2 +- .../Controllers/ProjectControllerTests.cs | 1 + .../Exceptionless.Tests.csproj | 2 +- 13 files changed, 175 insertions(+), 217 deletions(-) delete mode 100644 src/Exceptionless.AppHost/Extensions/VolumeNameGenerator.cs diff --git a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj index 27ad3a8162..39b3aa2419 100644 --- a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj +++ b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index 423bb813ae..f98b742995 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -1,5 +1,4 @@ -using Aspire.Hosting.Lifecycle; -using Aspire.Hosting.Utils; +using Aspire.Hosting.Lifecycle; using HealthChecks.Elasticsearch; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -100,7 +99,7 @@ public static IResourceBuilder WithDataVolume(this IResou { ArgumentNullException.ThrowIfNull(builder); - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/usr/share/elasticsearch/data"); + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/usr/share/elasticsearch/data"); } public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source) diff --git a/src/Exceptionless.AppHost/Extensions/VolumeNameGenerator.cs b/src/Exceptionless.AppHost/Extensions/VolumeNameGenerator.cs deleted file mode 100644 index 6ecbc551f8..0000000000 --- a/src/Exceptionless.AppHost/Extensions/VolumeNameGenerator.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace Aspire.Hosting.Utils; - -internal static class VolumeNameGenerator -{ - public static string CreateVolumeName(IResourceBuilder builder, string suffix) where T : IResource - { - if (!HasOnlyValidChars(suffix)) - { - throw new ArgumentException($"The suffix '{suffix}' contains invalid characters. Only [a-zA-Z0-9_.-] are allowed.", nameof(suffix)); - } - - // Creates a volume name with the form < c > $"{applicationName}-{sha256 of apphost path}-{resourceName}-{suffix}, e.g. "myapplication-a345f2451-postgres-data". - // Create volume name like "{Sanitize(appname).Lower()}-{sha256.Lower()}-postgres-data" - - // Compute a short hash of the content root path to differentiate between multiple AppHost projects with similar volume names - var safeApplicationName = Sanitize(builder.ApplicationBuilder.Environment.ApplicationName).ToLowerInvariant(); - var applicationHash = builder.ApplicationBuilder.Configuration["AppHost:Sha256"]![..10].ToLowerInvariant(); - var resourceName = builder.Resource.Name; - return $"{safeApplicationName}-{applicationHash}-{resourceName}-{suffix}"; - } - - public static string Sanitize(string name) - { - return string.Create(name.Length, name, static (s, name) => - { - // According to the error message from docker CLI, volume names must be of form "[a-zA-Z0-9][a-zA-Z0-9_.-]" - var nameSpan = name.AsSpan(); - - for (var i = 0; i < nameSpan.Length; i++) - { - var c = nameSpan[i]; - - s[i] = IsValidChar(i, c) ? c : '_'; - } - }); - } - - private static bool HasOnlyValidChars(string value) - { - for (var i = 0; i < value.Length; i++) - { - if (!IsValidChar(i, value[i])) - { - return false; - } - } - return true; - } - - private static bool IsValidChar(int i, char c) - { - if (i == 0 && !(char.IsAsciiLetter(c) || char.IsNumber(c))) - { - // First char must be a letter or number - return false; - } - else if (!(char.IsAsciiLetter(c) || char.IsNumber(c) || c == '_' || c == '.' || c == '-')) - { - // Subsequent chars must be a letter, number, underscore, period, or hyphen - return false; - } - - return true; - } -} diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index 034a52ffc7..8cd43c0752 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -20,7 +20,7 @@ - + @@ -31,7 +31,7 @@ - + diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 2487c773a4..740b6d44f6 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -53,6 +53,8 @@ protected override async Task ProcessQueueEntryAsync(QueueEntryContex { var entry = context.QueueEntry; var ep = entry.Value; + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(ep.OrganizationId).Project(ep.ProjectId)); + string payloadPath = Path.ChangeExtension(entry.Value.FilePath, ".payload"); var payloadTask = AppDiagnostics.PostsMarkFileActiveTime.TimeAsync(() => _eventPostService.GetEventPostPayloadAsync(payloadPath)); var projectTask = _projectRepository.GetByIdAsync(ep.ProjectId, o => o.Cache()); @@ -72,178 +74,175 @@ protected override async Task ProcessQueueEntryAsync(QueueEntryContex return JobResult.FailedWithMessage($"Unable to process payload '{payloadPath}' ({payload.LongLength} bytes): Maximum event post size limit ({_appOptions.MaximumEventPostSize} bytes) reached."); } - using (_logger.BeginScope(new ExceptionlessState().Organization(ep.OrganizationId).Project(ep.ProjectId))) + AppDiagnostics.PostsCompressedSize.Record(payload.Length); + + bool isDebugLogLevelEnabled = _logger.IsEnabled(LogLevel.Debug); + bool isInternalProject = ep.ProjectId == _appOptions.InternalProjectId; + if (!isInternalProject && _logger.IsEnabled(LogLevel.Information)) { - AppDiagnostics.PostsCompressedSize.Record(payload.Length); + using (_logger.BeginScope(new ExceptionlessState().Tag("processing").Tag("compressed").Tag(ep.ContentEncoding).Value(payload.Length))) + _logger.LogInformation("Processing post: id={QueueEntryId} path={FilePath} project={ProjectId} ip={IpAddress} v={ApiVersion} agent={UserAgent}", entry.Id, payloadPath, ep.ProjectId, ep.IpAddress, ep.ApiVersion, ep.UserAgent); + } - bool isDebugLogLevelEnabled = _logger.IsEnabled(LogLevel.Debug); - bool isInternalProject = ep.ProjectId == _appOptions.InternalProjectId; - if (!isInternalProject && _logger.IsEnabled(LogLevel.Information)) - { - using (_logger.BeginScope(new ExceptionlessState().Tag("processing").Tag("compressed").Tag(ep.ContentEncoding).Value(payload.Length))) - _logger.LogInformation("Processing post: id={QueueEntryId} path={FilePath} project={ProjectId} ip={IpAddress} v={ApiVersion} agent={UserAgent}", entry.Id, payloadPath, ep.ProjectId, ep.IpAddress, ep.ApiVersion, ep.UserAgent); - } + var project = await projectTask; + if (project is null) + { + if (!isInternalProject) _logger.LogError("Unable to process EventPost {FilePath}: Unable to load project: {ProjectId}", payloadPath, ep.ProjectId); + await Task.WhenAll(CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime), organizationTask); + return JobResult.Success; + } - var project = await projectTask; - if (project is null) + long maxEventPostSize = _appOptions.MaximumEventPostSize; + byte[] uncompressedData = payload; + if (!String.IsNullOrEmpty(ep.ContentEncoding)) + { + if (!isInternalProject && isDebugLogLevelEnabled) { - if (!isInternalProject) _logger.LogError("Unable to process EventPost {FilePath}: Unable to load project: {Project}", payloadPath, ep.ProjectId); - await Task.WhenAll(CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime), organizationTask); - return JobResult.Success; + using (_logger.BeginScope(new ExceptionlessState().Tag("decompressing").Tag(ep.ContentEncoding))) + _logger.LogDebug("Decompressing EventPost: {QueueEntryId} ({CompressedBytes} bytes)", entry.Id, payload.Length); } - long maxEventPostSize = _appOptions.MaximumEventPostSize; - byte[] uncompressedData = payload; - if (!String.IsNullOrEmpty(ep.ContentEncoding)) + maxEventPostSize = _maximumUncompressedEventPostSize; + try { - if (!isInternalProject && isDebugLogLevelEnabled) - { - using (_logger.BeginScope(new ExceptionlessState().Tag("decompressing").Tag(ep.ContentEncoding))) - _logger.LogDebug("Decompressing EventPost: {QueueEntryId} ({CompressedBytes} bytes)", entry.Id, payload.Length); - } - - maxEventPostSize = _maximumUncompressedEventPostSize; - try + AppDiagnostics.PostsDecompressionTime.Time(() => { - AppDiagnostics.PostsDecompressionTime.Time(() => - { - uncompressedData = uncompressedData.Decompress(ep.ContentEncoding); - }); - } - catch (Exception ex) - { - AppDiagnostics.PostsDecompressionErrors.Add(1); - await Task.WhenAll(CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime), organizationTask); - return JobResult.FailedWithMessage($"Unable to decompress EventPost data '{payloadPath}' ({payload.Length} bytes compressed): {ex.Message}"); - } + uncompressedData = uncompressedData.Decompress(ep.ContentEncoding); + }); } - - AppDiagnostics.PostsUncompressedSize.Record(payload.LongLength); - if (uncompressedData.Length > maxEventPostSize) + catch (Exception ex) { - var org = await organizationTask; - await _usageService.IncrementTooBigAsync(org.Id, project.Id); - await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); - return JobResult.FailedWithMessage($"Unable to process decompressed EventPost data '{payloadPath}' ({payload.Length} bytes compressed, {uncompressedData.Length} bytes): Maximum uncompressed event post size limit ({maxEventPostSize} bytes) reached."); + AppDiagnostics.PostsDecompressionErrors.Add(1); + await Task.WhenAll(CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime), organizationTask); + return JobResult.FailedWithMessage($"Unable to decompress EventPost data '{payloadPath}' ({payload.Length} bytes compressed): {ex.Message}"); } + } - if (!isInternalProject && isDebugLogLevelEnabled) - { - using (_logger.BeginScope(new ExceptionlessState().Tag("uncompressed").Value(uncompressedData.Length))) - _logger.LogDebug("Processing uncompressed EventPost: {QueueEntryId} ({UncompressedBytes} bytes)", entry.Id, uncompressedData.Length); - } + AppDiagnostics.PostsUncompressedSize.Record(payload.LongLength); + if (uncompressedData.Length > maxEventPostSize) + { + var org = await organizationTask; + await _usageService.IncrementTooBigAsync(org.Id, project.Id); + await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); + return JobResult.FailedWithMessage($"Unable to process decompressed EventPost data '{payloadPath}' ({payload.Length} bytes compressed, {uncompressedData.Length} bytes): Maximum uncompressed event post size limit ({maxEventPostSize} bytes) reached."); + } - var createdUtc = _timeProvider.GetUtcNow().UtcDateTime; - var events = ParseEventPost(ep, createdUtc, uncompressedData, entry.Id, isInternalProject); - if (events is null || events.Count == 0) - { - await Task.WhenAll(CompleteEntryAsync(entry, ep, createdUtc), organizationTask); - return JobResult.Success; - } + if (!isInternalProject && isDebugLogLevelEnabled) + { + using (_logger.BeginScope(new ExceptionlessState().Tag("uncompressed").Value(uncompressedData.Length))) + _logger.LogDebug("Processing uncompressed EventPost: {QueueEntryId} ({UncompressedBytes} bytes)", entry.Id, uncompressedData.Length); + } - if (context.CancellationToken.IsCancellationRequested) - { - await Task.WhenAll(AbandonEntryAsync(entry), organizationTask); - return JobResult.Cancelled; - } + var createdUtc = _timeProvider.GetUtcNow().UtcDateTime; + var events = ParseEventPost(ep, createdUtc, uncompressedData, entry.Id, isInternalProject); + if (events is null || events.Count == 0) + { + await Task.WhenAll(CompleteEntryAsync(entry, ep, createdUtc), organizationTask); + return JobResult.Success; + } - var organization = await organizationTask; - if (organization is null) - { - if (!isInternalProject) - _logger.LogError("Unable to process EventPost {FilePath}: Unable to load organization: {OrganizationId}", payloadPath, project.OrganizationId); + if (context.CancellationToken.IsCancellationRequested) + { + await Task.WhenAll(AbandonEntryAsync(entry), organizationTask); + return JobResult.Cancelled; + } - await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); - return JobResult.Success; - } + var organization = await organizationTask; + if (organization is null) + { + if (!isInternalProject) + _logger.LogError("Unable to process EventPost {FilePath}: Unable to load organization: {OrganizationId}", payloadPath, project.OrganizationId); - // Don't process all the events if it will put the account over its limits. - int eventsToProcess = await _usageService.GetEventsLeftAsync(organization.Id); - if (eventsToProcess < 1) - { - if (!isInternalProject) - _logger.LogDebug("Unable to process EventPost {FilePath}: Over plan limits", payloadPath); + await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); + return JobResult.Success; + } - await _usageService.IncrementBlockedAsync(organization.Id, project.Id, events.Count); + // Don't process all the events if it will put the account over its limits. + int eventsToProcess = await _usageService.GetEventsLeftAsync(organization.Id); + if (eventsToProcess < 1) + { + if (!isInternalProject) + _logger.LogDebug("Unable to process EventPost {FilePath}: Over plan limits", payloadPath); - await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); - return JobResult.Success; - } + await _usageService.IncrementBlockedAsync(organization.Id, project.Id, events.Count); - // Keep track of the original event payload size, we can save some processing for retries in the case it was a massive batch. - bool isSingleEvent = events.Count == 1; + await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); + return JobResult.Success; + } - // Discard any events over the plan limit. - if (eventsToProcess < events.Count) - { - int discarded = events.Count - eventsToProcess; - events = events.Take(eventsToProcess).ToList(); + // Keep track of the original event payload size, we can save some processing for retries in the case it was a massive batch. + bool isSingleEvent = events.Count == 1; - await _usageService.IncrementBlockedAsync(organization.Id, project.Id, discarded); - } + // Discard any events over the plan limit. + if (eventsToProcess < events.Count) + { + int discarded = events.Count - eventsToProcess; + events = events.Take(eventsToProcess).ToList(); - int errorCount = 0; - var eventsToRetry = new List(); - try + await _usageService.IncrementBlockedAsync(organization.Id, project.Id, discarded); + } + + int errorCount = 0; + var eventsToRetry = new List(); + try + { + var contexts = await _eventPipeline.RunAsync(events, organization, project, ep); + if (!isInternalProject && isDebugLogLevelEnabled) { - var contexts = await _eventPipeline.RunAsync(events, organization, project, ep); - if (!isInternalProject && isDebugLogLevelEnabled) - { - using (_logger.BeginScope(new ExceptionlessState().Value(contexts.Count))) - _logger.LogDebug("Ran {@Value} events through the pipeline: id={QueueEntryId} success={SuccessCount} error={ErrorCount}", contexts.Count, entry.Id, contexts.Count(r => r.IsProcessed), contexts.Count(r => r.HasError)); - } + using (_logger.BeginScope(new ExceptionlessState().Value(contexts.Count))) + _logger.LogDebug("Ran {@Value} events through the pipeline: id={QueueEntryId} success={SuccessCount} error={ErrorCount}", contexts.Count, entry.Id, contexts.Count(r => r.IsProcessed), contexts.Count(r => r.HasError)); + } - // increment the plan usage counters (note: OverageHandler already incremented usage by 1) - int processedEvents = contexts.Count(c => c.IsProcessed); - await _usageService.IncrementTotalAsync(organization.Id, project.Id, processedEvents); + // increment the plan usage counters (note: OverageHandler already incremented usage by 1) + int processedEvents = contexts.Count(c => c.IsProcessed); + await _usageService.IncrementTotalAsync(organization.Id, project.Id, processedEvents); - int discardedEvents = contexts.Count(c => c.IsDiscarded); - await _usageService.IncrementDiscardedAsync(organization.Id, project.Id, discardedEvents); + int discardedEvents = contexts.Count(c => c.IsDiscarded); + await _usageService.IncrementDiscardedAsync(organization.Id, project.Id, discardedEvents); - foreach (var ctx in contexts) - { - if (ctx.IsCancelled) - continue; + foreach (var ctx in contexts) + { + if (ctx.IsCancelled) + continue; - if (!ctx.HasError) - continue; + if (!ctx.HasError) + continue; - if (!isInternalProject) _logger.LogError(ctx.Exception, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ctx.ErrorMessage); - if (ctx.Exception is ValidationException or MiniValidatorException) - continue; + if (!isInternalProject) _logger.LogError(ctx.Exception, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ctx.ErrorMessage); + if (ctx.Exception is ValidationException or MiniValidatorException) + continue; - errorCount++; - if (!isSingleEvent) - { - // Put this single event back into the queue so we can retry it separately. - eventsToRetry.Add(ctx.Event); - } + errorCount++; + if (!isSingleEvent) + { + // Put this single event back into the queue so we can retry it separately. + eventsToRetry.Add(ctx.Event); } } - catch (Exception ex) + } + catch (Exception ex) + { + if (!isInternalProject) _logger.LogError(ex, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ex.Message); + if (ex is ArgumentException || ex is DocumentNotFoundException) { - if (!isInternalProject) _logger.LogError(ex, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ex.Message); - if (ex is ArgumentException || ex is DocumentNotFoundException) - { - await CompleteEntryAsync(entry, ep, createdUtc); - return JobResult.Success; - } - - errorCount++; - if (!isSingleEvent) - eventsToRetry.AddRange(events); + await CompleteEntryAsync(entry, ep, createdUtc); + return JobResult.Success; } - if (eventsToRetry.Count > 0) - await AppDiagnostics.PostsRetryTime.TimeAsync(() => RetryEventsAsync(eventsToRetry, ep, entry, project, isInternalProject)); + errorCount++; + if (!isSingleEvent) + eventsToRetry.AddRange(events); + } - if (isSingleEvent && errorCount > 0) - await AbandonEntryAsync(entry); - else - await CompleteEntryAsync(entry, ep, createdUtc); + if (eventsToRetry.Count > 0) + await AppDiagnostics.PostsRetryTime.TimeAsync(() => RetryEventsAsync(eventsToRetry, ep, entry, project, isInternalProject)); - return JobResult.Success; - } + if (isSingleEvent && errorCount > 0) + await AbandonEntryAsync(entry); + else + await CompleteEntryAsync(entry, ep, createdUtc); + + return JobResult.Success; } private List? ParseEventPost(EventPostInfo ep, DateTime createdUtc, byte[] uncompressedData, string queueEntryId, bool isInternalProject) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs index c261d53791..22d1598e19 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs @@ -29,11 +29,12 @@ public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ICacheCl public override async Task HandleItemAsync(WorkItemContext context) { var wi = context.GetData(); - using var _ = Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId)); + using var _ = Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId).Tag("Delete").Tag("Bot")); Log.LogInformation("Received remove bot events work item OrganizationId={OrganizationId} ProjectId={ProjectId}, ClientIpAddress={ClientIpAddress}, UtcStartDate={UtcStartDate}, UtcEndDate={UtcEndDate}", wi.OrganizationId, wi.ProjectId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); await context.ReportProgressAsync(0, $"Starting deleting of bot events... OrganizationId={wi.OrganizationId}"); long deleted = await _eventRepository.RemoveAllAsync(wi.OrganizationId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); await context.ReportProgressAsync(100, $"Bot events deleted: {deleted} OrganizationId={wi.OrganizationId}"); + Log.LogInformation("Removed {Deleted} bot events OrganizationId={OrganizationId} ProjectId={ProjectId}, ClientIpAddress={ClientIpAddress}, UtcStartDate={UtcStartDate}, UtcEndDate={UtcEndDate}", deleted, wi.OrganizationId, wi.ProjectId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); } } diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index 67392fcb99..82963add54 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -1441,4 +1441,17 @@ private async Task> GetUserCountByProjectIdsAsync(ICo return totals; } + + protected override Task> DeleteModelsAsync(ICollection events) + { + var user = CurrentUser; + foreach (var projectEvents in events.GroupBy(ev => ev.ProjectId)) + { + var ev = projectEvents.First(); + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(ev.OrganizationId).Project(ev.ProjectId).Tag("Delete").Identity(user.EmailAddress).Property("User", user).SetHttpContext(HttpContext)); + _logger.LogInformation("User {User} deleted {RemovedCount} events in project ({ProjectId})", user.Id, projectEvents.Count(), ev.ProjectId); + } + + return base.DeleteModelsAsync(events); + } } diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs index 77d3e7df9c..8708a2e5b1 100644 --- a/src/Exceptionless.Web/Controllers/OrganizationController.cs +++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs @@ -191,7 +191,7 @@ protected override async Task> DeleteModelsAsync(ICollection await _organizationService.SoftDeleteOrganizationAsync(organization, user.Id); } - return Enumerable.Empty(); + return []; } #endregion diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index 51f3c1faa5..ddde1b07c3 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -39,7 +39,6 @@ public class StackController : RepositoryApiController _webHookNotificationQueue; - private readonly BillingManager _billingManager; private readonly FormattingPluginManager _formattingPluginManager; private readonly AppOptions _options; @@ -52,7 +51,6 @@ public StackController( WebHookDataPluginManager webHookDataPluginManager, IQueue webHookNotificationQueue, ICacheClient cacheClient, - BillingManager billingManager, FormattingPluginManager formattingPluginManager, SemanticVersionParser semanticVersionParser, IMapper mapper, @@ -70,7 +68,6 @@ ILoggerFactory loggerFactory _webHookDataPluginManager = webHookDataPluginManager; _webHookNotificationQueue = webHookNotificationQueue; _cache = cacheClient; - _billingManager = billingManager; _formattingPluginManager = formattingPluginManager; _semanticVersionParser = semanticVersionParser; _options = options; @@ -655,4 +652,17 @@ private async Task> GetUserCountByProjectIdsAsync(ICo return totals; } + + protected override Task> DeleteModelsAsync(ICollection stacks) + { + var user = CurrentUser; + foreach (var projectStacks in stacks.GroupBy(ev => ev.ProjectId)) + { + var stack = projectStacks.First(); + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(stack.OrganizationId).Project(stack.ProjectId).Tag("Delete").Identity(user.EmailAddress).Property("User", user).SetHttpContext(HttpContext)); + _logger.LogInformation("User {User} deleted {RemovedCount} stacks in project ({ProjectId})", user.Id, projectStacks.Count(), stack.ProjectId); + } + + return base.DeleteModelsAsync(stacks); + } } diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index c2b85bd122..0511a84537 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -20,7 +20,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/src/Exceptionless.Web/Extensions/LoggerExtensions.cs b/src/Exceptionless.Web/Extensions/LoggerExtensions.cs index 22ee809de9..a9f4022445 100644 --- a/src/Exceptionless.Web/Extensions/LoggerExtensions.cs +++ b/src/Exceptionless.Web/Extensions/LoggerExtensions.cs @@ -36,7 +36,7 @@ internal static class LoggerExtensions LoggerMessage.Define( LogLevel.Information, new EventId(5, nameof(UserDeletingOrganization)), - "User {User} deleting organization: {OrganizationName} ({OrganizationId})"); + "User {User} deleting organization: {OrganizationName} ({OrganizationId})"); private static readonly Action _userLoggedIn = LoggerMessage.Define( diff --git a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs index c3010d96a6..f3c6d45491 100644 --- a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs @@ -99,6 +99,7 @@ public async Task CanGetProjectConfiguration() Assert.True(response.Content.Headers.ContentLength > 0); var config = await response.DeserializeAsync(); + Assert.NotNull(config); Assert.True(config.Settings.GetBoolean("IncludeConditionalData")); Assert.Equal(0, config.Version); } diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index bbfa917b2e..b1eae3cd37 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -9,7 +9,7 @@ - +