diff --git a/src/Spard.Service/BackgroundServices/ExamplesLoader.cs b/src/Spard.Service/BackgroundServices/ExamplesLoader.cs index 88cf926..5414681 100644 --- a/src/Spard.Service/BackgroundServices/ExamplesLoader.cs +++ b/src/Spard.Service/BackgroundServices/ExamplesLoader.cs @@ -1,13 +1,16 @@ using Spard.Service.Contracts; using Spard.Service.Models; using System.Text.Json; +using System.Text.Json.Serialization; namespace Spard.Service.BackgroundServices; /// /// Loads SPARD examples from folder into . /// -internal sealed class ExamplesLoader : BackgroundService +internal sealed class ExamplesLoader( + IExamplesRepository examplesRepository, + ILogger logger) : BackgroundService { /// /// Folder name containing examples. @@ -19,18 +22,6 @@ internal sealed class ExamplesLoader : BackgroundService /// public const string IndexFileName = "index.json"; - private readonly IExamplesRepository _examplesRepository; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of class. - /// - public ExamplesLoader(IExamplesRepository examplesRepository, ILogger logger) - { - _examplesRepository = examplesRepository; - _logger = logger; - } - protected override Task ExecuteAsync(CancellationToken stoppingToken) { var examplesFolderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ExampleFolderName); @@ -38,18 +29,20 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) if (!File.Exists(indexFilePath)) { - _logger.LogWarning("Examples file {fileName} not found!", indexFilePath); + logger.LogWarning("Examples file {fileName} not found!", indexFilePath); return Task.CompletedTask; } - var examplesDict = JsonSerializer.Deserialize>(File.ReadAllText(indexFilePath)) - ?? new Dictionary(); + var examplesDict = JsonSerializer.Deserialize( + File.ReadAllText(indexFilePath), + ExampleModelsContext.Default.DictionaryStringExampleModel) + ?? []; foreach (var exampleEntry in examplesDict) { if (!int.TryParse(exampleEntry.Key, out var id)) { - _logger.LogWarning("Example id {exampleId} is not a number!", exampleEntry.Key); + logger.LogWarning("Example id {exampleId} is not a number!", exampleEntry.Key); continue; } @@ -57,7 +50,7 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) if (!File.Exists(spardFileName)) { - _logger.LogWarning("Example SPARD file {fileName} not found!", spardFileName); + logger.LogWarning("Example SPARD file {fileName} not found!", spardFileName); continue; } @@ -70,9 +63,12 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) Transform = spardText }; - _examplesRepository.AddExample(id, example); + examplesRepository.AddExample(id, example); } return Task.CompletedTask; } } + +[JsonSerializable(typeof(Dictionary))] +internal partial class ExampleModelsContext : JsonSerializerContext { } diff --git a/src/Spard.Service/Dockerfile b/src/Spard.Service/Dockerfile index 3d1d602..7c200c1 100644 --- a/src/Spard.Service/Dockerfile +++ b/src/Spard.Service/Dockerfile @@ -1,10 +1,11 @@ -FROM mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine AS base +FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS base ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false RUN apk add --no-cache icu-libs WORKDIR /app -EXPOSE 5000 +EXPOSE 8080 -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["src/Spard.Service/Spard.Service.csproj", "src/Spard.Service/"] COPY ["src/Spard.Service.Contract/Spard.Service.Contract.csproj", "src/Spard.Service.Contract/"] @@ -12,9 +13,9 @@ COPY ["src/Spard/Spard.csproj", "src/Spard/"] RUN dotnet restore "src/Spard.Service/Spard.Service.csproj" -r linux-musl-x64 COPY . . WORKDIR "/src/src/Spard.Service" -RUN dotnet publish "Spard.Service.csproj" -p:PublishSingleFile=true -r linux-musl-x64 -p:PublishTrimmed=true -p:InvariantGlobalization=false -c Release -o /app/publish +RUN dotnet publish "Spard.Service.csproj" -p:PublishSingleFile=true -r linux-musl-x64 -p:PublishTrimmed=true -p:InvariantGlobalization=false -c $BUILD_CONFIGURATION -o /app/publish FROM base AS final WORKDIR /app COPY --from=build /app/publish . -ENTRYPOINT ["./Spard.Service", "--urls", "http://*:5000"] \ No newline at end of file +ENTRYPOINT ["./Spard.Service"] \ No newline at end of file diff --git a/src/Spard.Service/Metrics/OtelMetrics.cs b/src/Spard.Service/Metrics/OtelMetrics.cs index b44ef71..e4a8875 100644 --- a/src/Spard.Service/Metrics/OtelMetrics.cs +++ b/src/Spard.Service/Metrics/OtelMetrics.cs @@ -7,6 +7,8 @@ namespace Spard.Service.Metrics; /// public sealed class OtelMetrics { + public const string MeterName = "Spard"; + private Counter TransformCounter { get; } private Counter TransformTableCounter { get; } @@ -15,12 +17,9 @@ public sealed class OtelMetrics private Counter GenerateSourceCodeCounter { get; } - public string MeterName { get; } - - public OtelMetrics(string meterName = "Spard") + public OtelMetrics(IMeterFactory meterFactory) { - var meter = new Meter(meterName); - MeterName = meterName; + var meter = meterFactory.Create(MeterName); TransformCounter = meter.CreateCounter("transforms"); TransformTableCounter = meter.CreateCounter("table-transforms"); diff --git a/src/Spard.Service/Program.cs b/src/Spard.Service/Program.cs index a010398..ee49b88 100644 --- a/src/Spard.Service/Program.cs +++ b/src/Spard.Service/Program.cs @@ -4,15 +4,18 @@ using Serilog; using Spard.Service.BackgroundServices; using Spard.Service.Configuration; +using Spard.Service.Contract; using Spard.Service.Contracts; using Spard.Service.EndpointDefinitions; -using Spard.Service.Helpers; using Spard.Service.Metrics; using Spard.Service.Services; +using System.Text.Json.Serialization; -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateSlimBuilder(args); builder.Host.UseSerilog((ctx, lc) => lc + .WriteTo.Console(new Serilog.Formatting.Display.MessageTemplateTextFormatter( + "[{Timestamp:yyyy/MM/dd HH:mm:ss} {Level}] {Message:lj} {Exception}{NewLine}")) .ReadFrom.Configuration(ctx.Configuration) .Filter.ByExcluding(logEvent => logEvent.Exception is BadHttpRequestException @@ -38,7 +41,12 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config services.AddHostedService(); AddRateLimits(services, configuration); - AddMetrics(services); + AddMetrics(services, configuration); + + services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolverChain.Insert(0, SpardSerializerContext.Default); + }); } static void AddRateLimits(IServiceCollection services, IConfiguration configuration) @@ -50,20 +58,26 @@ static void AddRateLimits(IServiceCollection services, IConfiguration configurat services.AddInMemoryRateLimiting(); } -static void AddMetrics(IServiceCollection services) +static void AddMetrics(IServiceCollection services, IConfiguration configuration) { - var meters = new OtelMetrics(); + services.AddSingleton(); services.AddOpenTelemetry().WithMetrics(builder => builder .ConfigureResource(rb => rb.AddService("Spard")) - .AddMeter(meters.MeterName) + .AddMeter(OtelMetrics.MeterName) .AddAspNetCoreInstrumentation() .AddRuntimeInstrumentation() .AddProcessInstrumentation() - .AddPrometheusExporter()); - - services.AddSingleton(meters); + .AddOtlpExporter(options => + { + var otelUri = configuration["OpenTelemetry:ServiceUri"]; + + if (otelUri != null) + { + options.Endpoint = new Uri(otelUri); + } + })); } static void Configure(WebApplication app) @@ -78,6 +92,11 @@ static void Configure(WebApplication app) TransformEndpointDefinitions.DefineExamplesEndpoint(app); app.UseIpRateLimiting(); +} - app.UseOpenTelemetryPrometheusScrapingEndpoint(); -} \ No newline at end of file +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(SpardExampleInfo))] +[JsonSerializable(typeof(TransformRequest))] +[JsonSerializable(typeof(TransformTableResult))] +[JsonSerializable(typeof(ProcessResult))] +internal partial class SpardSerializerContext : JsonSerializerContext { } \ No newline at end of file diff --git a/src/Spard.Service/Spard.Service.csproj b/src/Spard.Service/Spard.Service.csproj index 4b79e9d..19921fc 100644 --- a/src/Spard.Service/Spard.Service.csproj +++ b/src/Spard.Service/Spard.Service.csproj @@ -102,13 +102,13 @@ - - - + + + - - + + diff --git a/src/Spard.Service/appsettings.json b/src/Spard.Service/appsettings.json index d0aa371..7b2d38c 100644 --- a/src/Spard.Service/appsettings.json +++ b/src/Spard.Service/appsettings.json @@ -21,7 +21,7 @@ ] }, "Serilog": { - "Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Console" ], + "Using": [ "Serilog.Sinks.Console" ], "MinimumLevel": { "Default": "Information", "Override": { @@ -29,28 +29,6 @@ "System": "Warning" } }, - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss} {Level}] {Message:lj} {Exception}{NewLine}", - "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Literate, Serilog.Sinks.Console" - } - }, - { - "Name": "File", - "Args": { - "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss} {Level}] {Message:lj} {Exception}{NewLine}", - "path": "logs/spard.log", - "shared": "true", - "fileSizeLimitBytes": "5000000", - "rollOnFileSizeLimit": true, - "rollingInterval": "Day", - "retainedFileCountLimit": 5, - "flushToDiskInterval": "1" - } - } - ], "Enrich": [ "FromLogContext", "WithThreadId", "WithMachineName" ] } } diff --git a/src/Spard/Spard.csproj b/src/Spard/Spard.csproj index a1ae1fb..132c1a6 100644 --- a/src/Spard/Spard.csproj +++ b/src/Spard/Spard.csproj @@ -20,6 +20,6 @@ - + \ No newline at end of file diff --git a/test/Spard.Service.IntegrationTests/ExamplesTests.cs b/test/Spard.Service.IntegrationTests/ExamplesTests.cs index 5e9919a..19c31ad 100644 --- a/test/Spard.Service.IntegrationTests/ExamplesTests.cs +++ b/test/Spard.Service.IntegrationTests/ExamplesTests.cs @@ -11,13 +11,13 @@ public sealed class ExamplesTests : TestsBase public async Task GetExamples_OkAsync() { var examples = await SpardClient.Examples.GetExamplesAsync(); - Assert.Greater(examples.Length, 0); + Assert.That(examples.Length, Is.GreaterThan(0)); var firstExample = examples.OrderBy(e => e.Id).First(); var example = await SpardClient.Examples.GetExampleAsync(firstExample.Id); - Assert.NotNull(example); - Assert.AreEqual(firstExample.Id, example.Id); - Assert.AreEqual(firstExample.Name, example.Name); + Assert.That(example, Is.Not.Null); + Assert.That(firstExample.Id, Is.EqualTo(example.Id)); + Assert.That(firstExample.Name, Is.EqualTo(example.Name)); } [Test] @@ -32,9 +32,9 @@ public async Task GetExample_Localized_OkAsync() var spardClientRus = ServiceCollectionExtensions.CreateSpardClient(options); var example = await spardClientRus.Examples.GetExampleAsync(5); - Assert.AreEqual("������� ������", example.Name); + Assert.That(example.Name, Is.EqualTo("������� ������")); var example2 = await SpardClient.Examples.GetExampleAsync(5); - Assert.AreEqual("Double replacement", example2.Name); + Assert.That(example2.Name, Is.EqualTo("Double replacement")); } } \ No newline at end of file diff --git a/test/Spard.Service.IntegrationTests/Spard.Service.IntegrationTests.csproj b/test/Spard.Service.IntegrationTests/Spard.Service.IntegrationTests.csproj index 01b1774..49b28e0 100644 --- a/test/Spard.Service.IntegrationTests/Spard.Service.IntegrationTests.csproj +++ b/test/Spard.Service.IntegrationTests/Spard.Service.IntegrationTests.csproj @@ -8,9 +8,9 @@ - + - + diff --git a/test/Spard.Service.IntegrationTests/SpardTests.cs b/test/Spard.Service.IntegrationTests/SpardTests.cs index 1e52a4e..8feb1cf 100644 --- a/test/Spard.Service.IntegrationTests/SpardTests.cs +++ b/test/Spard.Service.IntegrationTests/SpardTests.cs @@ -14,8 +14,8 @@ public async Task GenerateTable_OkAsync() var actualResult = await SpardClient.Spard.GenerateTableAsync(transform); - Assert.AreEqual(result.Replace("\r", ""), actualResult.Result.Replace("\r", "")); - Assert.Greater(actualResult.Duration, TimeSpan.Zero); + Assert.That(result.Replace("\r", ""), Is.EqualTo(actualResult.Result.Replace("\r", ""))); + Assert.That(actualResult.Duration, Is.GreaterThan(TimeSpan.Zero)); } [Test] @@ -26,7 +26,7 @@ public async Task GenerateSourceCode_OkAsync() var actualResult = await SpardClient.Spard.GenerateSourceCodeAsync(transform); - Assert.AreEqual(result.Replace("\r", ""), actualResult.Result.Replace("\r", "")); - Assert.Greater(actualResult.Duration, TimeSpan.Zero); + Assert.That(result.Replace("\r", ""), Is.EqualTo(actualResult.Result.Replace("\r", ""))); + Assert.That(actualResult.Duration, Is.GreaterThan(TimeSpan.Zero)); } } diff --git a/test/Spard.Service.IntegrationTests/TransformTests.cs b/test/Spard.Service.IntegrationTests/TransformTests.cs index 1300e64..e9bde26 100644 --- a/test/Spard.Service.IntegrationTests/TransformTests.cs +++ b/test/Spard.Service.IntegrationTests/TransformTests.cs @@ -20,7 +20,7 @@ public async Task RunAllExamples_OkAsync() { if (!exampleResults.TryGetValue(example.Id, out var expectedResult)) { - Assert.Fail("Unknown example: {0}", example.Id); + Assert.Fail($"Unknown example: {example.Id}"); } var exampleData = await SpardClient.Examples.GetExampleAsync(example.Id); @@ -31,8 +31,8 @@ public async Task RunAllExamples_OkAsync() Transform = exampleData.Transform }); - Assert.AreEqual(expectedResult.Replace("\r", ""), result.Result.Replace("\r", "")); - Assert.Greater(result.Duration, TimeSpan.Zero); + Assert.That(expectedResult.Replace("\r", ""), Is.EqualTo(result.Result.Replace("\r", ""))); + Assert.That(result.Duration, Is.GreaterThan(TimeSpan.Zero)); } } @@ -49,11 +49,11 @@ public async Task RunTableTransform_OkAsync() Transform = transform }); - Assert.AreEqual(result, actualResult.Result); - Assert.IsTrue(actualResult.IsStandardResultTheSame); - Assert.Greater(actualResult.ParseDuration, TimeSpan.Zero); - Assert.Greater(actualResult.StandardTransformDuration, TimeSpan.Zero); - Assert.Greater(actualResult.TableBuildDuration, TimeSpan.Zero); - Assert.Greater(actualResult.TableTransformDuration, TimeSpan.Zero); + Assert.That(result, Is.EqualTo(actualResult.Result)); + Assert.That(actualResult.IsStandardResultTheSame, Is.True); + Assert.That(actualResult.ParseDuration, Is.GreaterThan(TimeSpan.Zero)); + Assert.That(actualResult.StandardTransformDuration, Is.GreaterThan(TimeSpan.Zero)); + Assert.That(actualResult.TableBuildDuration, Is.GreaterThan(TimeSpan.Zero)); + Assert.That(actualResult.TableTransformDuration, Is.GreaterThan(TimeSpan.Zero)); } } diff --git a/test/Spard.UnitTests/Spard.UnitTests.csproj b/test/Spard.UnitTests/Spard.UnitTests.csproj index 7cc5b11..d246199 100644 --- a/test/Spard.UnitTests/Spard.UnitTests.csproj +++ b/test/Spard.UnitTests/Spard.UnitTests.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -8,12 +8,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/test/Spard.UnitTests/TableTransformerTests.cs b/test/Spard.UnitTests/TableTransformerTests.cs index 1a728c2..9598149 100644 --- a/test/Spard.UnitTests/TableTransformerTests.cs +++ b/test/Spard.UnitTests/TableTransformerTests.cs @@ -140,7 +140,7 @@ private static void RunTableTest(string transform, string[] tests, TableTestMode var res1 = transformer.TransformToText(test); sw1.Stop(); - Assert.AreEqual(res1, res2); + Assert.That(res1, Is.EqualTo(res2)); var sw3 = new Stopwatch(); @@ -150,7 +150,7 @@ private static void RunTableTest(string transform, string[] tests, TableTestMode var res3 = tableTransformer2.TransformToText(test); sw3.Stop(); - Assert.AreEqual(res1, res3); + Assert.That(res1, Is.EqualTo(res3)); } var sw5 = new Stopwatch(); @@ -161,10 +161,10 @@ private static void RunTableTest(string transform, string[] tests, TableTestMode var res5 = new string(tt.ToArray()); sw5.Stop(); - Assert.AreEqual(res1, res5); + Assert.That(res1, Is.EqualTo(res5)); } - Assert.Pass("{0}: {1} {2} {3}", test, sw1.Elapsed, sw2.Elapsed, sw5.Elapsed); + Assert.Pass($"{test}: {sw1.Elapsed} {sw2.Elapsed} {sw5.Elapsed}"); } } } diff --git a/test/Spard.UnitTests/TreeTransformerTests.cs b/test/Spard.UnitTests/TreeTransformerTests.cs index b9e13de..978c088 100644 --- a/test/Spard.UnitTests/TreeTransformerTests.cs +++ b/test/Spard.UnitTests/TreeTransformerTests.cs @@ -300,11 +300,11 @@ public static void TestTextExpressionTree(string spard, params TestData[] rules) try { var result = tree.TransformToText(pair.Item1); - Assert.AreEqual(pair.Item2, result); + Assert.That(pair.Item2, Is.EqualTo(result)); } catch (TransformException) { - Assert.IsNull(pair.Item2); + Assert.That(pair.Item2, Is.Null); } } }