-
-
Notifications
You must be signed in to change notification settings - Fork 507
Add project sample data generation #2231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,21 +35,33 @@ public GenerateSampleEventsWorkItemHandler( | |
|
|
||
| public override Task<ILock?> GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) | ||
| { | ||
| return _lockProvider.TryAcquireAsync(nameof(GenerateSampleEventsWorkItemHandler), TimeSpan.FromMinutes(30), cancellationToken); | ||
| var generateSampleEventsWorkItem = (GenerateSampleEventsWorkItem)workItem; | ||
| string cacheKey = IsProjectScoped(generateSampleEventsWorkItem) | ||
| ? $"{nameof(GenerateSampleEventsWorkItemHandler)}:{generateSampleEventsWorkItem.ProjectId}" | ||
| : nameof(GenerateSampleEventsWorkItemHandler); | ||
|
|
||
| return _lockProvider.TryAcquireAsync(cacheKey, TimeSpan.FromMinutes(30), cancellationToken); | ||
| } | ||
|
|
||
| public override async Task HandleItemAsync(WorkItemContext context) | ||
| { | ||
| var workItem = context.GetData<GenerateSampleEventsWorkItem>()!; | ||
| int eventCount = Math.Clamp(workItem.EventCount, 1, 10000); | ||
| int daysBack = Math.Clamp(workItem.DaysBack, 1, 365); | ||
| int acceptedDaysBack = Math.Min(daysBack, 3); | ||
|
|
||
| Log.LogInformation("Generating {EventCount} sample events over {DaysBack} days", eventCount, daysBack); | ||
| await context.ReportProgressAsync(0, $"Generating {eventCount} sample events"); | ||
| Log.LogInformation("Generating {EventCount} sample events over {DaysBack} days", eventCount, acceptedDaysBack); | ||
| await context.ReportProgressAsync(0, $"Generating {eventCount} sample events over {acceptedDaysBack} days"); | ||
|
|
||
| var generator = new RandomEventGenerator(_timeProvider); | ||
| var utcNow = _timeProvider.GetUtcNow().UtcDateTime; | ||
| var minDate = utcNow.AddDays(-daysBack); | ||
| var minDate = utcNow.AddDays(-acceptedDaysBack); | ||
|
|
||
| if (IsProjectScoped(workItem)) | ||
| { | ||
| await GenerateProjectSampleEventsAsync(context, generator, workItem, eventCount, minDate, utcNow); | ||
| return; | ||
| } | ||
|
|
||
| var projectResults = await _projectRepository.GetByOrganizationIdAsync(SampleDataService.TEST_ORG_ID); | ||
| var projectList = projectResults.Documents.ToList(); | ||
|
|
@@ -69,7 +81,7 @@ public override async Task HandleItemAsync(WorkItemContext context) | |
| int eventsPerProject = eventCount / projectList.Count; | ||
| int remainder = eventCount % projectList.Count; | ||
| int totalProcessed = 0; | ||
| const int batchSize = 50; | ||
| const int batchSize = 100; | ||
|
|
||
| for (int p = 0; p < projectList.Count; p++) | ||
| { | ||
|
|
@@ -98,4 +110,52 @@ public override async Task HandleItemAsync(WorkItemContext context) | |
| await context.ReportProgressAsync(100, $"Generated {totalProcessed} sample events across {projectList.Count} projects"); | ||
| Log.LogInformation("Generated {TotalEvents} sample events across {ProjectCount} projects", totalProcessed, projectList.Count); | ||
| } | ||
|
|
||
| private async Task GenerateProjectSampleEventsAsync(WorkItemContext context, RandomEventGenerator generator, GenerateSampleEventsWorkItem workItem, int eventCount, DateTime minDate, DateTime utcNow) | ||
| { | ||
| if (String.IsNullOrEmpty(workItem.OrganizationId) || String.IsNullOrEmpty(workItem.ProjectId)) | ||
| { | ||
| Log.LogWarning("Unable to generate project sample events because organization id or project id was not specified"); | ||
| return; | ||
| } | ||
|
|
||
| var organization = await _organizationRepository.GetByIdAsync(workItem.OrganizationId); | ||
| if (organization is null) | ||
| { | ||
| Log.LogWarning("Organization {OrganizationId} not found when generating sample events", workItem.OrganizationId); | ||
| return; | ||
| } | ||
|
|
||
| var project = await _projectRepository.GetByIdAsync(workItem.ProjectId); | ||
| if (project is null || project.OrganizationId != organization.Id) | ||
| { | ||
| Log.LogWarning("Project {ProjectId} not found in organization {OrganizationId} when generating sample events", workItem.ProjectId, workItem.OrganizationId); | ||
| return; | ||
| } | ||
|
|
||
| int totalProcessed = 0; | ||
| const int batchSize = 100; | ||
| var events = generator.Generate(organization.Id, project.Id, eventCount, minDate, utcNow); | ||
|
|
||
| for (int i = 0; i < events.Count; i += batchSize) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use new Enumerable.Chunk and foreach over the chunks. |
||
| { | ||
| if (context.CancellationToken.IsCancellationRequested) | ||
| break; | ||
|
|
||
| var batch = events.Skip(i).Take(batchSize).ToList(); | ||
| await _eventPipeline.RunAsync(batch, organization, project); | ||
| totalProcessed += batch.Count; | ||
|
|
||
| int percentage = (int)Math.Min(99, totalProcessed * 100.0 / eventCount); | ||
| await context.ReportProgressAsync(percentage, $"Processed {totalProcessed}/{eventCount} events"); | ||
| } | ||
|
|
||
| await context.ReportProgressAsync(100, $"Generated {totalProcessed} sample events for project {project.Id}"); | ||
| Log.LogInformation("Generated {TotalEvents} sample events for project {ProjectId}", totalProcessed, project.Id); | ||
| } | ||
|
|
||
| private static bool IsProjectScoped(GenerateSampleEventsWorkItem workItem) | ||
| { | ||
| return !String.IsNullOrEmpty(workItem.OrganizationId) && !String.IsNullOrEmpty(workItem.ProjectId); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| using Exceptionless.Core.Models.WorkItems; | ||
| using Exceptionless.Core.Repositories; | ||
| using Foundatio.Caching; | ||
| using Foundatio.Jobs; | ||
| using Foundatio.Lock; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Exceptionless.Core.Jobs.WorkItemHandlers; | ||
|
|
||
| public class ResetProjectDataWorkItemHandler : WorkItemHandlerBase | ||
| { | ||
| private readonly IEventRepository _eventRepository; | ||
| private readonly IStackRepository _stackRepository; | ||
| private readonly ICacheClient _cacheClient; | ||
| private readonly ILockProvider _lockProvider; | ||
|
|
||
| public ResetProjectDataWorkItemHandler(IEventRepository eventRepository, IStackRepository stackRepository, ICacheClient cacheClient, ILockProvider lockProvider, ILoggerFactory loggerFactory) : base(loggerFactory) | ||
| { | ||
| _eventRepository = eventRepository; | ||
| _stackRepository = stackRepository; | ||
| _cacheClient = cacheClient; | ||
| _lockProvider = lockProvider; | ||
| } | ||
|
|
||
| public override Task<ILock?> GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) | ||
| { | ||
| string cacheKey = $"{nameof(ResetProjectDataWorkItemHandler)}:{((ResetProjectDataWorkItem)workItem).ProjectId}"; | ||
| return _lockProvider.TryAcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); | ||
| } | ||
|
|
||
| public override async Task HandleItemAsync(WorkItemContext context) | ||
| { | ||
| var workItem = context.GetData<ResetProjectDataWorkItem>()!; | ||
|
|
||
| using (Log.BeginScope(new ExceptionlessState().Organization(workItem.OrganizationId).Project(workItem.ProjectId))) | ||
| { | ||
| Log.LogInformation("Received reset project data work item for project: {ProjectId}", workItem.ProjectId); | ||
| await context.ReportProgressAsync(0, "Starting project data reset..."); | ||
|
|
||
| long removedEvents = await _eventRepository.RemoveAllByProjectIdAsync(workItem.OrganizationId, workItem.ProjectId); | ||
| await context.ReportProgressAsync(50, $"Events removed: {removedEvents}"); | ||
|
|
||
| long removedStacks = await _stackRepository.RemoveAllByProjectIdAsync(workItem.OrganizationId, workItem.ProjectId); | ||
| await _cacheClient.RemoveByPrefixAsync(String.Concat("stack-filter:", workItem.OrganizationId, ":", workItem.ProjectId)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably create a static cache key for this stack filter and reference it here, so it doesn't break over time and we know usages. |
||
|
|
||
| await context.ReportProgressAsync(100, $"Events removed: {removedEvents}, stacks removed: {removedStacks}"); | ||
| Log.LogInformation("Reset project data for project {ProjectId}. Events removed: {RemovedEvents}, stacks removed: {RemovedStacks}", workItem.ProjectId, removedEvents, removedStacks); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ namespace Exceptionless.Core.Models.WorkItems; | |
|
|
||
| public record GenerateSampleEventsWorkItem | ||
| { | ||
| public string? OrganizationId { get; init; } | ||
| public string? ProjectId { get; init; } | ||
|
Comment on lines
+5
to
+6
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't these be required? |
||
| public int EventCount { get; init; } = 100; | ||
| public int DaysBack { get; init; } = 7; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace Exceptionless.Core.Models.WorkItems; | ||
|
|
||
| public record ResetProjectDataWorkItem | ||
| { | ||
| public required string OrganizationId { get; init; } | ||
| public required string ProjectId { get; init; } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.