Google Analytics and other tools could help you monitor your application in a generic way. If there has been a few users or clients who keep complaining about performance of your application, you'll need to monitor their activities and the system response time for these users in particular.

With the help of MiniProfiler and Log4Net, this task can be done within an hour.

Before we start, here are my assumptions:

  1. Your application is an ASP .NET MVC or Web Forms application.
  2. You want to know how long it takes the server to serve the HTTP requests from the moment your application receive the request until a response has been sent out.
    The metric of choice is milliseconds.
  3. You have your client or user ID saved in Session right after they login to the application.

After you have done all the hard work, you should see a similar result:



Let’s do it.

  1. Right click your application in the Solution Explorer to Manage NuGet Packages. Then search and install MiniProfiler.
  2. You should see a new reference added to your project.
    Commit or check-in your code to source control now if you have one.
  3. Next, use NuGet again to install Log4Net.
  4. And you’ll see new reference:
    Commit or check-in your code to source control now if you have one.
  5. If you have a multi-tiered architect, you need to consult your team about which tier, project or folder you should place the new code that I will give to you in next step. If you don’t have multi-tiered architect (meaning, separate projects for MVC, Logic, Data Access,…), you should create the special App_Code folder:
  6. Copy and Paste the Profiler class to under the App_Code or your folder of choice:

    // -------------------------------------------------------------------------------------------------------------------- // <copyright file="Profiler.cs" company="Believe2014"> // WhenYouBelieve2014@gmail.com // </copyright> // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Web; using StackExchange.Profiling; namespace Mvc4Application { /// <summary> /// The profiler. /// </summary> public class Profiler { /// <summary> /// The key to obtain current user's login ID. /// </summary> public const string SESSION_KEY_CURRENT_USER = "CurrentUser.LoginID"; /// <summary> /// The initialize. /// </summary> internal static void Initialize() { MiniProfiler.Settings.Storage = new Log4NetStorage(); if (MiniProfiler.Settings.IgnoredPaths == null) return; // add partials of the URLs that you don't want to profile List<string> ignored = MiniProfiler.Settings.IgnoredPaths.ToList(); ignored.Add("images"); ignored.Add("style"); ignored.Add("include"); ignored.Add("jsdebug"); ignored.Add(".axd"); ignored.Add(".js"); ignored.Add(".png"); ignored.Add(".ashx"); MiniProfiler.Settings.IgnoredPaths = ignored.ToArray(); } /// <summary> /// Start profiling the current request. /// </summary> /// <param name="context"> /// The request context. /// </param> /// <remarks> /// The current user may not belong to the profiler target which is set in the web.config file. /// In that case, don't start Miniprofiler. /// </remarks> internal static void Start(HttpContext context) { if (context == null || context.Session == null) return; var user = context.Session[SESSION_KEY_CURRENT_USER] as string; if (string.IsNullOrEmpty(user)) return; if (!ProfilerSetting.CheckUser(user)) return; MiniProfiler.Start(); if (MiniProfiler.Current == null) return; MiniProfiler.Current.User = user; } /// <summary> /// stop profiling the current request. /// </summary> /// <remarks> /// MiniProfiler internally decide when to write messages to Log4Net. /// Log4Net is configured in the web.config to write messages that it receives from MiniProfiler to the disk at its own /// timing. /// </remarks> internal static void Stop() { MiniProfiler.Stop(); if (MiniProfiler.Settings.Storage != null && MiniProfiler.Current != null) MiniProfiler.Settings.Storage.Save(MiniProfiler.Current); } /// <summary> /// The profiler setting. /// </summary> internal class ProfilerSetting { /// <summary> /// The AppSetting key for list of user login IDs to run profile. /// </summary> private const string CONFIG_KEY_PROFILER_TARGET_LOGIN_LIST = "ProfilerTargetLoginCommaSeparatedList"; /// <summary> /// Initializes static members of the <see cref="ProfilerSetting" /> class. /// </summary> /// <remarks> /// Load the ProfilerTargetLoginCommaSeparatedList setting from web.config. /// </remarks> static ProfilerSetting() { string appSettingValue; try { appSettingValue = ConfigurationManager.AppSettings[CONFIG_KEY_PROFILER_TARGET_LOGIN_LIST]; } catch (Exception anyError) { throw new ConfigurationErrorsException(string.Format("Cannot read AppSettings for key<{0}>", CONFIG_KEY_PROFILER_TARGET_LOGIN_LIST), anyError); } if (appSettingValue != null) ProfilerTargetLoginCommaSeparatedList = appSettingValue.Split(','); } /// <summary> /// Gets or sets the profiler target login comma separated list. /// </summary> public static string[] ProfilerTargetLoginCommaSeparatedList { get; set; } /// <summary> /// Check the user against the ProfilerTargetLoginCommaSeparatedList setting in web.config. /// </summary> /// <param name="user"> /// The user. /// </param> /// <returns> /// True if the list contains the user; Otherwise, false. /// </returns> public static bool CheckUser(string user) { if (user == null || ProfilerTargetLoginCommaSeparatedList == null) return false; return ProfilerTargetLoginCommaSeparatedList.Any( loginIdToCheck => string.Compare(loginIdToCheck, user, StringComparison.CurrentCultureIgnoreCase) == 0); } } } }
  7. This code has 2 classes: Profiler and ProfilerSetting (this is a nested class).
    The Profiler class reads the current user’s login ID from the Session using the key “CurrentUser.LoginID”.
    The ProfilerSetting class reads a list of user login IDs from web.config file.
  8. Create the config entry for indicating whose requests should be profiled.
    In this example, I presume usernames are email addresses.
  9. Copy and paste Log4NetStorage class to the same above:

    // -------------------------------------------------------------------------------------------------------------------- // <copyright file="Log4NetStorage.cs" company="Believe2014"> // WhenYouBelieve2014@gmail.com // </copyright> // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using log4net; using log4net.Config; using StackExchange.Profiling; using StackExchange.Profiling.Storage; namespace Mvc4Application { /// <summary> /// User Log4Net as storage. /// </summary> internal class Log4NetStorage : IStorage { /// <summary> /// The logger by Log4Net. /// </summary> private static readonly ILog Log4NetLogger; /// <summary> /// Initializes static members of the <see cref="Log4NetStorage" /> class. /// </summary> static Log4NetStorage() { XmlConfigurator.Configure(); Log4NetLogger = LogManager.GetLogger(typeof (Log4NetStorage)); } /// <summary> /// Returns a list of <see cref="P:StackExchange.Profiling.MiniProfiler.Id"/>s that haven't been seen by /// <paramref name="user"/>. /// </summary> /// <param name="user"> /// User identified by the current <c>MiniProfiler.Settings.UserProvider</c> /// </param> /// <returns> /// the list of key values. /// </returns> public List<Guid> GetUnviewedIds(string user) { throw new NotSupportedException("This method should never run"); } /// <summary> /// list the result keys. /// </summary> /// <param name="maxResults"> /// The max results. /// </param> /// <param name="start"> /// The start. /// </param> /// <param name="finish"> /// The finish. /// </param> /// <param name="orderBy"> /// order by. /// </param> /// <returns> /// the list of keys in the result. /// </returns> public IEnumerable<Guid> List(int maxResults, DateTime? start = null, DateTime? finish = null, ListResultsOrder orderBy = ListResultsOrder.Descending) { throw new NotSupportedException("This method should never run"); } /// <summary> /// Returns a <see cref="T:StackExchange.Profiling.MiniProfiler"/> from storage based on <paramref name="id"/>, which /// should map to <see cref="P:StackExchange.Profiling.MiniProfiler.Id"/>. /// </summary> /// <param name="id"> /// The id. /// </param> /// <remarks> /// Should also update that the resulting profiler has been marked as viewed by its profiling /// <see cref="P:StackExchange.Profiling.MiniProfiler.User"/>. /// </remarks> /// <returns> /// The <see cref="T:StackExchange.Profiling.MiniProfiler"/>. /// </returns> public MiniProfiler Load(Guid id) { throw new NotSupportedException("This method should never run"); } /// <summary> /// Stores <paramref name="profiler"/> under its <see cref="P:StackExchange.Profiling.MiniProfiler.Id"/>. /// </summary> /// <param name="profiler"> /// The results of a profiling session. /// </param> /// <remarks> /// Should also ensure the profiler is stored as being un-viewed by its profiling /// <see cref="P:StackExchange.Profiling.MiniProfiler.User"/>. /// </remarks> public void Save(MiniProfiler profiler) { if (profiler == null || Log4NetLogger == null) return; Log4NetLogger.Info(string.Format("User<{0}>: {1}", profiler.User, profiler)); } /// <summary> /// Sets a particular profiler session so it is considered "un-viewed" /// </summary> /// <param name="user"> /// The user. /// </param> /// <param name="id"> /// The id. /// </param> public void SetUnviewed(string user, Guid id) { // do nothing } /// <summary> /// Sets a particular profiler session to "viewed" /// </summary> /// <param name="user"> /// The user. /// </param> /// <param name="id"> /// The id. /// </param> public void SetViewed(string user, Guid id) { throw new NotSupportedException("This method should never run"); } } }
  10. This class saves the state of a MiniProfiler instance to a Log4NetLogger at information level.
  11. Right click the 2 class files you have just added, look at their properties, change Build Action to Compile to make sure other code behind or controller classes can call them.
  12. Check and make sure that you save the current user’s login ID to the Session using the same key as the Profiler class expects.

  13. // POST: /Account/Login /// <summary> /// The login. /// </summary> /// <param name="model"> /// The model. /// </param> /// <param name="returnUrl"> /// The return url. /// </param> /// <returns> /// The <see cref="ActionResult"/>. /// </returns> [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Login(LoginModel model, string returnUrl) { if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, model.RememberMe)) { Session[Profiler.SESSION_KEY_CURRENT_USER] = model.UserName; return RedirectToLocal(returnUrl); } // If we got this far, something failed, redisplay form ModelState.AddModelError(string.Empty, "The user name or password provided is incorrect."); return View(model); }


  14. Specify the folder for Log4Net to create and write log files.



    For your convenience, you can copy the config file content:
    <?xml version="1.0" encoding="utf-8"?> <!-- For more information on how to configure your ASP.NET application, please visit http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /> </configSections> <connectionStrings> <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-Mvc4Application-20140604154121;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-Mvc4Application-20140604154121.mdf" providerName="System.Data.SqlClient" /> </connectionStrings> <appSettings> <add key="webpages:Version" value="" /> <add key="webpages:Enabled" value="false" /> <add key="PreserveLoginUrl" value="true" /> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" /> <!-- All 3rd party config keys go above this line --> <add key="ProfilerTargetLoginCommaSeparatedList" value="AgentSmith@gmail.com,WhenYouBelieve2014@gmail.com" /> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> <authentication mode="Forms"> <forms loginUrl="~/Account/Login" timeout="2880" /> </authentication> <pages> <namespaces> <add namespace="System.Web.Helpers" /> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Optimization" /> <add namespace="System.Web.Routing" /> <add namespace="System.Web.WebPages" /> </namespaces> </pages> </system.web> <system.webServer> <validation validateIntegratedModeConfiguration="false" /> <handlers> <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" /> <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" /> <remove name="ExtensionlessUrlHandler-Integrated-4.0" /> <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> </handlers> </system.webServer> <log4net> <root> <level value="ALL" /> <appender-ref ref="RollingLogFileAppender" /> </root> <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender"> <file value="C:\Codeplex\miniprofilerlog4net\MiniExample\Mvc4Application\App_Data\rolling.log" /> <appendToFile value="true" /> <rollingStyle value="Date" /> <datePattern value="yyyyMMdd-HHmm" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" /> </layout> </appender> </log4net> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="DotNetOpenAuth.Core" publicKeyToken="2780ccd10d57b246" /> <bindingRedirect oldVersion="" newVersion="" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="DotNetOpenAuth.AspNet" publicKeyToken="2780ccd10d57b246" /> <bindingRedirect oldVersion="" newVersion="" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="" newVersion="" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="" newVersion="" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="" newVersion="" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="" newVersion="" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" /> <bindingRedirect oldVersion="" newVersion="" /> </dependentAssembly> </assemblyBinding> </runtime> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v12.0" /> </parameters> </defaultConnectionFactory> </entityFramework> </configuration>
  15. Modify the Global class to initialize the storage, start and stop profilers.
    // -------------------------------------------------------------------------------------------------------------------- // <copyright file="Global.asax.cs" company="Believe2014"> // WhenYouBelieve2014@gmail.com // </copyright> // <summary> // The mvc application. // </summary> // -------------------------------------------------------------------------------------------------------------------- using System; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace Mvc4Application { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 /// <summary> /// The mvc application. /// </summary> public class MvcApplication : HttpApplication { /// <summary> /// The application_ start. /// </summary> protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); AuthConfig.RegisterAuth(); Profiler.Initialize(); } /// <summary> /// The event when the application acquires request state. /// </summary> /// <param name="sender"> /// The sender. /// </param> /// <param name="e"> /// The event argument.. /// </param> protected void Application_AcquireRequestState(object sender, EventArgs e) { Profiler.Start(HttpContext.Current); } /// <summary> /// This function is called by ASP .NET at the end of every http request. /// </summary> /// <param name="sender"> /// The sender. /// </param> /// <param name="e"> /// The event argument. /// </param> protected void Application_EndRequest(object sender, EventArgs e) { Profiler.Stop(); } } }