Background: Let's say I am writing an application and need to have the ability to log at various levels. For example, depending on the settings (maybe a global setting), I could display or write to file logs based on these general categories: debug, warn, and error. Here debug should output all messages that can help a developer debug the application, warn could be something the user is doing but is not liked, and error would be when something goes wrong.
Question: While writing each unit of the application (function, method, statement, etc.), what is the standard practice for determining if a log should be output or not. For example, in the psuedocode below, am I on the right path or is there some other way to do this?
myfunction(params)
do something
// need to log for debugging purposes
can_I_log('debug')
if I_can_log
print debug message
do something more
// need to log for debugging purposes
can_I_log('debug')
if I_can_log
print debug message
do something more
// something went wrong
can_I_log('error')
if I_can_log
print error message
Basically, before/after each important statement or chunk of code, I am checking if a global log setting allows me to log this porti开发者_如何学运维on (by using the can_I_log() function/method). For example, if global setting is 'warn' and I use can_I_log('debug') then I_can_log would be false (where error > warn > debug) Is this how it's done in practice? Does this approach adversely affect the speed of execution (I would think it does)? Is there a better way to do this?
A question:
why do you want to have these global debugging states
maintained? If an event happen, at that point in time, it can be either just an event you want to log, or a warning you want to log, or a serious error that you want to log.
Here is my take.
Instead of checking each time, if some event happens, you should log it but with different levels:
for example:
file copy failed. You can treat this as warning
(if you can still continue without this file) or error
(if the functionality following this code cannot work without the file).
In first case - warning - you will do:
log_message("file copy failed", WARN);
In second case - error - you will do:
log_message("file copy failed", ERR);
And lets say your log file is /var/logs then, it may look like this:
In first case:
Warning file copy failed.
In second case:
Error file copy failed.
So your log_message() function will do the work.
PS: you may want to look at how syslog does it and use that to not reinvent the wheel.
Restructuring
Honestly, the main change you want to make is the change where the "if" statement is happening. You are essentially repeating the same code over and over and over in your program. This is never a good idea.
Instead, I would highly recommend have a log function which takes a message and a level and then logs or doesn't log based on the global setting. This will make mistakes less likely and make it easier to read the resultant code because your logging messages won't take up 4 lines each any more.
So, instead, I would make it look like this:
function log(String mess, Enum level) {
if (globalLevel >= level) {
print level mess
}
}
myfunction(params) {
do something
// need to log for debugging purposes
log (message, debug)
do something more
// need to log for debugging purposes
log (message, debug)
do something more
// something went wrong
log (message, error)
}
Now your code is shorter and clearer. And if, at some point in the future, you decide to start storing your log in a file or a database, you have one print statement to change, not a million.
Avoiding the If
Further, depending on your language, you might even be able to avoid the if statement in the log function altogether. To do this, you would need make three logging functions and then apply appropriate use of inheritance, lambda functions, or macros. The drawback of this is that varying the logging levels will require writing some additional code, and not just changing a constant.
In the following examples, I'll assume that you have three functions: log_debug, log_warn, log_error and your code would look like: myfunction(params) { do something // need to log for debugging purposes log_debug (message) do something more // need to log for debugging purposes log_debug (message) do something more // something went wrong log_error (message) }
So, in C, for instance, if the debug level is known at compile time, then you can use pragmas as follows:
#if __DEBUG__
void log_debug(char* c) {
printf("debug: %s\n",c);
}
void log_warn(char* c) {
printf("warn: %s\n",c);
}
#endif
#if __WARN__
void log_debug(char* c) { }
void log_warn(char* c) {
printf("warn: %s\n",c);
}
#endif
#if __ERROR__
void log_debug(char* c) { }
void log_warn(char* c) { }
#endif
void log_error(char* c) {
printf("error: %s\n",c);
}
Doing so would provide you with a set of functions which would either log or not log appropriately with no if statements. If it's not known at compile time, a similar thing could be done using function pointers at run time (similar to the Ocaml example which appears later in this answer).
In Java, you could do the following using inheritance:
interface ILogger {
public static final int DEBUG = 3;
public static final int WARN = 2;
public static final int ERROR = 1;
public void logError(String message);
public void logWarn(String message);
public void logDebug(String message);
}
class Logger {
public static ILogger getLogger(int level) {
if (level == DEBUG) { return new DebugLogger; }
if (level == WARN) { return new WarnLogger; }
if (level == ERROR) { return new ErrorLogger; }
return null;
}
public static ILogger logger = null;
}
class DebugLogger implements ILogger {
public void logError(String message) {
System.err.println("Error: "+message);
}
public void logWarn(String message) {
System.err.println("Warn: "+message);
}
public void logDebug(String message) {
System.err.println("Debug: "+message);
}
}
class WarnLogger implements ILogger {
public void logError(String message) {
}
public void logWarn(String message) {
System.err.println("Warn: "+message);
}
public void logDebug(String message) {
System.err.println("Debug: "+message);
}
}
class ErrorLogger implements ILogger {
public void log_error(String message) {
}
public void log_warn(String message) {
}
public void log_debug(String message) {
System.err.println("Debug: "+message);
}
}
Then at the beginning of your code, you have the line:
Logger.logger = Logger.getLogger(ILogger.WARN);
which would set up which logger is being used and to log you call
Logger.logger.logDebug("Something happened!");
or so forth. Essentially, what we're doing is determining up front what set of methods will be called and then having a reference to the class which has the right methods stored in a global variable.
In Ocaml, we could use functions as first-order objects to assign appropriate functions to the right names. The code would write code like:
let log_level = "WARN"
let log type message = (print_string (type ^ ": " ^ message); print_newline ())
let dontlog message = ()
let log_error = log "Error"
let log_warn = if (log_level = "WARN" || log_level = "DEBUG") then (log "WARN") else dontlog
let log_debug = if (log_level = "DEBUG") then (log "DEBUG") else dontlog
This assigns an appropriate function to an appropriate name exactly once and then after that, we just call the log functions by name. Although at first glance it may look like there's an if statement in log_warn and log_debug, those are function assignments rather than function definitions, so the if statement only happens when the let statement is executed by the program and not when the function is actually called. So when log_debug is invoked either the log "DEBUG" or dontlog functions are called. A similar approach could be used in any language with functions as first-order objects (although, obviously, the code would need to be adjusted for the syntax and semantics of the particular language).
精彩评论