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