Last active
March 21, 2023 20:37
-
-
Save stefanolsen/62b64d974d1ec87473dfa694b2faa1be to your computer and use it in GitHub Desktop.
Code listings for blog post about Hangfire and Optimizely. Read about it here: https://stefanolsen.com/posts/4-tips-and-tricks-for-hangfire-on-optimizely-cms/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Text.Json; | |
using Hangfire.Server; | |
using Microsoft.ApplicationInsights; | |
using Microsoft.ApplicationInsights.DataContracts; | |
using Microsoft.ApplicationInsights.Extensibility; | |
namespace DemoSite.Hangfire; | |
public class ApplicationInsightsJobFilter : IServerFilter | |
{ | |
private const string ContextItemKey = "AI:Operation"; | |
private readonly TelemetryClient _telemetryClient; | |
public ApplicationInsightsJobFilter(TelemetryClient telemetryClient) => _telemetryClient = telemetryClient; | |
public void OnPerforming(PerformingContext filterContext) | |
{ | |
ArgumentNullException.ThrowIfNull(filterContext); | |
if (!_telemetryClient.IsEnabled()) | |
{ | |
return; | |
} | |
var requestTelemetry = new RequestTelemetry | |
{ | |
Name = $"JOB {filterContext.BackgroundJob.Job.Type.Name}.{filterContext.BackgroundJob.Job.Method.Name}", | |
Properties = | |
{ | |
{"JobId", filterContext.BackgroundJob.Id}, | |
{"JobCreatedAt", filterContext.BackgroundJob.CreatedAt.ToString("u")} | |
} | |
}; | |
try | |
{ | |
requestTelemetry.Properties.Add("JobArguments", JsonSerializer.Serialize(filterContext.BackgroundJob.Job.Args)); | |
} | |
catch | |
{ | |
requestTelemetry.Properties.Add("JobArguments", "Failed to serialize type"); | |
} | |
// Track Hangfire Job as a Request (operation) in AI | |
var operation = _telemetryClient.StartOperation(requestTelemetry); | |
filterContext.Items[ContextItemKey] = operation; | |
} | |
public void OnPerformed(PerformedContext filterContext) | |
{ | |
ArgumentNullException.ThrowIfNull(filterContext); | |
if (!_telemetryClient.IsEnabled()) | |
{ | |
return; | |
} | |
if (filterContext.Items[ContextItemKey] is not IOperationHolder<RequestTelemetry> operation) | |
{ | |
return; | |
} | |
operation.Telemetry.Success = false; | |
if (filterContext.Canceled || | |
filterContext.Exception is OperationCanceledException or JobAbortedException) | |
{ | |
// Cancellation is not a real exception. Log it separately. | |
_telemetryClient.TrackEvent("Job canceled."); | |
} | |
else if (filterContext.Exception is JobPerformanceException) | |
{ | |
// Unwrap and log only the inner exception. | |
_telemetryClient.TrackException(filterContext.Exception.InnerException); | |
} | |
else if (filterContext.Exception != null) | |
{ | |
_telemetryClient.TrackException(filterContext.Exception); | |
} | |
else | |
{ | |
_telemetryClient.TrackEvent("Job finished."); | |
operation.Telemetry.Success = true; | |
} | |
_telemetryClient.StopOperation(operation); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Globalization; | |
using EPiServer.Core; | |
using EPiServer.Logging; | |
using EPiServer.ServiceLocation; | |
using Hangfire.Client; | |
using Hangfire.Server; | |
namespace DemoSite.Hangfire; | |
public class CaptureContentLanguageFilter : IClientFilter, IServerFilter | |
{ | |
private const string JobParameterName = "ContentLanguage"; | |
private static readonly ILogger _logger = LogManager.GetLogger(typeof(CaptureContentLanguageFilter)); | |
private readonly Injected<IContentLanguageAccessor> _contentLanguageAccessor; | |
public void OnCreating(CreatingContext filterContext) | |
{ | |
ArgumentNullException.ThrowIfNull(filterContext); | |
// Store the current content language code on the job parameter for later. | |
filterContext.SetJobParameter(JobParameterName, _contentLanguageAccessor.Service.Language.Name); | |
} | |
public void OnCreated(CreatedContext filterContext) | |
{ | |
} | |
public void OnPerforming(PerformingContext filterContext) | |
{ | |
ArgumentNullException.ThrowIfNull(filterContext); | |
try | |
{ | |
var languageCode = filterContext.GetJobParameter<string>(JobParameterName); | |
if (languageCode != null) | |
{ | |
// Set the content language of the job thread to the language that was stored on the job. | |
_contentLanguageAccessor.Service.Language = CultureInfo.GetCultureInfo(languageCode); | |
} | |
} | |
catch (CultureNotFoundException exception) | |
{ | |
_logger.Warning($"Unable to set CurrentCulture for job {filterContext.BackgroundJob.Id}.", exception); | |
} | |
} | |
public void OnPerformed(PerformedContext filterContext) | |
{ | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using EPiServer.Logging; | |
using Hangfire.Logging; | |
namespace DemoSite.Hangfire; | |
public class OptimizelyLogProvider : ILogProvider | |
{ | |
public ILog GetLogger(string name) => new OptimizelyLogger(name); | |
} | |
public class OptimizelyLogger : ILog | |
{ | |
private readonly ILogger _logger; | |
public OptimizelyLogger(string name) => _logger = LogManager.Instance.GetLogger(name); | |
public bool Log(LogLevel logLevel, Func<string> messageFunc, Exception exception = null) | |
{ | |
Level level = GetLevel(logLevel); | |
if (messageFunc == null) | |
{ | |
// Before calling a method with an actual message, LogLib first probes | |
// whether the corresponding log level is enabled by passing a `null` | |
// messageFunc instance. | |
return _logger.IsEnabled(level); | |
} | |
_logger.Log(level, messageFunc(), exception); | |
return true; | |
} | |
private static Level GetLevel( | |
LogLevel logLevel) => | |
logLevel switch | |
{ | |
LogLevel.Trace => Level.Trace, | |
LogLevel.Debug => Level.Debug, | |
LogLevel.Info => Level.Information, | |
LogLevel.Warn => Level.Warning, | |
LogLevel.Error => Level.Error, | |
LogLevel.Fatal => Level.Critical, | |
_ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null) | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using EPiServer.Framework; | |
using EPiServer.Framework.Initialization; | |
using EPiServer.Scheduler; | |
using EPiServer.ServiceLocation; | |
using Hangfire; | |
using Microsoft.ApplicationInsights; | |
using Microsoft.Extensions.DependencyInjection; | |
namespace DemoSite.Hangfire; | |
[ModuleDependency(typeof(ServiceContainerInitialization))] | |
public class HangfireInitialization : IInitializableModule | |
{ | |
private BackgroundJobServer _backgroundJobServer; | |
public void Initialize(InitializationEngine context) | |
{ | |
// This always needs to be added, because job clients need to capture the language code for the job server. | |
GlobalJobFilters.Filters.Add(new CaptureContentLanguageFilter()); | |
var options = context.Locate.Advanced.GetRequiredService<SchedulerOptions>(); | |
if (!options.Enabled) | |
{ | |
// Scheduled jobs are disabled on this server. Exit. | |
return; | |
} | |
// Scheduled jobs are enabled on this server. Start the server | |
_backgroundJobServer = new BackgroundJobServer(); | |
var telemetryClient = context.Locate.Advanced.GetService<TelemetryClient>(); | |
if (telemetryClient == null) | |
{ | |
// If AI is not enabled (on development environment), skip adding the filter. | |
return; | |
} | |
// Add server filter to wrap each job execution in its own AI operation. | |
GlobalJobFilters.Filters.Add(new ApplicationInsightsJobFilter(telemetryClient)); | |
} | |
public void Uninitialize(InitializationEngine context) => | |
_backgroundJobServer?.Dispose(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using Hangfire; | |
using Hangfire.SqlServer; | |
using Microsoft.Extensions.DependencyInjection; | |
namespace DemoSite.Hangfire; | |
public class Startup | |
{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
// TODO: Find a good spot for this, among your other service configuration lines. | |
services.AddHangfire(config => config | |
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170) | |
// Add the Optimizely log provider here. | |
.UseLogProvider(new OptimizelyLogProvider()) | |
.UseSimpleAssemblyNameTypeSerializer() | |
.UseRecommendedSerializerSettings() | |
.UseSqlServerStorage(commerceConnectionString, new SqlServerStorageOptions | |
{ | |
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), | |
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), | |
QueuePollInterval = TimeSpan.Zero, | |
UseRecommendedIsolationLevel = true, | |
DisableGlobalLocks = true | |
})); | |
// Hangfire Server is normally added here. But to skip it if not needed, we do it in the HangfireInitialization module. | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment