diff --git a/CrowdParlay.Social.sln b/CrowdParlay.Social.sln
index f2bd8ae..28e73ef 100644
--- a/CrowdParlay.Social.sln
+++ b/CrowdParlay.Social.sln
@@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Social.Infrastr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Social.UnitTests", "tests\CrowdParlay.Social.UnitTests\CrowdParlay.Social.UnitTests.csproj", "{8F95AF80-A278-49DB-97EB-F8DB96F083D5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Social.Aspects", "src\CrowdParlay.Social.Aspects\CrowdParlay.Social.Aspects.csproj", "{4DBC1701-9141-4AC2-80C1-CA9050FF7F0B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -52,6 +54,10 @@ Global
{8F95AF80-A278-49DB-97EB-F8DB96F083D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F95AF80-A278-49DB-97EB-F8DB96F083D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F95AF80-A278-49DB-97EB-F8DB96F083D5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4DBC1701-9141-4AC2-80C1-CA9050FF7F0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4DBC1701-9141-4AC2-80C1-CA9050FF7F0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4DBC1701-9141-4AC2-80C1-CA9050FF7F0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4DBC1701-9141-4AC2-80C1-CA9050FF7F0B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1C0DE505-4898-4FB4-A5AA-C01630770F79} = {340CE817-E3FB-4753-94AA-E5BFB13E6F6A}
@@ -61,5 +67,6 @@ Global
{640B084C-59F5-448D-845A-7D6A8E1A7F99} = {340CE817-E3FB-4753-94AA-E5BFB13E6F6A}
{47776236-58AD-449A-B3BF-6ED960C3D12C} = {340CE817-E3FB-4753-94AA-E5BFB13E6F6A}
{8F95AF80-A278-49DB-97EB-F8DB96F083D5} = {00C5EC71-2662-4897-8C51-8C1355111519}
+ {4DBC1701-9141-4AC2-80C1-CA9050FF7F0B} = {340CE817-E3FB-4753-94AA-E5BFB13E6F6A}
EndGlobalSection
EndGlobal
diff --git a/src/CrowdParlay.Social.Api/CrowdParlay.Social.Api.csproj b/src/CrowdParlay.Social.Api/CrowdParlay.Social.Api.csproj
index 5d39314..6a447cd 100644
--- a/src/CrowdParlay.Social.Api/CrowdParlay.Social.Api.csproj
+++ b/src/CrowdParlay.Social.Api/CrowdParlay.Social.Api.csproj
@@ -10,17 +10,13 @@
-
-
-
-
-
+
diff --git a/src/CrowdParlay.Social.Api/Extensions/ConfigureAuthenticationExtensions.cs b/src/CrowdParlay.Social.Api/Extensions/ConfigureAuthenticationExtensions.cs
index 85233b8..4817a47 100644
--- a/src/CrowdParlay.Social.Api/Extensions/ConfigureAuthenticationExtensions.cs
+++ b/src/CrowdParlay.Social.Api/Extensions/ConfigureAuthenticationExtensions.cs
@@ -7,9 +7,9 @@
namespace CrowdParlay.Social.Api.Extensions;
-public static class ConfigureAuthenticationExtensions
+partial class ServiceCollectionExtensions
{
- public static IServiceCollection ConfigureAuthentication(this IServiceCollection services, IConfiguration configuration)
+ private static IServiceCollection ConfigureAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var dataProtectionRedisConnectionString = configuration["DATA_PROTECTION_REDIS_CONNECTION_STRING"]!;
var dataProtectionRedisMultiplexer = ConnectionMultiplexer.Connect(dataProtectionRedisConnectionString);
diff --git a/src/CrowdParlay.Social.Api/Extensions/ConfigureCorsExtensions.cs b/src/CrowdParlay.Social.Api/Extensions/ConfigureCorsExtensions.cs
index cc52142..b5a7f19 100644
--- a/src/CrowdParlay.Social.Api/Extensions/ConfigureCorsExtensions.cs
+++ b/src/CrowdParlay.Social.Api/Extensions/ConfigureCorsExtensions.cs
@@ -1,8 +1,8 @@
namespace CrowdParlay.Social.Api.Extensions;
-public static class ConfigureCorsExtensions
+partial class ServiceCollectionExtensions
{
- public static IServiceCollection ConfigureCors(this IServiceCollection services, IConfiguration configuration)
+ private static IServiceCollection ConfigureCors(this IServiceCollection services, IConfiguration configuration)
{
var corsOrigins =
configuration["CORS_ORIGINS"]?.Split(';')
diff --git a/src/CrowdParlay.Social.Api/Extensions/ConfigureEndpointsExtensions.cs b/src/CrowdParlay.Social.Api/Extensions/ConfigureEndpointsExtensions.cs
index 1b39143..a1f3566 100644
--- a/src/CrowdParlay.Social.Api/Extensions/ConfigureEndpointsExtensions.cs
+++ b/src/CrowdParlay.Social.Api/Extensions/ConfigureEndpointsExtensions.cs
@@ -3,9 +3,9 @@
namespace CrowdParlay.Social.Api.Extensions;
-public static class ConfigureEndpointsExtensions
+partial class ServiceCollectionExtensions
{
- public static IServiceCollection ConfigureEndpoints(this IServiceCollection services)
+ internal static IServiceCollection ConfigureEndpoints(this IServiceCollection services)
{
var mvcBuilder = services.AddControllers(options =>
{
diff --git a/src/CrowdParlay.Social.Api/Extensions/ConfigureServices.cs b/src/CrowdParlay.Social.Api/Extensions/ConfigureServices.cs
index b7898fc..faf33d4 100644
--- a/src/CrowdParlay.Social.Api/Extensions/ConfigureServices.cs
+++ b/src/CrowdParlay.Social.Api/Extensions/ConfigureServices.cs
@@ -4,7 +4,7 @@
namespace CrowdParlay.Social.Api.Extensions;
-public static class ConfigureApiExtensions
+public static partial class ServiceCollectionExtensions
{
public static IServiceCollection AddApi(this IServiceCollection services, IConfiguration configuration)
{
@@ -13,7 +13,6 @@ public static IServiceCollection AddApi(this IServiceCollection services, IConfi
.ConfigureAuthentication(configuration)
.ConfigureCors(configuration)
.ConfigureSignalR(configuration)
- .ConfigureTelemetry(configuration)
.AddExceptionHandler()
.AddProblemDetails()
.AddAuthorization();
diff --git a/src/CrowdParlay.Social.Api/Extensions/ConfigureSignalRExtensions.cs b/src/CrowdParlay.Social.Api/Extensions/ConfigureSignalRExtensions.cs
index 80fadc6..de07674 100644
--- a/src/CrowdParlay.Social.Api/Extensions/ConfigureSignalRExtensions.cs
+++ b/src/CrowdParlay.Social.Api/Extensions/ConfigureSignalRExtensions.cs
@@ -1,8 +1,8 @@
namespace CrowdParlay.Social.Api.Extensions;
-public static class ConfigureSignalRExtensions
+partial class ServiceCollectionExtensions
{
- public static IServiceCollection ConfigureSignalR(this IServiceCollection services, IConfiguration configuration)
+ private static IServiceCollection ConfigureSignalR(this IServiceCollection services, IConfiguration configuration)
{
var redisConnectionString =
configuration["REDIS_CONNECTION_STRING"]
diff --git a/src/CrowdParlay.Social.Api/Extensions/ConfigureTelemetryExtensions.cs b/src/CrowdParlay.Social.Api/Extensions/ConfigureTelemetryExtensions.cs
deleted file mode 100644
index c1fb9d6..0000000
--- a/src/CrowdParlay.Social.Api/Extensions/ConfigureTelemetryExtensions.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using OpenTelemetry.Trace;
-
-namespace CrowdParlay.Social.Api.Extensions;
-
-public static class ConfigureTelemetryExtensions
-{
- public static IServiceCollection ConfigureTelemetry(this IServiceCollection services, IConfiguration configuration)
- {
- var otlpExporterEndpoint =
- configuration["TELEMETRY_OTLP_EXPORTER_ENDPOINT"]
- ?? throw new InvalidOperationException("Missing required configuration 'CORS_ORIGINS'.");
-
- var telemetrySourceName =
- configuration["TELEMETRY_SOURCE_NAME"]
- ?? throw new InvalidOperationException("Missing required configuration 'TELEMETRY_SOURCE_NAME'.");
-
- var openTelemetryBuilder = services.AddOpenTelemetry();
-
- openTelemetryBuilder.WithMetrics(metrics => metrics
- .AddMeter(telemetrySourceName)
- .AddMeter("Microsoft.AspNetCore.Hosting")
- .AddMeter("Microsoft.AspNetCore.Server.Kestrel"));
-
- openTelemetryBuilder.WithTracing(builder => builder
- .AddSource(telemetrySourceName)
- .AddOtlpExporter(options => options.Endpoint = new Uri(otlpExporterEndpoint))
- .AddAspNetCoreInstrumentation()
- .AddGrpcClientInstrumentation());
-
- return services;
- }
-}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Api/Program.cs b/src/CrowdParlay.Social.Api/Program.cs
index f4fb2d4..3b06ac8 100644
--- a/src/CrowdParlay.Social.Api/Program.cs
+++ b/src/CrowdParlay.Social.Api/Program.cs
@@ -1,4 +1,7 @@
+using CrowdParlay.Social.Infrastructure.Communication;
using Serilog;
+using Serilog.Enrichers.OpenTelemetry;
+using Serilog.Sinks.OpenTelemetry;
namespace CrowdParlay.Social.Api;
@@ -8,5 +11,13 @@ public class Program
private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder => builder.UseStartup())
- .UseSerilog();
+ .UseSerilog((context, logger) => logger
+ .ReadFrom.Configuration(context.Configuration)
+ .Enrich.WithOpenTelemetrySpanId()
+ .Enrich.WithOpenTelemetryTraceId()
+ .WriteTo.OpenTelemetry(sink =>
+ {
+ sink.Endpoint = context.Configuration.GetRequiredSection("OpenTelemetry").Get()?.OtlpEndpoint;
+ sink.Protocol = OtlpProtocol.HttpProtobuf;
+ }));
}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Api/Startup.cs b/src/CrowdParlay.Social.Api/Startup.cs
index 0efc47a..d462371 100644
--- a/src/CrowdParlay.Social.Api/Startup.cs
+++ b/src/CrowdParlay.Social.Api/Startup.cs
@@ -1,12 +1,10 @@
using CrowdParlay.Social.Api.Extensions;
using CrowdParlay.Social.Api.Hubs;
-using CrowdParlay.Social.Application;
-using CrowdParlay.Social.Infrastructure.Communication;
-using CrowdParlay.Social.Infrastructure.Persistence;
-using Elastic.Apm.NetCoreAll;
+using CrowdParlay.Social.Application.Extensions;
+using CrowdParlay.Social.Infrastructure.Communication.Extensions;
+using CrowdParlay.Social.Infrastructure.Persistence.Extensions;
using Microsoft.AspNetCore.Http.Connections;
using Serilog;
-using Serilog.Enrichers.OpenTelemetry;
namespace CrowdParlay.Social.Api;
@@ -14,13 +12,6 @@ public class Startup(IConfiguration configuration)
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
- Log.Logger = new LoggerConfiguration()
- .ReadFrom.Configuration(configuration)
- .Enrich.WithOpenTelemetrySpanId()
- .Enrich.WithOpenTelemetryTraceId()
- .CreateLogger();
-
- app.UseAllElasticApm(configuration);
app.UseExceptionHandler();
app.UseSerilogRequestLogging();
app.UseHealthChecks("/healthz");
@@ -39,6 +30,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
public void ConfigureServices(IServiceCollection services) => services
.AddApi(configuration)
.AddApplication()
- .AddPersistence(configuration)
+ .AddPersistence()
.AddCommunication(configuration);
}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Application/CrowdParlay.Social.Application.csproj b/src/CrowdParlay.Social.Application/CrowdParlay.Social.Application.csproj
index b08ee04..92b82d0 100644
--- a/src/CrowdParlay.Social.Application/CrowdParlay.Social.Application.csproj
+++ b/src/CrowdParlay.Social.Application/CrowdParlay.Social.Application.csproj
@@ -22,6 +22,7 @@
+
diff --git a/src/CrowdParlay.Social.Application/DTOs/UserDto.cs b/src/CrowdParlay.Social.Application/DTOs/UserDto.cs
index 80f97fe..ca9e636 100644
--- a/src/CrowdParlay.Social.Application/DTOs/UserDto.cs
+++ b/src/CrowdParlay.Social.Application/DTOs/UserDto.cs
@@ -3,7 +3,7 @@ namespace CrowdParlay.Social.Application.DTOs;
public class UserDto
{
public required Guid Id { get; set; }
- public required string Username { get; set; }
- public required string DisplayName { get; set; }
+ public required string? Username { get; set; }
+ public required string? DisplayName { get; set; }
public required string? AvatarUrl { get; set; }
}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Application/ConfigureServices.cs b/src/CrowdParlay.Social.Application/Extensions/ConfigureServices.cs
similarity index 83%
rename from src/CrowdParlay.Social.Application/ConfigureServices.cs
rename to src/CrowdParlay.Social.Application/Extensions/ConfigureServices.cs
index f7497e2..ddbda9a 100644
--- a/src/CrowdParlay.Social.Application/ConfigureServices.cs
+++ b/src/CrowdParlay.Social.Application/Extensions/ConfigureServices.cs
@@ -4,9 +4,9 @@
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
-namespace CrowdParlay.Social.Application;
+namespace CrowdParlay.Social.Application.Extensions;
-public static class ConfigureServicesExtensions
+public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplication(this IServiceCollection services) => services
.AddScoped()
diff --git a/src/CrowdParlay.Social.Application/Services/CommentsService.cs b/src/CrowdParlay.Social.Application/Services/CommentsService.cs
index 10f715a..c049b72 100644
--- a/src/CrowdParlay.Social.Application/Services/CommentsService.cs
+++ b/src/CrowdParlay.Social.Application/Services/CommentsService.cs
@@ -1,13 +1,18 @@
using System.Diagnostics;
using CrowdParlay.Social.Application.Abstractions;
using CrowdParlay.Social.Application.DTOs;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Social.Domain.Abstractions;
using CrowdParlay.Social.Domain.DTOs;
using CrowdParlay.Social.Domain.Entities;
using Mapster;
+using Metalama.Framework.Code;
+
+// ReSharper disable ExplicitCallerInfoArgument
namespace CrowdParlay.Social.Application.Services;
+[TraceMethods(Accessibility.Public, Accessibility.Private)]
public class CommentsService(
IUnitOfWorkFactory unitOfWorkFactory,
ICommentsRepository commentsRepository,
@@ -16,6 +21,7 @@ public class CommentsService(
: ICommentsService
{
private readonly ISubjectsService _subjectsService = new SubjectsService(commentsRepository);
+ private readonly ActivitySource _activitySource = new(typeof(CommentsService).FullName!);
public async Task GetByIdAsync(string commentId, Guid? viewerId)
{
@@ -47,18 +53,18 @@ public async Task ReplyToCommentAsync(string commentId, Guid au
private async Task CreateAsync(string subjectId, Guid authorId, string content)
{
- var source = new ActivitySource(nameof(CommentsService));
- using var activity = source.CreateActivity("Create comment", ActivityKind.Server);
-
Comment comment;
- using (var unitOfWork = await unitOfWorkFactory.CreateAsync())
+ using (_activitySource.StartActivity("Save and retrieve entity"))
{
- var commentId = await unitOfWork.CommentsRepository.CreateAsync(subjectId, authorId, content);
- comment = await unitOfWork.CommentsRepository.GetByIdAsync(commentId, authorId);
- await unitOfWork.CommitAsync();
+ using (var unitOfWork = await unitOfWorkFactory.CreateAsync())
+ {
+ var commentId = await unitOfWork.CommentsRepository.CreateAsync(subjectId, authorId, content);
+ comment = await unitOfWork.CommentsRepository.GetByIdAsync(commentId, authorId);
+ await unitOfWork.CommitAsync();
+ }
}
- using (source.CreateActivity("Update dependant metadata", ActivityKind.Server))
+ using (_activitySource.StartActivity("Update ancestors"))
{
var ancestors = await commentsRepository.GetAncestorsAsync(comment.Id, null);
await commentsRepository.IncludeCommentInAncestorsMetadataAsync(ancestors, authorId);
@@ -78,11 +84,8 @@ public async Task SetReactionsAsync(string commentId, Guid authorId, ISet EnrichAsync(Comment comment) => (await EnrichAsync([comment])).First();
+ private async Task EnrichAsync([TraceIgnore] Comment comment) => (await EnrichAsync([comment])).First();
- private async Task> EnrichAsync(IReadOnlyList comments)
+ private async Task> EnrichAsync([TraceIgnore] IReadOnlyList comments)
{
var authorIds = comments
.SelectMany(comment => comment.LastCommentsAuthorIds.Append(comment.AuthorId))
diff --git a/src/CrowdParlay.Social.Application/Services/DiscussionsService.cs b/src/CrowdParlay.Social.Application/Services/DiscussionsService.cs
index 54a5dd9..520f28c 100644
--- a/src/CrowdParlay.Social.Application/Services/DiscussionsService.cs
+++ b/src/CrowdParlay.Social.Application/Services/DiscussionsService.cs
@@ -1,13 +1,16 @@
using CrowdParlay.Social.Application.Abstractions;
using CrowdParlay.Social.Application.DTOs;
using CrowdParlay.Social.Application.Exceptions;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Social.Domain.Abstractions;
using CrowdParlay.Social.Domain.DTOs;
using CrowdParlay.Social.Domain.Entities;
using Mapster;
+using Metalama.Framework.Code;
namespace CrowdParlay.Social.Application.Services;
+[TraceMethods(Accessibility.Public, Accessibility.Private)]
public class DiscussionsService(
IUnitOfWorkFactory unitOfWorkFactory,
IDiscussionsRepository discussionsRepository,
diff --git a/src/CrowdParlay.Social.Application/Services/SubjectsService.cs b/src/CrowdParlay.Social.Application/Services/SubjectsService.cs
index 13b1eac..d9a8563 100644
--- a/src/CrowdParlay.Social.Application/Services/SubjectsService.cs
+++ b/src/CrowdParlay.Social.Application/Services/SubjectsService.cs
@@ -1,10 +1,13 @@
using CrowdParlay.Social.Application.Abstractions;
using CrowdParlay.Social.Application.Exceptions;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Social.Domain.Abstractions;
using CrowdParlay.Social.Domain.ValueObjects;
+using Metalama.Framework.Code;
namespace CrowdParlay.Social.Application.Services;
+[TraceMethods(Accessibility.Public, Accessibility.Private)]
public class SubjectsService(ISubjectsRepository subjectsRepository) : ISubjectsService
{
public async Task> GetReactionsAsync(string subjectId, Guid authorId) =>
diff --git a/src/CrowdParlay.Social.Aspects/CrowdParlay.Social.Aspects.csproj b/src/CrowdParlay.Social.Aspects/CrowdParlay.Social.Aspects.csproj
new file mode 100644
index 0000000..e31a07d
--- /dev/null
+++ b/src/CrowdParlay.Social.Aspects/CrowdParlay.Social.Aspects.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/CrowdParlay.Social.Aspects/MethodTracingOptions.cs b/src/CrowdParlay.Social.Aspects/MethodTracingOptions.cs
new file mode 100644
index 0000000..a502af8
--- /dev/null
+++ b/src/CrowdParlay.Social.Aspects/MethodTracingOptions.cs
@@ -0,0 +1,18 @@
+using Metalama.Framework.Aspects;
+
+namespace CrowdParlay.Social.Aspects;
+
+[CompileTime]
+internal class MethodTracingOptions
+{
+ public string? ActivityName;
+ public TraceKind? TraceKind;
+ public readonly HashSet IgnoredParameters = new();
+
+ public void Fallback(MethodTracingOptions other)
+ {
+ ActivityName ??= other.ActivityName;
+ TraceKind ??= other.TraceKind;
+ IgnoredParameters.UnionWith(other.IgnoredParameters);
+ }
+}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Aspects/TraceAspectProjectFabric.cs b/src/CrowdParlay.Social.Aspects/TraceAspectProjectFabric.cs
new file mode 100644
index 0000000..2947b10
--- /dev/null
+++ b/src/CrowdParlay.Social.Aspects/TraceAspectProjectFabric.cs
@@ -0,0 +1,30 @@
+using Metalama.Framework.Aspects;
+using Metalama.Framework.Code;
+using Metalama.Framework.Fabrics;
+
+namespace CrowdParlay.Social.Aspects;
+
+public class TraceAspectProjectFabric : TransitiveProjectFabric
+{
+ public override void AmendProject(IProjectAmender amender)
+ {
+ var implicitlyTracedMethods = amender
+ .SelectDeclarationsWithAttribute()
+ .OfType()
+ .SelectMany(type =>
+ {
+ var typeAttribute = type.Attributes.GetConstructedAttributesOfType().Single();
+ return type.Methods.Where(method =>
+ method.HasImplementation
+ && typeAttribute.Accessibility.Contains(method.Accessibility)
+ && !method.Attributes.OfAttributeType(typeof(TraceAttribute)).Any()
+ && !method.Attributes.OfAttributeType(typeof(TraceIgnoreAttribute)).Any());
+ });
+
+ implicitlyTracedMethods.AddAspect(method =>
+ {
+ var declaringTypeAttribute = method.DeclaringType.Attributes.GetConstructedAttributesOfType().Single();
+ return new TraceAttribute(declaringTypeAttribute.MethodTracingOptions);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Aspects/TraceAttribute.cs b/src/CrowdParlay.Social.Aspects/TraceAttribute.cs
new file mode 100644
index 0000000..0f4e7e0
--- /dev/null
+++ b/src/CrowdParlay.Social.Aspects/TraceAttribute.cs
@@ -0,0 +1,76 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using Metalama.Framework.Aspects;
+using Metalama.Framework.Code;
+
+namespace CrowdParlay.Social.Aspects;
+
+[SuppressMessage("ReSharper", "ConvertToPrimaryConstructor")]
+[AttributeUsage(AttributeTargets.Method), CompileTime]
+public sealed class TraceAttribute : OverrideMethodAspect
+{
+ internal MethodTracingOptions Options = new();
+
+ public TraceAttribute() { }
+ internal TraceAttribute(MethodTracingOptions options) => Options = options;
+ public TraceAttribute(string activityName) => Options.ActivityName = activityName;
+ public TraceAttribute(TraceKind traceKind) => Options.TraceKind = traceKind;
+
+ public TraceAttribute(string activityName, TraceKind traceKind)
+ {
+ Options.ActivityName = activityName;
+ Options.TraceKind = traceKind;
+ }
+
+ public override void BuildAspect(IAspectBuilder builder)
+ {
+ var typeAttribute = builder.Target.DeclaringType.Attributes.GetConstructedAttributesOfType().SingleOrDefault();
+ if (typeAttribute is not null)
+ Options.Fallback(typeAttribute.MethodTracingOptions);
+
+ var ignoredParameters = builder.Target.Parameters
+ .Where(parameter => parameter.Attributes.OfAttributeType(typeof(TraceIgnoreAttribute)).Any())
+ .Select(parameter => parameter.Name);
+
+ Options.IgnoredParameters.UnionWith(ignoredParameters);
+ base.BuildAspect(builder);
+ }
+
+ public override dynamic? OverrideMethod()
+ {
+ var activitySource = new ActivitySource(meta.Target.Method.DeclaringType.FullName);
+ var activity = activitySource.CreateActivity(
+ Options.ActivityName ?? meta.Target.Method.Name,
+ (ActivityKind?)Options.TraceKind ?? ActivityKind.Internal)!;
+
+ var parametersForSerialization = meta.Target.Parameters.Where(parameter => !Options.IgnoredParameters.Contains(parameter.Name));
+ var options = new JsonSerializerOptions { MaxDepth = 3 };
+ foreach (var parameter in parametersForSerialization)
+ {
+ activity.AddTag($"parameters.{parameter.Name}.presence","YES" );
+ try
+ {
+ var serializedValue = JsonSerializer.Serialize(parameter.Value, options);
+ activity.AddTag($"parameters.{parameter.Name}", serializedValue);
+ }
+ catch
+ {
+ // Parameters of unserializable types (e.g. CancellationToken having nested IntPtr property) are ignored
+ }
+ }
+
+ using (activity.Start())
+ {
+ try
+ {
+ return meta.Proceed();
+ }
+ catch (Exception exception)
+ {
+ activity.AddException(exception);
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Aspects/TraceIgnoreAttribute.cs b/src/CrowdParlay.Social.Aspects/TraceIgnoreAttribute.cs
new file mode 100644
index 0000000..4a4c273
--- /dev/null
+++ b/src/CrowdParlay.Social.Aspects/TraceIgnoreAttribute.cs
@@ -0,0 +1,4 @@
+namespace CrowdParlay.Social.Aspects;
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter)]
+public class TraceIgnoreAttribute : Attribute;
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Aspects/TraceKind.cs b/src/CrowdParlay.Social.Aspects/TraceKind.cs
new file mode 100644
index 0000000..a4a4598
--- /dev/null
+++ b/src/CrowdParlay.Social.Aspects/TraceKind.cs
@@ -0,0 +1,24 @@
+using Metalama.Framework.Aspects;
+using System.Diagnostics;
+
+namespace CrowdParlay.Social.Aspects;
+
+/// Compile-time version of
+[RunTimeOrCompileTime]
+public enum TraceKind
+{
+ /// Internal operation within an application, as opposed to operations with remote parents or children. This is the default value.
+ Internal,
+
+ /// Requests incoming from external component.
+ Server,
+
+ /// Outgoing request to the external component.
+ Client,
+
+ /// Output provided to external components.
+ Producer,
+
+ /// Output received from an external component.
+ Consumer
+}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Aspects/TraceMethodsAttribute.cs b/src/CrowdParlay.Social.Aspects/TraceMethodsAttribute.cs
new file mode 100644
index 0000000..72b3a76
--- /dev/null
+++ b/src/CrowdParlay.Social.Aspects/TraceMethodsAttribute.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+using Metalama.Framework.Aspects;
+using Metalama.Framework.Code;
+
+namespace CrowdParlay.Social.Aspects;
+
+[SuppressMessage("ReSharper", "ConvertToPrimaryConstructor")]
+[AttributeUsage(AttributeTargets.Class), CompileTime]
+public class TraceMethodsAttribute : Attribute
+{
+ internal readonly Accessibility[] Accessibility = [Metalama.Framework.Code.Accessibility.Public];
+ internal readonly MethodTracingOptions MethodTracingOptions = new();
+
+ public TraceMethodsAttribute() { }
+ public TraceMethodsAttribute(TraceKind traceKind) => MethodTracingOptions.TraceKind = traceKind;
+ public TraceMethodsAttribute(params Accessibility[] accessibility) => Accessibility = accessibility;
+
+ public TraceMethodsAttribute(TraceKind traceKind, params Accessibility[] accessibility)
+ {
+ if (!accessibility.Any())
+ throw new ArgumentException("At least one accessibility must be specified.", nameof(accessibility));
+
+ MethodTracingOptions.TraceKind = traceKind;
+ Accessibility = accessibility;
+ }
+}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Infrastructure.Communication/CrowdParlay.Social.Infrastructure.Communication.csproj b/src/CrowdParlay.Social.Infrastructure.Communication/CrowdParlay.Social.Infrastructure.Communication.csproj
index 8c5a6f5..02d5128 100644
--- a/src/CrowdParlay.Social.Infrastructure.Communication/CrowdParlay.Social.Infrastructure.Communication.csproj
+++ b/src/CrowdParlay.Social.Infrastructure.Communication/CrowdParlay.Social.Infrastructure.Communication.csproj
@@ -13,6 +13,15 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/src/CrowdParlay.Social.Infrastructure.Communication/Extensions/ConfigureOpenTelemetry.cs b/src/CrowdParlay.Social.Infrastructure.Communication/Extensions/ConfigureOpenTelemetry.cs
new file mode 100644
index 0000000..0863b70
--- /dev/null
+++ b/src/CrowdParlay.Social.Infrastructure.Communication/Extensions/ConfigureOpenTelemetry.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+namespace CrowdParlay.Social.Infrastructure.Communication.Extensions;
+
+partial class ServiceCollectionExtensions
+{
+ private static IServiceCollection ConfigureOpenTelemetry(this IServiceCollection services, IConfiguration configuration)
+ {
+ var openTelemetryConfiguration = configuration.GetRequiredSection("OpenTelemetry");
+ var openTelemetrySettings = openTelemetryConfiguration.Get()!;
+
+ services
+ .AddOptions()
+ .ValidateDataAnnotations()
+ .ValidateOnStart()
+ .Bind(openTelemetryConfiguration);
+
+ var openTelemetryBuilder = services.AddOpenTelemetry().UseOtlpExporter();
+
+ openTelemetryBuilder.ConfigureResource(resource => resource
+ .AddService(openTelemetrySettings.ServiceName));
+
+ openTelemetryBuilder.WithTracing(tracing => tracing
+ .AddSource(openTelemetrySettings.AdditionalTraceSources.Split(';'))
+ .AddAspNetCoreInstrumentation()
+ .AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddEntityFrameworkCoreInstrumentation()
+ .AddMassTransitInstrumentation()
+ .AddRedisInstrumentation());
+
+ openTelemetryBuilder.WithMetrics(metrics => metrics
+ .AddMeter(openTelemetrySettings.MeterName)
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation());
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Infrastructure.Communication/ConfigureServices.cs b/src/CrowdParlay.Social.Infrastructure.Communication/Extensions/ConfigureServices.cs
similarity index 88%
rename from src/CrowdParlay.Social.Infrastructure.Communication/ConfigureServices.cs
rename to src/CrowdParlay.Social.Infrastructure.Communication/Extensions/ConfigureServices.cs
index d449a69..0842a19 100644
--- a/src/CrowdParlay.Social.Infrastructure.Communication/ConfigureServices.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Communication/Extensions/ConfigureServices.cs
@@ -5,9 +5,9 @@
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
-namespace CrowdParlay.Social.Infrastructure.Communication;
+namespace CrowdParlay.Social.Infrastructure.Communication.Extensions;
-public static class ConfigureServices
+public static partial class ServiceCollectionExtensions
{
public static IServiceCollection AddCommunication(this IServiceCollection services, IConfiguration configuration)
{
@@ -28,6 +28,7 @@ public static IServiceCollection AddCommunication(this IServiceCollection servic
});
return services
+ .ConfigureOpenTelemetry(configuration)
.AddScoped()
.AddScoped()
.Decorate()
diff --git a/src/CrowdParlay.Social.Infrastructure.Communication/OpenTelemetrySettings.cs b/src/CrowdParlay.Social.Infrastructure.Communication/OpenTelemetrySettings.cs
new file mode 100644
index 0000000..b373b01
--- /dev/null
+++ b/src/CrowdParlay.Social.Infrastructure.Communication/OpenTelemetrySettings.cs
@@ -0,0 +1,11 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CrowdParlay.Social.Infrastructure.Communication;
+
+public record OpenTelemetrySettings
+{
+ [Required] public required string ServiceName { get; init; }
+ [Required] public required string OtlpEndpoint { get; init; }
+ [Required] public required string MeterName { get; init; }
+ [Required] public required string AdditionalTraceSources { get; init; }
+}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Infrastructure.Communication/Services/RedisUsersCache.cs b/src/CrowdParlay.Social.Infrastructure.Communication/Services/RedisUsersCache.cs
index 049684a..fd7090e 100644
--- a/src/CrowdParlay.Social.Infrastructure.Communication/Services/RedisUsersCache.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Communication/Services/RedisUsersCache.cs
@@ -1,10 +1,12 @@
using System.Text.Json;
using CrowdParlay.Social.Application.DTOs;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Social.Infrastructure.Communication.Abstractions;
using StackExchange.Redis;
namespace CrowdParlay.Social.Infrastructure.Communication.Services;
+[TraceMethods]
public class RedisUsersCache(IDatabase redis) : IUsersCache
{
public async Task GetUserByIdAsync(Guid userId)
@@ -29,10 +31,10 @@ public class RedisUsersCache(IDatabase redis) : IUsersCache
return userIds.Zip(users).ToDictionary(x => x.First, x => x.Second);
}
- public async Task SaveAsync(UserDto user) =>
+ public async Task SaveAsync([TraceIgnore] UserDto user) =>
await redis.StringSetAsync(user.Id.ToString(), JsonSerializer.Serialize(user), TimeSpan.FromMinutes(1));
- public async Task SaveAsync(IEnumerable users)
+ public async Task SaveAsync([TraceIgnore] IEnumerable users)
{
var transaction = redis.CreateTransaction();
diff --git a/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersService.cs b/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersService.cs
index 0b93f9e..34197c6 100644
--- a/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersService.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersService.cs
@@ -1,11 +1,13 @@
using CrowdParlay.Social.Application.Abstractions;
using CrowdParlay.Social.Application.DTOs;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Users.gRPC;
using Grpc.Core;
using Mapster;
namespace CrowdParlay.Social.Infrastructure.Communication.Services;
+[TraceMethods]
public class UsersService(Users.gRPC.UsersService.UsersServiceClient usersClient) : IUsersService
{
public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken)
diff --git a/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersServiceResilienceDecorator.cs b/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersServiceResilienceDecorator.cs
index f39c7c6..a2a1732 100644
--- a/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersServiceResilienceDecorator.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Communication/Services/UsersServiceResilienceDecorator.cs
@@ -23,7 +23,13 @@ public class UsersServiceResilienceDecorator(
catch (RpcException exception)
{
logger.LogError(exception, message: null);
- return null;
+ return new UserDto
+ {
+ Id = id,
+ Username = null,
+ DisplayName = null,
+ AvatarUrl = null
+ };
}
}
@@ -39,7 +45,13 @@ public class UsersServiceResilienceDecorator(
catch (RpcException exception)
{
logger.LogError(exception, message: null);
- return ids.ToDictionary(id => id, _ => null);
+ return ids.ToDictionary(id => id, id => new UserDto
+ {
+ Id = id,
+ Username = null,
+ DisplayName = null,
+ AvatarUrl = null
+ });
}
}
}
\ No newline at end of file
diff --git a/src/CrowdParlay.Social.Infrastructure.Persistence/ConfigureServices.cs b/src/CrowdParlay.Social.Infrastructure.Persistence/Extensions/ConfigureServices.cs
similarity index 81%
rename from src/CrowdParlay.Social.Infrastructure.Persistence/ConfigureServices.cs
rename to src/CrowdParlay.Social.Infrastructure.Persistence/Extensions/ConfigureServices.cs
index c6c1fcc..6794743 100644
--- a/src/CrowdParlay.Social.Infrastructure.Persistence/ConfigureServices.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Persistence/Extensions/ConfigureServices.cs
@@ -1,29 +1,28 @@
using CrowdParlay.Social.Domain.Abstractions;
using CrowdParlay.Social.Infrastructure.Persistence.Services;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
-namespace CrowdParlay.Social.Infrastructure.Persistence;
+namespace CrowdParlay.Social.Infrastructure.Persistence.Extensions;
-public static class ConfigurePersistenceExtensions
+public static class ServiceCollectionExtensions
{
- public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration) => services
- .AddMongoDb(configuration)
+ public static IServiceCollection AddPersistence(this IServiceCollection services) => services
+ .AddMongoDb()
.AddScoped()
.AddScoped()
.AddScoped()
.AddHostedService()
.AddHostedService();
- private static IServiceCollection AddMongoDb(this IServiceCollection services, IConfiguration configuration)
+ private static IServiceCollection AddMongoDb(this IServiceCollection services)
{
services
.AddOptions()
.ValidateDataAnnotations()
.ValidateOnStart()
- .Bind(configuration.GetSection("MongoDb"));
+ .BindConfiguration("MongoDb");
services.AddSingleton(provider =>
{
diff --git a/src/CrowdParlay.Social.Infrastructure.Persistence/Services/CommentsRepository.cs b/src/CrowdParlay.Social.Infrastructure.Persistence/Services/CommentsRepository.cs
index 4055dd0..e203512 100644
--- a/src/CrowdParlay.Social.Infrastructure.Persistence/Services/CommentsRepository.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Persistence/Services/CommentsRepository.cs
@@ -1,5 +1,6 @@
using System.Linq.Expressions;
using CrowdParlay.Social.Application.Exceptions;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Social.Domain.Abstractions;
using CrowdParlay.Social.Domain.DTOs;
using CrowdParlay.Social.Domain.Entities;
@@ -14,6 +15,7 @@
namespace CrowdParlay.Social.Infrastructure.Persistence.Services;
+[TraceMethods]
public class CommentsRepository(IClientSessionHandle session, IMongoDatabase database) : ICommentsRepository
{
private readonly IMongoCollection _comments = database.GetCollection(Collections.Comments);
@@ -111,7 +113,7 @@ public async Task> GetAncestorsAsync(string commentId, Guid? view
.Project(CreateCommentProjectionExpression(viewerId))
.ToListAsync();
- public async Task IncludeCommentInAncestorsMetadataAsync(IEnumerable ancestors, Guid authorId)
+ public async Task IncludeCommentInAncestorsMetadataAsync([TraceIgnore] IEnumerable ancestors, Guid authorId)
{
var updates = ancestors.Select(ancestor =>
{
@@ -134,7 +136,7 @@ public async Task IncludeCommentInAncestorsMetadataAsync(IEnumerable an
await _comments.BulkWriteAsync(session, updates);
}
- public async Task ExcludeCommentFromAncestorsMetadataAsync(IEnumerable ancestors)
+ public async Task ExcludeCommentFromAncestorsMetadataAsync([TraceIgnore] IEnumerable ancestors)
{
var filter = Filter.In(comment => comment.Id, ancestors.Select(comment => ObjectId.Parse(comment.Id)));
var update = Update.Inc(comment => comment.CommentCount, -1);
diff --git a/src/CrowdParlay.Social.Infrastructure.Persistence/Services/DiscussionsRepository.cs b/src/CrowdParlay.Social.Infrastructure.Persistence/Services/DiscussionsRepository.cs
index eaaa098..124474e 100644
--- a/src/CrowdParlay.Social.Infrastructure.Persistence/Services/DiscussionsRepository.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Persistence/Services/DiscussionsRepository.cs
@@ -1,5 +1,6 @@
using System.Linq.Expressions;
using CrowdParlay.Social.Application.Exceptions;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Social.Domain.Abstractions;
using CrowdParlay.Social.Domain.DTOs;
using CrowdParlay.Social.Domain.Entities;
@@ -14,6 +15,7 @@
namespace CrowdParlay.Social.Infrastructure.Persistence.Services;
+[TraceMethods]
public class DiscussionsRepository(IClientSessionHandle session, IMongoDatabase database) : IDiscussionsRepository
{
private readonly IMongoCollection _discussions = database.GetCollection(Collections.Discussions);
diff --git a/src/CrowdParlay.Social.Infrastructure.Persistence/Services/GenericSubjectsRepository.cs b/src/CrowdParlay.Social.Infrastructure.Persistence/Services/GenericSubjectsRepository.cs
index c797d3d..b1702aa 100644
--- a/src/CrowdParlay.Social.Infrastructure.Persistence/Services/GenericSubjectsRepository.cs
+++ b/src/CrowdParlay.Social.Infrastructure.Persistence/Services/GenericSubjectsRepository.cs
@@ -1,4 +1,5 @@
using CrowdParlay.Social.Application.Exceptions;
+using CrowdParlay.Social.Aspects;
using CrowdParlay.Social.Domain.Abstractions;
using CrowdParlay.Social.Infrastructure.Persistence.Models;
using MongoDB.Bson;
@@ -6,6 +7,7 @@
namespace CrowdParlay.Social.Infrastructure.Persistence.Services;
+[TraceMethods]
public class GenericSubjectsRepository(IClientSessionHandle session, IMongoDatabase database, string collection)
: ISubjectsRepository where TDocument : ISubjectDocument
{
diff --git a/tests/CrowdParlay.Social.IntegrationTests/CrowdParlay.Social.IntegrationTests.csproj b/tests/CrowdParlay.Social.IntegrationTests/CrowdParlay.Social.IntegrationTests.csproj
index b839ac7..e255c64 100644
--- a/tests/CrowdParlay.Social.IntegrationTests/CrowdParlay.Social.IntegrationTests.csproj
+++ b/tests/CrowdParlay.Social.IntegrationTests/CrowdParlay.Social.IntegrationTests.csproj
@@ -15,6 +15,7 @@
+
diff --git a/tests/CrowdParlay.Social.IntegrationTests/Services/OpenTelemetryInMemoryStorage.cs b/tests/CrowdParlay.Social.IntegrationTests/Services/OpenTelemetryInMemoryStorage.cs
new file mode 100644
index 0000000..1b96e20
--- /dev/null
+++ b/tests/CrowdParlay.Social.IntegrationTests/Services/OpenTelemetryInMemoryStorage.cs
@@ -0,0 +1,10 @@
+using System.Diagnostics;
+using OpenTelemetry.Metrics;
+
+namespace CrowdParlay.Social.IntegrationTests.Services;
+
+public class OpenTelemetryInMemoryStorage
+{
+ public readonly List Activities = new();
+ public readonly List Metrics = new();
+}
\ No newline at end of file
diff --git a/tests/CrowdParlay.Social.IntegrationTests/Services/TestWebApplicationFactory.cs b/tests/CrowdParlay.Social.IntegrationTests/Services/TestWebApplicationFactory.cs
index be014ba..051f098 100644
--- a/tests/CrowdParlay.Social.IntegrationTests/Services/TestWebApplicationFactory.cs
+++ b/tests/CrowdParlay.Social.IntegrationTests/Services/TestWebApplicationFactory.cs
@@ -1,9 +1,14 @@
-using CrowdParlay.Social.Infrastructure.Communication.Services;
+using System.Diagnostics;
+using CrowdParlay.Social.Infrastructure.Communication.Services;
using MassTransit;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using SimpleActivityExportProcessor = OpenTelemetry.SimpleActivityExportProcessor;
namespace CrowdParlay.Social.IntegrationTests.Services;
@@ -18,28 +23,57 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
{
["MongoDb:ConnectionString"] = mongoDbConfiguration.ConnectionString,
["MongoDb:Database"] = mongoDbConfiguration.Database,
+ ["OpenTelemetry:ServiceName"] = "social",
+ ["OpenTelemetry:OtlpEndpoint"] = "http://localhost:8200",
+ ["OpenTelemetry:AdditionalTraceSources"] = "CrowdParlay.Social.*",
+ ["OpenTelemetry:MeterName"] = "CrowdParlay.Social.Metrics",
["REDIS_CONNECTION_STRING"] = redisConfiguration.ConnectionString,
["USERS_GRPC_ADDRESS"] = "https://users:5104",
- ["TELEMETRY_SOURCE_NAME"] = "Social",
- ["TELEMETRY_OTLP_EXPORTER_ENDPOINT"] = "http://localhost:8200",
["CORS_ORIGINS"] = "http://localhost;http://localhost:1234",
["DATA_PROTECTION_REDIS_CONNECTION_STRING"] = redisConfiguration.ConnectionString,
}));
- builder.ConfigureServices(services =>
- {
- services.RemoveAll(typeof(IUsersService));
- services.AddScoped();
- services.Decorate();
+ builder.ConfigureServices(ConfigureMockServices);
+ builder.ConfigureServices(ConfigureOpenTelemetryInMemoryExporter);
+ builder.ConfigureServices(ConfigureRabbitMqTestHarness);
+ }
- var massTransitDescriptors = services
- .Where(service => service.ServiceType.Namespace?.Split('.') is [nameof(MassTransit), ..])
- .ToArray();
+ private static void ConfigureMockServices(IServiceCollection services)
+ {
+ services.RemoveAll(typeof(IUsersService));
+ services.AddScoped();
+ services.Decorate();
+ }
- foreach (var descriptor in massTransitDescriptors)
- services.Remove(descriptor);
+ private static void ConfigureOpenTelemetryInMemoryExporter(IServiceCollection services)
+ {
+ var storage = new OpenTelemetryInMemoryStorage();
+ services.AddSingleton(storage);
+
+ var openTelemetryBuilder = services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
+ {
+ var inMemoryExporter = new InMemoryExporter(storage.Activities);
+ tracerProviderBuilder.AddProcessor(new SimpleActivityExportProcessor(inMemoryExporter));
+ services.AddSingleton(inMemoryExporter);
+ });
- services.AddMassTransitTestHarness(bus => bus.AddDelayedMessageScheduler());
+ openTelemetryBuilder.WithMetrics(meterProviderBuilder =>
+ {
+ var inMemoryExporter = new InMemoryExporter(storage.Metrics);
+ meterProviderBuilder.AddReader(new BaseExportingMetricReader(inMemoryExporter));
+ services.AddSingleton(inMemoryExporter);
});
}
+
+ private static void ConfigureRabbitMqTestHarness(IServiceCollection services)
+ {
+ var massTransitDescriptors = services
+ .Where(service => service.ServiceType.Namespace?.Split('.') is [nameof(MassTransit), ..])
+ .ToArray();
+
+ foreach (var descriptor in massTransitDescriptors)
+ services.Remove(descriptor);
+
+ services.AddMassTransitTestHarness(bus => bus.AddDelayedMessageScheduler());
+ }
}
\ No newline at end of file
diff --git a/tests/CrowdParlay.Social.IntegrationTests/Services/UsersServiceMock.cs b/tests/CrowdParlay.Social.IntegrationTests/Services/UsersServiceMock.cs
index 0990074..de6ee0a 100644
--- a/tests/CrowdParlay.Social.IntegrationTests/Services/UsersServiceMock.cs
+++ b/tests/CrowdParlay.Social.IntegrationTests/Services/UsersServiceMock.cs
@@ -1,5 +1,8 @@
+using CrowdParlay.Social.Aspects;
+
namespace CrowdParlay.Social.IntegrationTests.Services;
+[TraceMethods]
public class UsersServiceMock : IUsersService
{
public Task GetByIdAsync(Guid id, CancellationToken cancellationToken) => Task.FromResult(new UserDto
diff --git a/tests/CrowdParlay.Social.IntegrationTests/Tests/TracingTests.cs b/tests/CrowdParlay.Social.IntegrationTests/Tests/TracingTests.cs
new file mode 100644
index 0000000..51278a5
--- /dev/null
+++ b/tests/CrowdParlay.Social.IntegrationTests/Tests/TracingTests.cs
@@ -0,0 +1,68 @@
+using System.Diagnostics;
+using CrowdParlay.Social.Application.Services;
+using CrowdParlay.Social.IntegrationTests.Services;
+using OpenTelemetry.Exporter;
+
+namespace CrowdParlay.Social.IntegrationTests.Tests;
+
+public class TracingTests(WebApplicationContext context) : IAssemblyFixture
+{
+ private readonly IServiceProvider _services = context.Services;
+
+ [Fact]
+ public async Task UsersServiceProducesTraces()
+ {
+ // Arrange
+ var storage = _services.GetRequiredService();
+ var exporter = _services.GetRequiredService>();
+
+ var scopeFactory = _services.GetRequiredService();
+ using var scope = scopeFactory.CreateScope();
+ var users = scope.ServiceProvider.GetRequiredService();
+
+ var userId = Guid.NewGuid();
+
+ // Act
+ await users.GetByIdAsync(userId);
+ exporter.ForceFlush();
+
+ // Assert
+ storage.Activities.Should()
+ .ContainSingle(activity =>
+ activity.Source.Name == typeof(UsersServiceMock).FullName
+ && activity.OperationName == nameof(UsersServiceMock.GetByIdAsync))
+ .Which.Tags.Should().Contain("parameters.id", $"\"{userId}\"");
+ }
+
+ [Fact]
+ public async Task CommentsServiceProducesTraces()
+ {
+ // Arrange
+ var storage = _services.GetRequiredService();
+ var exporter = _services.GetRequiredService>();
+
+ var scopeFactory = _services.GetRequiredService();
+ using var scope = scopeFactory.CreateScope();
+ var discussions = scope.ServiceProvider.GetRequiredService();
+ var comments = scope.ServiceProvider.GetRequiredService();
+
+ var authorId = Guid.NewGuid();
+ var discussion = await discussions.CreateAsync(authorId, "Discussion 1", "Content.");
+ await comments.ReplyToDiscussionAsync(discussion.Id, authorId, "Comment.");
+
+ // Act
+ await comments.GetRepliesAsync(discussion.Id, true, null, 0, 100);
+ exporter.ForceFlush();
+
+ // Assert
+ storage.Activities.Should()
+ .ContainSingle(activity =>
+ activity.Source.Name == typeof(CommentsService).FullName
+ && activity.OperationName == nameof(CommentsService.GetRepliesAsync))
+ .Which.Tags.Should().Contain("parameters.subjectId", $"\"{discussion.Id}\"")
+ .And.Contain("parameters.flatten", "true")
+ .And.Contain("parameters.viewerId", "null")
+ .And.Contain("parameters.offset", "0")
+ .And.Contain("parameters.count", "100");
+ }
+}
\ No newline at end of file