Azure App Configuration

One of the great benefits that I receive at my place of work is every second Friday we have the option to take a day to research and develop tools and technologies that we feel may be of benefit to the wider department. As part of my “Freedom Friday” this past week I decided to spend some time looking into a relatively new service in Azure called Azure App Configuration.

It’s described (straight to the point) as “…a service to centrally manage application settings and feature flags.” and I was very curious as to how it all worked and if it would be a good fit to our existing components. More importantly I had heard that it supported dynamic configuration which would allow a running application to receive new configuration values without the need to be restarted.

In most apps that I’ve developed, anytime the configuration changed (for example a value in one of the config files), that always required a restart to take effect, for example dropping the log level from Information to Debug. The idea that we could change this while the app was running was quite compelling.

As a very quick proof of concept I hacked together a .Net Framework console app (The component where I would like to later implement this is .Net Framework app otherwise I would have used .Net Core). The basic premise of this PoC, was to achieve the following objectives:

  1. Dynamically receive app configuration settings from the Azure App Configuration service.
  2. Dynamically switch between feature flags configured in Azure App Configuration service.
  3. Dynamically switch the log level while the app was running without any user interaction.

I’ll be honest and say that the majority of the code I used for objective 1 was pretty much from the tutorial in the docs, however one thing that this sample didn’t do was dynamically refresh the values while the app was running. It required an action of pressing a command key and then re-querying the app config setting. The same applied for another sample I saw in my research where you were required to reload a web page. This wasn’t quite what I was after, the context where I wanted to do this was in a Windows service where no user interaction would be possible.

So what to do? Well, from my past experience of having developed Azure Functions using the Azure SignalR service, I knew that services have the ability to raise events via Azure Event Grid. It’s a very powerful and convenient feature. My plan was to raise an event (in this case every time a configuration value changed) and publish this to a topic in Azure Service Bus. My console program would then listen for this message and react accordingly by attempting to refresh my local configuration values.

So here’s how things panned out in the code (please note, this is not production ready and is just this way to illustrate how things can work!)

using System;
using System.Configuration;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;
using Serilog;
using Serilog.Core;
using Serilog.Events;

public static class Program
    {
        private static IConfiguration _configuration;
        private static IConfigurationRefresher _refresher;
        private const string ServiceBusConnectionString = "<Service Bus Connection String>";
        private const string TopicName = "appconfigupdatedevent";
        private const string SubscriptionName = "appconfigupdatedsubscription";
        private static ISubscriptionClient _subscriptionClient;
        private static IServiceCollection _services;
        private static readonly LoggingLevelSwitch LevelSwitch = new LoggingLevelSwitch();

        private static readonly ManualResetEvent ShutdownEvent = new ManualResetEvent(false);

        private static readonly Logger Logger = new LoggerConfiguration()
            .MinimumLevel.ControlledBy(LevelSwitch)
            .WriteTo.Console()
            .CreateLogger();
  public static async Task Main(string[] args)
        {
            //Configure Azure App Configuration Service
            _configuration = new ConfigurationBuilder()
                .AddAzureAppConfiguration(options =>
                {
                    options.Connect(ConfigurationManager.AppSettings["ConnectionString"])
                        .ConfigureRefresh(refresh =>
                        {
                            refresh.Register("TestApp:Settings:Message").SetCacheExpiration(TimeSpan.FromSeconds(10));
                            refresh.Register("TestApp:Settings:LogLevel").SetCacheExpiration(TimeSpan.FromSeconds(10));
                        });
                    options.UseFeatureFlags(flagOptions => flagOptions.CacheExpirationTime = TimeSpan.FromSeconds(1));
                    _refresher = options.GetRefresher();
            }).Build();

            _services = new ServiceCollection();
            _services.AddSingleton(_configuration).AddFeatureManagement();
            
            //Helper function to ensure the log level is set to match the value in the Azure App Configuration Service
            SetLogLevelFromConfig();
            
            //Set up my connection to Azure Service Bus and begin listening for new messages
            _subscriptionClient = new SubscriptionClient(ServiceBusConnectionString, TopicName, SubscriptionName);
            RegisterOnMessageHandlerAndReceiveMessages();

            //Essentially my test function where I'll continue to print out the current settings and switch between feature flags.
            PrintMessageInALoop();
            
            //Close my connection to Azure Service Bus 
            await _subscriptionClient.CloseAsync();
        }

        private static void SetLogLevelFromConfig()
        {
            switch (_configuration["TestApp:Settings:LogLevel"])
            {
                case "Information":
                    LevelSwitch.MinimumLevel = LogEventLevel.Information;
                    break;
                case "Debug":
                    LevelSwitch.MinimumLevel = LogEventLevel.Debug;
                    break;
                case "Error":
                    LevelSwitch.MinimumLevel = LogEventLevel.Error;
                    break;
                case "Warning":
                    LevelSwitch.MinimumLevel = LogEventLevel.Warning;
                    break;
                default:
                    LevelSwitch.MinimumLevel = LogEventLevel.Information;
                    break;
            }
        }

        private static async void PrintMessageInALoop()
        {
            Console.CancelKeyPress += (s, e) =>
            {
                e.Cancel = true;
            };

            using (var serviceProvider = _services.BuildServiceProvider())
            {
                var featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

                while (!ShutdownEvent.WaitOne(1000))
                {
                    if (await featureManager.IsEnabledAsync("Beta"))
                    {
                        Console.WriteLine($"Welcome to the beta! {_configuration["TestApp:Settings:Message"]}");
                    }
                    else
                    {
                        Console.WriteLine($"Welcome to the live version! {_configuration["TestApp:Settings:Message"]}");
                    }

                    Logger.Information("This is an Information message");
                    Logger.Warning("This is a Warning message");
                    Logger.Error("This is a Error message");
                    Logger.Debug("This is a Debug message");
                }
            }
        }
        private static void RegisterOnMessageHandlerAndReceiveMessages()
        {
            var messageHandlerOptions = new MessageHandlerOptions(ExceptionReceivedHandler)
            {
                MaxConcurrentCalls = 1,
                AutoComplete = false
            };

            _subscriptionClient.RegisterMessageHandler(ProcessMessagesAsync, messageHandlerOptions);
        }

        private static async Task ProcessMessagesAsync(Message message, CancellationToken token)
        {
            await _refresher.TryRefreshAsync();
            SetLogLevelFromConfig();
            await _subscriptionClient.CompleteAsync(message.SystemProperties.LockToken);
        }

        private static Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs)
        {
            Console.WriteLine($"Message handler encountered an exception {exceptionReceivedEventArgs.Exception}.");
            var context = exceptionReceivedEventArgs.ExceptionReceivedContext;
            Console.WriteLine("Exception context for troubleshooting:");
            Console.WriteLine($"- Endpoint: {context.Endpoint}");
            Console.WriteLine($"- Entity Path: {context.EntityPath}");
            Console.WriteLine($"- Executing Action: {context.Action}");
            return Task.CompletedTask;
        }   
    }
}

So what’s going on? Using Serilog, I configured it to be able to set the log level dynamically:

private static readonly LoggingLevelSwitch LevelSwitch = new LoggingLevelSwitch();
private static readonly Logger Logger = new LoggerConfiguration()
            .MinimumLevel.ControlledBy(LevelSwitch)
            .WriteTo.Console()
            .CreateLogger();

I then do some configuration for the Azure App Configuration service, in this case setting the connection string to the connection string in my App.Config file. I also set how long the cache of the settings and feature flags should be, in reality these should be larger values but for the purposes of testing these are set to very low values of a few seconds. I also indicate to the service that I want to make use of feature flags by setting the UseFeatureFlags option.

Once all that’s wired up I read the current config value of the logLevel setting and set it using the SetLogLevelFromConfig function. This function is very error prone so don’t use it in real life (i.e. there is zero validation and it will default to Information without giving feedback to the consumer as to why this is happening).

I then set up the connection to Service Bus, listening for new messages on the configured topic name. Should a new message arrive the ProcessMessagesAsync method is executed which will then call the following lines:

await _refresher.TryRefreshAsync();
SetLogLevelFromConfig();

This will cause the App Configuration service to be called and provided the cache has expired on the settings / feature flags you want you should receive new values for these.

Please note again, the code above where I receive messages from the topic in Service Bus has largely been copied verbatim from Get started with Service Bus topics.

For the above example code to work you must have the following configured in Azure:

  1. Azure Service Bus with a topic name of appconfigupdatedevent and a subscription name of appconfigupdatedsubscription.
  2. An App Configuration service (free tier is fine for this demo) where you have configured a new event subscription with an event type of ‘Key-value modified’ and the endpoint type configured to the above Service Bus topic.
  3. The two settings I configured are:
    1. TestApp:Settings:Message – This is just a random text message.
    2. TestApp:Settings:LogLevel – This represents the log level you wish to set with a value set as per the values in SetLogLevelFromConfig.
  4. I have one feature flag set – Beta. This is to represent switching a feature flag toggle on and off.

So to summarise, while the console app is executing, what you should see is a continual print out of the current settings and feature flag setting. Changing the value of these variables via the portal should then result in the values dynamically changing as the new message is received off the bus. No user interaction required 🙂

2 thoughts on “Azure App Configuration”

  1. […] One thing I read about a while back was a technique where logs are buffered in memory and on a critical failure these are flushed to the disk, giving you all the necessary log context while not suffering huge amounts of logs being written to disk. I haven’t had a requirement to do this but I believe it’s called a ring buffer. The option to switch the log level dynamically is also an option here but it would really be ‘after the fact’ and the same issue would need to occur to capture the log events (perhaps there are additional ‘Debug‘ level events that can be written). I’ve described one technique to do this in one of my earlier posts. […]

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s