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
10 changes: 5 additions & 5 deletions src/Exceptionless.AppHost/Exceptionless.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Aspire.AppHost.Sdk/13.3.4">
<Project Sdk="Aspire.AppHost.Sdk/13.3.5">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
Expand All @@ -8,10 +8,10 @@
<NoWarn>$(NoWarn);ASPIRECERTIFICATES001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Azure.Storage" Version="13.3.4" />
<PackageReference Include="Aspire.Hosting.Browsers" Version="13.3.4-preview.1.26268.8" />
<PackageReference Include="Aspire.Hosting.JavaScript" Version="13.3.4" />
<PackageReference Include="Aspire.Hosting.Redis" Version="13.3.4" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" Version="13.3.5" />
<PackageReference Include="Aspire.Hosting.Browsers" Version="13.3.5-preview.1.26270.6" />
<PackageReference Include="Aspire.Hosting.JavaScript" Version="13.3.5" />
<PackageReference Include="Aspire.Hosting.Redis" Version="13.3.5" />
<PackageReference Include="AspNetCore.HealthChecks.Elasticsearch" Version="9.0.0" />
</ItemGroup>

Expand Down
5 changes: 5 additions & 0 deletions src/Exceptionless.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Exceptionless.Core.Queues.Models;
using Exceptionless.Core.Repositories;
using Exceptionless.Core.Repositories.Configuration;
using Exceptionless.Core.Seed;
using Exceptionless.Core.Serialization;
using Exceptionless.Core.Services;
using Exceptionless.Core.Utility;
Expand Down Expand Up @@ -94,6 +95,10 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO
services.AddSingleton<IElasticConfiguration>(s => s.GetRequiredService<ExceptionlessElasticConfiguration>());
services.AddStartupAction<ExceptionlessElasticConfiguration>();

services.AddSingleton<DataSeedService>();
services.AddSingleton<IDataSeed, PredefinedSavedViewsDataSeed>();
services.AddStartupAction<DataSeedService>();

services.AddStartupAction("Create Sample Data", CreateSampleDataAsync);

services.AddSingleton(typeof(IWorkItemHandler), typeof(Bootstrapper).Assembly, typeof(ReindexWorkItemHandler).Assembly);
Expand Down
3 changes: 3 additions & 0 deletions src/Exceptionless.Core/Exceptionless.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<EmbeddedResource Include="Mail\Templates\user-email-verify.html" />
<EmbeddedResource Include="Mail\Templates\user-password-reset.html" />
</ItemGroup>
<ItemGroup>
<None Update="Seed\predefined-saved-views.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="6.0.1" />
<PackageReference Include="Exceptionless.RandomData" Version="2.0.1" />
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Core/Models/SavedView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public record SavedView : IOwnedByOrganizationWithIdentity, IHaveDates
/// <summary>Whether the dashboard chart is shown for this view. Null means use the default.</summary>
public bool? ShowChart { get; set; }

/// <summary>Stable identifier used to synchronize predefined saved views across organizations.</summary>
[MaxLength(150)]
public string? PredefinedKey { get; set; }

/// <summary>Display name shown in the sidebar and picker.</summary>
[Required]
[MaxLength(100)]
Expand Down
32 changes: 32 additions & 0 deletions src/Exceptionless.Core/Seed/DataSeedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Foundatio.Extensions.Hosting.Startup;
using Microsoft.Extensions.Logging;

namespace Exceptionless.Core.Seed;

public class DataSeedService : IStartupAction
{
private readonly IEnumerable<IDataSeed> _seeds;
private readonly ILogger _logger;

public DataSeedService(IEnumerable<IDataSeed> seeds, ILoggerFactory loggerFactory)
{
_seeds = seeds;
_logger = loggerFactory.CreateLogger<DataSeedService>();
}

public Task RunAsync(CancellationToken shutdownToken = default)
{
return SeedAsync(shutdownToken);
}

public async Task SeedAsync(CancellationToken cancellationToken = default)
{
foreach (var seed in _seeds)
{
cancellationToken.ThrowIfCancellationRequested();

_logger.LogInformation("Running data seed {DataSeedName}", seed.Name);
await seed.SeedAsync(cancellationToken);
}
}
}
8 changes: 8 additions & 0 deletions src/Exceptionless.Core/Seed/IDataSeed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Exceptionless.Core.Seed;

public interface IDataSeed
{
string Name { get; }

Task SeedAsync(CancellationToken cancellationToken = default);
}
134 changes: 134 additions & 0 deletions src/Exceptionless.Core/Seed/PredefinedSavedViewsDataSeed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Exceptionless.Core.Models;
using Exceptionless.Core.Repositories;
using Foundatio.Lock;
using Foundatio.Repositories;
using Microsoft.Extensions.Logging;

namespace Exceptionless.Core.Seed;

public class PredefinedSavedViewsDataSeed : IDataSeed
{
public const string SystemOrganizationId = "000000000000000000000001";
public const string SystemUserId = "000000000000000000000001";
public const string SeedFileName = "predefined-saved-views.json";

private static readonly JsonSerializerOptions JsonOptions = new()
{
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
};

private readonly ISavedViewRepository _savedViewRepository;
private readonly ILockProvider _lockProvider;
private readonly ILogger _logger;

public PredefinedSavedViewsDataSeed(ISavedViewRepository savedViewRepository, ILockProvider lockProvider, ILoggerFactory loggerFactory)
{
_savedViewRepository = savedViewRepository;
_lockProvider = lockProvider;
_logger = loggerFactory.CreateLogger<PredefinedSavedViewsDataSeed>();
}

public string Name => "Predefined Saved Views";

public Task SeedAsync(CancellationToken cancellationToken = default)
{
return _lockProvider.TryUsingAsync("data-seed:predefined-saved-views", async () =>
{
cancellationToken.ThrowIfCancellationRequested();

if (await _savedViewRepository.CountByOrganizationIdAsync(SystemOrganizationId) > 0)
return;

var definitions = await ReadDefaultSavedViewsAsync(cancellationToken);
var savedViews = definitions.Select(CreateSavedView).ToList();
await _savedViewRepository.AddAsync(savedViews, o => o.Cache().ImmediateConsistency());
_logger.LogInformation("Seeded {Count} predefined saved views", savedViews.Count);
}, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15));
}

public static async Task<IReadOnlyCollection<PredefinedSavedViewDefinition>> ReadDefaultSavedViewsAsync(CancellationToken cancellationToken = default)
{
await using var stream = File.OpenRead(GetSeedFilePath());
var definitions = await JsonSerializer.DeserializeAsync<List<PredefinedSavedViewDefinition>>(stream, JsonOptions, cancellationToken);
return definitions ?? [];
}

private static string GetSeedFilePath()
{
var seedFileName = Path.GetFileName(SeedFileName);
return Path.Combine(AppContext.BaseDirectory, "Seed", seedFileName);
}

private static SavedView CreateSavedView(PredefinedSavedViewDefinition definition)
{
return new SavedView
{
OrganizationId = SystemOrganizationId,
CreatedByUserId = SystemUserId,
PredefinedKey = definition.Key,
Name = definition.Name,
Slug = definition.Slug,
ViewType = definition.ViewType,
Filter = definition.Filter,
Time = definition.Time,
Sort = definition.Sort,
FilterDefinitions = GetRawJson(definition.FilterDefinitions),
Columns = definition.Columns?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
ColumnOrder = definition.ColumnOrder is null ? null : [.. definition.ColumnOrder],
ShowStats = definition.ShowStats,
ShowChart = definition.ShowChart,
Version = 1
};
}

public static string? GetRawJson(JsonElement? value)
{
if (value is not { } element || element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return null;

return element.GetRawText();
}
}

public sealed record PredefinedSavedViewDefinition
{
[JsonPropertyName("key")]
public required string Key { get; init; }

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("slug")]
public required string Slug { get; init; }

[JsonPropertyName("viewType")]
public required string ViewType { get; init; }

[JsonPropertyName("filter")]
public string? Filter { get; init; }

[JsonPropertyName("time")]
public string? Time { get; init; }

[JsonPropertyName("sort")]
public string? Sort { get; init; }

[JsonPropertyName("filterDefinitions")]
public JsonElement? FilterDefinitions { get; init; }

[JsonPropertyName("columns")]
public IReadOnlyDictionary<string, bool>? Columns { get; init; }

[JsonPropertyName("columnOrder")]
public IReadOnlyCollection<string>? ColumnOrder { get; init; }

[JsonPropertyName("showStats")]
public bool? ShowStats { get; init; }

[JsonPropertyName("showChart")]
public bool? ShowChart { get; init; }
}
141 changes: 141 additions & 0 deletions src/Exceptionless.Core/Seed/predefined-saved-views.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
[
{
"key": "events:errors",
"name": "Errors",
"slug": "errors",
"viewType": "events",
"filter": "type:error",
"time": "[now-7d TO now]",
"sort": "-date",
"filterDefinitions": [
{
"type": "date",
"term": "date",
"value": "[now-7d TO now]"
},
{
"type": "type",
"value": ["error"],
"hidden": true
}
],
"columns": {
"level": false,
"message": false,
"name": false,
"source": false,
"type": false
},
"showStats": true,
"showChart": true
},
{
"key": "events:logs",
"name": "Logs",
"slug": "logs",
"viewType": "events",
"filter": "type:log",
"time": "[now-7d TO now]",
"sort": "-date",
"filterDefinitions": [
{
"type": "date",
"term": "date",
"value": "[now-7d TO now]"
},
{
"type": "type",
"value": ["log"],
"hidden": true
}
],
"columns": {
"level": false,
"message": false,
"name": false,
"source": false,
"type": false
},
"showStats": true,
"showChart": true
},
{
"key": "issues:most-frequent-404s",
"name": "Most Frequent 404s",
"slug": "most-frequent-404s",
"viewType": "issues",
"filter": "(status:open OR status:regressed) type:404",
"time": "[now-7d TO now]",
"sort": "-events",
"filterDefinitions": [
{
"type": "date",
"term": "date",
"value": "[now-7d TO now]"
},
{
"type": "status",
"value": ["open", "regressed"],
"hidden": true
},
{
"type": "type",
"value": ["404"],
"hidden": true
}
],
"showStats": true,
"showChart": true
},
{
"key": "issues:most-frequent-errors",
"name": "Most Frequent Errors",
"slug": "most-frequent-errors",
"viewType": "issues",
"filter": "type:error (status:open OR status:regressed)",
"time": "[now-7d TO now]",
"sort": "-events",
"filterDefinitions": [
{
"type": "date",
"term": "date",
"value": "[now-7d TO now]"
},
{
"type": "type",
"value": ["error"],
"hidden": true
},
{
"type": "status",
"value": ["open", "regressed"],
"hidden": true
}
],
"showStats": true,
"showChart": true
},
{
"key": "issues:most-used-features",
"name": "Most Used Features",
"slug": "most-used-features",
"viewType": "issues",
"filter": "type:usage",
"time": "[now-7d TO now]",
"sort": "-events",
"filterDefinitions": [
{
"type": "date",
"term": "date",
"value": "[now-7d TO now]"
},
{
"type": "type",
"value": ["usage"],
"hidden": true
}
],
"showStats": true,
"showChart": true
}
]
Loading