From 4f80e253e2f5015faa21faf90545a62c94f6ce98 Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Wed, 23 Apr 2025 23:20:12 +0200 Subject: [PATCH 1/6] add opentelemetry export --- .../Configuration/AppConfiguration.cs | 2 + .../AppObservabilityConfiguration.cs | 6 +++ .../ObservabilityHostingExtensions.cs | 37 +++++++++++++++ .../FacturXDotNet.API.csproj | 4 ++ src/FacturXDotNet.API/Program.cs | 46 ++++++++++++++----- src/FacturXDotNet.API/appsettings.json | 4 ++ 6 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 src/FacturXDotNet.API/Configuration/AppObservabilityConfiguration.cs create mode 100644 src/FacturXDotNet.API/Extensions/ObservabilityHostingExtensions.cs 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..ba4a16c2 100644 --- a/src/FacturXDotNet.API/FacturXDotNet.API.csproj +++ b/src/FacturXDotNet.API/FacturXDotNet.API.csproj @@ -20,6 +20,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/FacturXDotNet.API/Program.cs b/src/FacturXDotNet.API/Program.cs index 71ccf0a2..b80548da 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; @@ -18,6 +19,9 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddSerilog(); + string serviceName = builder.Configuration.GetValue("ServiceName", "default"); + Log.Information("Service named {ServiceName}", serviceName); + builder.Services.Configure(builder.Configuration); builder.Services.AddCors( opt => opt.AddDefaultPolicy(p => p.DisallowCredentials().AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("Content-Disposition")) @@ -26,12 +30,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}/") + ); + Log.Information("Service hosted at {ServerUrl}", serverUrl); builder.Services.AddHealthChecks(); builder.Services.AddOpenApi( @@ -48,10 +57,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 +69,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); + Log.Information("Service exports OpenTelemetry data to OTP at {Endpoint}", otlpUri); + builder.AddObservability(serviceName, otlpUri); + } + builder.Services.AddTransient(); WebApplication app = builder.Build(); @@ -72,11 +89,18 @@ FacturX.NET API - Work in progress.
app.UseCors(); - app.MapOpenApi(); - app.MapScalarApiReference(); + app.MapHealthChecks("/health"); + Log.Information("Service health check at {HealthCheckUrl}", new Uri(serverUrl, "health")); + + app.MapOpenApi("/openapi/v1.json"); + Log.Information("Service serves OpenAPI specification at {OpenApiUrl}", new Uri(serverUrl, "openapi/v1.json")); + Uri scalarUrl = new(serverUrl, "scalar"); + app.MapScalarApiReference(); + Log.Information("Service serves Scalar UI at {ScalarUrl}", scalarUrl); app.MapGet("/", () => Results.LocalRedirect($"{configuration.Value.Hosting.BasePath}/scalar")).ExcludeFromDescription(); - app.MapHealthChecks("/health"); + Log.Information("Service redirects / to Scalar UI at {ScalarUrl}", scalarUrl); + app.MapGroup("/info").MapInformationEndpoints().WithTags("Information"); app.MapGroup("/generate").MapGenerateEndpoints().WithTags("Generate"); app.MapGroup("/extract").MapExtractEndpoints().WithTags("Extract"); diff --git a/src/FacturXDotNet.API/appsettings.json b/src/FacturXDotNet.API/appsettings.json index c28069ff..42ae495a 100644 --- a/src/FacturXDotNet.API/appsettings.json +++ b/src/FacturXDotNet.API/appsettings.json @@ -5,7 +5,11 @@ "Microsoft.AspNetCore": "Warning" } }, + "ServiceName": "debug", "Hosting": { "UnsafeEnvironment": false + }, + "Observability": { + "OtlpEndpoint": "http://localhost:4317" } } From 2cd41d57fa303e18d2e3592c7491841a481457ba Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Wed, 23 Apr 2025 23:20:33 +0200 Subject: [PATCH 2/6] add docker compose file for local development --- ...ocker-compose.local.generated.override.yml | 21 ++++++ .../Local/docker-compose.local.yaml | 34 ++++++++++ src/FacturXDotNet.API/Local/otel.config.yaml | 65 +++++++++++++++++++ src/FacturXDotNet.sln.DotSettings.user | 1 + 4 files changed, 121 insertions(+) create mode 100644 src/.idea/.idea.FacturXDotNet/Docker/docker-compose.local.generated.override.yml create mode 100644 src/FacturXDotNet.API/Local/docker-compose.local.yaml create mode 100644 src/FacturXDotNet.API/Local/otel.config.yaml 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/Local/docker-compose.local.yaml b/src/FacturXDotNet.API/Local/docker-compose.local.yaml new file mode 100644 index 00000000..e3d60a67 --- /dev/null +++ b/src/FacturXDotNet.API/Local/docker-compose.local.yaml @@ -0,0 +1,34 @@ +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: + - 8000: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.sln.DotSettings.user b/src/FacturXDotNet.sln.DotSettings.user index 78f3b559..b0abe4f6 100644 --- a/src/FacturXDotNet.sln.DotSettings.user +++ b/src/FacturXDotNet.sln.DotSettings.user @@ -13,6 +13,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded From 3c801465d049a409c16dd5b1922325f2a8a38011 Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Wed, 23 Apr 2025 23:24:41 +0200 Subject: [PATCH 3/6] update compose file --- .github/workflows/configs/api.docker-compose.yml.template | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/configs/api.docker-compose.yml.template b/.github/workflows/configs/api.docker-compose.yml.template index 6fe0194e..18b70479 100644 --- a/.github/workflows/configs/api.docker-compose.yml.template +++ b/.github/workflows/configs/api.docker-compose.yml.template @@ -5,8 +5,10 @@ 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: localhost:6756 ports: - {PORT}:{PORT} \ No newline at end of file From c5837dcff102071d206e5137be001a72f974ce60 Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Wed, 23 Apr 2025 23:56:49 +0200 Subject: [PATCH 4/6] minor --- src/FacturXDotNet.API/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FacturXDotNet.API/Program.cs b/src/FacturXDotNet.API/Program.cs index b80548da..b423244a 100644 --- a/src/FacturXDotNet.API/Program.cs +++ b/src/FacturXDotNet.API/Program.cs @@ -73,7 +73,7 @@ FacturX.NET API - Work in progress.
if (otlpEndpoint != null) { Uri otlpUri = new(otlpEndpoint); - Log.Information("Service exports OpenTelemetry data to OTP at {Endpoint}", otlpUri); + Log.Information("Service exports OpenTelemetry data through OTP at {Endpoint}", otlpUri); builder.AddObservability(serviceName, otlpUri); } From d646467e9cac93359910d5c541c08dbf926f5ff8 Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Thu, 24 Apr 2025 00:23:49 +0200 Subject: [PATCH 5/6] fix compose file --- .../workflows/configs/api.docker-compose.yml.template | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/configs/api.docker-compose.yml.template b/.github/workflows/configs/api.docker-compose.yml.template index 18b70479..2cca15d4 100644 --- a/.github/workflows/configs/api.docker-compose.yml.template +++ b/.github/workflows/configs/api.docker-compose.yml.template @@ -9,6 +9,13 @@ services: Hosting__Host: https://{BUILD-NAME}.facturxdotnet.org Hosting__BasePath: /api Hosting__UnsafeEnvironment: true - Observability__OtlpEndpoint: localhost:6756 + 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 From d3e691c080907f434704e4a63a889c88ca53d18d Mon Sep 17 00:00:00 2001 From: ismailbennani Date: Thu, 24 Apr 2025 00:39:35 +0200 Subject: [PATCH 6/6] remove serilog --- .../FacturXDotNet.API.csproj | 3 +- .../Local/docker-compose.local.yaml | 4 ++- src/FacturXDotNet.API/Program.cs | 28 +++++++++---------- src/FacturXDotNet.API/appsettings.json | 1 + src/FacturXDotNet.sln.DotSettings.user | 2 ++ 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/FacturXDotNet.API/FacturXDotNet.API.csproj b/src/FacturXDotNet.API/FacturXDotNet.API.csproj index ba4a16c2..d255eb6c 100644 --- a/src/FacturXDotNet.API/FacturXDotNet.API.csproj +++ b/src/FacturXDotNet.API/FacturXDotNet.API.csproj @@ -20,14 +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 index e3d60a67..68e4595d 100644 --- a/src/FacturXDotNet.API/Local/docker-compose.local.yaml +++ b/src/FacturXDotNet.API/Local/docker-compose.local.yaml @@ -22,7 +22,9 @@ services: P_ADDR: 0.0.0.0:8000 RUST_LOG: debug ports: - - 8000:8000 + - 8080:8000 + expose: + - 8000 otel-collector: container_name: Otel-Collector diff --git a/src/FacturXDotNet.API/Program.cs b/src/FacturXDotNet.API/Program.cs index b423244a..8ce8cdae 100644 --- a/src/FacturXDotNet.API/Program.cs +++ b/src/FacturXDotNet.API/Program.cs @@ -10,17 +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"); - Log.Information("Service named {ServiceName}", serviceName); + bootstrapLogger.LogInformation("Service named {ServiceName}", serviceName); builder.Services.Configure(builder.Configuration); builder.Services.AddCors( @@ -40,7 +42,7 @@ ? $"/{basePath}" : $"/{basePath}/") ); - Log.Information("Service hosted at {ServerUrl}", serverUrl); + bootstrapLogger.LogInformation("Service hosted at {ServerUrl}", serverUrl); builder.Services.AddHealthChecks(); builder.Services.AddOpenApi( @@ -73,7 +75,7 @@ FacturX.NET API - Work in progress.
if (otlpEndpoint != null) { Uri otlpUri = new(otlpEndpoint); - Log.Information("Service exports OpenTelemetry data through OTP at {Endpoint}", otlpUri); + bootstrapLogger.LogInformation("Service exports OpenTelemetry data through OTP at {Endpoint}", otlpUri); builder.AddObservability(serviceName, otlpUri); } @@ -90,16 +92,16 @@ FacturX.NET API - Work in progress.
app.UseCors(); app.MapHealthChecks("/health"); - Log.Information("Service health check at {HealthCheckUrl}", new Uri(serverUrl, "health")); + bootstrapLogger.LogInformation("Service health check at {HealthCheckUrl}", new Uri(serverUrl, "health")); app.MapOpenApi("/openapi/v1.json"); - Log.Information("Service serves OpenAPI specification at {OpenApiUrl}", new Uri(serverUrl, "openapi/v1.json")); + bootstrapLogger.LogInformation("Service serves OpenAPI specification at {OpenApiUrl}", new Uri(serverUrl, "openapi/v1.json")); Uri scalarUrl = new(serverUrl, "scalar"); app.MapScalarApiReference(); - Log.Information("Service serves Scalar UI at {ScalarUrl}", scalarUrl); + bootstrapLogger.LogInformation("Service serves Scalar UI at {ScalarUrl}", scalarUrl); app.MapGet("/", () => Results.LocalRedirect($"{configuration.Value.Hosting.BasePath}/scalar")).ExcludeFromDescription(); - Log.Information("Service redirects / to Scalar UI at {ScalarUrl}", scalarUrl); + bootstrapLogger.LogInformation("Service redirects {RootUrl} to Scalar UI at {ScalarUrl}", serverUrl, scalarUrl); app.MapGroup("/info").MapInformationEndpoints().WithTags("Information"); app.MapGroup("/generate").MapGenerateEndpoints().WithTags("Generate"); @@ -110,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 42ae495a..a5c4fd2b 100644 --- a/src/FacturXDotNet.API/appsettings.json +++ b/src/FacturXDotNet.API/appsettings.json @@ -7,6 +7,7 @@ }, "ServiceName": "debug", "Hosting": { + "Host": "http://localhost:5295", "UnsafeEnvironment": false }, "Observability": { diff --git a/src/FacturXDotNet.sln.DotSettings.user b/src/FacturXDotNet.sln.DotSettings.user index b0abe4f6..520b3860 100644 --- a/src/FacturXDotNet.sln.DotSettings.user +++ b/src/FacturXDotNet.sln.DotSettings.user @@ -41,6 +41,8 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded