[WP7] – Loguer simplement et efficacement

Il y a peu de temps, la mise à jour du SDK Windows Phone a été mise à disposition des développeurs. Si elle apporte la possibilité de pouvoir développer sous Windows 8 (avec Visual Studio 2010 par contre), elle permet surtout de pouvoir développer et tester nos applications pour des téléphones “bas de gamme” (sous Tango) comme le Nokia Lumia 610 par exemple.

Pourquoi loguer ?

A cause de la diversité des périphériques (devices) et de leurs caractéristiques techniques, il peut être intéressant d’intégrer des logs au sein de vos applications (Parce que vos applications peuvent avoir des “comportements” différents selon les téléphones). Il ne suffit pas simplement de loguer, il faut aussi loguer efficacement.

Que faut-il loguer

On associe souvent les logs à des erreurs, mais ils peuvent très bien être utilisés pour s’informer, étudier les comportements de l’utilisateur (qui pourraient conduire à un plantage de l’application par exemple.. ou pas :-)).
S’il est clair qu’il faut logger avec des messages clairs, il est devenu maintenant indispensable de récupérer les informations systèmes du téléphone dans ses logs.

Les informations systèmes du téléphone

Pour récupérer les informations sur le téléphone, il n’y a plus besoin de présenter la classe DeviceExtendedProperties que vous connaissiez assurément.
En consultant le lien précédent, vous aurez sûrement remarqué le message suivant :

In Windows Phone OS 7.0, this class was used to query device-specific properties. In Windows Phone OS 7.1, most of the properties in DeviceExtendedProperties were deprecated, and the new DeviceStatus class should be used instead. However, where appropriate, you can still use any of the below properties that are not deprecated.

La classe DeviceStatus expose les propriétés de la classe DeviceExtendedProperties publiquement (et déjà typées).

Nous pouvons d’ailleurs remarquer de nouvelles propriétés qui s’avèrent pratiques pour les devices ayant un clavier physiques :-).

Si vous souhaitez en savoir davantage sur la classe DeviceStatus, n’hésite pas à lire toute l’information disponible sur la MSDN.

Voici une toute petite méthode qui vous permettra de récupérer les infos de base importante du téléphone :

    public class Log
    {
        public static string GetSystemInfo()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("==========================");
            sb.Append("MyApplication ");
            sb.AppendLine(Helpers.GetAppAttribute("Version"));
            sb.AppendFormat("{0} {1}rn", new object[] { DeviceStatus.DeviceManufacturer, DeviceStatus.DeviceName });
            sb.AppendFormat("OS Version: {0}rn", new object[] { Environment.OSVersion });
            sb.AppendFormat("DHV. & DFV. : {0}/{1}rn", new object[] {
				DeviceStatus.DeviceHardwareVersion,
				DeviceStatus.DeviceFirmwareVersion
			});
            sb.AppendFormat("RAM: ACMU {0} / APMU {1} / DTM {2}rn", new object[] {
				DeviceStatus.ApplicationCurrentMemoryUsage / 1024L / 1024L,
				DeviceStatus.ApplicationPeakMemoryUsage / 1024L / 1024L,
				DeviceStatus.DeviceTotalMemory / 1024L / 1024L
			});
            sb.AppendLine("==========================");
            return sb.ToString();
        }
    }

Ce qui m’affiche sur mon téléphone actuellement :

==========================
MyApplication 1.0.0.0
SAMSUNG SGH-i677
OS Version: Microsoft Windows CE 7.10.7720
DHV. & DFV. : 23.1.0.8/2103.11.10.1
RAM: ACMU 8 / APMU 8 / DTM 354
==========================

A propos de la méthode GetAppAttribute, elle est tirée d’un thread sur StackOverflow. Elle permet de pouvoir récupérer des informations tirée du fichier WMAppManifest.xml

    public class Helpers
    {
        public static string GetAppAttribute(string attributeName)
        {
            string appManifestName = "WMAppManifest.xml";
            string appNodeName = "App";

            var settings = new XmlReaderSettings();
            settings.XmlResolver = new XmlXapResolver();

            using (XmlReader rdr = XmlReader.Create(appManifestName, settings))
            {
                rdr.ReadToDescendant(appNodeName);
                if (!rdr.IsStartElement())
                {
                    throw new System.FormatException(appManifestName + " is missing " + appNodeName);
                }

                return rdr.GetAttribute(attributeName);
            }
        }
    }

Un exemple de classe pour loguer

Une classe pour loguer n’est pas difficile à réaliser et je vous en laisse un exemple.
Tout d’abord, elle est basée sur une classe simple pour manipuler des fichiers

using System;
using System.Diagnostics;
using System.IO;
using System.IO.IsolatedStorage;

namespace PhoneAppIsolatedStorageTests.Core
{
    public class FileHelper
    {
        public static void Create(string path, string text)
        {
            try
            {
                using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    using (var stream = store.OpenFile(path, FileMode.CreateNew))
                    {
                        using (var sw = new StreamWriter(stream))
                        {
                            sw.Write(text);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Error: {0}, while writing the file: {1}", new object[] {
                    ex,
                    path
                });
            }
        }

        public static void Append(string path, string text)
        {
            try
            {
                using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    using (var stream = store.OpenFile(path, FileMode.Append))
                    {
                        using (var sw = new StreamWriter(stream))
                        {
                            sw.Write(text);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Error: {0}, while writing the file: {1}", new object[] {
                    ex,
                    path
                });
            }
        }

        public static bool Exists(string path)
        {
            if (string.IsNullOrEmpty(path))
                return false;

            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                return store.FileExists(path);
            }
        }

        public static string Read(string path)
        {
            string result = string.Empty;
            try
            {
                using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    using (var stream = store.OpenFile(path, FileMode.OpenOrCreate))
                    {
                        using (var sr = new StreamReader(stream))
                        {
                            result = sr.ReadToEnd();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Error: {0}, while reading the file: {1}", new object[] {
                    ex,
                    path
                });
            }
            return result;
        }

        public static void Delete(string path)
        {
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (store.FileExists(path))
                {
                    store.DeleteFile(path);
                }
            }
        }
    }
}

et voici ma petite classe de logging

using System;
using System.Text;
using Microsoft.Phone.Info;
using Microsoft.Phone.Tasks;

namespace PhoneAppIsolatedStorageTests.Core
{
    public class LogHelper
    {
        private static string _logPath = "log.txt";

        public static void Delete()
        {
            FileHelper.Delete(_logPath);
        }

        public static void WriteLine(string format, params object[] args)
        {
            LogHelper.WriteLine(string.Format(format, args));
        }
        public static void WriteLine(object obj)
        {
            LogHelper.WriteLine(obj.ToString());
        }
        public static void WriteLine(string message)
        {
            if (!FileHelper.Exists(_logPath)) {
                string text = "";
                FileHelper.Create(_logPath, text);
            }
            StringBuilder sb = new StringBuilder();

            sb.AppendLine(DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
            sb.AppendLine("==========================");
            sb.AppendLine(message);
            FileHelper.Append(_logPath, sb.ToString());
        }

        public static string GetSystemInfo()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("==========================");
            sb.Append("MyApplication ");
            sb.AppendLine(Helpers.GetAppAttribute("Version"));
            sb.AppendFormat("{0} {1}rn", new object[] { DeviceStatus.DeviceManufacturer, DeviceStatus.DeviceName });
            sb.AppendFormat("OS Version: {0}rn", new object[] { Environment.OSVersion });
            sb.AppendFormat("DHV. & DFV. : {0}/{1}rn", new object[] {
				DeviceStatus.DeviceHardwareVersion,
				DeviceStatus.DeviceFirmwareVersion
			});
            sb.AppendFormat("RAM: ACMU {0} / APMU {1} / DTM {2}rn", new object[] {
				DeviceStatus.ApplicationCurrentMemoryUsage / 1024L / 1024L,
				DeviceStatus.ApplicationPeakMemoryUsage / 1024L / 1024L,
				DeviceStatus.DeviceTotalMemory / 1024L / 1024L
			});
            sb.AppendLine("==========================");
            return sb.ToString();
        }

        public static bool Send()
        {
            EmailComposeTask emailTask = new EmailComposeTask();

            string sysInfo = LogHelper.GetSystemInfo(); //We only need system info when we send log info
            string logContent = FileHelper.Read(_logPath);
            
            emailTask.Body = string.Format("{0}nr{1}", sysInfo, logContent);
            emailTask.To = "myapp@domain.truc";
            emailTask.Subject = "My Application Logs";
            emailTask.Show();
            return true;
        }
    }
}

Juste avant de finir …

On demande souvent s’il faut ou pas demander à l’utilisateur l’autorisation de loguer, si c’est légal. Le fait d’étudier le comportement de l’utilisateur, récupérer les erreurs de son application n’est pas du tout illégal, et au contraire, même recommandé. Par contre, les méthodes d’envoi sont soumises à autorisation de l’utilisateur (Surtout pour Windows Phone). Si vous souhaitez permettre à l’utilisateur d’envoyer les logs de façon silencieuse, il faudra ABSOLUMENT mettre une option quelque part permettant à l’utilisateur d’activer ou pas cette fonctionnalité. Si c’est un envoi par mail, vous n’en avez pas besoin dans la mesure ou l’utilisateur est maître de l’envoi et qu’il peut ou pas le réaliser !

Voilà, vous avez là tout ce qu’il faut pour loguer au sein de vos applications! Vous êtes curieux ? Alors, n’hésitez pas à voir dans ce projet codeplex que j’ai réalisé avec Cyril et Julien une autre approche du logging dont je parlerai peut-être une autre fois.

Bon développement et à bientôt !