Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Exceptionless.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO
handlers.Register<ReindexWorkItem>(s.GetRequiredService<ReindexWorkItemHandler>);
handlers.Register<RemoveBotEventsWorkItem>(s.GetRequiredService<RemoveBotEventsWorkItemHandler>);
handlers.Register<RemoveStacksWorkItem>(s.GetRequiredService<RemoveStacksWorkItemHandler>);
handlers.Register<ResetProjectDataWorkItem>(s.GetRequiredService<ResetProjectDataWorkItemHandler>);
handlers.Register<SetLocationFromGeoWorkItem>(s.GetRequiredService<SetLocationFromGeoWorkItemHandler>);
handlers.Register<SetProjectIsConfiguredWorkItem>(s.GetRequiredService<SetProjectIsConfiguredWorkItemHandler>);
handlers.Register<UpdateProjectNotificationSettingsWorkItem>(s.GetRequiredService<UpdateProjectNotificationSettingsWorkItemHandler>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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++)
{
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (project is null || project.OrganizationId != organization.Id)
if (project is null || !String.Equals(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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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; }
}
14 changes: 14 additions & 0 deletions src/Exceptionless.Core/Utility/SampleDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,18 @@ await _workItemQueue.EnqueueAsync(new GenerateSampleEventsWorkItem
});
_logger.LogInformation("Enqueued sample event generation: {EventCount} events over {DaysBack} days", eventCount, daysBack);
}

public async Task<string> EnqueueSampleEventsAsync(string organizationId, string projectId, int eventCount = 100, int daysBack = 7)
{
string workItemId = await _workItemQueue.EnqueueAsync(new GenerateSampleEventsWorkItem
{
OrganizationId = organizationId,
ProjectId = projectId,
EventCount = eventCount,
DaysBack = daysBack
});

_logger.LogInformation("Enqueued sample event generation for project {ProjectId}: {EventCount} events over {DaysBack} days", projectId, eventCount, daysBack);
return workItemId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,32 @@
$state.go("app.project-frequent", { projectId: vm._projectId });
}

function generateSampleData() {
if (vm.isGeneratingSampleData) {
return;
}

function onSuccess() {
notificationService.success(
translateService.T("Sample data generation has been queued. Events will appear shortly.")
);
}

function onFailure() {
notificationService.error(
translateService.T("An error occurred while generating sample data for your project.")
);
}

vm.isGeneratingSampleData = true;
return projectService
.generateSampleData(vm._projectId)
.then(onSuccess, onFailure)
.finally(function () {
vm.isGeneratingSampleData = false;
});
}

this.$onInit = function $onInit() {
vm._projectId = $stateParams.id;
vm._canRedirect = $stateParams.redirect === "true";
Expand All @@ -177,9 +203,11 @@
vm.copyCommandLineCode = copyCommandLineCode;
vm.copied = copied;
vm.currentProjectType = {};
vm.generateSampleData = generateSampleData;
vm.isBashShell = isBashShell;
vm.isCommandLine = isCommandLine;
vm.isDotNet = isDotNet;
vm.isGeneratingSampleData = false;
vm.isJavaScript = isJavaScript;
vm.isNode = isNode;
vm.navigateToDashboard = navigateToDashboard;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,19 @@
</div>
<footer class="panel-footer">
<div class="pull-right">
<button
type="button"
class="btn btn-primary m-r-xs"
ng-click="vm.generateSampleData()"
ng-disabled="vm.isGeneratingSampleData"
>
<i
class="fa fa-fw"
ng-class="vm.isGeneratingSampleData ? 'fa-spinner fa-spin' : 'fa-database'"
></i>
<span ng-if="!vm.isGeneratingSampleData">{{::'Generate Sample Data' | translate}}</span>
<span ng-if="vm.isGeneratingSampleData">{{::'Generating Sample Data...' | translate}}</span>
</button>
<a
ui-sref="app.project-frequent({ projectId: vm.project.id })"
class="btn btn-default"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
return _cachedRestangular.one("users", userId).one("projects", id).one("notifications").get();
}

function generateSampleData(id) {
return Restangular.one("projects", id).post("sample-data");
}

function getIntegrationNotificationSettings(id, integration) {
return _cachedRestangular.one("projects", id).one(integration, "notifications").get();
}
Expand Down Expand Up @@ -113,7 +117,7 @@
}

function resetData(id) {
return Restangular.one("projects", id).one("reset-data").get();
return Restangular.one("projects", id).post("reset-data");
}

function update(id, project) {
Expand Down Expand Up @@ -145,6 +149,7 @@
getByOrganizationId: getByOrganizationId,
getConfig: getConfig,
getNotificationSettings: getNotificationSettings,
generateSampleData: generateSampleData,
getIntegrationNotificationSettings: getIntegrationNotificationSettings,
isNameAvailable: isNameAvailable,
promoteTab: promoteTab,
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Web/ClientApp.angular/lang/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@
"An error occurred while trying to remove the project.": "An error occurred while trying to remove the project.",
"Download_Configure_Project": "Download & Configure Project {{projectName}} Client",
"The Exceptionless client can be integrated into your project in just a few easy steps.": "The Exceptionless client can be integrated into your project in just a few easy steps.",
"Generate Sample Data": "Generate Sample Data",
"Generating Sample Data...": "Generating Sample Data...",
"Sample data generation has been queued. Events will appear shortly.": "Sample data generation has been queued. Events will appear shortly.",
"An error occurred while generating sample data for your project.": "An error occurred while generating sample data for your project.",
"Select your project type:": "Select your project type:",
"Please select a project type": "Please select a project type",
"Execute the following in your shell:": "Execute the following in your shell:",
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Web/ClientApp.angular/lang/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@
"An error occurred while trying to remove the project.": "在删除项目的过程中发生了错误。",
"Download_Configure_Project": "下载配置项目{{projectName}}的客户端",
"The Exceptionless client can be integrated into your project in just a few easy steps.": "通过以下几个简单的步骤就可以轻松地将 Exceptionless 客户端整合到您的项目中去。",
"Generate Sample Data": "生成示例数据",
"Generating Sample Data...": "正在生成示例数据...",
"Sample data generation has been queued. Events will appear shortly.": "示例数据生成已加入队列。事件稍后将显示。",
"An error occurred while generating sample data for your project.": "生成项目示例数据时发生错误。",
"Select your project type:": "请选择您的项目类型:",
"Please select a project type": "请选择项目类型",
"Execute the following in your shell:": "在您的shell中执行以下命令:",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ClientConfiguration, NewProject, NotificationSettings, UpdateProject, ViewProject } from '$features/projects/models';
import type { StringValueFromBody } from '$features/shared/models';
import type { StringValueFromBody, WorkInProgressResult } from '$features/shared/models';
import type { WebSocketMessageValue } from '$features/websockets/models';

import { accessToken } from '$features/auth/index.svelte';
Expand Down Expand Up @@ -30,6 +30,7 @@ export const queryKeys = {
deleteProject: (ids: string[] | undefined) => [...queryKeys.ids(ids), 'delete'] as const,
deletePromotedTab: (id: string | undefined) => [...queryKeys.id(id), 'demote-tab'] as const,
deleteSlack: (id: string | undefined) => [...queryKeys.id(id), 'delete-slack'] as const,
generateSampleData: (id: string | undefined) => [...queryKeys.id(id), 'sample-data'] as const,
id: (id: string | undefined) => [...queryKeys.type, id] as const,
ids: (ids: string[] | undefined) => [...queryKeys.type, ...(ids ?? [])] as const,
integrationNotificationSettings: (id: string | undefined, integration: string) => [...queryKeys.id(id), integration, 'notification-settings'] as const,
Expand Down Expand Up @@ -78,6 +79,12 @@ export interface DeleteSlackRequest {
};
}

export interface GenerateSampleDataRequest {
route: {
id: string | undefined;
};
}

export interface GetOrganizationProjectsParams {
filter?: string;
limit?: number;
Expand Down Expand Up @@ -276,6 +283,21 @@ export function deleteSlack(request: DeleteSlackRequest) {
}));
}

export function generateSampleData(request: GenerateSampleDataRequest) {
return createMutation<WorkInProgressResult, ProblemDetails, void>(() => ({
enabled: () => !!accessToken.current && !!request.route.id,
mutationFn: async () => {
const client = useFetchClient();
const response = await client.postJSON<WorkInProgressResult>(`projects/${request.route.id}/sample-data`, undefined, {
expectedStatusCodes: [202]
});

return response.data!;
},
mutationKey: queryKeys.generateSampleData(request.route.id)
}));
}

export function getOrganizationProjectsQuery(request: GetOrganizationProjectsRequest) {
const queryClient = useQueryClient();

Expand Down Expand Up @@ -508,13 +530,15 @@ export function putProjectIntegrationNotificationSettings(request: PutProjectInt
}

export function resetData(request: ResetDataRequest) {
return createMutation<void, ProblemDetails, void>(() => ({
return createMutation<WorkInProgressResult, ProblemDetails, void>(() => ({
enabled: () => !!accessToken.current && !!request.route.id,
mutationFn: async () => {
const client = useFetchClient();
await client.post(`projects/${request.route.id}/reset-data`, undefined, {
const response = await client.postJSON<WorkInProgressResult>(`projects/${request.route.id}/reset-data`, undefined, {
expectedStatusCodes: [202]
});

return response.data!;
},
mutationKey: queryKeys.resetData(request.route.id)
}));
Expand Down
Loading