MetroLog.Maui: logging taylor-made for Maui

The amazing performance-driven MetroLog logging library was finally ported to .net 6 and MAUI!

MetroLog.Maui: logging taylor-made for Maui
Platform Logo Package
MetroLog.Maui Nuget

Github Repository: https://github.com/roubachof/MetroLog

Logging is your best friend

So, I'm a big fan of logging cause:

  • It helps you to understand what's happening in your app
  • It increases your debug session productivity tremendously
  • You can retrieve logs from your users to investigate a crash

Years ago, when your core project was still a PCL library, I came across MetroLog who is a really simple logging framework with a very small CPU footprint, which is just what we need for our mobile apps.

Issue: it has never been ported to netstandard. But since the api used fits into the netstandard api surface, you could just restore the package with .net45 target. And it worked flawlessly with just a little warning in your VS.

Eventually with the rise of .net 6 and MAUI, I got tired of this little warning and decided to port it to .net 6 and microsoft logging extensions.

But also to add some MAUI goodness out of the box like:

  • Share logs as zip
  • Display logs in a Page
  • Shake the device to display the log Page
0:00
/

Setup

What I always liked about MetroLog, is the simplicity of the setup. Configuration is made through the crossing of a target and log levels.

A target specify how the logs will be stored. For example, you can use the TraceTarget to show your logs in the debug output. Or you could use the FileSnapshotTarget to store your logs in a file.

The log levels describe the level of criticality. You bind each target to a set of log levels. It goes from Trace to Fatal.

Here there is no weird xml or json config, very simple code, that explains it all. On top of this very easy way to configure your logger I added a layer that is compatible with the microsoft logging extensions abstractions. So the setup is even smoother and very MAUI friendly.

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>()
            .ConfigureFonts(
                fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });

        
        builder.Logging
#if DEBUG
            .AddTraceLogger(
                options =>
                {
                    options.MinLevel = LogLevel.Trace;
                    options.MaxLevel = LogLevel.Critical;
                }) // Will write to the Debug Output
#endif
            .AddInMemoryLogger(
                options =>
                {
                    options.MaxLines = 1024;
                    options.MinLevel = LogLevel.Debug;
                    options.MaxLevel = LogLevel.Critical;
                })
#if RELEASE
            .AddStreamingFileLogger(
                options =>
                {
                    options.RetainDays = 2;
                    options.FolderPath = Path.Combine(
                        FileSystem.CacheDirectory,
                        "MetroLogs");
                })
#endif
            .AddConsoleLogger(
                options =>
                {
                    options.MinLevel = LogLevel.Information;
                    options.MaxLevel = LogLevel.Critical;
                }); // Will write to the Console Output (logcat for android)

        builder.Services.AddSingleton(LogOperatorRetriever.Instance);
        builder.Services.AddSingleton<MainPage>();

        return builder.Build();
    }
}

In this cas we add 3 different targets to our configuration:

  • Trace target (Trace -> Critical): only available for DEBUG builds, will write logs to the Debug Output
  • In memory target (Debug -> Critical)
  • File target (Info -> Critical): only available for RELEASE builds

We only specify the levels for the in memory logger, it means the other loggers will have the default min/max levels.

Default min level is Info, default max level is Fatal.

Usage

You can now inject your ILogger in your constructor, and use it as you want.

public class DownloadService 
{
    private readonly ILogger _logger;
    
    public DowloadService(ILogger<DownloadService> logger)
    
    public async Task<File> DowloadAsync()
    {
        _logger.Info("DowloadAsync()");
        
        try 
        {
            await _client.GetAsync();
        }
        catch (Exception exception) 
        {
            _logger.Error("Oops something bad happened", exception);
        }
    }
}

MAUI Goodness

With the MetroLog.Maui package, you gain access to some sweet candies. First you get a LogController.

LogController

The package provides a LogController that acts like a view model providing commands and service to a view.

public class LogController : INotifyPropertyChanged
{
    public ICommand ToggleShakeCommand { get; }

    public ICommand GoToLogsPageCommand { get; }

    public ICommand ClosePageCommand { get; }

    public bool CanGetCompressedLogs => LogCompressor != null;

    public bool CanGetLogsString => LogLister != null;

    public bool IsShakeEnabled
    {
        get => _isShakeEnabled;
        set
        {
            if (_isShakeEnabled != value)
            {
                ToggleAccelerometer(value);
                _isShakeEnabled = value;
            }

            OnPropertyChanged();
        }
    }

    public static void SuspendShake();

    public static void ResumeShakeIfNeeded();

    public async Task<MemoryStream?> GetCompressedLogs();

    public async Task<List<string>?> GetLogList();
}

We can see that you will be able to retrieve your compressed logs and even your logs as a list of string that you will be to display or whatever.

To use it, just instantiate it, it will be ready for a spin!

var logController = new LogController();

// will show the MetroLogPage by default
logController.GoToLogsCommand.Execute();

By setting IsShakeEnabled to true, you can display your log page by simply shaking your device.

Because there is several navigation frameworks out there, you first need to initialize the LogController with the navigation function, and the pop page function.

using MetroLog.Maui;

namespace MetroLogSample.Maui;

public partial class App : Application
{
    public App(MainPage mainPage)
    {
        InitializeComponent();

        MainPage = new NavigationPage(mainPage);

        LogController.InitializeNavigation(
            page => MainPage!.Navigation.PushModalAsync(page),
            () => MainPage!.Navigation.PopModalAsync());
    }
}

MetroLogPage

The package provides a default log page bound to the LogController.

This page displays the logs, and give the possibility to share your logs as a zip and refresh them.

WARNING

  • you need to add the MemoryTarget if you want to display the logs (the AddInMemoryLogger extension).
  • you need to add the StreamingFileTarget if you want to share the logs (the AddStreamingFileLogger extension).

Wrap-Up

Use MetroLog.Maui to handle all your logging needs in your MAUI app.

You can also use the MetroLog.Net6 package in your .net6 libs/backend/console app/whatever.

You can access a sample app here to help you understand better the full potential of the library: https://github.com/roubachof/MetroLog