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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/ClearHostedEndpoint/ClearHostedEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using ClearMeasure.HostedService;
using Microsoft.Data.SqlClient;
using ClearMeasure.HostedEndpoint.Configuration;
using ClearMeasure.HostedEndpoint.Infrastructure.Behaviors;
using Microsoft.Extensions.Configuration;

namespace ClearMeasure.HostedEndpoint;
Expand All @@ -17,7 +18,7 @@
private IEndpointInstance? _endpointInstance;
private IServiceCollection? _nsbServiceCollection;

protected ClearHostedEndpoint(IConfiguration configuration) : base(configuration)

Check warning on line 21 in src/ClearHostedEndpoint/ClearHostedEndpoint.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedEndpoint.ClearHostedEndpoint(IConfiguration)'

Check warning on line 21 in src/ClearHostedEndpoint/ClearHostedEndpoint.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedEndpoint.ClearHostedEndpoint(IConfiguration)'
{
}

Expand Down Expand Up @@ -138,6 +139,12 @@
// Configure concurrency
endpointConfiguration.LimitMessageProcessingConcurrencyTo(EndpointOptions.MaxConcurrency);

// Register timing behavior if enabled
if (EndpointOptions.EnableTimingBehavior)
{
endpointConfiguration.Pipeline.Register(typeof(TimingBehavior), "Logs handler execution time");
}

return endpointConfiguration;
}

Expand Down
6 changes: 6 additions & 0 deletions src/ClearHostedEndpoint/Configuration/EndpointOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,10 @@ public class EndpointOptions
/// Gets or sets how long to keep outbox deduplication data. Default is 7 days.
/// </summary>
public TimeSpan OutboxTimeToKeepDeduplicationData { get; set; } = TimeSpan.FromDays(7);

/// <summary>
/// Gets or sets whether to enable timing behavior for NServiceBus handlers. Default is false.
/// When enabled, logs handler execution time with Application Insights context.
/// </summary>
public bool EnableTimingBehavior { get; set; } = false;
}
59 changes: 59 additions & 0 deletions src/ClearHostedEndpoint/Infrastructure/Behaviors/TimingBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.Extensions.Logging;
using NServiceBus.Pipeline;
using System.Diagnostics;

namespace ClearMeasure.HostedEndpoint.Infrastructure.Behaviors;

/// <summary>
/// Pipeline behavior that logs the execution time of NServiceBus message handlers.
/// </summary>
public class TimingBehavior : Behavior<IInvokeHandlerContext>
{
private readonly ILogger<TimingBehavior> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="TimingBehavior"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <exception cref="ArgumentNullException">Thrown when logger is null.</exception>
public TimingBehavior(ILogger<TimingBehavior> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

/// <summary>
/// Invokes the behavior and logs the execution time.
/// </summary>
/// <param name="context">The handler invocation context.</param>
/// <param name="next">The next behavior in the pipeline.</param>
public override async Task Invoke(IInvokeHandlerContext context, Func<Task> next)
{
var stopwatch = Stopwatch.StartNew();
var messageType = context.MessageBeingHandled?.GetType().Name ?? "Unknown";
var handlerType = context.MessageHandler.HandlerType.Name;

try
{
await next();
stopwatch.Stop();

_logger.LogInformation(
"Handler {HandlerType} processed message {MessageType} in {ElapsedMilliseconds}ms",
handlerType,
messageType,
stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
stopwatch.Stop();

_logger.LogError(ex,
"Handler {HandlerType} failed processing message {MessageType} after {ElapsedMilliseconds}ms",
handlerType,
messageType,
stopwatch.ElapsedMilliseconds);

throw;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public void Constructor_ShouldSetDefaultValues()
options.EnableConsoleLogging.Should().BeTrue();
options.EnableFileLogging.Should().BeTrue();
options.EnableApplicationInsights.Should().BeFalse();
options.ApplicationInsightsInstrumentationKey.Should().BeNull();
options.ApplicationInsightsConnectionString.Should().BeNull();
options.ApplicationName.Should().BeNull();
options.CloudInstance.Should().BeNull();
}

[Fact]
Expand All @@ -37,7 +39,9 @@ public void Properties_ShouldBeSettable()
options.EnableConsoleLogging = false;
options.EnableFileLogging = false;
options.EnableApplicationInsights = true;
options.ApplicationInsightsInstrumentationKey = "test-key";
options.ApplicationInsightsConnectionString = "InstrumentationKey=test-key";
options.ApplicationName = "TestApp";
options.CloudInstance = "TestInstance";

// Assert
options.LogLevel.Should().Be(LogEventLevel.Debug);
Expand All @@ -46,7 +50,9 @@ public void Properties_ShouldBeSettable()
options.EnableConsoleLogging.Should().BeFalse();
options.EnableFileLogging.Should().BeFalse();
options.EnableApplicationInsights.Should().BeTrue();
options.ApplicationInsightsInstrumentationKey.Should().Be("test-key");
options.ApplicationInsightsConnectionString.Should().Be("InstrumentationKey=test-key");
options.ApplicationName.Should().Be("TestApp");
options.CloudInstance.Should().Be("TestInstance");
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/ClearHostedService/ClearHostedService.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using ClearMeasure.HostedService.Configuration;
using ClearMeasure.HostedService.Exceptions;
using ClearMeasure.HostedService.Interfaces;
using ClearMeasure.HostedService.Infrastructure.TelemetryConverters;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using Serilog;
using Serilog.Events;
using Serilog.Sinks.ApplicationInsights.TelemetryConverters;

using ILogger = Serilog.ILogger;

Expand All @@ -18,14 +21,14 @@
/// </summary>
public abstract class ClearHostedService : IHostedService, IHostedServiceLifecycle, IDisposable
{
protected IServiceProvider? _serviceProvider;

Check warning on line 24 in src/ClearHostedService/ClearHostedService.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedService._serviceProvider'

Check warning on line 24 in src/ClearHostedService/ClearHostedService.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedService._serviceProvider'
protected IConfiguration Configuration;

Check warning on line 25 in src/ClearHostedService/ClearHostedService.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedService.Configuration'

Check warning on line 25 in src/ClearHostedService/ClearHostedService.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedService.Configuration'
private ILogger? _logger;
private Task? _executingTask;
private CancellationTokenSource? _stoppingCts;
private bool _disposed;

protected ClearHostedService(IConfiguration configuration)

Check warning on line 31 in src/ClearHostedService/ClearHostedService.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedService.ClearHostedService(IConfiguration)'

Check warning on line 31 in src/ClearHostedService/ClearHostedService.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'ClearHostedService.ClearHostedService(IConfiguration)'
{
Configuration = configuration;
}
Expand Down Expand Up @@ -214,6 +217,29 @@
outputTemplate: options.OutputTemplate);
}

if (options.EnableApplicationInsights && !string.IsNullOrWhiteSpace(options.ApplicationInsightsConnectionString))
{
// Configure telemetry configuration with custom initializers
var telemetryConfiguration = new TelemetryConfiguration
{
ConnectionString = options.ApplicationInsightsConnectionString
};

// Add cloud role name converter if application name is provided
if (!string.IsNullOrWhiteSpace(options.ApplicationName))
{
telemetryConfiguration.TelemetryInitializers.Add(new CloudRoleNameConverter(options.ApplicationName));
}

// Add cloud instance converter
telemetryConfiguration.TelemetryInitializers.Add(new CloudInstanceConverter(options.CloudInstance));

// Add Application Insights sink
loggerConfig.WriteTo.ApplicationInsights(
telemetryConfiguration,
TelemetryConverter.Traces);
}

Log.Logger = loggerConfig.CreateLogger();
}

Expand Down
1 change: 1 addition & 0 deletions src/ClearHostedService/ClearMeasure.HostedService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
</ItemGroup>

</Project>
28 changes: 19 additions & 9 deletions src/ClearHostedService/Configuration/LoggingOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,25 @@ public class LoggingOptions
/// </summary>
public bool EnableFileLogging { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether ApplicationInsights is enabled.
/// </summary>
public bool EnableApplicationInsights { get; set; } = false;

/// <summary>
/// Gets or sets the ApplicationInsights instrumentation key.
/// </summary>
public string? ApplicationInsightsInstrumentationKey { get; set; }
/// <summary>
/// Gets or sets a value indicating whether ApplicationInsights is enabled.
/// </summary>
public bool EnableApplicationInsights { get; set; } = false;

/// <summary>
/// Gets or sets the ApplicationInsights connection string.
/// </summary>
public string? ApplicationInsightsConnectionString { get; set; }

/// <summary>
/// Gets or sets the application name for cloud role name telemetry.
/// </summary>
public string? ApplicationName { get; set; }

/// <summary>
/// Gets or sets the cloud instance identifier (machine name or resource group + instance).
/// </summary>
public string? CloudInstance { get; set; }

/// <summary>
/// Gets or sets the output template for log messages.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace ClearMeasure.HostedService.Infrastructure.TelemetryConverters;

/// <summary>
/// Telemetry initializer that sets the CloudRoleInstance property.
/// Uses either the provided cloud instance identifier or defaults to the machine name.
/// </summary>
public class CloudInstanceConverter : ITelemetryInitializer
{
private readonly string _cloudInstance;

/// <summary>
/// Initializes a new instance of the <see cref="CloudInstanceConverter"/> class.
/// </summary>
/// <param name="cloudInstance">The cloud instance identifier (machine name or resource group + instance). If null, uses Environment.MachineName.</param>
public CloudInstanceConverter(string? cloudInstance = null)
{
_cloudInstance = string.IsNullOrWhiteSpace(cloudInstance)
? Environment.MachineName
: cloudInstance;
}

/// <summary>
/// Initializes the telemetry item by setting the CloudRoleInstance property.
/// </summary>
/// <param name="telemetry">The telemetry item to initialize.</param>
public void Initialize(ITelemetry telemetry)
{
if (telemetry == null)
{
return;
}

telemetry.Context.Cloud.RoleInstance = _cloudInstance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace ClearMeasure.HostedService.Infrastructure.TelemetryConverters;

/// <summary>
/// Telemetry initializer that sets the CloudRoleName property from the application name.
/// </summary>
public class CloudRoleNameConverter : ITelemetryInitializer
{
private readonly string _applicationName;

/// <summary>
/// Initializes a new instance of the <see cref="CloudRoleNameConverter"/> class.
/// </summary>
/// <param name="applicationName">The application name to use for CloudRoleName.</param>
/// <exception cref="ArgumentNullException">Thrown when applicationName is null or empty.</exception>
public CloudRoleNameConverter(string applicationName)
{
if (string.IsNullOrWhiteSpace(applicationName))
{
throw new ArgumentNullException(nameof(applicationName), "Application name cannot be null or empty.");
}

_applicationName = applicationName;
}

/// <summary>
/// Initializes the telemetry item by setting the CloudRoleName property.
/// </summary>
/// <param name="telemetry">The telemetry item to initialize.</param>
public void Initialize(ITelemetry telemetry)
{
if (telemetry == null)
{
return;
}

telemetry.Context.Cloud.RoleName = _applicationName;
}
}
Loading