Xamarin.Forms Logging with AppCenter

All too often, when we write an app, we are so excited to write a cool algorithm or build a beautiful control that we don’t build out things like logging as much as we should. Then, when a weird problem crops up, it is harder to get the information we need to troubleshoot the problem easily.

To make matters worse, our friends on the web side of software development have all kinds of cool and free analytics packages that track page navigation, clicks, etc – right out of the box! What about us mobile people!?!

This blog post covers some simple patterns for wiring up some of these common logging features in a default, vanilla, shell template Xamarin.Forms project and a free AppCenter account. Let’s do this.

Check out a complete working example of the patterns described in this blog post.

Install AppCenter

Get yourself an AppCenter account and create at least two new apps. Create one for each of the platforms your Xamarin.Forms solution is going to support. In my case, I used the XF Shell template and as of right now, that template only supports iOS and Android. So, I created two new apps in AppCenter, one for iOS and another for Android. AppCenter also works with Xamarin UWP and a whole host of other technologies.

You will also need to install the nuget packages for the AppCenter SDK. In this example, I just used Analytics and Crashes nugets. AppCenter has LOTS more to offer. Here are detailed installation instructions.

AppState and Logging Services

For this example, make a couple simple services and use the built in Xamarin.Forms DependencyService. I am going to follow the pattern in the shell template and put the interfaces in the same folder with the implementations. I wouldn’t normally do this. Usually, I put the interfaces in their own folder, but when in Rome, do as the Romans, right?

One of the most weirdly useful features of AppCenter is the InstallId. AppCenter assigns each installation of your application a Guid. This identifier is unique to the installation, not the user, not the device, but to the combination of all three. In this example, I included some code that gets the InstallId from AppCenter and stores it in the AppState Service. If you want to be able to track analytics on each of your users, it is most efficient and secure to pull that install id back into your app’s cloud data store (relational database or otherwise) as associate it with the appropriate user, rather then push identifying info into AppCenter.

Note that you should be up front about what data you collect and how you use it in any privacy policy or terms and conditions disclaimer on your app. If you are working with users in the EU, this is complicated by the GDPR.

Read more about the AppCenter Install Id (aka Installation Identifier) here.


Let’s talk about the two services. The first service is for the AppState:

In this service, the SetAppLogLevel() method sets the log level at which the event message will go to AppCenter at all. This is useful because often, you want to display a ton of messages during development and QA, and then little to none when you go to production. By setting this level programatically, you have fine-grained power to do exactly that, and you can do it dynamically.

On a past project, I used a pattern like this to link the log level with a user’s account. When a user logged in, their log level was fetched from a REST endpoint along with their authorization role. If a user called in for tech support, we could reset their log level and have them log out and log back in. This would reset their log level and instantly, a very detailed set of logging records would flow into AppCenter for trouble shooting. When the problem was resolved, we could toggle their log level back to normal and the next time they logged in, their original setting was restored.

 public interface IAppState
 {
     Task Init();
     Guid GetInstallId();
     AppLogLevel GetAppLogLevel();
     void SetAppLogLevel(AppLogLevel level);
 }

Here is the implementation of the AppState Service:

using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Essentials;

namespace AppCenterAnalytics.Services
{
    public class AppState : IAppState
    {
        private Guid _installId = Guid.Empty;
        private AppLogLevel _logLevel;

        public AppLogLevel GetAppLogLevel()
        {
            return _logLevel;
        }

        public Guid GetInstallId()
        {
            return _installId;
        }

        public async Task Init()
        {
            if (await CheckAppCenter())
            {
                //https://docs.microsoft.com/en-us/appcenter/sdk/other-apis/xamarin#identify-installations//
                System.Guid? installId = await AppCenter.GetInstallIdAsync();
                if (installId != null) { _installId = (Guid)installId; }
            }
        }

        public void SetAppLogLevel(AppLogLevel level)
        {
            //this controls what logging message should be set to AppCenter at all
            _logLevel = level;
        }

        private async Task<bool> CheckAppCenter()
        {
            var retValue = true; //let's be optimistic
            try
            {
                if (Connectivity.NetworkAccess == NetworkAccess.Internet)
                {
                    // Connection to internet is available
                    //check to make sure analytics is wired up
                    bool isAnalyticsEnabled = await Analytics.IsEnabledAsync();
                    if (!isAnalyticsEnabled)
                    {
                        //we can't really report to appcenter that appcenter isn't working!
                        Debug.WriteLine($"[{this.GetType().ToString()}] Warning: AppCenter Analytics is NOT enabled.");
                        retValue = false;
                    }

                    bool isCrashEnabled = await Crashes.IsEnabledAsync();
                    if (!isCrashEnabled)
                    {
                        //we can't really report to appcenter that appcenter isn't working!
                        Debug.WriteLine($"[{this.GetType().ToString()}] Warning: AppCenter Crash Reporting is NOT enabled.");
                        retValue = false;
                    }
                }
            }
            catch (Exception ex)
            {
                //we can't really report to appcenter that appcenter isn't working!
                Debug.WriteLine(
                     $"[{this.GetType().ToString()}] Exception while checking if AppCenter is enabled {ex.Message} {ex.StackTrace}"
                );
                retValue = false;
            }
            return retValue;
        }
    }
}

To set the AppLogLevel, use the homegrown enum, below.

public enum AppLogLevel
{
    Verbose = 0,
    Debug = 1,
    Info = 2,
    Warn = 3,
    Error = 4,
    Fatal = 5,
    None = 6
}

The second service is for logging methods. The big feature here is to block event tracking for events generated by a simulator/emulator, and to block event tracking for events generated by the app running in DevOps Test Cloud (Xamarin Test Cloud). Xamarin Test Cloud is physically located in Denmark and always seems to cause confusion when looking at logs!

In addition, the logging service does not bother sending messages to AppCenter if the AppLogLevel is set to a level that is higher than the current setting. Because the enum for the AppLogLevel has numbers assigned to each value, this is done with a quick comparison.

public interface ILogging
{
    void LogEvent(AppLogLevel level, string message);
    void LogEvent(AppLogLevel level, string message, Dictionary<string, string> dict);
}

Here is the implementation of the Logging Service:

using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics;
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Essentials;
using Xamarin.Forms;

[assembly: Xamarin.Forms.Dependency(typeof(AppCenterAnalytics.Services.AppState))]

namespace AppCenterAnalytics.Services
{
    public class Logging : ILogging
    {
        private IAppState _appState;

        //The Xamarin.Forms.DependencyService is a SERVICE LOCATOR and not an IoC CONTAINER
        //if it was an IoC container, we could inject these into the constructor like this:
        //public Logging(IAppState appState)...
        public Logging()
        {
            _appState = DependencyService.Get<IAppState>();
        }

        public void LogEvent(AppLogLevel level, string message)
        {
            if (_appState.GetAppLogLevel() <= level &amp;&amp; !IsOnTestCloud() &amp;&amp; !IsEmulatorOrSimulator())
            {
                Analytics.TrackEvent($"{level}: {message}");
            }
        }

        public void LogEvent(AppLogLevel level, string message, Dictionary<string, string> dict)
        {
            if (_appState.GetAppLogLevel() <= level &amp;&amp; !IsOnTestCloud() &amp;&amp; !IsEmulatorOrSimulator())
            {
                //include the installId with the message - this may only be marginally useful
                //but you can see it isn't too hard to include additional info
                dict.Add("InstallId", _appState.GetInstallId().ToString());
                Analytics.TrackEvent($"{level}: {message}", dict);
            }
        }

        private bool IsEmulatorOrSimulator()
        {
            //Thank you, Xamarin.Essentials!
            //This used to be a really big pain, now it is a one-liner!
            return DeviceInfo.DeviceType == DeviceType.Virtual;
        }

        private bool IsOnTestCloud()
        {
            //Xamarin Test Cloud (App Center Test Cloud) is in Denmark... including these stats could give you some unexpected results.
            var isInTestCloud = Environment.GetEnvironmentVariable("XAMARIN_TEST_CLOUD");
            return isInTestCloud != null &amp;&amp; isInTestCloud.Equals("1");
        }
    }
}

Finally, we need to register the new services in App.xaml.cs, like this:

public App()
{
      InitializeComponent();

      DependencyService.Register<AppState>();
      //Logging gets AppState in its constructor, so it must be registered after AppState
      DependencyService.Register<Logging>();
      //MockDataStore gets AppState and Logging in its constructor, so it must be registered after AppState and Logging
      DependencyService.Register<MockDataStore>();

      MainPage = new AppShell();
}

We also need to set the AppLogLevel in our AppState Service. In the App.xaml.cs OnStart() method, add this:

     protected override void OnStart()
        {
            //Remember, there is always a semi-colon at the end of each!
            AppCenter.Start("ios={Your iOS App secret here};" +
                  "uwp={Your UWP App secret here};" +
                  "android={Your Droid App secret here};",
                  typeof(Analytics), typeof(Crashes));

            IAppState AppState = DependencyService.Get<IAppState>();
            AppState.Init();

            //initialize the log levels for the app and for what messages are sent to the console
#if DEBUG
            AppState.SetAppLogLevel(AppLogLevel.Verbose);
#else
            AppState.SetAppLogLevel(AppLogLevel.Info);
#endif
        }

Show me page navigation logging!

Let’s log an AppCenter event each time your user navigates to and from a view in our app. First, make a BaseContentPage.cs and stick it in the Views folder.

using AppCenterAnalytics.Services;
using Xamarin.Forms;

namespace AppCenterAnalytics.Views
{
    public class BaseContentPage : Xamarin.Forms.ContentPage
    {
        private ILogging _logging;

        public BaseContentPage()
        {
            _logging = DependencyService.Get<ILogging>();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();

            //if you use nested folders to help id your views, use: this.GetType().FullName
            _logging.LogEvent(AppLogLevel.Info, $"{this.GetType().Name} Appeared");
        }

        protected override void OnDisappearing()
        {
            base.OnDisappearing();

            //you may not really need to know that a user navigated away from a page,
            //but just in case, here is the code
            //if you use nested folders to help id your views, use: this.GetType().FullName
            _logging.LogEvent(AppLogLevel.Info, $"{this.GetType().Name} Disappeared");
        }
    }
}

A base content page is similar to a base view model, in that each of our views are going to derive from it. Views are a little more tricky in that they have two partial classes, one is a XAML page and the other is the code-behind. Each one of the code behinds will derive from a BaseContentPage rather than a ContentPage. AboutPage.Xaml.cs would look like this:

public partial class AboutPage : BaseContentPage

The XAML files will need an xmlns (xml namespace) that points to the views folder, and each one of the <ContentPage> tags in the XAML file will need to look something like this:

<v:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
     ...
     x:Class="AppCenterAnalytics.Views.AboutPage"
     xmlns:v="clr-namespace:AppCenterAnalytics.Views"
     xmlns:vm="clr-namespace:AppCenterAnalytics.ViewModels"
     Title="{Binding Title}"> ...

Any view that derives from the new BaseContentPage will log an event in App Center. If you take a look at your “Log Flow” tab in AppCenter, it should look something like this when you navigate around the app.

Logging from the ViewModels

If you need to log a button command, or anything else inside your view model, simply create an instance of your singleton logging service in your BaseViewModel.cs, like this:

//The Get<T> method returns the instance of the platform implementation of interface T as a singleton, by default.
public IAppState AppState => DependencyService.Get<IAppState>();
public ILogging Logging => DependencyService.Get<ILogging>();

Now, you can use the Logging service from any of the viewmodels that derive from BaseViewModel.cs, like this one in the AboutViewModel.cs. Because we have set our non-debug mode log level to AppLogLevel.Info, this message will persist until the log level is set to a higher level.

private async Task NavigateToXamarinHomePage()
{
     base.Logging.LogEvent(AppLogLevel.Info, "Opened Xamarin.com");
     await Browser.OpenAsync("https://xamarin.com");
 }

Check out a complete working example of the patterns described in this blog post.

Have fun. Log Early. Log Often. ;-p

3 thoughts on “Xamarin.Forms Logging with AppCenter”

  1. Hello Robin,
    Thanks for that article. I thought to do something similar but I’m concerned about the event limit in AppCenter. They say „The maximum number of distinct custom events that can be tracked daily is 200.“. I’m not sure how to understand that and how fast I will reach this limit with a bunch of users out there. Can you add a section for that in your article and explain what that actually means?

    https://docs.microsoft.com/en-us/appcenter/analytics/event-metrics#limits

  2. Thanks for your question, Pepper!
    “The maximum number of distinct custom events that can be tracked daily is 200. This count resets at 12 AM UTC. However, there is no limit on the maximum number of events instances sent per device.”
    I pretty sure this means that you can track up to 200 named events every day, however there is no limit on the number of times devices can send AppCenter a message that the event occurred.
    Using the page navigation example in the blog post, if you had an app that had 100 pages, you could have an ‘on appeared’ and an ‘on disappeared’ event for each page. These 200 events would be tracked for all of devices using your app. So the number of events is capped, but not the volume of the instances of those events is not capped.
    If you had this many events, I would certainly suggest wiring up Azure Application Insights to AppCenter and crunching all of that data in Application Insights, especially if you are making use of parameters on all of those events! Best of Luck!

  3. I wrote a step-by-step, walk-through article about the question of backing up Analytics data to a more persistent storage medium like Azure a while ago on the App Center blog. You may find this article useful.

    https://devblogs.microsoft.com/appcenter/become-a-dev-rockstar-by-learning-about-your-users-with-visual-studio-app-center-and-azure/

    Plus, you can find more recipes like the one above to integrate App Center with your Xamarin app in my book that is also in the same article.

Leave a Comment

Your email address will not be published. Required fields are marked *