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 @@
-
+