2
\$\begingroup\$

I have worked on one application related for booking space and required to store activity of user into table and access from admin side. For that I have created one database entity and EventLogger class.

public partial class UserEventLog
{
    public int ID { get; set; }
    public string UserID { get; set; }
    public Enum_SourceType SourceType { get; set; }
    public Enum_EventType EventType { get; set; }
    public string EventId { get; set; }
    public string EventName { get; set; }
    public Enum_LogType LogType { get; set; }
    public string LogMessage { get; set; }
    public string LogDescription { get; set; }
    public string IPAddress { get; set; }
    public string HostName { get; set; }
    public DateTimeOffset EventTriggeredOn { get; set; }
    public virtual AspNetUser User { get; set; }
}

related enum

public enum Enum_SourceType
{
    User,
    Order
}
public enum Enum_LogType
{
    Information,
    Warning,
    Error
}
public enum Enum_EventType
{
    Create,
    Update,
    Delete,
    Duplicate,
    DownLoad,
    Preview,
    Search,
    AppliedCoupon,
    Cancel,
    LoggedIn,
    Register,
    SendMail,
    ConfirmEmail,
    Account,
    View,
    SendMessage
}

and I have created EventLogger class for log event from whole application.

  public class EventLogger 
{

    public static IUserEventLogService UserEventLogService
    {
        get
        {
            return Core.ContainerManager.GetConfiguredContainer().Resolve<IUserEventLogService>();
        }
    }
    public static IUnitOfWorkAsync UnitOfWork
    {
        get
        {
            return Core.ContainerManager.GetConfiguredContainer().Resolve<IUnitOfWorkAsync>();
        }
    }
    public static async Task Log(DateTimeOffset eventTriggeredOn, Enum_EventType eventType, string eventName, Enum_LogType logType, string logMessage,string eventId = "", string logDescription = "")
    {

        var currentUserId = System.Web.HttpContext.Current.User.Identity.GetUserId();
        string ipAddress = GetLocalIPAddress();
        var userEventLog = new UserEventLog()
        {
            EventId = eventId,
            LogType = logType,
            EventName = eventName,
            EventTriggeredOn = eventTriggeredOn,
            EventType = eventType,
            LogMessage = logMessage,
            LogDescription = logDescription,
            IPAddress = ipAddress,
            UserID = currentUserId,
            ObjectState = Repository.Pattern.Infrastructure.ObjectState.Added,
            SourceType = Enum_SourceType.User
        };
        UserEventLogService.Insert(userEventLog);
       await UnitOfWork.SaveChangesAsync();
    }
    public static string GetLocalIPAddress()
    {
        string visitorIPAddress = GetVistorIp(HttpContext.Current.Request.ServerVariables);  
        return visitorIPAddress;
    }
    static string GetVistorIp(NameValueCollection headers)
    {
        if (!string.IsNullOrWhiteSpace(headers["HTTP_X_FORWARDED_FOR"]))
            return headers["HTTP_X_FORWARDED_FOR"];
        if (!string.IsNullOrWhiteSpace(headers["HTTP_CF_CONNECTING_IP"]))
            return headers["HTTP_CF_CONNECTING_IP"];
        return headers["REMOTE_ADDR"];
    }
}

Please suggest me if this solution is right or require any modification or any good solution for suggestion for my implementation.

\$\endgroup\$
1
  • \$\begingroup\$ Drop the static class and inherit from an interface and use DI, your code will be testable and injectable to whomever needs it! \$\endgroup\$
    – PmanAce
    Apr 9, 2018 at 16:50

2 Answers 2

7
\$\begingroup\$

Again enums should not have any prefix. Drop all those Enum_! You may like to take a look to .NET Naming Guidelines.

Inside the class UserEventLog you do not need to repeat Log prefix for its properties. For example just Message is enough. Think about a Window object: did you ever see properties named WindowTitle, WindowSize, WindowCanMaximize and so on?

Validate values, I might assign null to LogMessage (or an invalid enum for EventType) and it will go straight to the DB engine (which might reject it or not).

Do not hide resolved services in a property like UnitOfWork and UserEventLogService. If you use those objects more than once in the same call then it's a performance hit you do not want to pay (because it has to be resolved for each call and it might event be a new object, it depends on how it's configured in your DI framework). Resolve them once in ctor or better accept them as parameters.

In addition of a single method Log() with all those parameters you may want to introduce few extension methods (or overloads):

public static async Task LogUserWarning(this EventLogger logger,
                                        DateTimeOffset eventTriggeredOn,
                                        string eventName,
                                        string message)
{
    // ...
}

Think twice before using default values for parameters. If you own this code and the calling code then it's not an issue (probably) but if your code is released as a library, to be used by someone else, then you're introducing a strong contract (simply changing default value will not update callers unless they're recompiled, not to mention that they have to check if new values are what they expect). For a real-world example of this issue you may read Adding option parameter to a library in c# without creating a breaking change .

Using String.Empty instead of null for a string entry in a DB may be a waste but it depends on DB engine you're using (for example compare SQL Server and Oracle).

I see no reason for EventLogger to do not be sealed.

Do not use System.Web.HttpContext.Current directly. Accessing a static property makes impossible to test your code in unit testing outside an HTTP request. The same is true for GetLocalIPAddress(). If really you can't avoid it then move it to a derived class, at least you can test all the underlying logic in isolation. Better would be to introduce a service to retrieve those information:

interface IHttpRequestInspector
{
    string GetIpAddress();
    string GetAuthenticatedUserName();
}

Now you can create a default implementation using the code you already have but you can also have a MockedHttpRequestInspector class which can be used in unit testing:

sealed class MockedHttpRequestInspector : IHttpRequestInspector
{
    public MockedHttpRequestInspector(string ipAddress, authenticatedUserName)
    {
        _ipAddress = ipAddres;
        _authenticatedUserName = authenticatedUserName;
    }

    public string GetIpAddress() => _ipAddress;
    public string GetAuthenticatedUserName() => _authenticatedUserName;

    private readonly string _ipAddress;
    private readonly string _authenticatedUserName;
}

Some properties you declared in UserEventLog are not used in code you show (for example UserEventLog.User). Drop them?

\$\endgroup\$
13
  • \$\begingroup\$ for log event i have used every time await EventLogger.Log(DateTimeOffset.Now, Enum_EventType.Update, "Update Listing", Enum_LogType.Information, "Completed", listing.ID.ToString()); \$\endgroup\$ Jan 25, 2018 at 9:34
  • \$\begingroup\$ in my implementation in every call savechanges() in db is there any way to reduce this database call. \$\endgroup\$ Jan 25, 2018 at 9:36
  • \$\begingroup\$ Then you may create a simple most used, overload or extension method for that. To simplify caller to EventLogger.LogInfo(EventType.Update, "Update Listing", "Completed", listing.ID.ToString()); \$\endgroup\$ Jan 25, 2018 at 9:36
  • \$\begingroup\$ You may introduce a mem-cache to save batches but I'd add such complexity only if you have a MEASURED requirement. Also ORM and DBE you're using may already have in-place effective optimizations. \$\endgroup\$ Jan 25, 2018 at 9:39
  • \$\begingroup\$ "Do not use System.Web.HttpContext.Current directly" then for get ip address i need to pass parameter in log() method right? and pass this ip value from calling method \$\endgroup\$ Jan 25, 2018 at 9:41
1
\$\begingroup\$

Logger are very important for any project. In general logger design should be flexible enough to provide custom implementation along with that it should be able to work with existing log providers available in market such as Log4net etc..

I have an application in GitHub where you can use it.

visit https://github.com/tgvishnu/CSharp/tree/master/Vishnu.Logger

  1. Log interface should expose simple method and add as extension methods

    public interface ILog
    {
       bool Log(LogLevel logLevel, Func<string> message, Exception exception = null);
    }
    
  2. ILogProvider interface return the ILog implementer

    public interface ILogProvider
    {
        ILog GetLogger(string loggerName);
    }
    
  3. Create an enum for different Log levels

    public enum LogLevel
    {
         Trace,
         Debug,
         Info,
         Warn,
         Error,
         Fatal
     }
    
  4. Add extension methods to ILog inteface for each type of enum

    public static class LogExtensions
    {
        private static void LogFormat(this ILog logger, LogLevel logLevel, string message, params object[] args)
        {
            var result = string.Format(CultureInfo.InvariantCulture, message, args);
            logger.Log(logLevel, () => message);
         }
    
         public static bool IsDebugEnabled(this ILog logger)
         {
            if (logger.IsDebugEnabled())
            {
                logger.Log(LogLevel.Debug, () => message);
            }
         }
    
         public static void Debug(this ILog logger, string message)
         {
             if (logger.IsDebugEnabled())
             {
                logger.Log(LogLevel.Debug, message);
             }
         }
         public static void Debug(this ILog logger, Func<string> message)
         {
             if (logger.IsDebugEnabled())
             {
                logger.Log(LogLevel.Debug, () => message, exception);
             }
         }
    
         public static void DebugException(this ILog logger, string message, Exception exception)
         {
             if (logger.IsDebugEnabled())
             {
                  logger.LogFormat(LogLevel.Debug, message, args);
              }
         }
    
         public static void DebugFormat(this ILog logger, string message, params object[] args)
         public static bool IsErrorEnabled(this ILog logger)
         public static void Error(this ILog logger, string message)
         public static void Error(this ILog logger, Func<string> message)
         public static void ErrorException(this ILog logger, string message, Exception exception)
         public static void ErrorFormat(this ILog logger, string message, params object[] args)
         public static bool IsTraceEnabled(this ILog logger)
             .......
    
         public static string DefaultMessageFormat(this ILog logger, string component, LogLevel level, string message, Exception ex)
        {
            var stringBuilder = new StringBuilder();
            stringBuilder.Append(DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss", CultureInfo.InvariantCulture));
            stringBuilder.Append(" ");
            stringBuilder.Append(component);
            stringBuilder.Append(" => ");
            stringBuilder.Append(("[" + level.ToString().ToUpper() + "]").PadRight(8));
            stringBuilder.Append(message);
            if (ex != null)
            {
             stringBuilder.Append(Environment.NewLine).Append(ex.GetType());
             stringBuilder.Append(Environment.NewLine).Append(ex.Message);
            stringBuilder.Append(Environment.NewLine).Append(ex.StackTrace);
             }
    
           return stringBuilder.ToString();
        }
    
  5. Create a provider class in the project , where you can define list of available for the solution. Add the log provider available, the log providers should be added in the sequence based on their usage.

    public static class SampleLogProviders
    {
        private static ILogProvider _currentLogProvider = null;
        public delegate bool IsLoggerAvailable();
        public delegate ILogProvider CreateLogProvider();
        public static readonly List<Tuple<IsLoggerAvailable, CreateLogProvider>> LogProviderCollection = new List<Tuple<IsLoggerAvailable, CreateLogProvider>>()
        {
             new Tuple<IsLoggerAvailable, CreateLogProvider>(DebugLogProvider.IsLoggerAvailable, ()=> new DebugLogProvider()),
             new Tuple<IsLoggerAvailable, CreateLogProvider>(ConsoleLogProvider.IsLoggerAvailable, ()=> new ConsoleLogProvider()),
        };
    
        public static ILog GetLogger(string name)
        {
            ILogProvider provider = _currentLogProvider ?? ResolveLogProvider();
            ILog result = provider.GetLogger(name) ?? new NoLog();
            return result;
         }
    
         private static ILogProvider ResolveLogProvider()
         {
              ILogProvider result = null;
              foreach(var provider in LogProviderCollection)
              {
                  if(provider.Item1() == true)
                  {
                       result = provider.Item2();
                       break;
                   }
               }
    
               return result;
             }
     }
    
  6. Create Log providers, in the above step we have used DebugProvider and ConsoleProvider, example of Console provider is shown below

    public class ConsoleLogProvider : ILogProvider
    {
        public ILog GetLogger(string loggerName)
        {
            return new ConsoleLog(loggerName);
        }
        public static bool IsLoggerAvailable()
        {
            return true;
        }
    }
    
  7. Create LogBase class (this is optional)

    public abstract class LogBase : ILog
    {
        public virtual bool Log(LogLevel logLevel, Func<string> message, Exception exception = null)
        {
            if (message == null)
            {
                return this.IsLoggerEnabled(logLevel);
            }
    
            this.WriteMessage(logLevel, message(), exception);
            return true;
        }
    
        protected string GetDetaultMessage(string component, LogLevel level, string message, Exception ex)
        {
            return this.DefaultMessageFormat(component, level, message, ex);
        }
    
        protected abstract void WriteMessage(LogLevel level, string message, Exception ex = null);
    
        protected virtual bool IsLoggerEnabled(LogLevel logLevel)
        {
            return true;
        }
    }
    
  8. Create Console Log class derived from LogBase or implement ILog interface directly.

    namespace Vishnu.Logger
    {
        public class ConsoleLog : LogBase
        {
            private readonly dynamic _logger;
    
            private Dictionary<LogLevel, ConsoleColor> _foregroundColors = new Dictionary<LogLevel, ConsoleColor>
                        {
                            { LogLevel.Fatal, ConsoleColor.Red },
                            { LogLevel.Error, ConsoleColor.Yellow },
                            { LogLevel.Warn, ConsoleColor.Magenta },
                            { LogLevel.Info, ConsoleColor.White },
                            { LogLevel.Debug, ConsoleColor.Gray },
                            { LogLevel.Trace, ConsoleColor.DarkGray },
                        };
    
            public ConsoleLog(dynamic logger)
            {
                _logger = logger;
            }
    
            protected override void WriteMessage(LogLevel level, string message, Exception ex = null)
            {
                ConsoleColor color = ConsoleColor.White;
                string msg = base.GetDetaultMessage((string)_logger, level, message, ex);
                if (_foregroundColors.TryGetValue(level, out color))
                {
                    var originalColor = Console.ForegroundColor;
                    try
                    {
                        Console.ForegroundColor = color;
                        Console.Out.WriteLine(msg);
                    }
                    finally
                    {
                        Console.ForegroundColor = originalColor;
                    }
                }
                else
                {
                    Console.Out.WriteLine(msg);
                }
            }
    
            protected override bool IsLoggerEnabled(LogLevel logLevel)
            {
                return base.IsLoggerEnabled(logLevel);
            }
        }
    }
    
  9. Create a Logger class in the main application, and enable log for each features that you want to log. Logging for each feature can be set from App.config settings

    public class Logger
    {
        private static readonly ILog _feature1 = Create(typeof(Feature1));
        private static readonly ILog _feature2 = Create(typeof(Feature2));
        private static readonly ILog _feature3 = Create("Feature3");
    
        public static ILog Feature1
        {
            get
            {
                return _feature1;
            }
        }
    
        public static ILog Feature2
        {
            get
            {
                return _feature2;
            }
        }
    
        public static ILog Feature3
        {
            get
            {
                return _feature3;
            }
        }
    
        internal static ILog Create(string name)
        {
            return InitializeLogFacade(name);
        }
    
        internal static ILog Create(Type type)
        {
            return Create(type.FullName);
        }
    
        private static ILog InitializeLogFacade(string name)
        {
            return LogProviders.GetLogger(name);
        }
    } 
    
  10. Use the logger as below

    public class Feature1
    {
        public static void GenerateLogs(string message)
        {
            Logger.Feature1.Debug(message);
            Logger.Feature1.Fatal(message);
            Logger.Feature1.Error(message);
            Logger.Feature1.Trace(message);
            Logger.Feature1.Info(message);
            Logger.Feature1.Warning(message);
            Logger.Feature1.DebugException(message, new Exception("Debug exception"));
            Logger.Feature1.FatalException(message, new Exception("Fatal exception"));
            Logger.Feature1.ErrorException(message, new Exception("Error exception"));
            Logger.Feature1.TraceException(message, new Exception("Trace exception"));
            Logger.Feature1.InfoException(message, new Exception("Info exception"));
            Logger.Feature1.WarningException(message, new Exception("Warning exception"));
    
        }
    }
    
\$\endgroup\$
2
  • \$\begingroup\$ Why so complicated? Why not pass the logger interface directly by DI to whoever needs it? No need for most of your code. \$\endgroup\$
    – PmanAce
    Apr 9, 2018 at 16:49
  • \$\begingroup\$ Design is to provide 1. compatibility any many providers available in market [SerilogLog, NLog, Log4Net, EntLib, LoupeLog..) as well as to add custom implementation (EventWriter, Debug, Console, text..). 2. Enable logging based on the features/components. If we go with DI, when the requirement changes to use providers in market then we need to change the interface, and to log only specific components / features with this supports all cases \$\endgroup\$ Apr 10, 2018 at 2:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.