From f10e7e80bd0a47b09c3b78452270896bc47a7e5b Mon Sep 17 00:00:00 2001 From: Koen Metsu Date: Mon, 11 Dec 2023 15:30:35 +0100 Subject: [PATCH 1/5] feat: replace otel with example and ignore it --- .gitignore | 2 ++ ...-collector-config.yaml => otel-collector-config.example.yaml | 0 2 files changed, 2 insertions(+) rename otel-collector-config.yaml => otel-collector-config.example.yaml (100%) diff --git a/.gitignore b/.gitignore index 68e8858dc..72b234324 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ wwwroot/ # Internal folders created by marten for prebuilding src/*/Internal +otel-collector-config.yaml + .vs/ .vscode/ .fake/ diff --git a/otel-collector-config.yaml b/otel-collector-config.example.yaml similarity index 100% rename from otel-collector-config.yaml rename to otel-collector-config.example.yaml From 4080ae5974d8f95b9f08af84745c7cd03c9a3429 Mon Sep 17 00:00:00 2001 From: Jan Lesage Date: Tue, 12 Dec 2023 09:48:34 +0100 Subject: [PATCH 2/5] chore: OR-1834 add metrics init --- .../Monitoring/OpenTelemetryMetrics.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/AssociationRegistry.Acm.Api/Infrastructure/Monitoring/OpenTelemetryMetrics.cs diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Monitoring/OpenTelemetryMetrics.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Monitoring/OpenTelemetryMetrics.cs new file mode 100644 index 000000000..d2050eb84 --- /dev/null +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Monitoring/OpenTelemetryMetrics.cs @@ -0,0 +1,83 @@ +namespace AssociationRegistry.Acm.Api.Infrastructure.Monitoring; + +using System; +using System.Diagnostics.Metrics; + +public class OpenTelemetryMetrics +{ + public class ElasticSearchProjections + { + public static Func MeterNameFunc = name => $"VR.ES.{name}"; + private readonly Meter _meter; + public Histogram NumberOfEnvelopesHandledHistogram { get; } + public int NumberOfEnvelopesHandledGauge { get; set; } + public int NumberOfEnvelopesHandledCounter { get; set; } + public Histogram NumberOfEnvelopesBehindHistogram { get; } + public int NumberOfEnvelopesBehindGauge { get; set; } + public int NumberOfEnvelopesBehindCounter { get; set; } + public int LastProcessedEventNumberGauge { get; set; } + public int LastProcessedEventNumberCounter { get; set; } + public int MaxEventNumberToProcessGauge { get; set; } + public int MaxEventNumberToProcessCounter { get; set; } + + private static class MeterNames + { + public static Func EnvelopesHandled = (meterName, meterType) + => $"{meterName}.{nameof(EnvelopesHandled)}.{meterType}"; + + public static Func EnvelopesBehind = (meterName, meterType) + => $"{meterName}.{nameof(EnvelopesBehind)}.{meterType}"; + + public static Func LastProcessedEvent = + (meterName, meterType) => $"{meterName}.{nameof(LastProcessedEvent)}.{meterType}"; + + public static Func MaxEventNumberToProcess = + (meterName, meterType) => $"{meterName}.{nameof(MaxEventNumberToProcess)}.{meterType}"; + + public static Func OrganisationsToRebuild = + (meterName, meterType) => $"{meterName}.{nameof(OrganisationsToRebuild)}.{meterType}"; + } + + public ElasticSearchProjections(string runnerName) + { + const string histogram = "Histogram"; + const string counter = "Counter"; + const string gauge = "Gauge"; + + var meterName = MeterNameFunc(runnerName); + _meter = new Meter(meterName); + + NumberOfEnvelopesHandledHistogram = _meter.CreateHistogram(MeterNames.EnvelopesHandled(meterName, histogram), + unit: "envelopes", description: "number of envelopes handled"); + + _meter.CreateObservableGauge(MeterNames.EnvelopesHandled(meterName, gauge), observeValue: () => NumberOfEnvelopesHandledGauge, + unit: "envelopes", description: "number of envelopes handled"); + + _meter.CreateObservableUpDownCounter(MeterNames.EnvelopesHandled(meterName, counter), + observeValue: () => NumberOfEnvelopesHandledCounter, unit: "envelopes", + description: "number of envelopes handled"); + + _meter.CreateObservableUpDownCounter(MeterNames.LastProcessedEvent(meterName, counter), + observeValue: () => LastProcessedEventNumberCounter); + + _meter.CreateObservableGauge(MeterNames.LastProcessedEvent(meterName, gauge), + observeValue: () => LastProcessedEventNumberGauge); + + _meter.CreateObservableUpDownCounter(MeterNames.MaxEventNumberToProcess(meterName, counter), + observeValue: () => MaxEventNumberToProcessCounter); + + _meter.CreateObservableGauge(MeterNames.MaxEventNumberToProcess(meterName, gauge), + observeValue: () => MaxEventNumberToProcessGauge); + + NumberOfEnvelopesBehindHistogram = _meter.CreateHistogram(MeterNames.EnvelopesBehind(meterName, histogram), + unit: "envelopes", description: "number of envelopes behind"); + + _meter.CreateObservableUpDownCounter(MeterNames.EnvelopesBehind(meterName, counter), + observeValue: () => NumberOfEnvelopesBehindCounter, unit: "envelopes", + description: "number of envelopes behind"); + + _meter.CreateObservableGauge(MeterNames.EnvelopesBehind(meterName, gauge), observeValue: () => NumberOfEnvelopesBehindGauge, + unit: "envelopes", description: "number of envelopes behind"); + } + } +} From 0394c49b4482b031d6e99f958e0a295d371b85fd Mon Sep 17 00:00:00 2001 From: Koen Metsu Date: Wed, 13 Dec 2023 15:34:56 +0100 Subject: [PATCH 3/5] feat: or-1834 wip instrumentation --- .../Extensions/HighWatermarkListener.cs | 45 ++++++++++++++++ .../Extensions/MartenExtensions.cs | 3 ++ .../UnexpectedAggregateVersionMiddleware.cs | 3 +- src/AssociationRegistry.Admin.Api/Program.cs | 3 +- .../Extensions/Instrumentation.cs | 43 +++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 53 ++++++++++--------- 6 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/HighWatermarkListener.cs create mode 100644 src/AssociationRegistry.OpenTelemetry/Extensions/Instrumentation.cs diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/HighWatermarkListener.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/HighWatermarkListener.cs new file mode 100644 index 000000000..91a088681 --- /dev/null +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/HighWatermarkListener.cs @@ -0,0 +1,45 @@ +namespace AssociationRegistry.Admin.Api.Infrastructure.Extensions; + +using Marten; +using Marten.Services; +using OpenTelemetry.Extensions; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +public class HighWatermarkListener : IDocumentSessionListener +{ + private readonly Instrumentation _instrumentation; + + public HighWatermarkListener(Instrumentation instrumentation) + { + _instrumentation = instrumentation; + } + + public void BeforeSaveChanges(IDocumentSession session) + { + } + + public async Task BeforeSaveChangesAsync(IDocumentSession session, CancellationToken token) + { + } + + public void AfterCommit(IDocumentSession session, IChangeSet commit) + { + } + + public void DocumentLoaded(object id, object document) + { + } + + public void DocumentAddedForStorage(object id, object document) + { + } + + public async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) + { + var highWatermark = commit.GetEvents().Max(x => x.Sequence); + _instrumentation.HighWaterMarkHistogram.Record(highWatermark); + _instrumentation.HighWaterMark = highWatermark; + } +} diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs index f99b77f98..7b4e11d53 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; +using OpenTelemetry.Extensions; using Schema.Detail; using Schema.Historiek; using VCodeGeneration; @@ -35,6 +36,8 @@ public static IServiceCollection AddMarten( opts.Serializer(CreateCustomMartenSerializer()); opts.Events.MetadataConfig.EnableAll(); + opts.Listeners.Add(new HighWatermarkListener(serviceProvider.GetRequiredService())); + opts.RegisterDocumentType(); opts.RegisterDocumentType(); diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Middleware/UnexpectedAggregateVersionMiddleware.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Middleware/UnexpectedAggregateVersionMiddleware.cs index 1351dc64d..11bc9afb9 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/Middleware/UnexpectedAggregateVersionMiddleware.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Middleware/UnexpectedAggregateVersionMiddleware.cs @@ -1,11 +1,10 @@ namespace AssociationRegistry.Admin.Api.Infrastructure.Middleware; -using Be.Vlaanderen.Basisregisters.AggregateSource; using Be.Vlaanderen.Basisregisters.Api.Exceptions; -using System.Threading.Tasks; using EventStore; using Extensions; using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; public class UnexpectedAggregateVersionMiddleware { diff --git a/src/AssociationRegistry.Admin.Api/Program.cs b/src/AssociationRegistry.Admin.Api/Program.cs index f9cbad68d..40b9dd25d 100755 --- a/src/AssociationRegistry.Admin.Api/Program.cs +++ b/src/AssociationRegistry.Admin.Api/Program.cs @@ -18,7 +18,6 @@ namespace AssociationRegistry.Admin.Api; using FluentValidation; using Framework; using IdentityModel.AspNetCore.OAuth2Introspection; -using Infrastructure; using Infrastructure.Configuration; using Infrastructure.ConfigurationBindings; using Infrastructure.ExceptionHandlers; @@ -26,7 +25,6 @@ namespace AssociationRegistry.Admin.Api; using Infrastructure.Json; using Infrastructure.Middleware; using JasperFx.CodeGeneration; -using JasperFx.Core; using Kbo; using Lamar.Microsoft.DependencyInjection; using Magda; @@ -294,6 +292,7 @@ private static void ConfigureServices(WebApplicationBuilder builder) .AddSingleton(appSettings) .AddSingleton(magdaTemporaryVertegenwoordigersSection) .AddSingleton() + .AddSingleton() .AddScoped() .AddScoped() .AddScoped() diff --git a/src/AssociationRegistry.OpenTelemetry/Extensions/Instrumentation.cs b/src/AssociationRegistry.OpenTelemetry/Extensions/Instrumentation.cs new file mode 100644 index 000000000..9bdc00226 --- /dev/null +++ b/src/AssociationRegistry.OpenTelemetry/Extensions/Instrumentation.cs @@ -0,0 +1,43 @@ +namespace AssociationRegistry.OpenTelemetry.Extensions; + +using System.Diagnostics; +using System.Diagnostics.Metrics; + +/// +/// It is recommended to use a custom type to hold references for +/// ActivitySource and Instruments. This avoids possible type collisions +/// with other components in the DI container. +/// +public class Instrumentation : IDisposable +{ + internal const string ActivitySourceName = "AssociationRegistry"; + internal const string MeterName = "AdminApi"; + private readonly Meter meter; + + public Instrumentation() + { + var version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); + ActivitySource = new ActivitySource(ActivitySourceName, version); + meter = new Meter(MeterName, version); + + HighWaterMarkHistogram = meter.CreateHistogram(name: "ar.highWatermark.h", unit: "events", description: "high watermark"); + + HighWaterMarkGauge = meter.CreateObservableGauge(name: "ar.highWatermark.g", + observeValue: () => HighWaterMark); + + HighWaterMarkCounter = meter.CreateObservableUpDownCounter(name: "ar.highWatermark.c", + observeValue: () => new Measurement(HighWaterMark)); + } + + public ObservableUpDownCounter HighWaterMarkCounter { get; set; } + public ObservableGauge HighWaterMarkGauge { get; set; } + public ActivitySource ActivitySource { get; } + public Histogram HighWaterMarkHistogram { get; } + public long HighWaterMark { get; set; } + + public void Dispose() + { + ActivitySource.Dispose(); + meter.Dispose(); + } +} diff --git a/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs b/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs index 4353946d7..c9f7349dc 100644 --- a/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs +++ b/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ namespace AssociationRegistry.OpenTelemetry.Extensions; -using System.Reflection; using global::OpenTelemetry.Exporter; using global::OpenTelemetry.Logs; using global::OpenTelemetry.Metrics; @@ -10,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Npgsql; +using System.Reflection; public static class ServiceCollectionExtensions { @@ -21,43 +21,45 @@ public static IServiceCollection AddOpenTelemetry(this IServiceCollection servic var collectorUrl = Environment.GetEnvironmentVariable("COLLECTOR_URL") ?? "http://localhost:4317"; Action configureResource = r => r - .AddService( - serviceName, - serviceVersion: assemblyVersion, - serviceInstanceId: Environment.MachineName) - .AddAttributes( - new Dictionary - { - ["deployment.environment"] = - Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLowerInvariant() - ?? "unknown", - }); + .AddService( + serviceName, + serviceVersion: assemblyVersion, + serviceInstanceId: Environment.MachineName) + .AddAttributes( + new Dictionary + { + ["deployment.environment"] = + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + ?.ToLowerInvariant() + ?? "unknown", + }); services.AddOpenTelemetryTracing( builder => builder - .AddSource(serviceName) - .ConfigureResource(configureResource).AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation( + .AddSource(serviceName) + .ConfigureResource(configureResource).AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation( options => { options.EnrichWithHttpRequest = (activity, request) => activity.SetParentId(request.Headers["traceparent"]); + options.Filter = context => context.Request.Method != HttpMethods.Options; }) - .AddNpgsql() - .AddOtlpExporter( + .AddNpgsql() + .AddOtlpExporter( options => { options.Protocol = OtlpExportProtocol.Grpc; options.Endpoint = new Uri(collectorUrl); }) - .AddSource("Wolverine")); + .AddSource("Wolverine")); services.AddLogging( builder => builder - .AddOpenTelemetry( + .AddOpenTelemetry( options => { options.ConfigureResource(configureResource); @@ -77,16 +79,17 @@ public static IServiceCollection AddOpenTelemetry(this IServiceCollection servic services.AddOpenTelemetryMetrics( options => options - .ConfigureResource(configureResource) - .AddRuntimeInstrumentation() - .AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation() - .AddOtlpExporter( + .ConfigureResource(configureResource) + .AddRuntimeInstrumentation() + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation() + .AddOtlpExporter( exporter => { exporter.Protocol = OtlpExportProtocol.Grpc; exporter.Endpoint = new Uri(collectorUrl); - })); + }) + .AddMeter(Instrumentation.MeterName)); return services; } From f30c3221a083f9354bd5e14f2c504973076f671a Mon Sep 17 00:00:00 2001 From: Jan Lesage Date: Wed, 13 Dec 2023 16:35:41 +0100 Subject: [PATCH 4/5] chore: add instrumentation and listeners for open telemetry --- .../Extensions/MartenExtensions.cs | 9 ++-- .../Extensions/StoreOptionsExtensions.cs | 6 ++- .../Metrics/HighWatermarkListener.cs | 43 ++++++++++++++++ .../Infrastructure/Metrics/Instrumentation.cs | 37 ++++++++++++++ src/AssociationRegistry.Acm.Api/Program.cs | 13 +++-- .../Extensions/MartenExtensions.cs | 2 +- .../HighWatermarkListener.cs | 4 +- .../Metrics}/Instrumentation.cs | 19 +++---- src/AssociationRegistry.Admin.Api/Program.cs | 5 +- .../Extensions/ServiceCollectionExtensions.cs | 13 +++-- .../IInstrumentation.cs | 7 +++ .../Metrics/HighWatermarkListener.cs | 51 +++++++++++++++++++ .../Infrastructure/Metrics/Instrumentation.cs | 40 +++++++++++++++ .../ConfigureMartenExtensions.cs | 3 ++ .../Program.cs | 3 +- 15 files changed, 223 insertions(+), 32 deletions(-) create mode 100644 src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs create mode 100644 src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/Instrumentation.cs rename src/AssociationRegistry.Admin.Api/Infrastructure/{Extensions => Metrics}/HighWatermarkListener.cs (87%) rename src/{AssociationRegistry.OpenTelemetry/Extensions => AssociationRegistry.Admin.Api/Infrastructure/Metrics}/Instrumentation.cs (53%) create mode 100644 src/AssociationRegistry.OpenTelemetry/IInstrumentation.cs create mode 100644 src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs create mode 100644 src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/MartenExtensions.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/MartenExtensions.cs index 84e656ea3..ecad0e639 100644 --- a/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/MartenExtensions.cs +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/MartenExtensions.cs @@ -14,9 +14,12 @@ using Newtonsoft.Json; using Schema.VerenigingenPerInsz; -public static class MartenExtentions +public static class MartenExtensions { - public static IServiceCollection AddMarten(this IServiceCollection services, PostgreSqlOptionsSection postgreSqlOptions, IConfiguration configuration) + public static IServiceCollection AddMarten( + this IServiceCollection services, + PostgreSqlOptionsSection postgreSqlOptions, + IConfiguration configuration) { var martenConfiguration = services .AddSingleton(postgreSqlOptions) @@ -28,7 +31,7 @@ public static IServiceCollection AddMarten(this IServiceCollection services, Pos opts.Events.StreamIdentity = StreamIdentity.AsString; opts.Serializer(CreateCustomMartenSerializer()); opts.Events.MetadataConfig.EnableAll(); - opts.AddPostgresProjections(); + opts.AddPostgresProjections(serviceProvider); opts.RegisterDocumentType(); opts.RegisterDocumentType(); diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs index a819ae8e2..c4c3691f9 100644 --- a/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs @@ -2,12 +2,16 @@ namespace AssociationRegistry.Acm.Api.Infrastructure.Extensions; using Marten; using Marten.Events.Projections; +using Metrics; +using Microsoft.Extensions.DependencyInjection; using Projections; +using System; public static class StoreOptionsExtensions { - public static void AddPostgresProjections(this StoreOptions source) + public static void AddPostgresProjections(this StoreOptions source, IServiceProvider serviceProvider) { source.Projections.Add(new VerenigingenPerInszProjection(), ProjectionLifecycle.Async); + source.Projections.AsyncListeners.Add(new HighWatermarkListener(serviceProvider.GetRequiredService())); } } diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs new file mode 100644 index 000000000..5fee10028 --- /dev/null +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs @@ -0,0 +1,43 @@ +namespace AssociationRegistry.Acm.Api.Infrastructure.Metrics; + +using Marten; +using Marten.Services; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +public class HighWatermarkListener : IDocumentSessionListener +{ + private readonly Instrumentation _instrumentation; + + public HighWatermarkListener(Instrumentation instrumentation) + { + _instrumentation = instrumentation; + } + + public void BeforeSaveChanges(IDocumentSession session) + { + } + + public async Task BeforeSaveChangesAsync(IDocumentSession session, CancellationToken token) + { + } + + public void AfterCommit(IDocumentSession session, IChangeSet commit) + { + } + + public void DocumentLoaded(object id, object document) + { + } + + public void DocumentAddedForStorage(object id, object document) + { + } + + public async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) + { + var highWatermark = commit.GetEvents().Max(x => x.Sequence); + _instrumentation.VerenigingPerInszHistogram.Record(highWatermark); + } +} diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/Instrumentation.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/Instrumentation.cs new file mode 100644 index 000000000..14b28ac48 --- /dev/null +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/Instrumentation.cs @@ -0,0 +1,37 @@ +namespace AssociationRegistry.Acm.Api.Infrastructure.Metrics; + +using OpenTelemetry; +using System; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +/// +/// It is recommended to use a custom type to hold references for +/// ActivitySource and Instruments. This avoids possible type collisions +/// with other components in the DI container. +/// +public class Instrumentation : IInstrumentation, IDisposable +{ + public string ActivitySourceName => "AssociationRegistry"; + public string MeterName => "AcmProjections"; + private readonly Meter meter; + + public Instrumentation() + { + var version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); + ActivitySource = new ActivitySource(ActivitySourceName, version); + meter = new Meter(MeterName, version); + + VerenigingPerInszHistogram = + meter.CreateHistogram(name: "ar.p.verenigingPerInsz.h", unit: "events", description: "vereniging per insz projection"); + } + + public ActivitySource ActivitySource { get; } + public Histogram VerenigingPerInszHistogram { get; } + + public void Dispose() + { + ActivitySource.Dispose(); + meter.Dispose(); + } +} diff --git a/src/AssociationRegistry.Acm.Api/Program.cs b/src/AssociationRegistry.Acm.Api/Program.cs index b8b720a13..ddabf22ce 100755 --- a/src/AssociationRegistry.Acm.Api/Program.cs +++ b/src/AssociationRegistry.Acm.Api/Program.cs @@ -16,6 +16,7 @@ namespace AssociationRegistry.Acm.Api; using Infrastructure.Configuration; using Infrastructure.ConfigurationBindings; using Infrastructure.Extensions; +using Infrastructure.Metrics; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -214,7 +215,8 @@ private static void LoadConfiguration(WebApplicationBuilder builder, params stri { builder.Configuration .AddJsonFile("appsettings.json") - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName.ToLowerInvariant()}.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName.ToLowerInvariant()}.json", optional: true, + reloadOnChange: false) .AddJsonFile($"appsettings.{Environment.MachineName.ToLowerInvariant()}.json", optional: true, reloadOnChange: false) .AddEnvironmentVariables() .AddCommandLine(args) @@ -249,7 +251,7 @@ private static void ConfigureServices(WebApplicationBuilder builder) builder.Services .AddSingleton(appSettings) .AddMarten(postgreSqlOptionsSection, builder.Configuration) - .AddOpenTelemetry() + .AddOpenTelemetry(new Instrumentation()) .AddHttpContextAccessor() .AddControllers(); @@ -303,7 +305,8 @@ private static void ConfigureServices(WebApplicationBuilder builder) cfg.AddPolicy( StartupConstants.AllowSpecificOrigin, configurePolicy: corsPolicy => corsPolicy - .WithOrigins(builder.Configuration.GetValue("Cors") ?? Array.Empty()) + .WithOrigins(builder.Configuration.GetValue("Cors") ?? + Array.Empty()) .WithMethods(StartupConstants.HttpMethodsAsString) .WithHeaders(StartupConstants.Headers) .WithExposedHeaders(StartupConstants.ExposedHeaders) @@ -337,7 +340,9 @@ private static void ConfigureServices(WebApplicationBuilder builder) JwtBearerDefaults.AuthenticationScheme, configureOptions: options => { - var configOptions = builder.Configuration.GetSection(nameof(OAuth2IntrospectionOptions)).Get(); + var configOptions = builder.Configuration.GetSection(nameof(OAuth2IntrospectionOptions)) + .Get(); + options.ClientId = configOptions.ClientId; options.ClientSecret = configOptions.ClientSecret; options.Authority = configOptions.Authority; diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs index 7b4e11d53..11c581e5d 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs @@ -8,11 +8,11 @@ using Marten; using Marten.Events; using Marten.Services; +using Metrics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; -using OpenTelemetry.Extensions; using Schema.Detail; using Schema.Historiek; using VCodeGeneration; diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/HighWatermarkListener.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/HighWatermarkListener.cs similarity index 87% rename from src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/HighWatermarkListener.cs rename to src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/HighWatermarkListener.cs index 91a088681..b664bcbae 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/HighWatermarkListener.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/HighWatermarkListener.cs @@ -1,8 +1,7 @@ -namespace AssociationRegistry.Admin.Api.Infrastructure.Extensions; +namespace AssociationRegistry.Admin.Api.Infrastructure.Metrics; using Marten; using Marten.Services; -using OpenTelemetry.Extensions; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -40,6 +39,5 @@ public async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, { var highWatermark = commit.GetEvents().Max(x => x.Sequence); _instrumentation.HighWaterMarkHistogram.Record(highWatermark); - _instrumentation.HighWaterMark = highWatermark; } } diff --git a/src/AssociationRegistry.OpenTelemetry/Extensions/Instrumentation.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/Instrumentation.cs similarity index 53% rename from src/AssociationRegistry.OpenTelemetry/Extensions/Instrumentation.cs rename to src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/Instrumentation.cs index 9bdc00226..4400a7e7e 100644 --- a/src/AssociationRegistry.OpenTelemetry/Extensions/Instrumentation.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/Instrumentation.cs @@ -1,5 +1,7 @@ -namespace AssociationRegistry.OpenTelemetry.Extensions; +namespace AssociationRegistry.Admin.Api.Infrastructure.Metrics; +using OpenTelemetry; +using System; using System.Diagnostics; using System.Diagnostics.Metrics; @@ -8,10 +10,10 @@ namespace AssociationRegistry.OpenTelemetry.Extensions; /// ActivitySource and Instruments. This avoids possible type collisions /// with other components in the DI container. /// -public class Instrumentation : IDisposable +public class Instrumentation : IInstrumentation, IDisposable { - internal const string ActivitySourceName = "AssociationRegistry"; - internal const string MeterName = "AdminApi"; + public string ActivitySourceName => "AssociationRegistry"; + public string MeterName => "AdminApi"; private readonly Meter meter; public Instrumentation() @@ -21,19 +23,10 @@ public Instrumentation() meter = new Meter(MeterName, version); HighWaterMarkHistogram = meter.CreateHistogram(name: "ar.highWatermark.h", unit: "events", description: "high watermark"); - - HighWaterMarkGauge = meter.CreateObservableGauge(name: "ar.highWatermark.g", - observeValue: () => HighWaterMark); - - HighWaterMarkCounter = meter.CreateObservableUpDownCounter(name: "ar.highWatermark.c", - observeValue: () => new Measurement(HighWaterMark)); } - public ObservableUpDownCounter HighWaterMarkCounter { get; set; } - public ObservableGauge HighWaterMarkGauge { get; set; } public ActivitySource ActivitySource { get; } public Histogram HighWaterMarkHistogram { get; } - public long HighWaterMark { get; set; } public void Dispose() { diff --git a/src/AssociationRegistry.Admin.Api/Program.cs b/src/AssociationRegistry.Admin.Api/Program.cs index 40b9dd25d..0a9949323 100755 --- a/src/AssociationRegistry.Admin.Api/Program.cs +++ b/src/AssociationRegistry.Admin.Api/Program.cs @@ -23,6 +23,7 @@ namespace AssociationRegistry.Admin.Api; using Infrastructure.ExceptionHandlers; using Infrastructure.Extensions; using Infrastructure.Json; +using Infrastructure.Metrics; using Infrastructure.Middleware; using JasperFx.CodeGeneration; using Kbo; @@ -44,7 +45,6 @@ namespace AssociationRegistry.Admin.Api; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -292,7 +292,6 @@ private static void ConfigureServices(WebApplicationBuilder builder) .AddSingleton(appSettings) .AddSingleton(magdaTemporaryVertegenwoordigersSection) .AddSingleton() - .AddSingleton() .AddScoped() .AddScoped() .AddScoped() @@ -305,7 +304,7 @@ private static void ConfigureServices(WebApplicationBuilder builder) .AddTransient() .AddMarten(postgreSqlOptionsSection, builder.Configuration) .AddElasticSearch(elasticSearchOptionsSection) - .AddOpenTelemetry() + .AddOpenTelemetry(new Instrumentation()) .AddHttpContextAccessor() .AddControllers(options => options.Filters.Add()); diff --git a/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs b/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs index c9f7349dc..fed196480 100644 --- a/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs +++ b/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs @@ -13,13 +13,16 @@ public static class ServiceCollectionExtensions { - public static IServiceCollection AddOpenTelemetry(this IServiceCollection services) + public static IServiceCollection AddOpenTelemetry(this IServiceCollection services, IInstrumentation? instrumentation = null) { var executingAssembly = Assembly.GetEntryAssembly()!; var serviceName = executingAssembly.GetName().Name!; var assemblyVersion = executingAssembly.GetName().Version?.ToString() ?? "unknown"; var collectorUrl = Environment.GetEnvironmentVariable("COLLECTOR_URL") ?? "http://localhost:4317"; + if (instrumentation is not null) + services.AddSingleton(instrumentation); + Action configureResource = r => r .AddService( serviceName, @@ -78,6 +81,7 @@ public static IServiceCollection AddOpenTelemetry(this IServiceCollection servic services.AddOpenTelemetryMetrics( options => + { options .ConfigureResource(configureResource) .AddRuntimeInstrumentation() @@ -88,8 +92,11 @@ public static IServiceCollection AddOpenTelemetry(this IServiceCollection servic { exporter.Protocol = OtlpExportProtocol.Grpc; exporter.Endpoint = new Uri(collectorUrl); - }) - .AddMeter(Instrumentation.MeterName)); + }); + + if (instrumentation is not null) + options.AddMeter(instrumentation.MeterName); + }); return services; } diff --git a/src/AssociationRegistry.OpenTelemetry/IInstrumentation.cs b/src/AssociationRegistry.OpenTelemetry/IInstrumentation.cs new file mode 100644 index 000000000..1d64aa4b2 --- /dev/null +++ b/src/AssociationRegistry.OpenTelemetry/IInstrumentation.cs @@ -0,0 +1,7 @@ +namespace AssociationRegistry.OpenTelemetry; + +public interface IInstrumentation +{ + public string ActivitySourceName { get; } + public string MeterName { get; } +} diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs new file mode 100644 index 000000000..1c84c1390 --- /dev/null +++ b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs @@ -0,0 +1,51 @@ +namespace AssociationRegistry.Public.ProjectionHost.Infrastructure.Metrics; + +using Marten; +using Marten.Events.Daemon; +using Marten.Services; +using Projections.Detail; + +public class HighWatermarkListener : IDocumentSessionListener +{ + private readonly Instrumentation _instrumentation; + + public HighWatermarkListener(Instrumentation instrumentation) + { + _instrumentation = instrumentation; + } + + public void BeforeSaveChanges(IDocumentSession session) + { + } + + public async Task BeforeSaveChangesAsync(IDocumentSession session, CancellationToken token) + { + } + + public void AfterCommit(IDocumentSession session, IChangeSet commit) + { + } + + public void DocumentLoaded(object id, object document) + { + } + + public void DocumentAddedForStorage(object id, object document) + { + } + + public async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) + { + var publiekDetailProgress = await session.DocumentStore.Advanced.ProjectionProgressFor( + new ShardName(typeof(PubliekVerenigingDetailProjection).Namespace), + token: token); + + _instrumentation.PubliekVerenigingDetailHistogram.Record(publiekDetailProgress); + + var publiekZoekenProgress = await session.DocumentStore.Advanced.ProjectionProgressFor( + new ShardName("PubliekVerenigingZoekenProjection"), + token: token); + + _instrumentation.PubliekVerenigingZoekenHistogram.Record(publiekZoekenProgress); + } +} diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs new file mode 100644 index 000000000..a5719dfc5 --- /dev/null +++ b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs @@ -0,0 +1,40 @@ +namespace AssociationRegistry.Public.ProjectionHost.Infrastructure.Metrics; + +using OpenTelemetry; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +/// +/// It is recommended to use a custom type to hold references for +/// ActivitySource and Instruments. This avoids possible type collisions +/// with other components in the DI container. +/// +public class Instrumentation : IInstrumentation, IDisposable +{ + public string ActivitySourceName => "AssociationRegistry"; + public string MeterName => "PublicApi"; + private readonly Meter meter; + + public Instrumentation() + { + var version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); + ActivitySource = new ActivitySource(ActivitySourceName, version); + meter = new Meter(MeterName, version); + + PubliekVerenigingDetailHistogram = meter.CreateHistogram(name: "ar.p.publiekVerenigingDetail.h", unit: "events", + description: "publiek vereniging detail histogram"); + + PubliekVerenigingZoekenHistogram = + meter.CreateHistogram(name: "ar.p.publiekVerenigingZoeken.h", unit: "events", description: "publiek vereniging zoeken histogram"); + } + + public ActivitySource ActivitySource { get; } + public Histogram PubliekVerenigingDetailHistogram { get; } + public Histogram PubliekVerenigingZoekenHistogram { get; } + + public void Dispose() + { + ActivitySource.Dispose(); + meter.Dispose(); + } +} diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs index b530d04d3..e4901abd1 100644 --- a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs +++ b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs @@ -9,6 +9,7 @@ namespace AssociationRegistry.Public.ProjectionHost.Infrastructure.Program.WebAp using Marten.Events.Daemon.Resiliency; using Marten.Events.Projections; using Marten.Services; +using Metrics; using Newtonsoft.Json; using Projections.Detail; using Projections.Search; @@ -86,6 +87,8 @@ static JsonNetSerializer CreateCustomMartenSerializer() ProjectionLifecycle.Async, projectionName: "PubliekVerenigingZoekenDocument"); + opts.Projections.AsyncListeners.Add(new HighWatermarkListener(serviceProvider.GetRequiredService())); + opts.Serializer(CreateCustomMartenSerializer()); opts.RegisterDocumentType(); diff --git a/src/AssociationRegistry.Public.ProjectionHost/Program.cs b/src/AssociationRegistry.Public.ProjectionHost/Program.cs index c7986fba7..f3337ae64 100644 --- a/src/AssociationRegistry.Public.ProjectionHost/Program.cs +++ b/src/AssociationRegistry.Public.ProjectionHost/Program.cs @@ -2,6 +2,7 @@ namespace AssociationRegistry.Public.ProjectionHost; using Be.Vlaanderen.Basisregisters.Aws.DistributedMutex; using Infrastructure.Json; +using Infrastructure.Metrics; using Infrastructure.Program; using Infrastructure.Program.WebApplication; using Infrastructure.Program.WebApplicationBuilder; @@ -63,7 +64,7 @@ public static async Task Main(string[] args) builder.Services .ConfigureRequestLocalization() - .AddOpenTelemetry() + .AddOpenTelemetry(new Instrumentation()) .ConfigureProjectionsWithMarten(builder.Configuration) .ConfigureSwagger() .ConfigureElasticSearch(elasticSearchOptions) From f764df86b6d91d74edcaa08f082b8f63d3ae6b4e Mon Sep 17 00:00:00 2001 From: Quinten Van Assche Date: Thu, 14 Dec 2023 10:52:06 +0100 Subject: [PATCH 5/5] feat: or-1834-metrics --- .gitignore | 2 - AssociationRegistry.sln | 2 +- .../Extensions/StoreOptionsExtensions.cs | 2 +- ...strumentation.cs => AcmInstrumentation.cs} | 16 +- .../Metrics/HighWatermarkListener.cs | 43 ----- .../Metrics/ProjectionStateListener.cs | 76 +++++++++ src/AssociationRegistry.Acm.Api/Program.cs | 2 +- .../Metrics/HighWatermarkListener.cs | 31 +--- .../Infrastructure/Metrics/Instrumentation.cs | 6 +- .../Metrics/AdminInstrumentation.cs | 45 +++++ .../Metrics/ProjectionStateListener.cs | 82 +++++++++ .../ConfigureMartenExtensions.cs | 4 + .../Program.cs | 6 +- .../Extensions/ServiceCollectionExtensions.cs | 158 ++++++++++++------ .../Metrics/HighWatermarkListener.cs | 51 ------ .../Infrastructure/Metrics/Instrumentation.cs | 40 ----- .../Metrics/ProjectionStateListener.cs | 78 +++++++++ .../Metrics/PubliekInstrumentation.cs | 45 +++++ .../ConfigureMartenExtensions.cs | 2 +- .../Program.cs | 4 +- 20 files changed, 467 insertions(+), 228 deletions(-) rename src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/{Instrumentation.cs => AcmInstrumentation.cs} (56%) delete mode 100644 src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs create mode 100644 src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/ProjectionStateListener.cs create mode 100644 src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/AdminInstrumentation.cs create mode 100644 src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs delete mode 100644 src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs delete mode 100644 src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs create mode 100644 src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs create mode 100644 src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/PubliekInstrumentation.cs diff --git a/.gitignore b/.gitignore index ca0c9af4d..2eea6097e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ wwwroot/ otel-collector-config.yaml src/*/Internal -otel-collector-config.yaml - .vs/ .vscode/ .fake/ diff --git a/AssociationRegistry.sln b/AssociationRegistry.sln index e7bd5b638..39214a2c5 100644 --- a/AssociationRegistry.sln +++ b/AssociationRegistry.sln @@ -20,8 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Solution Items", ".Solutio definition_of_done.md = definition_of_done.md readme.md = readme.md CONTRIBUTING.md = CONTRIBUTING.md - otel-collector-config.yaml = otel-collector-config.yaml regen-marten.sh = regen-marten.sh + otel-collector-config.yaml = otel-collector-config.yaml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C812C887-6495-405A-8B99-4F686D243126}" diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs index c4c3691f9..ac3b4c796 100644 --- a/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Extensions/StoreOptionsExtensions.cs @@ -12,6 +12,6 @@ public static class StoreOptionsExtensions public static void AddPostgresProjections(this StoreOptions source, IServiceProvider serviceProvider) { source.Projections.Add(new VerenigingenPerInszProjection(), ProjectionLifecycle.Async); - source.Projections.AsyncListeners.Add(new HighWatermarkListener(serviceProvider.GetRequiredService())); + source.Projections.AsyncListeners.Add(new ProjectionStateListener(serviceProvider.GetRequiredService())); } } diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/Instrumentation.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/AcmInstrumentation.cs similarity index 56% rename from src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/Instrumentation.cs rename to src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/AcmInstrumentation.cs index 14b28ac48..efdf47807 100644 --- a/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/Instrumentation.cs +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/AcmInstrumentation.cs @@ -10,24 +10,28 @@ namespace AssociationRegistry.Acm.Api.Infrastructure.Metrics; /// ActivitySource and Instruments. This avoids possible type collisions /// with other components in the DI container. /// -public class Instrumentation : IInstrumentation, IDisposable +public class AcmInstrumentation : IInstrumentation, IDisposable { public string ActivitySourceName => "AssociationRegistry"; public string MeterName => "AcmProjections"; private readonly Meter meter; - public Instrumentation() + public AcmInstrumentation() { - var version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); + var version = typeof(AcmInstrumentation).Assembly.GetName().Version?.ToString(); ActivitySource = new ActivitySource(ActivitySourceName, version); meter = new Meter(MeterName, version); - VerenigingPerInszHistogram = - meter.CreateHistogram(name: "ar.p.verenigingPerInsz.h", unit: "events", description: "vereniging per insz projection"); + _verenigingPerInszGauge = + meter.CreateObservableGauge(name: "ar.acm.p.verenigingPerInsz.g", unit: "events", + description: "vereniging per insz projection", + observeValue: () => VerenigingPerInszEventValue); } public ActivitySource ActivitySource { get; } - public Histogram VerenigingPerInszHistogram { get; } + private ObservableGauge _verenigingPerInszGauge; + + public long VerenigingPerInszEventValue = 0; public void Dispose() { diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs deleted file mode 100644 index 5fee10028..000000000 --- a/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/HighWatermarkListener.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace AssociationRegistry.Acm.Api.Infrastructure.Metrics; - -using Marten; -using Marten.Services; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -public class HighWatermarkListener : IDocumentSessionListener -{ - private readonly Instrumentation _instrumentation; - - public HighWatermarkListener(Instrumentation instrumentation) - { - _instrumentation = instrumentation; - } - - public void BeforeSaveChanges(IDocumentSession session) - { - } - - public async Task BeforeSaveChangesAsync(IDocumentSession session, CancellationToken token) - { - } - - public void AfterCommit(IDocumentSession session, IChangeSet commit) - { - } - - public void DocumentLoaded(object id, object document) - { - } - - public void DocumentAddedForStorage(object id, object document) - { - } - - public async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) - { - var highWatermark = commit.GetEvents().Max(x => x.Sequence); - _instrumentation.VerenigingPerInszHistogram.Record(highWatermark); - } -} diff --git a/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/ProjectionStateListener.cs b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/ProjectionStateListener.cs new file mode 100644 index 000000000..6c80a59c5 --- /dev/null +++ b/src/AssociationRegistry.Acm.Api/Infrastructure/Metrics/ProjectionStateListener.cs @@ -0,0 +1,76 @@ +namespace AssociationRegistry.Acm.Api.Infrastructure.Metrics; + +using Marten; +using Marten.Events.Daemon; +using Marten.Internal.Operations; +using Marten.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using Projections; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +public class ProjectionStateListener : DocumentSessionListenerBase +{ + private readonly AcmInstrumentation _acmInstrumentation; + + public ProjectionStateListener(AcmInstrumentation acmInstrumentation) + { + _acmInstrumentation = acmInstrumentation; + } + + public override Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) + { + if (commit is not IUnitOfWork uow) return Task.CompletedTask; + var operations = uow.OperationsFor(); + + foreach (var operation in operations) + { + var range = GetEventRange(operation); + + if (range is null) continue; + + if (range.ShardName.ProjectionName == + typeof(VerenigingenPerInszProjection).FullName) + _acmInstrumentation.VerenigingPerInszEventValue =range.SequenceCeiling; + } + + return Task.CompletedTask; + } + + private static EventRange? GetEventRange(IStorageOperation opperation) + { + return opperation.GetType().Name switch + { + "InsertProjectionProgress" => opperation.GetType().GetField("_progress", BindingFlags.NonPublic|BindingFlags.Instance).GetValue(opperation) as EventRange, + "UpdateProjectionProgress" => opperation.GetType().GetProperty("Range").GetValue(opperation) as EventRange, + _ => null + }; + } +} + +public class AllFieldsContractResolver : DefaultContractResolver +{ + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + var props = type + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(p => base.CreateProperty(p, memberSerialization)) + .Union(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(f => base.CreateProperty(f, memberSerialization))) + .ToList(); + + props.ForEach(p => + { + p.Writable = true; + p.Readable = true; + }); + + return props; + } +} diff --git a/src/AssociationRegistry.Acm.Api/Program.cs b/src/AssociationRegistry.Acm.Api/Program.cs index ddabf22ce..674b0bd42 100755 --- a/src/AssociationRegistry.Acm.Api/Program.cs +++ b/src/AssociationRegistry.Acm.Api/Program.cs @@ -251,7 +251,7 @@ private static void ConfigureServices(WebApplicationBuilder builder) builder.Services .AddSingleton(appSettings) .AddMarten(postgreSqlOptionsSection, builder.Configuration) - .AddOpenTelemetry(new Instrumentation()) + .AddOpenTelemetry(new AcmInstrumentation()) .AddHttpContextAccessor() .AddControllers(); diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/HighWatermarkListener.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/HighWatermarkListener.cs index b664bcbae..b5277fe9d 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/HighWatermarkListener.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/HighWatermarkListener.cs @@ -6,7 +6,7 @@ namespace AssociationRegistry.Admin.Api.Infrastructure.Metrics; using System.Threading; using System.Threading.Tasks; -public class HighWatermarkListener : IDocumentSessionListener +public class HighWatermarkListener : DocumentSessionListenerBase { private readonly Instrumentation _instrumentation; @@ -15,29 +15,14 @@ public HighWatermarkListener(Instrumentation instrumentation) _instrumentation = instrumentation; } - public void BeforeSaveChanges(IDocumentSession session) + public override async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) { - } - - public async Task BeforeSaveChangesAsync(IDocumentSession session, CancellationToken token) - { - } - - public void AfterCommit(IDocumentSession session, IChangeSet commit) - { - } + var events = commit.GetEvents().ToArray(); - public void DocumentLoaded(object id, object document) - { - } - - public void DocumentAddedForStorage(object id, object document) - { - } - - public async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) - { - var highWatermark = commit.GetEvents().Max(x => x.Sequence); - _instrumentation.HighWaterMarkHistogram.Record(highWatermark); + if (events.Any()) + { + var highWatermark = events.Max(x => x.Sequence); + _instrumentation.HighWatermarkEventValue = highWatermark; + } } } diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/Instrumentation.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/Instrumentation.cs index 4400a7e7e..874c8e46a 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/Instrumentation.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Metrics/Instrumentation.cs @@ -22,11 +22,13 @@ public Instrumentation() ActivitySource = new ActivitySource(ActivitySourceName, version); meter = new Meter(MeterName, version); - HighWaterMarkHistogram = meter.CreateHistogram(name: "ar.highWatermark.h", unit: "events", description: "high watermark"); + _highWatermarkGauge = meter.CreateObservableGauge(name: "ar.beheer.p.highWatermark.g", unit: "events", + description: "high watermark", observeValue: () => HighWatermarkEventValue); } public ActivitySource ActivitySource { get; } - public Histogram HighWaterMarkHistogram { get; } + private ObservableGauge _highWatermarkGauge; + public long HighWatermarkEventValue = 0; public void Dispose() { diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/AdminInstrumentation.cs b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/AdminInstrumentation.cs new file mode 100644 index 000000000..28a06582d --- /dev/null +++ b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/AdminInstrumentation.cs @@ -0,0 +1,45 @@ +namespace AssociationRegistry.Admin.ProjectionHost.Infrastructure.Metrics; + +using AssociationRegistry.OpenTelemetry; +using System; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +/// +/// It is recommended to use a custom type to hold references for +/// ActivitySource and Instruments. This avoids possible type collisions +/// with other components in the DI container. +/// +public class AdminInstrumentation : IInstrumentation, IDisposable +{ + public string ActivitySourceName => "AssociationRegistry"; + public string MeterName => "AdminProjections"; + private readonly Meter meter; + + public AdminInstrumentation() + { + var version = typeof(AdminInstrumentation).Assembly.GetName().Version?.ToString(); + ActivitySource = new ActivitySource(ActivitySourceName, version); + meter = new Meter(MeterName, version); + + _verenigingZoeken = meter.CreateObservableGauge(name: "ar.beheer.p.zoeken.g", unit: "events", description: "Beheer zoeken projection", observeValue:() => VerenigingZoekenEventValue); + _verenigingDetail = meter.CreateObservableGauge(name: "ar.beheer.p.detail.g", unit: "events", description: "Beheer detail projection", observeValue:() => VerenigingDetailEventValue); + _historiek = meter.CreateObservableGauge(name: "ar.beheer.p.historiek.g", unit: "events", description: "Beheer detail projection", observeValue:() => VerenigingHistoriekEventValue); + } + public ActivitySource ActivitySource { get; } + + private ObservableGauge _verenigingZoeken; + public long VerenigingZoekenEventValue { get; set; } + private ObservableGauge _verenigingDetail; + public long VerenigingDetailEventValue { get; set; } + + private ObservableGauge _historiek; + public long VerenigingHistoriekEventValue { get; set; } + + + public void Dispose() + { + ActivitySource.Dispose(); + meter.Dispose(); + } +} diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs new file mode 100644 index 000000000..9c3360358 --- /dev/null +++ b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs @@ -0,0 +1,82 @@ +namespace AssociationRegistry.Admin.ProjectionHost.Infrastructure.Metrics; + +using Marten; +using Marten.Events.Daemon; +using Marten.Internal.Operations; +using Marten.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Projections.Detail; +using Projections.Historiek; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +public class ProjectionStateListener : DocumentSessionListenerBase +{ + private readonly AdminInstrumentation _adminInstrumentation; + + public ProjectionStateListener(AdminInstrumentation adminInstrumentation) + { + _adminInstrumentation = adminInstrumentation; + } + + public override Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) + { + if (commit is not IUnitOfWork uow) return Task.CompletedTask; + var operations = uow.OperationsFor(); + + foreach (var operation in operations) + { + var range = GetEventRange(operation); + + if (range is null) continue; + + if (range.ShardName.ProjectionName == "BeheerVerenigingZoekenDocument") + _adminInstrumentation.VerenigingZoekenEventValue = range.SequenceCeiling; + + if (range.ShardName.ProjectionName == typeof(BeheerVerenigingDetailProjection).FullName) + _adminInstrumentation.VerenigingDetailEventValue = range.SequenceCeiling; + + if (range.ShardName.ProjectionName == typeof(BeheerVerenigingHistoriekProjection).FullName) + _adminInstrumentation.VerenigingHistoriekEventValue = range.SequenceCeiling; + } + + return Task.CompletedTask; + } + + private static EventRange? GetEventRange(IStorageOperation opperation) + { + return opperation.GetType().Name switch + { + "InsertProjectionProgress" => opperation.GetType().GetField("_progress", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(opperation) as EventRange, + "UpdateProjectionProgress" => opperation.GetType().GetProperty("Range").GetValue(opperation) as EventRange, + _ => null + }; + } +} + +public class AllFieldsContractResolver : DefaultContractResolver +{ + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + var props = type + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(p => base.CreateProperty(p, memberSerialization)) + .Union(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(f => base.CreateProperty(f, memberSerialization))) + .ToList(); + + props.ForEach(p => + { + p.Writable = true; + p.Readable = true; + }); + + return props; + } +} diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs index 581287fa5..f76c8ff44 100644 --- a/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs +++ b/src/AssociationRegistry.Admin.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs @@ -9,6 +9,7 @@ namespace AssociationRegistry.Admin.ProjectionHost.Infrastructure.Program.WebApp using Marten.Events.Daemon.Resiliency; using Marten.Events.Projections; using Marten.Services; +using Metrics; using Newtonsoft.Json; using Projections.Detail; using Projections.Historiek; @@ -80,6 +81,9 @@ static JsonNetSerializer CreateCustomMartenSerializer() opts.Projections.OnException(_ => true).Stop(); + opts.Projections.AsyncListeners.Add( + new ProjectionStateListener(serviceProvider.GetRequiredService())); + opts.Projections.Add(ProjectionLifecycle.Async); opts.Projections.Add(ProjectionLifecycle.Async); diff --git a/src/AssociationRegistry.Admin.ProjectionHost/Program.cs b/src/AssociationRegistry.Admin.ProjectionHost/Program.cs index 465ec8981..aaf6da913 100644 --- a/src/AssociationRegistry.Admin.ProjectionHost/Program.cs +++ b/src/AssociationRegistry.Admin.ProjectionHost/Program.cs @@ -2,6 +2,7 @@ namespace AssociationRegistry.Admin.ProjectionHost; using Be.Vlaanderen.Basisregisters.Aws.DistributedMutex; using Infrastructure.Json; +using Infrastructure.Metrics; using Infrastructure.Program; using Infrastructure.Program.WebApplication; using Infrastructure.Program.WebApplicationBuilder; @@ -31,7 +32,8 @@ public static async Task Main(string[] args) builder.Configuration .AddJsonFile("appsettings.json") - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName.ToLowerInvariant()}.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName.ToLowerInvariant()}.json", optional: true, + reloadOnChange: false) .AddJsonFile($"appsettings.{Environment.MachineName.ToLowerInvariant()}.json", optional: true, reloadOnChange: false) .AddEnvironmentVariables() .AddCommandLine(args); @@ -60,7 +62,7 @@ public static async Task Main(string[] args) builder.Services .ConfigureRequestLocalization() - .AddOpenTelemetry() + .AddOpenTelemetry(new AdminInstrumentation()) .ConfigureProjectionsWithMarten(builder.Configuration) .ConfigureSwagger() .ConfigureElasticSearch(elasticSearchOptions) diff --git a/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs b/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs index fed196480..5c90c51ac 100644 --- a/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs +++ b/src/AssociationRegistry.OpenTelemetry/Extensions/ServiceCollectionExtensions.cs @@ -13,53 +13,73 @@ public static class ServiceCollectionExtensions { - public static IServiceCollection AddOpenTelemetry(this IServiceCollection services, IInstrumentation? instrumentation = null) + public static IServiceCollection AddOpenTelemetry(this IServiceCollection services) { - var executingAssembly = Assembly.GetEntryAssembly()!; - var serviceName = executingAssembly.GetName().Name!; - var assemblyVersion = executingAssembly.GetName().Version?.ToString() ?? "unknown"; - var collectorUrl = Environment.GetEnvironmentVariable("COLLECTOR_URL") ?? "http://localhost:4317"; + var (serviceName, collectorUrl, configureResource) = GetResources(); - if (instrumentation is not null) - services.AddSingleton(instrumentation); + return services.AddTracking(configureResource, collectorUrl, serviceName) + .AddLogging(configureResource, collectorUrl) + .AddMetrics(configureResource, collectorUrl); + } - Action configureResource = r => r - .AddService( - serviceName, - serviceVersion: assemblyVersion, - serviceInstanceId: Environment.MachineName) - .AddAttributes( - new Dictionary - { - ["deployment.environment"] = - Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") - ?.ToLowerInvariant() - ?? "unknown", - }); + public static IServiceCollection AddOpenTelemetry(this IServiceCollection services, params T[] instrumentations) + where T : class, IInstrumentation + { + var (serviceName, collectorUrl, configureResource) = GetResources(); - services.AddOpenTelemetryTracing( - builder => - builder - .AddSource(serviceName) - .ConfigureResource(configureResource).AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation( - options => + foreach (var instrumentation in instrumentations) + { + services.AddSingleton(instrumentation); + } + + return services.AddTracking(configureResource, collectorUrl, serviceName) + .AddLogging(configureResource, collectorUrl) + .AddMetrics(configureResource, collectorUrl, builder => { - options.EnrichWithHttpRequest = - (activity, request) => activity.SetParentId(request.Headers["traceparent"]); + foreach (var instrumentation in instrumentations) + { + builder.AddMeter(instrumentation.MeterName); + } + }); + } - options.Filter = context => context.Request.Method != HttpMethods.Options; - }) - .AddNpgsql() + private static IServiceCollection AddMetrics( + this IServiceCollection services, + Action configureResource, + string collectorUrl, + Action? extraConfig = null) + { + return services.AddOpenTelemetryMetrics( + options => + { + options + .ConfigureResource(configureResource) + .AddRuntimeInstrumentation() + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation() .AddOtlpExporter( - options => + exporter => { - options.Protocol = OtlpExportProtocol.Grpc; - options.Endpoint = new Uri(collectorUrl); + exporter.Protocol = OtlpExportProtocol.Grpc; + exporter.Endpoint = new Uri(collectorUrl); }) - .AddSource("Wolverine")); + .InvokeIfNotNull(extraConfig); + }); + } - services.AddLogging( + private static T InvokeIfNotNull(this T obj, Action? method) + { + method?.Invoke(obj); + + return obj; + } + + private static IServiceCollection AddLogging( + this IServiceCollection services, + Action configureResource, + string collectorUrl) + { + return services.AddLogging( builder => builder .AddOpenTelemetry( @@ -78,26 +98,58 @@ public static IServiceCollection AddOpenTelemetry(this IServiceCollection servic exporter.Endpoint = new Uri(collectorUrl); }); })); + } - services.AddOpenTelemetryMetrics( - options => - { - options - .ConfigureResource(configureResource) - .AddRuntimeInstrumentation() - .AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation() + private static IServiceCollection AddTracking( + this IServiceCollection services, + Action configureResource, + string collectorUrl, + string serviceName) + { + return services.AddOpenTelemetryTracing( + builder => + builder + .AddSource(serviceName) + .ConfigureResource(configureResource).AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation( + options => + { + options.EnrichWithHttpRequest = + (activity, request) => activity.SetParentId(request.Headers["traceparent"]); + + options.Filter = context => context.Request.Method != HttpMethods.Options; + }) + .AddNpgsql() .AddOtlpExporter( - exporter => + options => { - exporter.Protocol = OtlpExportProtocol.Grpc; - exporter.Endpoint = new Uri(collectorUrl); - }); + options.Protocol = OtlpExportProtocol.Grpc; + options.Endpoint = new Uri(collectorUrl); + }) + .AddSource("Wolverine")); + } - if (instrumentation is not null) - options.AddMeter(instrumentation.MeterName); - }); + private static (string serviceName, string collectorUrl, Action configureResource) GetResources() + { + var executingAssembly = Assembly.GetEntryAssembly()!; + var serviceName = executingAssembly.GetName().Name!; + var assemblyVersion = executingAssembly.GetName().Version?.ToString() ?? "unknown"; + var collectorUrl = Environment.GetEnvironmentVariable("COLLECTOR_URL") ?? "http://localhost:4317"; + + Action configureResource = r => r + .AddService( + serviceName, + serviceVersion: assemblyVersion, + serviceInstanceId: Environment.MachineName) + .AddAttributes( + new Dictionary + { + ["deployment.environment"] = + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + ?.ToLowerInvariant() + ?? "unknown", + }); - return services; + return (serviceName, collectorUrl, configureResource); } } diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs deleted file mode 100644 index 1c84c1390..000000000 --- a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/HighWatermarkListener.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace AssociationRegistry.Public.ProjectionHost.Infrastructure.Metrics; - -using Marten; -using Marten.Events.Daemon; -using Marten.Services; -using Projections.Detail; - -public class HighWatermarkListener : IDocumentSessionListener -{ - private readonly Instrumentation _instrumentation; - - public HighWatermarkListener(Instrumentation instrumentation) - { - _instrumentation = instrumentation; - } - - public void BeforeSaveChanges(IDocumentSession session) - { - } - - public async Task BeforeSaveChangesAsync(IDocumentSession session, CancellationToken token) - { - } - - public void AfterCommit(IDocumentSession session, IChangeSet commit) - { - } - - public void DocumentLoaded(object id, object document) - { - } - - public void DocumentAddedForStorage(object id, object document) - { - } - - public async Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) - { - var publiekDetailProgress = await session.DocumentStore.Advanced.ProjectionProgressFor( - new ShardName(typeof(PubliekVerenigingDetailProjection).Namespace), - token: token); - - _instrumentation.PubliekVerenigingDetailHistogram.Record(publiekDetailProgress); - - var publiekZoekenProgress = await session.DocumentStore.Advanced.ProjectionProgressFor( - new ShardName("PubliekVerenigingZoekenProjection"), - token: token); - - _instrumentation.PubliekVerenigingZoekenHistogram.Record(publiekZoekenProgress); - } -} diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs deleted file mode 100644 index a5719dfc5..000000000 --- a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/Instrumentation.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace AssociationRegistry.Public.ProjectionHost.Infrastructure.Metrics; - -using OpenTelemetry; -using System.Diagnostics; -using System.Diagnostics.Metrics; - -/// -/// It is recommended to use a custom type to hold references for -/// ActivitySource and Instruments. This avoids possible type collisions -/// with other components in the DI container. -/// -public class Instrumentation : IInstrumentation, IDisposable -{ - public string ActivitySourceName => "AssociationRegistry"; - public string MeterName => "PublicApi"; - private readonly Meter meter; - - public Instrumentation() - { - var version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); - ActivitySource = new ActivitySource(ActivitySourceName, version); - meter = new Meter(MeterName, version); - - PubliekVerenigingDetailHistogram = meter.CreateHistogram(name: "ar.p.publiekVerenigingDetail.h", unit: "events", - description: "publiek vereniging detail histogram"); - - PubliekVerenigingZoekenHistogram = - meter.CreateHistogram(name: "ar.p.publiekVerenigingZoeken.h", unit: "events", description: "publiek vereniging zoeken histogram"); - } - - public ActivitySource ActivitySource { get; } - public Histogram PubliekVerenigingDetailHistogram { get; } - public Histogram PubliekVerenigingZoekenHistogram { get; } - - public void Dispose() - { - ActivitySource.Dispose(); - meter.Dispose(); - } -} diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs new file mode 100644 index 000000000..8774683fe --- /dev/null +++ b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/ProjectionStateListener.cs @@ -0,0 +1,78 @@ +namespace AssociationRegistry.Public.ProjectionHost.Metrics; + +using Marten; +using Marten.Events.Daemon; +using Marten.Internal.Operations; +using Marten.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Projections.Detail; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +public class ProjectionStateListener : DocumentSessionListenerBase +{ + private readonly PubliekInstrumentation _publiekInstrumentation; + + public ProjectionStateListener(PubliekInstrumentation publiekInstrumentation) + { + _publiekInstrumentation = publiekInstrumentation; + } + + public override Task AfterCommitAsync(IDocumentSession session, IChangeSet commit, CancellationToken token) + { + if (commit is not IUnitOfWork uow) return Task.CompletedTask; + var operations = uow.OperationsFor(); + + foreach (var operation in operations) + { + var range = GetEventRange(operation); + + if (range is null) continue; + + if (range.ShardName.ProjectionName == "PubliekVerenigingZoekenDocument") + _publiekInstrumentation.VerenigingZoekenEventValue = range.SequenceCeiling; + + if (range.ShardName.ProjectionName == typeof(PubliekVerenigingDetailProjection).FullName) + _publiekInstrumentation.VerenigingDetailEventValue = range.SequenceCeiling; + } + + return Task.CompletedTask; + } + + private static EventRange? GetEventRange(IStorageOperation opperation) + { + return opperation.GetType().Name switch + { + "InsertProjectionProgress" => opperation.GetType().GetField("_progress", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(opperation) as EventRange, + "UpdateProjectionProgress" => opperation.GetType().GetProperty("Range").GetValue(opperation) as EventRange, + _ => null, + }; + } +} + +public class AllFieldsContractResolver : DefaultContractResolver +{ + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + var props = type + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(p => base.CreateProperty(p, memberSerialization)) + .Union(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(f => base.CreateProperty(f, memberSerialization))) + .ToList(); + + props.ForEach(p => + { + p.Writable = true; + p.Readable = true; + }); + + return props; + } +} diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/PubliekInstrumentation.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/PubliekInstrumentation.cs new file mode 100644 index 000000000..e81c2b222 --- /dev/null +++ b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Metrics/PubliekInstrumentation.cs @@ -0,0 +1,45 @@ +namespace AssociationRegistry.Public.ProjectionHost.Metrics; + +using AssociationRegistry.OpenTelemetry; +using System; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +/// +/// It is recommended to use a custom type to hold references for +/// ActivitySource and Instruments. This avoids possible type collisions +/// with other components in the DI container. +/// +public class PubliekInstrumentation : IInstrumentation, IDisposable +{ + public string ActivitySourceName => "AssociationRegistry"; + public string MeterName => "PubliekProjections"; + private readonly Meter meter; + + public PubliekInstrumentation() + { + var version = typeof(PubliekInstrumentation).Assembly.GetName().Version?.ToString(); + ActivitySource = new ActivitySource(ActivitySourceName, version); + meter = new Meter(MeterName, version); + + _verenigingZoeken = meter.CreateObservableGauge(name: "ar.publiek.p.zoeken.g", unit: "events", + description: "Beheer zoeken projection", + observeValue: () => VerenigingZoekenEventValue); + + _verenigingDetail = meter.CreateObservableGauge(name: "ar.publiek.p.detail.g", unit: "events", + description: "Beheer detail projection", + observeValue: () => VerenigingDetailEventValue); + } + + private ObservableGauge _verenigingDetail; + public long VerenigingDetailEventValue { get; set; } + private ObservableGauge _verenigingZoeken; + public long VerenigingZoekenEventValue { get; set; } + public ActivitySource ActivitySource { get; } + + public void Dispose() + { + ActivitySource.Dispose(); + meter.Dispose(); + } +} diff --git a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs index e4901abd1..44fa3448e 100644 --- a/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs +++ b/src/AssociationRegistry.Public.ProjectionHost/Infrastructure/Program/WebApplicationBuilder/ConfigureMartenExtensions.cs @@ -87,7 +87,7 @@ static JsonNetSerializer CreateCustomMartenSerializer() ProjectionLifecycle.Async, projectionName: "PubliekVerenigingZoekenDocument"); - opts.Projections.AsyncListeners.Add(new HighWatermarkListener(serviceProvider.GetRequiredService())); + opts.Projections.AsyncListeners.Add(new ProjectionStateListener(serviceProvider.GetRequiredService())); opts.Serializer(CreateCustomMartenSerializer()); diff --git a/src/AssociationRegistry.Public.ProjectionHost/Program.cs b/src/AssociationRegistry.Public.ProjectionHost/Program.cs index f3337ae64..d69f6f048 100644 --- a/src/AssociationRegistry.Public.ProjectionHost/Program.cs +++ b/src/AssociationRegistry.Public.ProjectionHost/Program.cs @@ -2,12 +2,12 @@ namespace AssociationRegistry.Public.ProjectionHost; using Be.Vlaanderen.Basisregisters.Aws.DistributedMutex; using Infrastructure.Json; -using Infrastructure.Metrics; using Infrastructure.Program; using Infrastructure.Program.WebApplication; using Infrastructure.Program.WebApplicationBuilder; using JasperFx.CodeGeneration; using Marten; +using Metrics; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.NewtonsoftJson; @@ -64,7 +64,7 @@ public static async Task Main(string[] args) builder.Services .ConfigureRequestLocalization() - .AddOpenTelemetry(new Instrumentation()) + .AddOpenTelemetry(new PubliekInstrumentation()) .ConfigureProjectionsWithMarten(builder.Configuration) .ConfigureSwagger() .ConfigureElasticSearch(elasticSearchOptions)