Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/configs/api.docker-compose.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -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}
- {PORT}:{PORT}
external_links:
- "otel-collector:otel-collector"

networks:
default:
name: observability
external: true

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/FacturXDotNet.API/Configuration/AppConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace FacturXDotNet.API.Configuration;

class AppObservabilityConfiguration
{
public string? OtlpEndpoint { get; set; }
}
37 changes: 37 additions & 0 deletions src/FacturXDotNet.API/Extensions/ObservabilityHostingExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 5 additions & 2 deletions src/FacturXDotNet.API/FacturXDotNet.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.11.2" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" />
<PackageReference Include="Scalar.AspNetCore" Version="2.1.6" />
<PackageReference Include="Semver" Version="3.0.0"/>
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/>
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)'=='Release'">
Expand Down
36 changes: 36 additions & 0 deletions src/FacturXDotNet.API/Local/docker-compose.local.yaml
Original file line number Diff line number Diff line change
@@ -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"
65 changes: 65 additions & 0 deletions src/FacturXDotNet.API/Local/otel.config.yaml
Original file line number Diff line number Diff line change
@@ -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
60 changes: 41 additions & 19 deletions src/FacturXDotNet.API/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string>("ServiceName", "default");
bootstrapLogger.LogInformation("Service named {ServiceName}", serviceName);

builder.Services.Configure<AppConfiguration>(builder.Configuration);
builder.Services.AddCors(
Expand All @@ -26,12 +32,17 @@

string host = builder.Configuration.GetSection("Hosting").GetValue<string>("Host") ?? "http://localhost";
string basePath = builder.Configuration.GetSection("Hosting").GetValue<string>("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(
Expand All @@ -48,10 +59,10 @@ FacturX.NET API - Work in progress. <br/>
""";
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;
}
Expand All @@ -60,6 +71,14 @@ FacturX.NET API - Work in progress. <br/>
);
builder.Services.AddEndpointsApiExplorer();

string? otlpEndpoint = builder.Configuration.GetSection("Observability")?.GetValue<string>("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<PackagesService>();

WebApplication app = builder.Build();
Expand All @@ -72,11 +91,18 @@ FacturX.NET API - Work in progress. <br/>

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");
Expand All @@ -86,9 +112,5 @@ FacturX.NET API - Work in progress. <br/>
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
bootstrapLogger.LogCritical(ex, "Application terminated unexpectedly");
}
5 changes: 5 additions & 0 deletions src/FacturXDotNet.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
"Microsoft.AspNetCore": "Warning"
}
},
"ServiceName": "debug",
"Hosting": {
"Host": "http://localhost:5295",
"UnsafeEnvironment": false
},
"Observability": {
"OtlpEndpoint": "http://localhost:4317"
}
}
3 changes: 3 additions & 0 deletions src/FacturXDotNet.sln.DotSettings.user
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultAssertionStrategy_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5113ab582e0b7d452adcb5acd8a6b415f964152b278eff8b47c1fec787632be_003FDefaultAssertionStrategy_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADelegateHandlerDescriptor_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2ffce84f34e723ee835b1112ff268f41ea8506e48c57fc56c80564cba092ce_003FDelegateHandlerDescriptor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHandlerDescriptor_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fdb9918dfba1bfa7abf458ada35bfe6fd45b0fbbd8a8d6440f0885a3daab359d7_003FHandlerDescriptor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHealthCheckServiceCollectionExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe92bddb6b6e94f33f34c3679ff3bcbe86adc9e7d91b0d8af7029189543e1252a_003FHealthCheckServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHelpBuilder_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb1c5d451bb26b069f9d814a6c79c3d1b991141621de2d9e46bcf16af27cd3fc_003FHelpBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHelpTextExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5b72d77125f16db194673a1a66f3e03cc8cb4f1fb49ced35d4c9aa5561174_003FHelpTextExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIFormatProvider_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2832e8c2b81f4641b3863f406ce3a519c90938_003F05_003Fc3b3999c_003FIFormatProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
Expand Down Expand Up @@ -40,6 +41,8 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASpinner_002EGenerated_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2785637749a235e9a285d99c8285b3ce12be9e079838976c3d8b3551a6eb6_003FSpinner_002EGenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASymbolResult_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F21317424d3675027d3784fccc07d603d802830f9542ce12a7ec94118e59e0_003FSymbolResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASymbol_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd5186e677c82768ac28b376eee6672ae2424436e7fa40cafed2794e91b039_003FSymbol_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUriSyntax_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7ed82aa0f48a6bf284b4aba7c70aff35142349e44fb4f6caec3d71611f9929_003FUriSyntax_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AXmlDiff_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F931696557be34753b8a96c2edec6c1f85000_003F6c_003F88b0c30b_003FXmlDiff_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AXmlParserInternal_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5f4ff1c965e4fd98c6a1aa6bff382abeb088d69c43a28a12a45e54db9cf13a_003FXmlParserInternal_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AXmlParser_002Ecs_002Fl_003AC_0021_003FUsers_003Flahki_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F1a2a86e0dbbd14ad398b73854bfa97b6cc97507fc3d2a3b7a7e0d73c90f08acb_003FXmlParser_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
Expand Down