LibLog

מדי פעם יוצא לנו לכתוב ספריות Reusable. (נקרא מדי פעם גם "תשתיות תוכנה")

כדי שספריות אלה יהיו איכותיות, כדאי שיהיה בהן בין השאר Logging, כדי שנוכל לקבל חיווי על מה הספריה עושה ובאיזה שגיאות היא נתקלה.

לצערנו אין בFramework כלי Log סביר, ולכן בדרך כלל אנו פונים לספריה חיצונית שעושה זאת בצורה מצטיינת. בד”כ log4net היא הספריה הנבחרת לצרכים אלה. אלא שלעתים מתכנת המשתמש בספריה שלנו משתמש כבר בתשתית Logים אחרת. (למשל NLog, Enterprise Library וכו’)

הדבר הזה מכריח את המשתמש להוסיף Reference לספריית Logם של הספריה, ומכריחה אותו בדרכים לא דרכים לגרום לספריית הLogים של הספריה להשתמש בספריית הLogים של האפליקציה, כדי שהLogים ירשמו למקומות הנכונים.

למרבה המזל, הדבר אפשרי, מאחר ותשתית Logים המכבדת את עצמה בד”כ מאפשרת מנגנון של Appenderים - מקומות אליהם לכתוב את הLog, ולכן ניתן ליצור Appender שיפנה למנגנון Logים אחר.

היו בעבר כבר כמה ניסיונות לגשר על פער זה: הרבה ספריות מכילות ממש אבסטרקטי לLog לו יש מימושים בDllים חיצוניים, למשל לCastle יש ממשק בשם ILogger שנראה כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public interface ILogger
{
bool IsDebugEnabled { get; }
bool IsErrorEnabled { get; }
bool IsFatalEnabled { get; }
bool IsInfoEnabled { get; }
bool IsWarnEnabled { get; }
ILogger CreateChildLogger(String loggerName);
void Debug(String message);
void Debug(Func<string> messageFactory);
void Debug(String message, Exception exception);
void DebugFormat(String format, params Object[] args);
void DebugFormat(Exception exception, String format, params Object[] args);
void DebugFormat(IFormatProvider formatProvider, String format, params Object[] args);
void DebugFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args);
void Error(String message);
void Error(Func<string> messageFactory);
void Error(String message, Exception exception);
void ErrorFormat(String format, params Object[] args);
void ErrorFormat(Exception exception, String format, params Object[] args);
void ErrorFormat(IFormatProvider formatProvider, String format, params Object[] args);
void ErrorFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args);
void Fatal(String message);
void Fatal(Func<string> messageFactory);
void Fatal(String message, Exception exception);
void FatalFormat(String format, params Object[] args);
void FatalFormat(Exception exception, String format, params Object[] args);
void FatalFormat(IFormatProvider formatProvider, String format, params Object[] args);
void FatalFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args);
void Info(String message);
void Info(Func<string> messageFactory);
void Info(String message, Exception exception);
void InfoFormat(String format, params Object[] args);
void InfoFormat(Exception exception, String format, params Object[] args);
void InfoFormat(IFormatProvider formatProvider, String format, params Object[] args);
void InfoFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args);
void Warn(String message);
void Warn(Func<string> messageFactory);
void Warn(String message, Exception exception);
void WarnFormat(String format, params Object[] args);
void WarnFormat(Exception exception, String format, params Object[] args);
void WarnFormat(IFormatProvider formatProvider, String format, params Object[] args);
void WarnFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args);
}

יש גם פרויקט ממש מגניב בשם Common.Logging שכתב Appenderים המאפשרים להמיר כתיבה לכל תשתית Logים מוכרת לתשתית Logים מוכרת אחרת. הם אפילו תומכים במספר גרסאות של תשתיות logים, למשל הם מאפשרים לכתוב מגרסה 1.1 של log4net לגרסה 1.2 של log4net!

אלא שכל הפתרונות הנ”ל לוקים בבעיה ההתחלתית: אנו מכריחים את המשתמש להוסיף Reference לתשתית Logים שהוא לא רוצה להשתמש בה ישירות.


אז על מה הטיפ? מסתבר שיש פרויקט בשם LibLog הפותר את הבעיה הזו. השימוש הוא כנ”ל: מתקינים מהNuGet חבילה בשם LibLog, ויש Logים בפרויקט. אלא שלא נוסף Reference:

NuGet-Install.png

ואז נוסף לנו קובץ cs לcsproj:

solution-explorer.png

עם namespace בשם SuperLibrary.Logging (כאשר SuperLibrary הnamespace של הספריה שלנו). קובץ זה מכיל הרבה ממשקים ומימושים שלהם.

בקובץ זה יש מחלקה בשם LogProvider תחת הnamespace הנ”ל המספק ממשק בשם ILog שנראה כמו לוג סטנדרטי:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// Simple interface that represent a logger.
/// </summary>
public interface ILog
{
/// <summary>
/// Log a message the specified log level.
/// </summary>
/// <param name="logLevel">The log level.</param>
/// <param name="messageFunc">The message function.</param>
/// <param name="exception">An optional exception.</param>
/// <param name="formatParameters">Optional format parameters for the message generated by the messagefunc. </param>
/// <returns>true if the message was logged. Otherwise false.</returns>
/// <remarks>
/// Note to implementers: the message func should not be called if the loglevel is not enabled
/// so as not to incur performance penalties.
///
/// To check IsEnabled call Log with only LogLevel and check the return value, no event will be written.
/// </remarks>
bool Log(LogLevel logLevel, Func<string> messageFunc, Exception exception = null, params object[] formatParameters );
}

כעת ניתן להשתמש במחלקה זו: קבלת Log:

1
private ILog mLog = LogProvider.GetLogger(typeof(MyReusableClass));

ואז כתיבה לLog:

1
mLog.Info("Hello log!");

אבל איך עובדת הכתיבה לLog? הכתיבה עובדת עם ספריה פופולרית, בהנחה והReference שלה זמין. איך זה עובד? הספריה בודקת בReflection האם אחת מהספריות הפופולריות זמינה ובמידה וכן, היא משתמשת בה בReflection. לדוגמה, כך נראה חלק מהמימוש הlog4netי של הספריה: (חתכתי את הרוב, אבל תסתכלו בקובץ)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
private static Func<string, object> GetGetLoggerMethodCall()
{
Type logManagerType = GetLogManagerType();
MethodInfo method = logManagerType.GetMethodPortable("GetLogger", new[] { typeof(string) });
ParameterExpression nameParam = Expression.Parameter(typeof(string), "name");
MethodCallExpression methodCall = Expression.Call(null, method, nameParam);
return Expression.Lambda<Func<string, object>>(methodCall, nameParam).Compile();
}
public override ILog GetLogger(string name)
{
return new Log4NetLogger(_getLoggerByNameDelegate(name));
}
internal static bool IsLoggerAvailable()
{
return ProviderIsAvailableOverride && GetLogManagerType() != null;
}
private static Type GetLogManagerType()
{
return Type.GetType("log4net.LogManager, log4net");
}
public class Log4NetLogger : ILog
{
private readonly dynamic _logger;
internal Log4NetLogger(dynamic logger)
{
_logger = logger;
}
public bool Log(LogLevel logLevel, Func<string> messageFunc, Exception exception, params object[]
{
if (messageFunc == null)
{
return IsLogLevelEnable(logLevel);
}
messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters);
if (exception != null)
{
return LogException(logLevel, messageFunc, exception);
}
switch (logLevel)
{
case LogLevel.Info:
if (_logger.IsInfoEnabled)
{
_logger.Info(messageFunc());
return true;
}
break;
case LogLevel.Warn:
if (_logger.IsWarnEnabled)
{
_logger.Warn(messageFunc());
return true;
}
break;
case LogLevel.Error:
if (_logger.IsErrorEnabled)
{
_logger.Error(messageFunc());
return true;
}
break;
case LogLevel.Fatal:
if (_logger.IsFatalEnabled)
{
_logger.Fatal(messageFunc());
return true;
}
break;
default:
if (_logger.IsDebugEnabled)
{
_logger.Debug(messageFunc()); // Log4Net doesn't have a 'Trace' level, so all Tra
return true;
}
break;
}
return false;
}
private bool LogException(LogLevel logLevel, Func<string> messageFunc, Exception exception)
{
switch (logLevel)
{
case LogLevel.Info:
if (_logger.IsDebugEnabled)
{
_logger.Info(messageFunc(), exception);
return true;
}
break;
case LogLevel.Warn:
if (_logger.IsWarnEnabled)
{
_logger.Warn(messageFunc(), exception);
return true;
}
break;
case LogLevel.Error:
if (_logger.IsErrorEnabled)
{
_logger.Error(messageFunc(), exception);
return true;
}
break;
case LogLevel.Fatal:
if (_logger.IsFatalEnabled)
{
_logger.Fatal(messageFunc(), exception);
return true;
}
break;
default:
if (_logger.IsDebugEnabled)
{
_logger.Debug(messageFunc(), exception);
return true;
}
break;
}
return false;
}
private bool IsLogLevelEnable(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Debug:
return _logger.IsDebugEnabled;
case LogLevel.Info:
return _logger.IsInfoEnabled;
case LogLevel.Warn:
return _logger.IsWarnEnabled;
case LogLevel.Error:
return _logger.IsErrorEnabled;
case LogLevel.Fatal:
return _logger.IsFatalEnabled;
default:
return _logger.IsDebugEnabled;
}
}
}

LibLog תומכת בספריות הבאות: NLog, EnterpriseLibrary, log4net, SeriLog, Loupe וכתיבה לConsole בצבע.
בנוסף, ניתן לספק מימוש אחר (ברמת האפליקציה) ע”י מימוש הממשקים ILogProvider וILog, וקריאה למתודה SetCurrentLogProvider של LogProvider.

היתרון של השיטה הזו היא שהיא תומכת בכל ספריות הLogים הפופולריות, בלי הוספת Referenceים חיצוניים, ובלי המצאת הגלגל מחדש.

סופ”ש עם לוגים טוב

שתף