diff --git a/Directory.Build.props b/Directory.Build.props index 25942916..7504d42e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,8 @@  - $(GitVersion_NuGetVersion) + + 3.5.1-alpha.2 Totalsoft Totalsoft totalsoft charisma diff --git a/NBB.sln b/NBB.sln index a5228d70..94114859 100644 --- a/NBB.sln +++ b/NBB.sln @@ -277,10 +277,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NBB.Worker", "template\Work EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{FF63A085-1882-4B60-8EF0-F416990EDCEB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBB.Tools.AutoMapperExtensions.Tests", "test\UnitTests\Tools\NBB.Tools.AutoMapperExtensions.Tests\NBB.Tools.AutoMapperExtensions.Tests.csproj", "{A0D92D4E-A1FD-48A6-A59D-21082695ECAC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NBB.Tools.AutoMapperExtensions.Tests", "test\UnitTests\Tools\NBB.Tools.AutoMapperExtensions.Tests\NBB.Tools.AutoMapperExtensions.Tests.csproj", "{A0D92D4E-A1FD-48A6-A59D-21082695ECAC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NBB.Tools.AutoMapperExtensions", "src\Tools\NBB.Tools.AutomapperExtensions\NBB.Tools.AutoMapperExtensions.csproj", "{9B8B7158-4194-442F-83D7-346FDFF51DB1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBB.Messaging.OpenTracing", "src\Messaging\NBB.Messaging.OpenTracing\NBB.Messaging.OpenTracing.csproj", "{B5C4D462-E7AF-4146-9618-75FF242E148C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serilog", "Serilog", "{48E5BC6A-5846-4AD7-BFFB-C1D04C5C1BC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBB.Tools.Serilog.OpenTracingSink", "src\Tools\Serilog\NBB.Tools.Serilog.OpenTracingSink\NBB.Tools.Serilog.OpenTracingSink.csproj", "{9DDF53B1-7728-4CF3-BEBE-2057A6956F59}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -645,6 +651,14 @@ Global {9B8B7158-4194-442F-83D7-346FDFF51DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B8B7158-4194-442F-83D7-346FDFF51DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B8B7158-4194-442F-83D7-346FDFF51DB1}.Release|Any CPU.Build.0 = Release|Any CPU + {B5C4D462-E7AF-4146-9618-75FF242E148C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5C4D462-E7AF-4146-9618-75FF242E148C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5C4D462-E7AF-4146-9618-75FF242E148C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5C4D462-E7AF-4146-9618-75FF242E148C}.Release|Any CPU.Build.0 = Release|Any CPU + {9DDF53B1-7728-4CF3-BEBE-2057A6956F59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DDF53B1-7728-4CF3-BEBE-2057A6956F59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DDF53B1-7728-4CF3-BEBE-2057A6956F59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DDF53B1-7728-4CF3-BEBE-2057A6956F59}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -773,6 +787,9 @@ Global {FF63A085-1882-4B60-8EF0-F416990EDCEB} = {90E022FB-CA1B-49DD-9BEA-CE7F8E74E8BB} {A0D92D4E-A1FD-48A6-A59D-21082695ECAC} = {FF63A085-1882-4B60-8EF0-F416990EDCEB} {9B8B7158-4194-442F-83D7-346FDFF51DB1} = {70EA3420-74E3-4C9D-9858-57EC01AD31B3} + {B5C4D462-E7AF-4146-9618-75FF242E148C} = {584C62C0-2AE6-4DD6-9BCF-8FF28B7122CE} + {48E5BC6A-5846-4AD7-BFFB-C1D04C5C1BC2} = {70EA3420-74E3-4C9D-9858-57EC01AD31B3} + {9DDF53B1-7728-4CF3-BEBE-2057A6956F59} = {48E5BC6A-5846-4AD7-BFFB-C1D04C5C1BC2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23A42379-616A-43EF-99BC-803DF151F54E} diff --git a/dependencies.props b/dependencies.props index 09ec7c9d..c1e6b274 100644 --- a/dependencies.props +++ b/dependencies.props @@ -17,5 +17,8 @@ 5.1.2 2.1.1 8.0.0 + 0.12.0 + 0.5.0 + 0.2.2 \ No newline at end of file diff --git a/src/Messaging/NBB.Messaging.OpenTracing/NBB.Messaging.OpenTracing.csproj b/src/Messaging/NBB.Messaging.OpenTracing/NBB.Messaging.OpenTracing.csproj new file mode 100644 index 00000000..b46c7497 --- /dev/null +++ b/src/Messaging/NBB.Messaging.OpenTracing/NBB.Messaging.OpenTracing.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/src/Messaging/NBB.Messaging.OpenTracing/Publisher/OpenTracingPublisherDecorator.cs b/src/Messaging/NBB.Messaging.OpenTracing/Publisher/OpenTracingPublisherDecorator.cs new file mode 100644 index 00000000..fd472dcf --- /dev/null +++ b/src/Messaging/NBB.Messaging.OpenTracing/Publisher/OpenTracingPublisherDecorator.cs @@ -0,0 +1,66 @@ +using NBB.Core.Abstractions; +using NBB.Correlation; +using NBB.Messaging.Abstractions; +using NBB.Messaging.DataContracts; +using OpenTracing; +using OpenTracing.Propagation; +using OpenTracing.Tag; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace NBB.Messaging.OpenTracing.Publisher +{ + public class OpenTracingPublisherDecorator : IMessageBusPublisher + { + private readonly IMessageBusPublisher _inner; + private readonly ITracer _tracer; + + public OpenTracingPublisherDecorator(IMessageBusPublisher inner, ITracer tracer) + { + _inner = inner; + _tracer = tracer; + } + + public Task PublishAsync(T message, CancellationToken cancellationToken, Action customizer = null, string topicName = null) + { + void NewCustomizer(MessagingEnvelope outgoingEnvelope) + { + if (_tracer.ActiveSpan != null) + { + _tracer.Inject(_tracer.ActiveSpan.Context, BuiltinFormats.TextMap, + new TextMapInjectAdapter(outgoingEnvelope.Headers)); + } + + customizer?.Invoke(outgoingEnvelope); + } + + string operationName = $"Publisher {message.GetType().GetPrettyName()}"; + using (var scope = _tracer.BuildSpan(operationName) + .WithTag(Tags.Component, "NBB.Messaging.Publisher") + .WithTag(Tags.SpanKind, Tags.SpanKindServer) + .WithTag("correlationId", CorrelationManager.GetCorrelationId()?.ToString()) + .StartActive(finishSpanOnDispose: true)) + { + try + { + return _inner.PublishAsync(message, cancellationToken, NewCustomizer); + } + catch (Exception exception) + { + scope.Span.Log(new Dictionary(3) + { + { LogFields.Event, Tags.Error.Key }, + { LogFields.ErrorKind, exception.GetType().Name }, + { LogFields.ErrorObject, exception } + }); + + scope.Span.SetTag(Tags.Error, true); + + throw; + } + } + } + } +} diff --git a/src/Messaging/NBB.Messaging.OpenTracing/Subscriber/MessagingPipelineExtensions.cs b/src/Messaging/NBB.Messaging.OpenTracing/Subscriber/MessagingPipelineExtensions.cs new file mode 100644 index 00000000..66ec262e --- /dev/null +++ b/src/Messaging/NBB.Messaging.OpenTracing/Subscriber/MessagingPipelineExtensions.cs @@ -0,0 +1,21 @@ +using NBB.Core.Pipeline; +using NBB.Messaging.DataContracts; + +namespace NBB.Messaging.OpenTracing.Subscriber +{ + public static class MessagingPipelineExtensions + { + /// + /// Adds to the pipeline a middleware that creates an OpenTracing span. + /// + /// The pipeline builder. + /// The pipeline builder for further configuring the pipeline. It is used used in the fluent configuration API. + public static IPipelineBuilder UseOpenTracing( + this IPipelineBuilder pipelineBuilder) + => UseMiddleware(pipelineBuilder); + + + private static IPipelineBuilder UseMiddleware(this IPipelineBuilder pipelineBuilder) where TMiddleware : IPipelineMiddleware + => pipelineBuilder.UseMiddleware(); + } +} diff --git a/src/Messaging/NBB.Messaging.OpenTracing/Subscriber/OpenTracingMiddleware.cs b/src/Messaging/NBB.Messaging.OpenTracing/Subscriber/OpenTracingMiddleware.cs new file mode 100644 index 00000000..0d72e682 --- /dev/null +++ b/src/Messaging/NBB.Messaging.OpenTracing/Subscriber/OpenTracingMiddleware.cs @@ -0,0 +1,58 @@ +using NBB.Core.Pipeline; +using NBB.Messaging.DataContracts; +using OpenTracing; +using OpenTracing.Propagation; +using OpenTracing.Tag; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NBB.Core.Abstractions; +using NBB.Correlation; +using NBB.Messaging.Abstractions; + +namespace NBB.Messaging.OpenTracing.Subscriber +{ + public class OpenTracingMiddleware : IPipelineMiddleware + { + private readonly ITracer _tracer; + + public OpenTracingMiddleware(ITracer tracer) + { + _tracer = tracer; + } + + public async Task Invoke(MessagingEnvelope data, CancellationToken cancellationToken, Func next) + { + var extractedSpanContext = _tracer.Extract(BuiltinFormats.TextMap, new TextMapExtractAdapter(data.Headers)); + string operationName = $"Subscriber {data.Payload.GetType().GetPrettyName()}"; + + using(var scope = _tracer.BuildSpan(operationName) + .AsChildOf(extractedSpanContext) + .WithTag(Tags.Component, "NBB.Messaging.Subscriber") + .WithTag(Tags.SpanKind, Tags.SpanKindServer) + .WithTag("correlationId", CorrelationManager.GetCorrelationId()?.ToString()) + .StartActive(finishSpanOnDispose: true)) { + + try + { + await next(); + } + catch (Exception exception) + { + scope.Span.Log(new Dictionary(3) + { + { LogFields.Event, Tags.Error.Key }, + { LogFields.ErrorKind, exception.GetType().Name }, + { LogFields.ErrorObject, exception } + }); + + scope.Span.SetTag(Tags.Error, true); + + throw; + } + } + return; + } + } +} diff --git a/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/OpenTracingContribFilter.cs b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/OpenTracingContribFilter.cs new file mode 100644 index 00000000..7a71dc41 --- /dev/null +++ b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/OpenTracingContribFilter.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using Serilog.Events; + +namespace NBB.Tools.Serilog.OpenTracingSink.Internal +{ + internal static class OpenTracingContribFilter + { + private static List ExcludedLogSources = new List() + { + "Microsoft.EntityFrameworkCore", + "Microsoft.AspNetCore.Hosting" + }; + + + public static bool ShouldExclude(LogEvent logEvent) + { + const string sourceContextPropertyName = global::Serilog.Core.Constants.SourceContextPropertyName; + + if (!logEvent.Properties.TryGetValue(sourceContextPropertyName, out var source) || + !(source is ScalarValue scalarSource) || + !(scalarSource.Value is string stringValue)) + { + return false; + } + + return ExcludedLogSources.Any(x => stringValue.StartsWith(x)); + } + } +} diff --git a/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/OpenTracingSink.cs b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/OpenTracingSink.cs new file mode 100644 index 00000000..a69133d4 --- /dev/null +++ b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/OpenTracingSink.cs @@ -0,0 +1,68 @@ +using OpenTracing; +using OpenTracing.Util; +using Serilog.Core; +using Serilog.Events; +using System; +using System.Collections.Generic; + +namespace NBB.Tools.Serilog.OpenTracingSink.Internal +{ + internal class OpenTracingSink : ILogEventSink + { + private readonly ITracer _tracer; + private readonly Func _filter; + + public OpenTracingSink(Func filter = null) + { + _tracer = GlobalTracer.Instance; + _filter = filter ?? (logEvent => false); + } + + public void Emit(LogEvent logEvent) + { + ISpan span = _tracer.ActiveSpan; + + if (span == null) + { + // Creating a new span for a log message seems brutal so we ignore messages if we can't attach it to an active span. + return; + } + + if (_tracer.IsNoopTracer()) + { + return; + } + + if (_filter(logEvent)) + { + return; + } + + var fields = new Dictionary(); + + try + { + fields[LogFields.Event] = "log"; + fields[LogFields.Message] = logEvent.RenderMessage(); + fields["level"] = logEvent.Level; + + if (logEvent.Exception != null) + { + fields[LogFields.ErrorKind] = logEvent.Exception.GetType().FullName; + fields[LogFields.ErrorObject] = logEvent.Exception; + } + + foreach (var property in logEvent.Properties) + { + fields[property.Key] = property.Value.ToString(); + } + } + catch (Exception logException) + { + fields["opentracing.contrib.netcore.error"] = logException.ToString(); + } + + span.Log(fields); + } + } +} diff --git a/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/TracerExtensions.cs b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/TracerExtensions.cs new file mode 100644 index 00000000..75c3d8bd --- /dev/null +++ b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/Internal/TracerExtensions.cs @@ -0,0 +1,20 @@ +using OpenTracing; +using OpenTracing.Noop; +using OpenTracing.Util; + +namespace NBB.Tools.Serilog.OpenTracingSink.Internal +{ + internal static class TracerExtensions + { + public static bool IsNoopTracer(this ITracer tracer) + { + if (tracer is NoopTracer) + return true; + + if (tracer is GlobalTracer && !GlobalTracer.IsRegistered()) + return true; + + return false; + } + } +} diff --git a/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/NBB.Tools.Serilog.OpenTracingSink.csproj b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/NBB.Tools.Serilog.OpenTracingSink.csproj new file mode 100644 index 00000000..74695087 --- /dev/null +++ b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/NBB.Tools.Serilog.OpenTracingSink.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/OpenTracingConfigurationExtensions.cs b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/OpenTracingConfigurationExtensions.cs new file mode 100644 index 00000000..cdee7296 --- /dev/null +++ b/src/Tools/Serilog/NBB.Tools.Serilog.OpenTracingSink/OpenTracingConfigurationExtensions.cs @@ -0,0 +1,28 @@ +using NBB.Tools.Serilog.OpenTracingSink.Internal; +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +using System; + +namespace NBB.Tools.Serilog.OpenTracingSink +{ + public static class OpenTracingConfigurationExtensions + { + public static LoggerConfiguration OpenTracing( + this LoggerSinkConfiguration sinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null, + bool exludeOpenTracingContribEvents = true + ) + { + if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); + + var sink = exludeOpenTracingContribEvents ? + new Internal.OpenTracingSink(OpenTracingContribFilter.ShouldExclude) : + new Internal.OpenTracingSink(); + + return sinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + } + } +} diff --git a/template/Worker/content/NBB.Worker/DependencyInjectionExtensions.cs b/template/Worker/content/NBB.Worker/DependencyInjectionExtensions.cs index 8600882e..1aa1a050 100644 --- a/template/Worker/content/NBB.Worker/DependencyInjectionExtensions.cs +++ b/template/Worker/content/NBB.Worker/DependencyInjectionExtensions.cs @@ -21,6 +21,19 @@ using NBB.Messaging.Abstractions; #if NatsMessagingTransport using NBB.Messaging.Nats; +using System.Reflection; +using Microsoft.Extensions.Logging; +#if OpenTracing +using NBB.Messaging.OpenTracing.Subscriber; +using NBB.Messaging.OpenTracing.Publisher; +using OpenTracing; +using OpenTracing.Noop; +using Jaeger; +using Jaeger.Samplers; +using Jaeger.Reporters; +using Jaeger.Senders; +using OpenTracing.Util; +#endif #elif KafkaMessagingTransport using NBB.Messaging.Kafka; #endif @@ -65,6 +78,9 @@ public static class DependencyInjectionExtensions #endif #if AddSamples services.Decorate(); +#endif +#if OpenTracing + services.Decorate(); #endif services.AddMessagingHost() .AddSubscriberServices(selector => @@ -83,6 +99,9 @@ public static class DependencyInjectionExtensions .UsePipeline(builder => builder .UseCorrelationMiddleware() .UseExceptionHandlingMiddleware() +#if OpenTracing + .UseMiddleware() +#endif #if Resiliency .UseDefaultResiliencyMiddleware() #endif @@ -107,6 +126,37 @@ public static class DependencyInjectionExtensions // AutoMapper services.AddAutoMapper(typeof(__AMappingProfile__).Assembly); #endif + +#if OpenTracing + // OpenTracing + services.AddOpenTracing(); + + services.AddSingleton(serviceProvider => + { + if (!configuration.GetValue("OpenTracing:Jeager:IsEnabled")) + { + return NoopTracerFactory.Create(); + } + + string serviceName = Assembly.GetEntryAssembly().GetName().Name; + + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + ITracer tracer = new Tracer.Builder(serviceName) + .WithLoggerFactory(loggerFactory) + .WithSampler(new ConstSampler(true)) + .WithReporter(new RemoteReporter.Builder() + .WithSender(new UdpSender( + configuration.GetValue("OpenTracing:Jeager:AgentHost"), + configuration.GetValue("OpenTracing:Jeager:AgentPort"), 0)) + .Build()) + .Build(); + + GlobalTracer.Register(tracer); + + return tracer; + }); +#endif } } } diff --git a/template/Worker/content/NBB.Worker/NBB.Worker.csproj b/template/Worker/content/NBB.Worker/NBB.Worker.csproj index cd63e42c..46d0f477 100644 --- a/template/Worker/content/NBB.Worker/NBB.Worker.csproj +++ b/template/Worker/content/NBB.Worker/NBB.Worker.csproj @@ -43,6 +43,8 @@ + + @@ -53,6 +55,8 @@ + + @@ -63,5 +67,7 @@ + + diff --git a/template/Worker/content/NBB.Worker/Program.cs b/template/Worker/content/NBB.Worker/Program.cs index 79e7e59a..09ab3400 100644 --- a/template/Worker/content/NBB.Worker/Program.cs +++ b/template/Worker/content/NBB.Worker/Program.cs @@ -13,6 +13,9 @@ #if SqlLogging using Serilog.Sinks.MSSqlServer; #endif +#if OpenTracing +using NBB.Tools.Serilog.OpenTracingSink; +#endif namespace NBB.Worker { @@ -98,6 +101,9 @@ private static void ConfigureSerilog(IConfiguration cofiguration) .Enrich.FromLogContext() .Enrich.With() .WriteTo.Console() +#if OpenTracing + .WriteTo.OpenTracing() +#endif #if SqlLogging .WriteTo.MSSqlServer(connectionString, "__Logs", autoCreateSqlTable: true, columnOptions: columnOptions) #endif diff --git a/template/Worker/development.props b/template/Worker/development.props index 5fb89975..e8693130 100644 --- a/template/Worker/development.props +++ b/template/Worker/development.props @@ -18,6 +18,7 @@ true true true + true true $(HealthCheck) @@ -31,6 +32,7 @@ $(ConditionalCompilationSymbols);FluentValidation $(ConditionalCompilationSymbols);AutoMapper $(ConditionalCompilationSymbols);MediatR + $(ConditionalCompilationSymbols);OpenTracing $(ConditionalCompilationSymbols);AddSamples \ No newline at end of file