diff --git a/.github/workflows/configs/api.docker-compose.yml.template b/.github/workflows/configs/api.docker-compose.yml.template index 6fe0194e..2cca15d4 100644 --- a/.github/workflows/configs/api.docker-compose.yml.template +++ b/.github/workflows/configs/api.docker-compose.yml.template @@ -5,8 +5,17 @@ services: image: ghcr.io/facturx-net/facturxdotnet-api:latest-{BUILD-NAME} environment: ASPNETCORE_HTTP_PORTS: {PORT} + ServiceName: {BUILD-NAME} Hosting__Host: https://{BUILD-NAME}.facturxdotnet.org Hosting__BasePath: /api Hosting__UnsafeEnvironment: true + Observability__OtlpEndpoint: http://otel-collector:4317 ports: - - {PORT}:{PORT} \ No newline at end of file + - {PORT}:{PORT} + external_links: + - "otel-collector:otel-collector" + +networks: + default: + name: observability + external: true \ No newline at end of file diff --git a/src/.idea/.idea.FacturXDotNet/Docker/docker-compose.local.generated.override.yml b/src/.idea/.idea.FacturXDotNet/Docker/docker-compose.local.generated.override.yml new file mode 100644 index 00000000..3a20288f --- /dev/null +++ b/src/.idea/.idea.FacturXDotNet/Docker/docker-compose.local.generated.override.yml @@ -0,0 +1,21 @@ +# This is a generated file. Not intended for manual editing. +services: + api: + build: + context: "D:\\source\\repos\\FacturXDotNet\\src\\FacturXDotNet.API" + dockerfile: "Dockerfile" + target: "base" + command: [] + entrypoint: + - "dotnet" + - "/app/bin/Debug/net9.0/FacturXDotNet.API.dll" + environment: + ASPNETCORE_ENVIRONMENT: "Development" + DOTNET_USE_POLLING_FILE_WATCHER: "true" + image: "facturxdotnet.api:dev" + ports: [] + volumes: + - "D:\\source\\repos\\FacturXDotNet\\src\\FacturXDotNet.API:/app:rw" + - "D:\\source\\repos\\FacturXDotNet\\src:/src:rw" + - "C:\\Users\\lahki\\.nuget\\packages:/home/app/.nuget/packages" + working_dir: "/app" diff --git a/src/FacturXDotNet.API/Configuration/AppConfiguration.cs b/src/FacturXDotNet.API/Configuration/AppConfiguration.cs index 8eaec3d0..58aceb8e 100644 --- a/src/FacturXDotNet.API/Configuration/AppConfiguration.cs +++ b/src/FacturXDotNet.API/Configuration/AppConfiguration.cs @@ -2,5 +2,7 @@ namespace FacturXDotNet.API.Configuration; class AppConfiguration { + public string? InstanceName { get; set; } = "default"; public AppHostingConfiguration Hosting { get; set; } = new(); + public AppObservabilityConfiguration Observability { get; set; } = new(); } diff --git a/src/FacturXDotNet.API/Configuration/AppObservabilityConfiguration.cs b/src/FacturXDotNet.API/Configuration/AppObservabilityConfiguration.cs new file mode 100644 index 00000000..842261a5 --- /dev/null +++ b/src/FacturXDotNet.API/Configuration/AppObservabilityConfiguration.cs @@ -0,0 +1,6 @@ +namespace FacturXDotNet.API.Configuration; + +class AppObservabilityConfiguration +{ + public string? OtlpEndpoint { get; set; } +} diff --git a/src/FacturXDotNet.API/Extensions/ObservabilityHostingExtensions.cs b/src/FacturXDotNet.API/Extensions/ObservabilityHostingExtensions.cs new file mode 100644 index 00000000..08d0112c --- /dev/null +++ b/src/FacturXDotNet.API/Extensions/ObservabilityHostingExtensions.cs @@ -0,0 +1,37 @@ +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace FacturXDotNet.API.Extensions; + +static class ObservabilityHostingExtensions +{ + public static void AddObservability(this WebApplicationBuilder builder, string serviceName, Uri otlpEndpoint) + { + builder.Logging.AddOpenTelemetry( + logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + } + ); + + builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService(serviceName)) + .WithMetrics( + metrics => + { + // Metrics provider from OpenTelemetry + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + // Metrics provides by ASP.NET Core in .NET 8 + .AddMeter("Microsoft.AspNetCore.Hosting") + .AddMeter("Microsoft.AspNetCore.Server.Kestrel"); + } + ) + .WithTracing(tracing => { tracing.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation(); }) + .UseOtlpExporter(OtlpExportProtocol.Grpc, otlpEndpoint); + } +} diff --git a/src/FacturXDotNet.API/FacturXDotNet.API.csproj b/src/FacturXDotNet.API/FacturXDotNet.API.csproj index a0fc0319..d255eb6c 100644 --- a/src/FacturXDotNet.API/FacturXDotNet.API.csproj +++ b/src/FacturXDotNet.API/FacturXDotNet.API.csproj @@ -20,10 +20,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + - - diff --git a/src/FacturXDotNet.API/Local/docker-compose.local.yaml b/src/FacturXDotNet.API/Local/docker-compose.local.yaml new file mode 100644 index 00000000..68e4595d --- /dev/null +++ b/src/FacturXDotNet.API/Local/docker-compose.local.yaml @@ -0,0 +1,36 @@ +services: + api: + build: .. + environment: + Observability__OtlpEndpoint: http://otel:4317 + ports: + - 80:8080 + links: + - "otel-collector:otel" + depends_on: + parseable: + condition: service_started + otel-collector: + condition: service_started + + parseable: + container_name: Parseable + image: parseable/parseable:latest + restart: always + command: [ "parseable", "local-store" ] + environment: + P_ADDR: 0.0.0.0:8000 + RUST_LOG: debug + ports: + - 8080:8000 + expose: + - 8000 + + otel-collector: + container_name: Otel-Collector + image: otel/opentelemetry-collector-contrib:latest + restart: always + volumes: + - ./otel.config.yaml:/etc/otelcol-contrib/config.yaml + links: + - "parseable:parseable" diff --git a/src/FacturXDotNet.API/Local/otel.config.yaml b/src/FacturXDotNet.API/Local/otel.config.yaml new file mode 100644 index 00000000..ed594cd6 --- /dev/null +++ b/src/FacturXDotNet.API/Local/otel.config.yaml @@ -0,0 +1,65 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 +processors: + batch: + +exporters: + otlphttp/parseablelogs: + endpoint: http://parseable:8000 + headers: + Authorization: "Basic YWRtaW46YWRtaW4=" + X-P-Stream: otel-logs + X-P-Log-Source: otel-logs + Content-Type: application/json + encoding: json + tls: + insecure: true + + otlphttp/parseabletraces: + endpoint: http://parseable:8000 + headers: + Authorization: "Basic YWRtaW46YWRtaW4=" + X-P-Stream: otel-traces + X-P-Log-Source: otel-traces + Content-Type: application/json + encoding: json + tls: + insecure: true + + otlphttp/parseablemetrics: + endpoint: http://parseable:8000 + headers: + Authorization: "Basic YWRtaW46YWRtaW4=" + X-P-Stream: otel-metrics + X-P-Log-Source: otel-metrics + Content-Type: application/json + encoding: json + tls: + insecure: true + +extensions: + health_check: + pprof: + zpages: + +service: + extensions: [ health_check, pprof, zpages ] + pipelines: + traces: + receivers: + - otlp + exporters: + - otlphttp/parseabletraces + metrics: + receivers: + - otlp + exporters: + - otlphttp/parseablemetrics + logs: + receivers: + - otlp + exporters: + - otlphttp/parseablelogs \ No newline at end of file diff --git a/src/FacturXDotNet.API/Program.cs b/src/FacturXDotNet.API/Program.cs index 71ccf0a2..8ce8cdae 100644 --- a/src/FacturXDotNet.API/Program.cs +++ b/src/FacturXDotNet.API/Program.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using FacturXDotNet.API; using FacturXDotNet.API.Configuration; +using FacturXDotNet.API.Extensions; using FacturXDotNet.API.Features.Extract; using FacturXDotNet.API.Features.Generate; using FacturXDotNet.API.Features.Information; @@ -9,14 +10,19 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Scalar.AspNetCore; -using Serilog; +using ILogger = Microsoft.Extensions.Logging.ILogger; -Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); +using ILoggerFactory bootstrapLoggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +ILogger bootstrapLogger = bootstrapLoggerFactory.CreateLogger("Program"); try { + bootstrapLogger.LogInformation("Hello World!"); + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - builder.Services.AddSerilog(); + + string serviceName = builder.Configuration.GetValue("ServiceName", "default"); + bootstrapLogger.LogInformation("Service named {ServiceName}", serviceName); builder.Services.Configure(builder.Configuration); builder.Services.AddCors( @@ -26,12 +32,17 @@ string host = builder.Configuration.GetSection("Hosting").GetValue("Host") ?? "http://localhost"; string basePath = builder.Configuration.GetSection("Hosting").GetValue("BasePath") ?? ""; - string serverUrl = (host.EndsWith('/') ? host[..^1] : host) - + (basePath == "" - ? "" - : basePath.StartsWith('/') - ? basePath - : $"/{basePath}"); + Uri serverUrl = new( + (host.EndsWith('/') ? host[..^1] : host) + + (basePath == "" + ? "/" + : basePath.StartsWith('/') + ? basePath.EndsWith('/') ? basePath : $"{basePath}/" + : basePath.EndsWith('/') + ? $"/{basePath}" + : $"/{basePath}/") + ); + bootstrapLogger.LogInformation("Service hosted at {ServerUrl}", serverUrl); builder.Services.AddHealthChecks(); builder.Services.AddOpenApi( @@ -48,10 +59,10 @@ FacturX.NET API - Work in progress.
"""; doc.Info.License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://github.com/FacturX-NET/FacturXDotNet/blob/main/LICENSE") }; doc.Info.Contact = new OpenApiContact - { Name = "Ismail Bennani", Email = "facturx.net@gmail.com", Url = new Uri("https://github.com/FacturX-NET/FacturXDotNet/issues") }; + { Name = "Ismail Bennani", Email = "contact@facturxdotnet.org", Url = new Uri("https://github.com/FacturX-NET/FacturXDotNet/issues") }; doc.Servers.Clear(); - doc.Servers.Add(new OpenApiServer { Url = serverUrl }); + doc.Servers.Add(new OpenApiServer { Url = serverUrl.ToString() }); return Task.CompletedTask; } @@ -60,6 +71,14 @@ FacturX.NET API - Work in progress.
); builder.Services.AddEndpointsApiExplorer(); + string? otlpEndpoint = builder.Configuration.GetSection("Observability")?.GetValue("OtlpEndpoint"); + if (otlpEndpoint != null) + { + Uri otlpUri = new(otlpEndpoint); + bootstrapLogger.LogInformation("Service exports OpenTelemetry data through OTP at {Endpoint}", otlpUri); + builder.AddObservability(serviceName, otlpUri); + } + builder.Services.AddTransient(); WebApplication app = builder.Build(); @@ -72,11 +91,18 @@ FacturX.NET API - Work in progress.
app.UseCors(); - app.MapOpenApi(); - app.MapScalarApiReference(); + app.MapHealthChecks("/health"); + bootstrapLogger.LogInformation("Service health check at {HealthCheckUrl}", new Uri(serverUrl, "health")); + + app.MapOpenApi("/openapi/v1.json"); + bootstrapLogger.LogInformation("Service serves OpenAPI specification at {OpenApiUrl}", new Uri(serverUrl, "openapi/v1.json")); + Uri scalarUrl = new(serverUrl, "scalar"); + app.MapScalarApiReference(); + bootstrapLogger.LogInformation("Service serves Scalar UI at {ScalarUrl}", scalarUrl); app.MapGet("/", () => Results.LocalRedirect($"{configuration.Value.Hosting.BasePath}/scalar")).ExcludeFromDescription(); - app.MapHealthChecks("/health"); + bootstrapLogger.LogInformation("Service redirects {RootUrl} to Scalar UI at {ScalarUrl}", serverUrl, scalarUrl); + app.MapGroup("/info").MapInformationEndpoints().WithTags("Information"); app.MapGroup("/generate").MapGenerateEndpoints().WithTags("Generate"); app.MapGroup("/extract").MapExtractEndpoints().WithTags("Extract"); @@ -86,9 +112,5 @@ FacturX.NET API - Work in progress.
} catch (Exception ex) { - Log.Fatal(ex, "Application terminated unexpectedly"); -} -finally -{ - Log.CloseAndFlush(); + bootstrapLogger.LogCritical(ex, "Application terminated unexpectedly"); } diff --git a/src/FacturXDotNet.API/appsettings.json b/src/FacturXDotNet.API/appsettings.json index c28069ff..a5c4fd2b 100644 --- a/src/FacturXDotNet.API/appsettings.json +++ b/src/FacturXDotNet.API/appsettings.json @@ -5,7 +5,12 @@ "Microsoft.AspNetCore": "Warning" } }, + "ServiceName": "debug", "Hosting": { + "Host": "http://localhost:5295", "UnsafeEnvironment": false + }, + "Observability": { + "OtlpEndpoint": "http://localhost:4317" } } diff --git a/src/FacturXDotNet.sln.DotSettings.user b/src/FacturXDotNet.sln.DotSettings.user index 78f3b559..520b3860 100644 --- a/src/FacturXDotNet.sln.DotSettings.user +++ b/src/FacturXDotNet.sln.DotSettings.user @@ -13,6 +13,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -40,6 +41,8 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded