diff --git a/src/BUTR.CrashReportServer/BUTR.CrashReportServer.csproj b/src/BUTR.CrashReportServer/BUTR.CrashReportServer.csproj index bb5ba83..affebb7 100644 --- a/src/BUTR.CrashReportServer/BUTR.CrashReportServer.csproj +++ b/src/BUTR.CrashReportServer/BUTR.CrashReportServer.csproj @@ -16,13 +16,29 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BUTR.CrashReportServer/Controllers/CrashUploadController.cs b/src/BUTR.CrashReportServer/Controllers/CrashUploadController.cs index 1b9deb5..6b55237 100644 --- a/src/BUTR.CrashReportServer/Controllers/CrashUploadController.cs +++ b/src/BUTR.CrashReportServer/Controllers/CrashUploadController.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.IO.Pipelines; using System.Text.Json; using System.Text.Json.Serialization; @@ -36,8 +37,20 @@ public sealed record CrashReportUploadBody(CrashReportModel CrashReport, ICollec private readonly AppDbContext _dbContext; private readonly GZipCompressor _gZipCompressor; - public CrashUploadController(ILogger logger, IOptionsSnapshot options, IOptionsSnapshot jsonSerializerOptions, AppDbContext dbContext, GZipCompressor gZipCompressor) + private readonly Counter _reportVersion; + + public CrashUploadController( + ILogger logger, + IOptionsSnapshot options, + IOptionsSnapshot jsonSerializerOptions, + AppDbContext dbContext, + GZipCompressor gZipCompressor, + IMeterFactory meterFactory) { + var meter = meterFactory.Create("BUTR.CrashReportServer.Controllers.CrashUploadController", "1.0.0"); + + _reportVersion = meter.CreateCounter("report-version", unit: "Count"); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); _options = options.Value ?? throw new ArgumentNullException(nameof(options)); @@ -86,6 +99,8 @@ private async Task UploadHtmlAsync(CancellationToken ct) if (version >= 13) await _dbContext.Set().AddAsync(new JsonEntity { Id = idEntity, CrashReportCompressed = compressedJsonStream.ToArray(), }, ct); await _dbContext.SaveChangesAsync(ct); + _reportVersion.Add(1, new[] {new KeyValuePair("Version", version)}); + return Ok($"{_options.BaseUri}/{idEntity.FileId}"); } @@ -112,6 +127,8 @@ private async Task UploadJsonAsync(CancellationToken ct) await _dbContext.Set().AddAsync(new FileEntity { Id = idEntity, DataCompressed = compressedHtmlStream.ToArray(), }, ct); await _dbContext.SaveChangesAsync(ct); + _reportVersion.Add(1, new[] {new KeyValuePair("Version", crashReport.Version)}); + return Ok($"{_options.BaseUri}/{idEntity.FileId}"); } diff --git a/src/BUTR.CrashReportServer/Controllers/ReportController.cs b/src/BUTR.CrashReportServer/Controllers/ReportController.cs index 976ecab..f053eaa 100644 --- a/src/BUTR.CrashReportServer/Controllers/ReportController.cs +++ b/src/BUTR.CrashReportServer/Controllers/ReportController.cs @@ -52,7 +52,7 @@ public ReportController(ILogger logger, AppDbContext dbContext [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool ValidateFileName(string? fileName) => fileName?.Length is 6 or 8 or 10 && fileName.All(IsHex); - private IActionResult? ValidateRequest(ref string filename) + private StatusCodeResult? ValidateRequest(ref string filename) { if (string.IsNullOrEmpty(filename)) return StatusCode((int) HttpStatusCode.InternalServerError); diff --git a/src/BUTR.CrashReportServer/Program.cs b/src/BUTR.CrashReportServer/Program.cs index aea588a..fdcd06d 100644 --- a/src/BUTR.CrashReportServer/Program.cs +++ b/src/BUTR.CrashReportServer/Program.cs @@ -2,8 +2,22 @@ using BUTR.CrashReportServer.Extensions; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.ResourceDetectors.Container; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +using Serilog; +using Serilog.Events; + +using System; using System.Security.Authentication; using System.Threading.Tasks; @@ -13,14 +27,88 @@ public static class Program { public static async Task Main(string[] args) { - var builder = CreateHostBuilder(args); - var host = builder.Build(); - await host.SeedDbContextAsync(); - await host.RunAsync(); + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateBootstrapLogger(); + + try + { + Log.Information("Starting web application"); + + var builder = CreateHostBuilder(args); + var host = builder.Build(); + await host.SeedDbContextAsync(); + await host.RunAsync(); + } + catch (Exception ex) + { + Log.Fatal(ex, "Application terminated unexpectedly"); + } + finally + { + await Log.CloseAndFlushAsync(); + } } public static IHostBuilder CreateHostBuilder(string[] args) => Host .CreateDefaultBuilder(args) + .ConfigureServices((ctx, services) => + { + if (ctx.Configuration.GetSection("Oltp") is { } oltpSection) + { + var openTelemetry = services.AddOpenTelemetry() + .ConfigureResource(builder => + { + builder.AddDetector(new ContainerResourceDetector()); + builder.AddService( + ctx.HostingEnvironment.ApplicationName, + ctx.HostingEnvironment.EnvironmentName, + typeof(Program).Assembly.GetName().Version?.ToString(), + false, + Environment.MachineName); + builder.AddTelemetrySdk(); + }); + + if (oltpSection.GetValue("MetricsEndpoint") is { } metricsEndpoint) + { + var metricsProtocol = oltpSection.GetValue("MetricsProtocol"); + openTelemetry.WithMetrics(builder => builder + .AddMeter("BUTR.CrashReportServer.Controllers.CrashUploadController") + .AddProcessInstrumentation() + .AddRuntimeInstrumentation(instrumentationOptions => + { + + }) + .AddAspNetCoreInstrumentation() + .AddOtlpExporter(o => + { + o.Endpoint = new Uri(metricsEndpoint); + o.Protocol = metricsProtocol; + })); + } + + if (oltpSection.GetValue("TracingEndpoint") is { } tracingEndpoint) + { + var tracingProtocol = oltpSection.GetValue("TracingProtocol"); + openTelemetry.WithTracing(builder => builder + .AddEntityFrameworkCoreInstrumentation(instrumentationOptions => + { + instrumentationOptions.SetDbStatementForText = true; + }) + .AddAspNetCoreInstrumentation(instrumentationOptions => + { + instrumentationOptions.RecordException = true; + }) + .AddOtlpExporter(o => + { + o.Endpoint = new Uri(tracingEndpoint); + o.Protocol = tracingProtocol; + })); + } + } + }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseKestrel(kestrelOptions => @@ -32,5 +120,32 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host }); webBuilder.UseStartup(); + }) + .UseSerilog((context, services, configuration) => + { + configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services); + }, writeToProviders: true) + .ConfigureLogging((ctx, builder) => + { + var oltpSection = ctx.Configuration.GetSection("Oltp"); + if (oltpSection == null!) return; + + var loggingEndpoint = oltpSection.GetValue("LoggingEndpoint"); + if (loggingEndpoint is null) return; + var loggingProtocol = oltpSection.GetValue("LoggingProtocol"); + + builder.AddOpenTelemetry(o => + { + o.IncludeScopes = true; + o.ParseStateValues = true; + o.IncludeFormattedMessage = true; + o.AddOtlpExporter((options, processorOptions) => + { + options.Endpoint = new Uri(loggingEndpoint); + options.Protocol = loggingProtocol; + }); + }); }); } \ No newline at end of file