Log to Azure Table Storage in asp.net core to

Asp.net core has abstracted the logging operations into the ILogger interface and instead of having too many frameworks for logging, you just need to add a logging provider such as Console, SeriLog, Windows Event Viewer. In this post, I will create a custom logging provider that logs to Azure table storage

Why Azure Table Storage?

Azure table storage can store petebytes of semi-structured data with high performance inserts and updates. It is also cheap which makes it very suitable for storing log messages. It also allow us to connect to the log messages from many tools such as Azure Storage explorer unlike the case when you log messages to the file system on an Azure App Service

Structure of a custom logger

Logger

This is where the actual code for logging will exist and it must implement the ILogger

public class AzureTableLogger : ILogger
{
Microsoft.WindowsAzure.Storage.Table.CloudTable loggingTable;

    public AzureTableLogger(string connectionString, string tableName)
    {
        var storageAccount = Microsoft.WindowsAzure.Storage.CloudStorageAccount.Parse(connectionString);
        var cloudTableClient = storageAccount.CreateCloudTableClient();
        loggingTable = cloudTableClient.GetTableReference(tableName);
        loggingTable.CreateIfNotExistsAsync();
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return null;
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        var log = new LogEntity
        {
            EventId = eventId.ToString(),
            LogLevel = logLevel.ToString(),
            Message = formatter(state, exception),//exception?.ToString(),
            PartitionKey = DateTime.Now.ToString("yyyyMMdd"),
            RowKey = Guid.NewGuid().ToString()
        };

        var insertOp = TableOperation.Insert(log);
        loggingTable.ExecuteAsync(insertOp).GetAwaiter().GetResult();
    }
}

public class LogEntity : Microsoft.WindowsAzure.Storage.Table.TableEntity
{
    public string LogLevel { get; set; }
    public string EventId { get; set; }
    public string Message { get; set; }
}

As you can see, we just use the Azure Table Storage SDK to connect to the storage account and create a cloud table client. The logger accepts the table name and connection string as a constructor parameter which will be injected using the LoggingProvider. This is the minimum version of a logger and it needs a lot of improvement but it is good as a start.

LoggingProvider

public class AzureTableLoggerProvider : ILoggerProvider
{
    private readonly string connectionStringName;
    private readonly string tableName;

    public AzureTableLoggerProvider(string connectionString, string tableName)
    {
        connectionStringName = connectionString;
        this.tableName = tableName;
    }
    public ILogger CreateLogger(string categoryName)
    {
        return new AzureTableLogger(connectionStringName, tableName);
    }

    public void Dispose()
    {

    }
}

In this provider, we just use the two parameters passed in the constructor to create a new logger instance

LoggingProviderExtension

 public static class AzureTableLoggerExtensions
    {
        public static ILoggerFactory AddTableStorage(this ILoggerFactory loggerFactory, string tableName, string connectionString)
        {
            loggerFactory.AddProvider(new AzureTableLoggerProvider(connectionString, tableName));
            return loggerFactory;
        }
    }

This last block is not a must but its for the sake of clean code and clarity. In this step, we just inject our logging provider into the ILoggerFactory. When the application starts, we will have to call the AddTableStorage method to be able to use the new logger.

Add the provider in startup.cs

   public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            loggerFactory.AddTableStorage("LoggingSample", "UseDevelopmentStorage=true");
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }

Now, our application is ready to log messages to Azure Table Storage. The sample above is using local storage emulator and will create a table named LoggingSample. Lets create a controller and log some messages

    [ApiController]
    [Route("[controller]")]
    public class CustomersController : Controller
    {
        private ILogger<CustomersController> logger;

        public CustomersController(ILogger<CustomersController> logger)
        {
            this.logger = logger;
        }

        public IActionResult Index()
        {
            logger.LogInformation("This is a message from customers controller");
            return Ok();
        }
    }

In the Index action, we called the logger to add a test message. Notice that you don’t have to specify anything other than the ILogger. Asp.net core DI will inject the proper logger at runtime.

Notice the new columns added to the table such as EventId and LogLevel. These are properties in the LogEntity classes added in the first code snippet. Notice also that it logs all messages logged by the asp.net infrastructure not just the test message that we added

Next steps

Our next step would be adding a middleware that will show a nice UI that will display log messages. This can come handy when you want to debug something and need to see the error messages

Migrate your asp.net settings to Azure App Configuration

Before asp.net core, we only had the web.config file to configure and read the application settings from code. The framework didn’t have a plugable way of adding an extra configuration providers. Any other confiruation data sources meant a custom solution and custom code to read and update configuration keys.

With asp.net core, we got the new appsettings.json as one source of configuration. Along with it came the environment variables, other files, secret keys that can be used to omit connection strings and passwords from being stored on source control.

Things event got better with Azure App Service that has a settings/configuration section that can override the application settings and connection strings sections. This applies to both asp.net framework and asp.net core.

Isn’t this enough already?

It is good as a start. but, it has a major issue. If you change any value in your configuration keys, it means the application must be restarted which affects the logged in users and may cause data loss.
Another issue is that our application code and configuration exists in the same place which contradicts with the twelve factor . These configuration keys cannot also be shared with other applications except using copy/paste

Azure App Configuration

Azure App Configuration is a service that is still in preview that can be a central place for all your configuration keys and feature flags.
It is so easy to add it to your asp.net core configuration provider using the provided nuget packages.

Add the need packages

First, install the package Microsoft.Azure.AppConfiguration.AspNetCore . You may need to check that you enabled prerelease versions if you are using visual studio

In your Program.cs file, add the following code which will inject Azure app configuration as a configuration provider. Notice that you will have to add a key named “AppConfig” in your connectionStrings section in appsettings.json that will store the connection string for your Azure App Configuration

public static IWebHostBuilder CreateWebHostBuilder(string&#091;] args) =>
    WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            var settings = config.Build();
            config.AddAzureAppConfiguration(settings&#091;"ConnectionStrings:AppConfig"]);
        })
        .UseStartup<Startup>();

Now, your application is ready to read any configuration keys from Azure App Configuration. All you need to do is to create a new instance by following this link

Reuse your existing configuration keys

It’s not only too easy to add Azure App Configuration to your asp.net core app but also migrating your existing keys. You don’t want to end up with a situation where you have to spend hours moving your keys manually or writing scripts to do it. Azure App Configuration can do that for you.

As you can see above, it is too simple. Just choose Import/export from the left menu then choose where you want App Configuration to import the values from:

  1. App Configuration: you can import the values from another Azure App Configuration instance
  2. App Service: which can be perfect in our case since it will import the values from your Azure App Service
  3. Configuration File: you can pass the appsettings.json directly and it will import the values from there

Once you did that, you will have all keys/connection strings imported and ready to go.

Conclusion

Azure App Configuration is a great service that still in preview but it enables you to offload your application from configuration keys and connection strings. It also has the auto refresh which will allow your app to always have the latest config keys without the need to restart your application and impact your users.
It also has feature management which is a great addition but we will talk about it in future posts.