Umbraco Community Runtime Validators

This is a short but sweet blog post, to let you know about the Umbraco Community Runtime Validators package is out and available for you to use.

This little package came about working with the lovely lot over at Gibe Digital and I suggested that perhaps the code for a specific client project be reused and turned into this project where the wider Umbraco community can benefit from it.

So a BIG thanks to the Gibe development team for peer reviewing the work and collaborating with me on it.

What is an Umbraco Runtime Validator?

This is some code that can run to determine if the Umbraco environment is configured correctly, typically for a Production environment and prevent it from booting and giving you a verbose reason as to why. This makes sure you have all the optimal settings needed to run your Umbraco site in a production environment.

Umbraco ships with the following Runtime Validators to help make your site ready and optimal for use in production.

  • JITOptimizer
    The application is built and published in Release mode (with JIT optimization enabled)
  • ModelsBuilderMode
    The config value Umbraco:CMS:ModelsBuilder:ModelsMode is set to Nothing
  • RuntimeMinfication
    The config value Umbraco:CMS:RuntimeMinification:CacheBuster is set to a fixed cache buster like Version or AppDomain
  • UmbracoApplicationUrl
    The config value Umbraco:CMS:WebRouting:UmbracoApplicationUrl is configured
  • UseHttps
    The config value Umbraco:CMS:Global:UseHttps is set to true

To learn more about Runtime Validators you can read the official documentation on what they are and how to write your own Umbraco Runtime Validator.

What’s in Umbraco Community Runtime Validators ?

This first release out to the wild, focuses on ensuring your production environment is configured correctly if you are hosting your Umbraco site in Azure in a load balanced environment and ships these Runtime Validators

Alright already… just tell me how to set it up

Well you can find the package over on Umbraco Marketplace with a lovely readme on how to get setup and running.

But the quick TL:DR version is

dotnet add package Umbraco.Community.RuntimeValidators

Create an Umbraco Composer that updates the RuntimeModeValidators collection

using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Community.RuntimeValidators.Validators.AzureLoadBalancing;
using Umbraco.Extensions;

namespace YourProject.Website

public class RuntimeValidatorsComposer : IComposer
{
	public void Compose(IUmbracoBuilder builder)
	{
		builder.RuntimeModeValidators()
			.Add<TempFilesValidator>()
			.Add<HostSyncValidator>()
			.Add<ExamineValidator>();
	}
}

Got an idea for a Runtime Validator

Then please submit an issue on the GitHub repository with your super duper fab idea and lets get the conversation going and have a useful community repo of Runtime Validators to make all our Umbraco sites that much better !

Quick Tip: Adding a Saved Search to LogViewer in Umbraco with code

Heyađź‘‹
I wanted to share a quick tip with you all, that I think more package developers in the Umbraco community should consider doing, by adding a Saved Search to the Log Viewer built inside Umbraco. Allowing users of your package to easily filter or find logs related to your package.

So let’s jump into the code and see how it easy it is to do along with some suggestions of saved searches you could add to your package by using a package migration.

Adding a Package Migration Plan

This code snippet adds a package migration plan, which is used to determine if any steps of a migration need to be performed for a package. Over time with upgrades and new versions of your package you may add one or more migrations to add database tables or run other pieces of code such as adding saved searches to the Log Viewer.

For more information on Package Migrations take a look at the documentation from Umbraco.

using QuickTipSavedLogSearch.MyPackage.Migrations;
using Umbraco.Cms.Core.Packaging;

namespace QuickTipSavedLogSearch.MyPackage
{
    public class MyPackageMigrationPlan : PackageMigrationPlan
    {
        public MyPackageMigrationPlan() : base("My Package")
        {
        }

        protected override void DefinePlan()
        {
            // Ensure you use a unique guid for your migration plan steps
            To<AddSavedSearchToLogViewer>(new Guid("7E17C412-6061-470C-A8C8-1E2A23EF3096"));
        }
    }
}

Adding Saved Searches

The key thing from the code snippet below is that we inject ILogViewerConfig into the constructor of the PackageMigration code, so that we can use the methods such as GetSavedSearches() and AddSavedSearch(). With this we can save useful and specific Log Queries to help users of our package find logs only specific to our package code.

using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Logging.Viewer;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Packaging;

namespace QuickTipSavedLogSearch.MyPackage.Migrations
{
    public class AddSavedSearchToLogViewer : PackageMigrationBase
    {
        private ILogViewerConfig _logViewerConfig;
        private ILogger<AddSavedSearchToLogViewer> _logger;

        public AddSavedSearchToLogViewer(
            IPackagingService packagingService, 
            IMediaService mediaService, 
            MediaFileManager mediaFileManager, 
            MediaUrlGeneratorCollection mediaUrlGenerators, 
            IShortStringHelper shortStringHelper, 
            IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, 
            IMigrationContext context, 
            IOptions<PackageMigrationSettings> packageMigrationsSettings,
            ILogViewerConfig logViewerConfig,
            ILogger<AddSavedSearchToLogViewer> logger) 
            : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context, packageMigrationsSettings)
        {
            _logViewerConfig = logViewerConfig;
            _logger = logger;
        }

        // Find all logs that are from our package that starts with our namespace 'QuickTipSavedLogSearch.MyPackage'
        const string AllLogsQuery = "StartsWith(SourceContext, 'QuickTipSavedLogSearch.MyPackage')";

        // Find all logs that are from our package that starts with our namespace 'QuickTipSavedLogSearch.MyPackage'
        // AND the log level is either Error or Fatal OR the log has an exception property
        const string ErrorLogsQuery = "StartsWith(SourceContext, 'QuickTipSavedLogSearch.MyPackage') and (@Level='Error' or @Level='Fatal' or Has(@Exception))";

        protected override void Migrate()
        {
            // Test logline to help show it in the saved search filter
            _logger.LogInformation("Adding saved searches to log viewer");

            // Check to see if the existing saved searches for the log viewer contains our searches we want to add
            var allSavedSearches = _logViewerConfig.GetSavedSearches();
            
            // If the saved searches already contains our searches, then we don't need to add them (use singleordefault null check to check)
            // Unlikely but a user may have added these searches themselves and Umbraco API for this doesn't handle it for us if we add a duplicate
            if (allSavedSearches.SingleOrDefault(x => x.Query.Equals(AllLogsQuery, StringComparison.OrdinalIgnoreCase)) != null && allSavedSearches.SingleOrDefault(x => x.Query.Equals(ErrorLogsQuery, StringComparison.OrdinalIgnoreCase)) != null)
            {
                _logger.LogInformation("Saved searches already exist, skipping adding them");
                return;
            }

            // Add the saved searches
            _logViewerConfig.AddSavedSearch("[My Awesome Package] Find all Logs", AllLogsQuery);
            _logViewerConfig.AddSavedSearch("[My Awesome Package] Find all errors", ErrorLogsQuery);

            // Test error/exception to help show it in the saved search filter
            _logger.LogError(new Exception("Some exception"), "Some error with a counter {Counter}", 40);

        }
    }
}

Log Queries

Log queries is a way to search and filter with expressions to find the logs we are interesting in reading more about. We add two queries with the example code above and we can take a brief look at what they are doing.

StartsWith(SourceContext, 'QuickTipSavedLogSearch.MyPackage')

This query finds all logs where the property SourceContext starts with the value QuickTipSavedLogSearch.MyPackage. The property SourceContext is the full namespace of where the log was generated from, so we can use this to easily help find logs that are coming from our own code in the namespace of our code.

StartsWith(SourceContext, 'QuickTipSavedLogSearch.MyPackage') and (@Level='Error' or @Level='Fatal' or Has(@Exception))

This query builds upon from the one above and finds all logs where the property SourceContext starts with the value QuickTipSavedLogSearch.MyPackage AND the Log Level is Error or Fatal or the Log has an @Exception property.

Its really quite simple and can make a big difference to the quality of life for developers and users using your package, to help and find the information directly tied to your package and remove the noise of other log entries.

I hope this helps inspire you to give a little DX love and to start experimenting with creating useful Log Queries for the Log Viewer inside Umbraco.