clean-up refactoring and more detailed doc

This commit is contained in:
Johann Dreo 2020-08-31 00:29:12 +02:00
commit 7ef6459c35
2 changed files with 215 additions and 101 deletions

View file

@ -12,7 +12,8 @@ Clutchlog allows to select which log messages will be displayed, based on their
- source code location: you can ask to display messages called from given files, functions and line number, all based on
regular expressions.
Appart from those, Clutchlog have classical logging system features: output selection, default to unbuffered mode, etc.
Appart from those, Clutchlog have classical logging system features: output selection and formatting,
default to unbuffered mode, etc.
Of course, Clutchlog is disabled by default if `NDEBUG` is not defined.
@ -29,12 +30,12 @@ To configure the display, you indicate the three types of locations, for example
```cpp
auto& log = clutchlog::logger();
log.depth(2); // Log functions called from "main" but not below.
log.threshold(clutchlog::level::info); // Log "info", "warning", "error" or "quiet" messages.
log.threshold(clutchlog::level::info); // Log only "info", "warning", "error" or "quiet" messages.
log.file("algebra/.*"); // Will match any file in the "algebra" directory.
log.func("(mul|add|sub|div)"); // Will match "multiply", for instance.
```
For more detailled examples, see the `tests` directory.
For more detailled examples, see the "API documentation" section below and the `tests` directory.
Rationale
@ -45,7 +46,7 @@ database.
Their aim is to provide a simple interface to efficiently store messages somewhere, which is appropriated when you have
a well known service running and you want to be able to trace complex users interactions across its states.
Clutchlog, however, targets the debugging of a (single-run) program.
Clutchlog, however, targets the debugging of a (typically single-run) program.
While you develop your software, it's common practice to output several detailled informations on the internal states
around the feature you are currently programming.
However, once the feature is up and running, those detailled informations are only useful if you encounter a bug
@ -58,10 +59,95 @@ To solve this problem, Clutchlog allows to disengage your debug log messages in
allowing for the fast tracking of a bug across the execution.
API documentation
=================
The main entrypoint is the `CLUTCHLOG` macro, which takes the desired log level and message.
The message can be anything which can be output in an `ostringstream`.
```cpp
// Simple string:
CLUTCHLOG(info, "hello world");
// Serialisable variable:
double value = 0;
CLUTCHLOG(error, value);
// passed using inline output stream operators:
CLUTCHLOG(debug, "hello " << value << " world");
```
To configure the global behaviour of the logger, you must first get a reference on its instance:
```cpp
auto& log = clutchlog::logger();
```
The format of the messages can be defined with the `format` method, passing a string with standardized tags surrounded by `{}`:
```cpp
log.format("{msg}");
```
Available tags are:
- `{msg}`: the logged message,
- `{name}`: the name of the current binary,
- `{level}`: the current log level (i.e. `Quiet`, `Error`, `Warning`, `Info`, `Debug` or `XDebug`),
- `{level_letter}`: the first letter of the current log level,
- `{file}`: the current file (absolute path),
- `{func}`: the current function,
- `{line}`: the current line number,
- `{depth}`: the current depth of the call stack,
- `{depth_marks}`: as many chevrons `>` as there is calls in the stack.
The default format is `"[{name}] {level_letter}:{depth_marks} {msg}\t\t\t\t\t{func} @ {file}:{line}\n"`,
it can be overriden at compile time by defining the `CLUTCHLOG_DEFAULT_FORMAT` macro.
The output stream can be configured using the `out` method:
```cpp
log.out(std::clog); // Defaults to clog.
```
One can configure the location(s) at which messages should actually be logged:
```cpp
log.depth(3); // Depth of the call stack, defaults to the maximum possible value.
log.threshold(clutchlog::level::error); // Log level, defaults to error.
```
File, function and line are indicated using regular expression:
```cpp
log.file(".*"); // File location, defaults to any.
log.func(".*"); // Function location, defaults to any.
log.line(".*"); // Line location, defaults to any.
```
A shortcut function can be used to indicates file, function and line regular expression at once:
```cpp
log.location(file, func, line); // Defaults to any, second and last parameters being optional.
```
The mark used with the `{depth_marks}` tag can be configured with the `depth_mark` method,
and its default with the `CLUTCHLOG_DEFAULT_DEPTH_MARK` macro:
```cpp
log.depth_mark(CLUTCHLOG_DEFAULT_DEPTH_MARK); // Defaults to ">".
```
All configuration setters have a getters counterpart, with the same name but taking no parameter,
for example:
```cpp
std::string mark = log.depth_mark();
```
To control more precisely the logging, one can use the low-level `log` method:
```cpp
log.log(clutchlog::level::xdebug, "hello world", "main.cpp", "main", 122);
```
A helper macro can helps to fill in the location with the actual one, as seen by the compiler:
```cpp
log.log(clutchlog::level::xdebug, "hello world", CLUTCHLOC);
```
Limitations
===========
Call stack depth is only implemented for Linux.
Because the call stack depth and binary name access are system-dependent,
Clutchlog is only implemented for Linux at the moment.
Build and tests

View file

@ -1,12 +1,13 @@
#ifndef __CLUTCHLOG_H__
#define __CLUTCHLOG_H__
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <cassert>
#include <cstdlib>
#include <iomanip>
#include <string>
#include <limits>
#include <sstream>
#include <regex>
#include <map>
@ -16,54 +17,74 @@
#include <libgen.h>
// #endif
// #ifdef WITH_THROWN_ASSERTS
// #undef assert
// #define assert( what ) { if(!(what)){CLUTCHLOG_RAISE(aion::Assert, "Failed assert: `" << #what << "`");} }
// #else
#include <cassert>
// #endif
/**********************************************************************
* Enable by default in Debug builds.
**********************************************************************/
#ifndef WITH_CLUTCHLOG
#ifndef NDEBUG
#define WITH_CLUTCHLOG
#endif
#endif
/**********************************************************************
* Macros definitions
**********************************************************************/
#ifdef WITH_CLUTCHLOG
#ifndef CLUTCHLOG_DEFAULT_FORMAT
#define CLUTCHLOG_DEFAULT_FORMAT "[{name}] {level}:{depth_marks} {msg}\t\t\t\t\t{func} @ {file}:{line}\n"
//! Default format of the messages.
#define CLUTCHLOG_DEFAULT_FORMAT "[{name}] {level_letter}:{depth_marks} {msg}\t\t\t\t\t{func} @ {file}:{line}\n"
#endif
#ifndef CLUTCHLOG_DEFAULT_DEPTH_MARK
#define CLUTCHLOG_DEFAULT_DEPTH_MARK ">"
#endif
//! Handy shortcuts to location.
#define CLUTCHLOC __FILE__, __FUNCTION__, __LINE__
//! Log a message at the given level.
#define CLUTCHLOG( LEVEL, WHAT ) { \
auto& logger = clutchlog::logger(); \
std::ostringstream msg ; msg << WHAT; \
logger.log(msg.str(), clutchlog::level::LEVEL, CLUTCHLOC); \
std::ostringstream msg ; msg << WHAT; \
logger.log(clutchlog::level::LEVEL, msg.str(), CLUTCHLOC); \
}
//! Dump the given container.
#define CLUTCHDUMP( LEVEL, CONTAINER ) { \
auto& logger = clutchlog::logger(); \
logger.dump(std::begin(CONTAINER), std::end(CONTAINER), clutchlog::level::LEVEL, CLUTCHLOC, "\n"); \
logger.dump(clutchlog::level::LEVEL, std::begin(CONTAINER), std::end(CONTAINER), CLUTCHLOC, "\n"); \
}
#else
// Disabled macros can still be used in Release builds.
#define CLUTCHLOG ( LEVEL, WHAT ) { do {/*nothing*/} while(false); }
#define CLUTCHDUMP ( LEVEL, WHAT ) { do {/*nothing*/} while(false); }
#endif
/**********************************************************************
* Implementation
**********************************************************************/
//! Singleton class.
class clutchlog
{
public:
/** High-level API @{ */
/** Get the logger instance.
*
* @code
* auto& log = clutchlog::logger();
* @endcode
*/
static clutchlog& logger()
{
static clutchlog instance;
return instance;
}
//! Available log levels.
enum level {quiet=0, error=1, warning=2, info=3, debug=4, xdebug=5};
/** }@ High-level API */
@ -78,13 +99,13 @@ class clutchlog
clutchlog() :
// system, main, log
_strip_calls(3),
_level_letters({
{level::quiet,"Q"},
{level::error,"E"},
{level::warning,"W"},
{level::info,"I"},
{level::debug,"D"},
{level::xdebug,"X"}
_level_words({
{level::quiet,"Quiet"},
{level::error,"Error"},
{level::warning,"Warning"},
{level::info,"Info"},
{level::debug,"Debug"},
{level::xdebug,"XDebug"}
}),
_format(CLUTCHLOG_DEFAULT_FORMAT),
_out(&std::clog),
@ -93,15 +114,12 @@ class clutchlog
_in_file(".*"),
_in_func(".*"),
_in_line(".*"),
_show_name(true),
_show_depth(true),
_show_location(true),
_show_level(true)
_depth_mark(CLUTCHLOG_DEFAULT_DEPTH_MARK)
{}
protected:
const size_t _strip_calls;
const std::map<level,std::string> _level_letters;
const std::map<level,std::string> _level_words;
std::string _format;
std::ostream* _out;
size_t _depth;
@ -109,10 +127,7 @@ class clutchlog
std::regex _in_file;
std::regex _in_func;
std::regex _in_line;
bool _show_name;
bool _show_depth;
bool _show_location;
bool _show_level;
std::string _depth_mark;
struct scope_t {
bool matches; // everything is compatible
@ -121,6 +136,7 @@ class clutchlog
bool there; // location is compatible
};
//! Gather information on the current location of the call.
scope_t locate(
const level& stage,
const std::string& file,
@ -165,6 +181,9 @@ class clutchlog
void depth(size_t d) {_depth = d;}
size_t depth() const {return _depth;}
void depth_mark(std::string mark) {_depth_mark = mark;}
std::string depth_mark() const {return _depth_mark;}
void threshold(level l) {_stage = l;}
level threshold() const {return _stage;}
@ -195,59 +214,61 @@ class clutchlog
const std::string& tag
) const
{
std::regex re;
try {
re = std::regex(mark);
} catch(const std::regex_error& e) {
std::cerr << "ERROR with a regular expression \"" << mark << "\": ";
switch(e.code()) {
case std::regex_constants::error_collate:
std::cerr << "the expression contains an invalid collating element name";
break;
case std::regex_constants::error_ctype:
std::cerr << "the expression contains an invalid character class name";
break;
case std::regex_constants::error_escape:
std::cerr << "the expression contains an invalid escaped character or a trailing escape";
break;
case std::regex_constants::error_backref:
std::cerr << "the expression contains an invalid back reference";
break;
case std::regex_constants::error_brack:
std::cerr << "the expression contains mismatched square brackets ('[' and ']')";
break;
case std::regex_constants::error_paren:
std::cerr << "the expression contains mismatched parentheses ('(' and ')')";
break;
case std::regex_constants::error_brace:
std::cerr << "the expression contains mismatched curly braces ('{' and '}')";
break;
case std::regex_constants::error_badbrace:
std::cerr << "the expression contains an invalid range in a {} expression";
break;
case std::regex_constants::error_range:
std::cerr << "the expression contains an invalid character range (e.g. [b-a])";
break;
case std::regex_constants::error_space:
std::cerr << "there was not enough memory to convert the expression into a finite state machine";
break;
case std::regex_constants::error_badrepeat:
std::cerr << "one of *?+{ was not preceded by a valid regular expression";
break;
case std::regex_constants::error_complexity:
std::cerr << "the complexity of an attempted match exceeded a predefined level";
break;
case std::regex_constants::error_stack:
std::cerr << "there was not enough memory to perform a match";
break;
default:
std::cerr << "unknown error";
}
std::cerr << std::endl;
throw;
} // catch
// Useless debug code, unless something fancy would be done with name tags.
// std::regex re;
// try {
// re = std::regex(mark);
//
// } catch(const std::regex_error& e) {
// std::cerr << "ERROR with a regular expression \"" << mark << "\": ";
// switch(e.code()) {
// case std::regex_constants::error_collate:
// std::cerr << "the expression contains an invalid collating element name";
// break;
// case std::regex_constants::error_ctype:
// std::cerr << "the expression contains an invalid character class name";
// break;
// case std::regex_constants::error_escape:
// std::cerr << "the expression contains an invalid escaped character or a trailing escape";
// break;
// case std::regex_constants::error_backref:
// std::cerr << "the expression contains an invalid back reference";
// break;
// case std::regex_constants::error_brack:
// std::cerr << "the expression contains mismatched square brackets ('[' and ']')";
// break;
// case std::regex_constants::error_paren:
// std::cerr << "the expression contains mismatched parentheses ('(' and ')')";
// break;
// case std::regex_constants::error_brace:
// std::cerr << "the expression contains mismatched curly braces ('{' and '}')";
// break;
// case std::regex_constants::error_badbrace:
// std::cerr << "the expression contains an invalid range in a {} expression";
// break;
// case std::regex_constants::error_range:
// std::cerr << "the expression contains an invalid character range (e.g. [b-a])";
// break;
// case std::regex_constants::error_space:
// std::cerr << "there was not enough memory to convert the expression into a finite state machine";
// break;
// case std::regex_constants::error_badrepeat:
// std::cerr << "one of *?+{ was not preceded by a valid regular expression";
// break;
// case std::regex_constants::error_complexity:
// std::cerr << "the complexity of an attempted match exceeded a predefined level";
// break;
// case std::regex_constants::error_stack:
// std::cerr << "there was not enough memory to perform a match";
// break;
// default:
// std::cerr << "unknown error";
// }
// std::cerr << std::endl;
// throw;
// } // catch
std::regex re(mark);
return std::regex_replace(form, re, tag);
}
@ -255,19 +276,24 @@ class clutchlog
std::string format,
const std::string& what,
const std::string& name,
const std::string& stage,
const level& stage,
const std::string& file,
const std::string& func,
const std::string& line,
const size_t line,
const size_t depth
) const
{
format = replace(format, "\\{msg\\}", what);
format = replace(format, "\\{name\\}", name);
format = replace(format, "\\{level\\}", stage);
format = replace(format, "\\{file\\}", file);
format = replace(format, "\\{func\\}", func);
format = replace(format, "\\{line\\}", line);
format = replace(format, "\\{level\\}", _level_words.at(stage));
std::ostringstream sline; sline << line;
format = replace(format, "\\{line\\}", sline.str());
std::string letter(1, _level_words.at(stage).at(0));
format = replace(format, "\\{level_letter\\}", letter);
std::ostringstream sdepth; sdepth << depth;
format = replace(format, "\\{depth\\}", sdepth.str());
@ -281,39 +307,41 @@ class clutchlog
}
void log(
const level& stage,
const std::string& what,
const level& stage, const std::string& file, const std::string& func, size_t line
const std::string& file, const std::string& func, size_t line
) const
{
scope_t scope = locate(stage, file, func, line);
if(scope.matches) {
std::ostringstream sline; sline << line;
*_out << format(_format, what, basename(getenv("_")),
_level_letters.at(stage), file, func,
sline.str(), scope.depth );
stage, file, func,
line, scope.depth );
_out->flush();
} // if scopes.matches
}
template<class In>
void dump(
const level& stage,
const In container_begin, const In container_end,
const level& stage, const std::string& file, const std::string& func, size_t line,
const std::string& file, const std::string& func, size_t line,
const std::string sep="\n"
) const
// FIXME use a file name as input
// FIXME use a file name template as input
{
scope_t scope = locate(stage, file, func, line);
if(scope.matches) {
std::ostringstream sline; sline << line;
*_out << "#"
*_out << "#" // FIXME add a _format_dump parameter?
<< format(_format, "", basename(getenv("_")),
_level_letters.at(stage), file, func,
sline.str(), scope.depth );
stage, file, func,
line, scope.depth );
std::copy(container_begin, container_end,
std::ostream_iterator<typename In::value_type>(*_out, sep.c_str()));
// No flush
} // if scopes.matches
}