Skip to content

Instantly share code, notes, and snippets.

@stefanolsen
Last active March 21, 2023 20:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stefanolsen/62b64d974d1ec87473dfa694b2faa1be to your computer and use it in GitHub Desktop.
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/
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);
}
}
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)
{
}
}
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)
};
}
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();
}
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