
Hi, here are my review results. I have organized it in two parts. A relatively short one answering the formal review questions and a second part that lists a lot of additional details. Attached is a short program which contains some of the tests I did. - Should the library be accepted? ================================= YES, but some things should be fixed first (see also details below): - Compiler warnings: I get tons of them. Zero should be the goal. - Completeness of documentation: Some things are simply missing and can only be found in the code - Compile time for the library - A lot of details are listed below. Of course, I would like to see all of them addressed, but my vote does not depend on every single one of them. - What is your evaluation of the design? ======================================== Very flexible, very modularized. I have two concerns, both explained in more detail below. a) In some aspects, it is too flexible, I think. This makes it hard to document (which can be seen in the current documentation) and hard to test (the latter is just me guessing). b) It is easy to use it in the wrong way, which leads to exceptions and lost log records, possibly after hours of running, probably in some rare and potentially critical situation. It would be better to somehow prevent the wrong usage at compile time. - What is your evaluation of the implementation? ================================================ I looked at the code only briefly, but what I saw looked good to me (see below). - What is your evaluation of the documentation? =============================================== Very nice examples, very well motivated tutorial. Good overview on details. Was good to read to get an idea of how the library can be used. But (at least for me) it was sometimes tough to combine the information given in examples to the things I want to do myself. Mostly this was because some information is missing (or too well hidden for me). I admit that I got lost on the last few pages, but that was probably due to me not actually experimenting with all of it. See below for specific remarks. - What is your evaluation of the potential usefulness of the library? ===================================================================== Very useful! Logging is one of the core aspects of almost all software. And it is not nearly as easy as it seems at first glance. Thus, it is very useful to have a well designed, well documented, well tested and well supported log library. - Did you try to use the library? With what compiler? Did you have any ====================================================================== problems? ========= I compiled the library (the version from the SVN trunk, because the packaged version from sourceforge did not compile on my system). I also did some experiments to see how easy or hard it was to transfer the information from the documentation to some "real" code. I stumbled over quite a few details, but am convinced that I could use the library, soon. The most annoying problem were the compile warnings I mentioned above. Ubuntu-8.04 64bit, gcc-4.2.4, boost-log from SVN trunk, boost-1.42 - How much effort did you put into your evaluation? A glance? A quick ===================================================================== reading? In-depth study? ======================== I spent several hours reading the documentation and several hours of experimenting (see details below), about 12 hours in total (which includes writing this review). - Are you knowledgeable about the problem domain? ================================================= I wrote the log library which is being used by some 100 components in our company for about two years now. It is based on POCO library (pocoproject.org). Before writing it, I analyzed several log other libraries too, of course. So I would not consider myself an expert, but not really a newbie, either :-) ------------------------------------------------------------------------ MORE DETAILS ------------------------------------------------------------------------ Missing Features: ================= Maybe I overlooked it, but I did not find the following: - Have several sinks share a formatter: The programs in my company typically produce several log files: - debug: Contains everything - warn: Contains everything which is at least a warning - error: Contains everything which is error or fatal Messages in all files are formatted in the same way. I understand that I could create several file sinks and each is given its own formatter. But that would mean that the same formatting is done several times. Is there a way to have several sinks with different filter but sharing the log records? Compiling: ========== - Library Code: As mentioned in an earlier mail, it takes awfully long to compile the library, which is due to one file: formatter_parser.cpp The parsing is based on Boost.Spirit (classic) and takes 20 minutes to compile on my maschine (Ubuntu-8.04 64bit, gcc-4.2.4, Intel Quad Core, 2.5GHz) I don't know how much can be gained by switching to Spirit 2.1. But I am sure a way has to be found to accelerate the compilation. Otherwise, users will do what I did: Get nervous about the long compilation time and tend to press Ctrl-C before the work is done. - My Code: All of the code in our company is compiled with the following settings (gcc-4.2.4): -Wall -Wreorder -Wnon-virtual-dtor -Wno-non-template-friend -Woverloaded-virtual -Wsign-promo -Wextra -fvisibility=hidden We strive for code that compiles without warnings, but with boost.log, I get tons of warnings. What I have seen so far should be easy to get rid of. But I will not be able to use the boost.log library unless the warnings are taken care of. Debugging: ========== - I tried to add a timestamp to each written record. I used core->add_global_attribute("TimeStamp", boost::make_shared< attrs::local_clock >()); backend->set_formatter( fmt::stream << fmt::date_time< std::tm >("TimeStamp") << ": [" << fmt::attr< severity_level >("Severity") << "] " << fmt::message() ); This compiled, but when I ran my program, I got an exception message which did not really tell what was wrong: ----------------- terminate called after throwing an instance of 'boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<boost::log_mt_posix::missing_value> >' what(): Requested attribute value not found ----------------- I think, at least it should tell which attribute value was not found. But the real reason for the exception was that the types of the attribute and the attribute value did not match. - Is there a way to get to these exceptions earlier? As of now, in the example above, the exception is thrown when a logger is used. This is much too late. I need to initialize logging and then be sure that logging will work for the components that use it (within limits of course, e.g. filesystem being full). But mis-configurations like above must be detected before the real work is done in the components using the log system. For instance when constructing the logger. - Of course, best would be to prevent such stuff at compile time (no idea how, though) Design: ======= - Flexibility: To be honest, in some aspects, I would have preferred to see less flexibility, for two reasons: a) the documentation effort rises and I suspect that the missing completeness of the documentation is partly a result of over-flexibility b) the testing effort rises (I guess). It will be hard to make sure that everything works as expected. An example is log record formatting via Boost.Lamda, Boost.Format, String Templates, Custom-Formatting, which is all shown by example in the tutorial, but the detailed description of Formatters does not even mention String Templates or Custom Formatting. - Easy to misuse: If your formatter requires a severity and the logger does not add one to the log record or adds a severity of the wrong type, using that logger results in an exception, see attached code. It is unlikely that this will happen in some part of the code which is executed frequently, because the developer would certainly notice. But what if someone would add a non-matching logger at some rarely executed but critical code section? Not only would the code fail with an exception, it would also fail to log the message! As of now, I would have to provide some factory method which produces loggers for anyone who needs one. And no logger is ever to be constructed otherwise. Just to make sure that everybody uses a logger that actually produces digestible log records. I can live with that, but I do not consider it a good solution. Code Quality: ============= - Readability: I did not read through all the code, and none in very much detail, but the parts I looked at, looked well organized and readable. - Names: - namespace trivial: I admit that I don't like that, especially since its content is not exactly trivial. Easy, basic or default maybe, I don't know. But not trivial :-) Documentation: ============== Very nice examples, very well motivated tutorial. Good overview on details. Was good to read to get an idea of how the library can be used. But (at least for me) it was sometimes tough to combine the information given in examples to the things I want to do myself. I admit that I got lost on the last few pages, but was probably due to me not actually experimenting with all of it. Some things should be looked into, though: Build process: - it would be nice, if the default build configuration were documented - I wonder which build configuration will be used when the library is included in boost? Tutorial: - Trivial logging with filters: The "Tip" that streaming expressions are not evaluated if the message is filtered should probably be a warning of potential hazard. It is certainly a good feature, but the semantics of the macros are not immediately obvious so I would give the "Tip" a more critical nature. - none of the code examples is complete. They are missing the namespace declarations documented in the introduction, at least. While brevity is good, it would be very helpful to have complete, compiling examples in the tutorial Missing Documentation: - Not all include files are mentioned. This makes it unnecessarily tough to follow the examples. For instance, boost/log/utility/empty_deleter.hpp is required to use logging::empty_deleter. I could not find this information in all of the turorial and details. Another, maybe even more critical example: The BOOST_LOG macro is defined in boost/log/sources/record_ostream.hpp Again I did not find that information in the tutorial. - I understand that I can use my own set of severity levels. It is well documented how to filter messages using these severity levels. But I have no idea how to format messages with these severity levels? I do not want the number, I'd like a string, of course. I looked it up in the trivial.hpp, but it should be part of the documentation. - Maybe I missed it, but I did not find an overview of the log rotation options, including a description of the placeholders in strings. Specific question: Can I tell the log rotation to take place at midnight (regardless of the amount of data which is being logged)? I would like to stress this last item one more time: The examples are excellent, but they should be accompanied by more formal tables which show the complete set of options, e.g. which placeholders can be used in a filename format or a list of public (protected?) member functions of a logger. For instance, I found the logger::strm() member in examples only.

On 03/10/2010 08:52 PM, Roland Bock wrote:
Missing Features: ================= Maybe I overlooked it, but I did not find the following: - Have several sinks share a formatter: The programs in my company typically produce several log files: - debug: Contains everything - warn: Contains everything which is at least a warning - error: Contains everything which is error or fatal Messages in all files are formatted in the same way. I understand that I could create several file sinks and each is given its own formatter. But that would mean that the same formatting is done several times. Is there a way to have several sinks with different filter but sharing the log records?
That's an interesting idea, not very simple to implement though. The problem is that some records can pass filtering for some of the sinks and not the others, while the sinks may share the formatter. Further on, formatting of the same log record for different sinks may occur in different threads. I'll add this feature to the TODO list, I need some time to think about it.
Compiling: ========== - Library Code: As mentioned in an earlier mail, it takes awfully long to compile the library, which is due to one file:
formatter_parser.cpp
The parsing is based on Boost.Spirit (classic) and takes 20 minutes to compile on my maschine (Ubuntu-8.04 64bit, gcc-4.2.4, Intel Quad Core, 2.5GHz)
I suppose, it's because of the old compiler. On my setup (GCC 4.4, Intel dual core, 3GHz) the whole library builds in about 1.5 minutes, in a single thread. I'll see what I can do about it. Hopefully, the switch to Spirit 2 will help.
I don't know how much can be gained by switching to Spirit 2.1. But I am sure a way has to be found to accelerate the compilation. Otherwise, users will do what I did: Get nervous about the long compilation time and tend to press Ctrl-C before the work is done.
You can reduce build times and the binary size by disabling parts of the library you don't use. See here: http://tinyurl.com/yjq622b The particular options you might be interested in are BOOST_LOG_USE_CHAR and BOOST_LOG_NO_SETTINGS_PARSERS_SUPPORT.
We strive for code that compiles without warnings, but with boost.log, I get tons of warnings. What I have seen so far should be easy to get rid of. But I will not be able to use the boost.log library unless the warnings are taken care of.
I'm using GCC 4.4 and don't get any warnings. Could you send your build log? Perhaps, off list, if it's too heavy.
Debugging: ========== - I tried to add a timestamp to each written record. This compiled, but when I ran my program, I got an exception message which did not really tell what was wrong: ----------------- terminate called after throwing an instance of 'boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<boost::log_mt_posix::missing_value>
' what(): Requested attribute value not found
I think, at least it should tell which attribute value was not found. But the real reason for the exception was that the types of the attribute and the attribute value did not match.
I will make error messages more telling. The reason I didn't do that earlier is that character conversion may fail and throw itself.
- Is there a way to get to these exceptions earlier? As of now, in the example above, the exception is thrown when a logger is used. This is much too late. I need to initialize logging and then be sure that logging will work for the components that use it (within limits of course, e.g. filesystem being full). But mis-configurations like above must be detected before the real work is done in the components using the log system. For instance when constructing the logger. - Of course, best would be to prevent such stuff at compile time (no idea how, though)
I can't see a way to detect such errors on an earlier stage, because filters, formatters and loggers are unrelated. Also, it's difficult to perform this kind of check because the setup may still be valid even if not all attributes are present in the core and/or loggers. One can use scoped attributes to temporarily add attributes. Moreover, some attributes may be required by particular sinks. If you don't want exceptions to be thrown, you can use things like flt::has_attr, std::nothrow in filters/formatters and exception handlers.
- Flexibility: To be honest, in some aspects, I would have preferred to see less flexibility, for two reasons: a) the documentation effort rises and I suspect that the missing completeness of the documentation is partly a result of over-flexibility b) the testing effort rises (I guess). It will be hard to make sure that everything works as expected.
It's never too flexible. :) Really, there will always be someone who wants that little thing work a bit differently.
An example is log record formatting via Boost.Lamda, Boost.Format, String Templates, Custom-Formatting, which is all shown by example in the tutorial, but the detailed description of Formatters does not even mention String Templates or Custom Formatting.
String templates are described here: http://tinyurl.com/ylxeblv But I'll add a reference in the section about formatters. Regarding Custom formatting, do you mean user-defined functions as formatters? If so, what can be said on that topic?
- Names: - namespace trivial: I admit that I don't like that, especially since its content is not exactly trivial. Easy, basic or default maybe, I don't know. But not trivial :-)
Trivial is the usage pattern, the implementation may not be trivial (although, it is quite simple, as for me).
Documentation: ==============
I admit that I got lost on the last few pages, but was probably due to me not actually experimenting with all of it.
Can you point me to the particular sections that were difficult to read?
Build process: - it would be nice, if the default build configuration were documented
By default the "all included" version is built. This comes from the Installation section that I mentioned earlier. I'll spell it out explicitly.
- I wonder which build configuration will be used when the library is included in boost?
I don't plan to change the configuration mechanism upon inclusion.
Tutorial: - Trivial logging with filters: The "Tip" that streaming expressions are not evaluated if the message is filtered should probably be a warning of potential hazard. It is certainly a good feature, but the semantics of the macros are not immediately obvious so I would give the "Tip" a more critical nature.
Ok.
- none of the code examples is complete. They are missing the namespace declarations documented in the introduction, at least. While brevity is good, it would be very helpful to have complete, compiling examples in the tutorial
I don't think adding namespaces and includes to examples in the readable documentation is a good idea. As you noted, these examples should be brief and free of such noise. However, I will reorganize the docs, so that the code samples will be taken from compilable external files.
Missing Documentation: - Not all include files are mentioned. This makes it unnecessarily tough to follow the examples. For instance, boost/log/utility/empty_deleter.hpp is required to use logging::empty_deleter. I could not find this information in all of the turorial and details. Another, maybe even more critical example: The BOOST_LOG macro is defined in boost/log/sources/record_ostream.hpp Again I did not find that information in the tutorial.
Yes, these small bits fell out of my scope. If only it was possible to generate the reference both header-based (like it is now) and content-based...
- I understand that I can use my own set of severity levels. It is well documented how to filter messages using these severity levels. But I have no idea how to format messages with these severity levels? I do not want the number, I'd like a string, of course. I looked it up in the trivial.hpp, but it should be part of the documentation.
It is described in the formatters section: http://tinyurl.com/yh3r7ue All you have to do is to define operator<<. The approach is also demonstrated in examples.
- Maybe I missed it, but I did not find an overview of the log rotation options, including a description of the placeholders in strings.
Specific question: Can I tell the log rotation to take place at midnight (regardless of the amount of data which is being logged)?
No, you currently cannot specify a specific time point of rotation, but you can specify the rotation period. I've been requested to support time point rotation, so I'll implement it some time.
I would like to stress this last item one more time: The examples are excellent, but they should be accompanied by more formal tables which show the complete set of options, e.g. which placeholders can be used in a filename format or a list of public (protected?) member functions of a logger. For instance, I found the logger::strm() member in examples only.
The functions and their arguments (including named ones) are described in the Reference section. That is the formal interface description. The more expanded description with code samples in the Detailed features description. If you found inconsistencies or something missing in particular, I'll be glad to fix it.

Andrey Semashev wrote:
Is there a way to have several sinks with different filter but sharing the log records?
That's an interesting idea, not very simple to implement though. The problem is that some records can pass filtering for some of the sinks and not the others, while the sinks may share the formatter. Further on, formatting of the same log record for different sinks may occur in different threads. I'll add this feature to the TODO list, I need some time to think about it.
I understand that it is difficult in all generality. Since it is an optimization, maybe it could be implemented for certain cases only? For instance restrict it to sinks which live in the same thread?
The parsing is based on Boost.Spirit (classic) and takes 20 minutes to compile on my maschine (Ubuntu-8.04 64bit, gcc-4.2.4, Intel Quad Core, 2.5GHz)
I suppose, it's because of the old compiler. On my setup (GCC 4.4, Intel dual core, 3GHz) the whole library builds in about 1.5 minutes, in a single thread. I'll see what I can do about it. Hopefully, the switch to Spirit 2 will help.
OK, good to know! I am hoping to update to 4.4, too. In the meantime, I hope for Spirit2 :-)
We strive for code that compiles without warnings, but with boost.log, I get tons of warnings. What I have seen so far should be easy to get rid of. But I will not be able to use the boost.log library unless the warnings are taken care of.
I'm using GCC 4.4 and don't get any warnings. Could you send your build log? Perhaps, off list, if it's too heavy.
The warnings for the code attached to the original posting are attached to this mail. Compile options are: -Wall -Wreorder -Wnon-virtual-dtor -Wno-non-template-friend -Woverloaded-virtual -Wsign-promo -Wextra -fvisibility=hidden There is a ticket for the date_time stuff, already. Currently I have to fix that by hand with each release...
I can't see a way to detect such errors on an earlier stage, because filters, formatters and loggers are unrelated. Also, it's difficult to perform this kind of check because the setup may still be valid even if not all attributes are present in the core and/or loggers. One can use scoped attributes to temporarily add attributes. Moreover, some attributes may be required by particular sinks.
Hmm. Well for my purposes, I could log a "Log Setup completed" message. If that crashes, no real harm is done.
If you don't want exceptions to be thrown, you can use things like flt::has_attr, std::nothrow in filters/formatters and exception handlers.
I don't mind exceptions. The main issue from my point of view is: No critical message must be lost. If I log a message I want to be pretty sure that it is logged somehow. If the sinks fail and exceptions are thrown, currently, the message is lost. It would be unreasonable for me to add a try/catch statement to each log statement. Maybe the message could be added to the exception? Or maybe the user could provide a callback that would be called in case of an exception? Not sure now, but messages must not be lost by the log system.
It's never too flexible. :) Really, there will always be someone who wants that little thing work a bit differently.
OK, we don't share the same philosophy here :-) Flexibility is good, of course. But like everything else, it can be overdone. I am not saying that this is the case already for Boost.Log, but in my personal opinion it is pretty close, at least. With each and every new way of doing the same stuff as before, it is becoming harder to test, to document and (for users) to find someone to explain what they are doing wrong. Just my 2cents.
String templates are described here:
But I'll add a reference in the section about formatters.
OK, that would be good. Personally, I never know when to look at the Utility section, so maybe more of such references could be added.
Trivial is the usage pattern, the implementation may not be trivial (although, it is quite simple, as for me).
OK, well something like this is not trivial: flt::attr< logging::trivial::severity_level >("Severity") >= logging::trivial::info It is not very complex, but trivial? Wouldn't say so. But I certainly do not insist on a different name :-)
Documentation: ==============
I admit that I got lost on the last few pages, but was probably due to me not actually experimenting with all of it.
Can you point me to the particular sections that were difficult to read?
Examples with a short explanation, why I think I had difficulties. - core: cite: [This section contains a more detailed description of library components and features. Some of them are presented in the Tutorial section...] This may be one of the main reasons for me to have some trouble. When I wrote that mini test program, I found myself continuously jumping from tutorial to details and back again. - sink_frontends: A mini summary with mini sample code would be helpful as introduction: What do I do with sink frontends? Construct Add filters Add backends Add attributes? <-- can I ? Hand over to core - utility: The init_log_to_file convenience function is mentioned in examples several times but never with a pointer to the utility section. The missing links to other sections might be a reason for me not to have really read the utility section. In fact I did not think I'd need it.
I don't think adding namespaces and includes to examples in the readable documentation is a good idea. As you noted, these examples should be brief and free of such noise. However, I will reorganize the docs, so that the code samples will be taken from compilable external files.
Oh, I did not mean to include the "noise". No the examples are fine per se. But it would be very helpful to have a link to a complete/compiling sample program at the end of each tutorial section.
Missing Documentation: - Not all include files are mentioned. This makes it unnecessarily tough to follow the examples. For instance, boost/log/utility/empty_deleter.hpp is required to use logging::empty_deleter. I could not find this information in all of the turorial and details. Another, maybe even more critical example: The BOOST_LOG macro is defined in boost/log/sources/record_ostream.hpp Again I did not find that information in the tutorial.
Yes, these small bits fell out of my scope. If only it was possible to generate the reference both header-based (like it is now) and content-based...
Well, aside from all the remarks I made about problems with the documentation, let me say: It is pretty good already!
- I understand that I can use my own set of severity levels. It is well documented how to filter messages using these severity levels. But I have no idea how to format messages with these severity levels? I do not want the number, I'd like a string, of course. I looked it up in the trivial.hpp, but it should be part of the documentation.
It is described in the formatters section:
All you have to do is to define operator<<. The approach is also demonstrated in examples.
OK, would be cool to have a pointer to that section from here, for instance: http://boost-log.sourceforge.net/libs/log/doc/html/log/tutorial/attributes.h...
- Maybe I missed it, but I did not find an overview of the log rotation options, including a description of the placeholders in strings.
That link seems to be damaged. It did not forward me to the real destination.
Specific question: Can I tell the log rotation to take place at midnight (regardless of the amount of data which is being logged)?
No, you currently cannot specify a specific time point of rotation, but you can specify the rotation period. I've been requested to support time point rotation, so I'll implement it some time.
Our administrators would really appreciate that :-)
I would like to stress this last item one more time: The examples are excellent, but they should be accompanied by more formal tables which show the complete set of options, e.g. which placeholders can be used in a filename format or a list of public (protected?) member functions of a logger. For instance, I found the logger::strm() member in examples only.
The functions and their arguments (including named ones) are described in the Reference section. That is the formal interface description. The more expanded description with code samples in the Detailed features description. If you found inconsistencies or something missing in particular, I'll be glad to fix it.
OK, maybe it would be sufficient to add links into the reference section? My problem is that I often found myself in the situation described above (difficulties): I am not sure if I have all the information I need. And I am not sure how to find out. Brief introductions with usage patterns in the detail-section would help a lot, I guess. Regards, Roland [ 0%] Creating "revision.c" if necessary Warning: svnversion returned 'exported' /home/rbock/metafeed/sources/trunk/sources/LibLogger: (Not a versioned resource) Unchanged revision information, not touching revision.[ch] [ 33%] Built target revision [ 66%] Building CXX object tests/CMakeFiles/Test.dir/test.o In file included from /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/local_time/conversion.hpp:13, from /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/formatters/date_time.hpp:40, from /home/rbock/metafeed/sources/trunk/sources/LibLogger/tests/test.cpp:13: /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp: In function ‘tm boost::posix_time::to_tm(const boost::posix_time::time_duration&)’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_sec’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_min’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_hour’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_mday’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_mon’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_year’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_wday’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_yday’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_isdst’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_gmtoff’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/date_time/posix_time/conversion.hpp:46: warning: missing initializer for member ‘tm::tm_zone’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp: In copy constructor ‘boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>::basic_logger(const boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>&) [with CharT = char, FinalT = boost::log_mt_posix::sources::logger_mt, ThreadingModelT = boost::log_mt_posix::sources::multi_thread_model<boost::log_mt_posix::aux::light_rw_mutex>]’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:396: instantiated from ‘boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, ThreadingModelT, FeaturesT>::basic_composite_logger(const boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, ThreadingModelT, FeaturesT>&) [with CharT = char, FinalT = boost::log_mt_posix::sources::logger_mt, ThreadingModelT = boost::log_mt_posix::sources::multi_thread_model<boost::log_mt_posix::aux::light_rw_mutex>, FeaturesT = boost::mpl::vector0<mpl_::na>]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:662: instantiated from here /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:131: warning: base class ‘struct boost::log_mt_posix::sources::multi_thread_model<boost::log_mt_posix::aux::light_rw_mutex>’ should be explicitly initialized in the copy constructor /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp: In copy constructor ‘boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>::basic_logger(const boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>&) [with CharT = wchar_t, FinalT = boost::log_mt_posix::sources::wlogger_mt, ThreadingModelT = boost::log_mt_posix::sources::multi_thread_model<boost::log_mt_posix::aux::light_rw_mutex>]’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:396: instantiated from ‘boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, ThreadingModelT, FeaturesT>::basic_composite_logger(const boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, ThreadingModelT, FeaturesT>&) [with CharT = wchar_t, FinalT = boost::log_mt_posix::sources::wlogger_mt, ThreadingModelT = boost::log_mt_posix::sources::multi_thread_model<boost::log_mt_posix::aux::light_rw_mutex>, FeaturesT = boost::mpl::vector0<mpl_::na>]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:691: instantiated from here /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:131: warning: base class ‘struct boost::log_mt_posix::sources::multi_thread_model<boost::log_mt_posix::aux::light_rw_mutex>’ should be explicitly initialized in the copy constructor /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp: At global scope: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp: In instantiation of ‘boost::log_mt_posix::basic_record<CharT> boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>::open_record_unlocked(const ArgsT&) [with ArgsT = boost::log_mt_posix::aux::empty_arg_list, CharT = char, FinalT = boost::log_mt_posix::sources::channel_logger<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, ThreadingModelT = boost::log_mt_posix::sources::single_thread_model]’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:597: instantiated from ‘typename boost::mpl::reverse_fold<FeaturesT, boost::log_mt_posix::sources::basic_logger<CharT, FinalT, boost::log_mt_posix::sources::single_thread_model>, boost::log_mt_posix::sources::aux::inherit_logger_features, mpl_::arg<1> >::type::record_type boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, boost::log_mt_posix::sources::single_thread_model, FeaturesT>::open_record() [with CharT = char, FinalT = boost::log_mt_posix::sources::channel_logger<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, FeaturesT = boost::mpl::vector1<boost::log_mt_posix::sources::channel<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >]’ /home/rbock/metafeed/sources/trunk/sources/LibLogger/tests/test.cpp:111: instantiated from here /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:264: warning: unused parameter ‘args’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/parameter/aux_/tagged_argument.hpp: In instantiation of ‘Arg& boost::parameter::aux::tagged_argument<KW, T>::operator[](const boost::parameter::aux::default_<Keyword, Default>&) const [with Default = severity_level, Keyword = boost::log_mt_posix::keywords::tag::severity, Arg = const severity_level]’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/severity_feature.hpp:224: instantiated from ‘typename BaseT::record_type boost::log_mt_posix::sources::basic_severity_logger<BaseT, LevelT>::open_record_unlocked(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::severity, const severity_level>, BaseT = boost::log_mt_posix::sources::basic_channel_logger<boost::log_mt_posix::sources::basic_logger<char, boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, boost::log_mt_posix::sources::single_thread_model>, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, LevelT = severity_level]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:602: instantiated from ‘typename boost::mpl::reverse_fold<FeaturesT, boost::log_mt_posix::sources::basic_logger<CharT, FinalT, boost::log_mt_posix::sources::single_thread_model>, boost::log_mt_posix::sources::aux::inherit_logger_features, mpl_::arg<1> >::type::record_type boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, boost::log_mt_posix::sources::single_thread_model, FeaturesT>::open_record(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::severity, const severity_level>, CharT = char, FinalT = boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, FeaturesT = boost::mpl::vector2<boost::log_mt_posix::sources::severity<severity_level>, boost::log_mt_posix::sources::channel<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >]’ /home/rbock/metafeed/sources/trunk/sources/LibLogger/tests/test.cpp:90: instantiated from here /home/rbock/metafeed/binaries/boost/1.41/include/boost/parameter/aux_/tagged_argument.hpp:123: warning: unused parameter ‘x’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp: In instantiation of ‘boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>::basic_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, CharT = char, FinalT = boost::log_mt_posix::sources::channel_logger<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, ThreadingModelT = boost::log_mt_posix::sources::single_thread_model]’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/channel_feature.hpp:125: instantiated from ‘boost::log_mt_posix::sources::basic_channel_logger<BaseT, ChannelT>::basic_channel_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, BaseT = boost::log_mt_posix::sources::basic_logger<char, boost::log_mt_posix::sources::channel_logger<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, boost::log_mt_posix::sources::single_thread_model>, ChannelT = std::basic_string<char, std::char_traits<char>, std::allocator<char> >]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:564: instantiated from ‘boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, boost::log_mt_posix::sources::single_thread_model, FeaturesT>::basic_composite_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, CharT = char, FinalT = boost::log_mt_posix::sources::channel_logger<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, FeaturesT = boost::mpl::vector1<boost::log_mt_posix::sources::channel<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/channel_logger.hpp:51: instantiated from ‘boost::log_mt_posix::sources::channel_logger<ChannelT>::channel_logger(const T0&) [with T0 = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, ChannelT = std::basic_string<char, std::char_traits<char>, std::allocator<char> >]’ /home/rbock/metafeed/sources/trunk/sources/LibLogger/tests/test.cpp:110: instantiated from here /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:141: warning: unused parameter ‘args’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp: In instantiation of ‘boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>::basic_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, CharT = char, FinalT = boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, ThreadingModelT = boost::log_mt_posix::sources::single_thread_model]’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/channel_feature.hpp:125: instantiated from ‘boost::log_mt_posix::sources::basic_channel_logger<BaseT, ChannelT>::basic_channel_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, BaseT = boost::log_mt_posix::sources::basic_logger<char, boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, boost::log_mt_posix::sources::single_thread_model>, ChannelT = std::basic_string<char, std::char_traits<char>, std::allocator<char> >]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/severity_feature.hpp:195: instantiated from ‘boost::log_mt_posix::sources::basic_severity_logger<BaseT, LevelT>::basic_severity_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, BaseT = boost::log_mt_posix::sources::basic_channel_logger<boost::log_mt_posix::sources::basic_logger<char, boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, boost::log_mt_posix::sources::single_thread_model>, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, LevelT = severity_level]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:564: instantiated from ‘boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, boost::log_mt_posix::sources::single_thread_model, FeaturesT>::basic_composite_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, CharT = char, FinalT = boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, FeaturesT = boost::mpl::vector2<boost::log_mt_posix::sources::severity<severity_level>, boost::log_mt_posix::sources::channel<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/severity_channel_logger.hpp:61: instantiated from ‘boost::log_mt_posix::sources::severity_channel_logger<LevelT, ChannelT>::severity_channel_logger(const T0&) [with T0 = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [15]>, LevelT = severity_level, ChannelT = std::basic_string<char, std::char_traits<char>, std::allocator<char> >]’ /home/rbock/metafeed/sources/trunk/sources/LibLogger/tests/test.cpp:88: instantiated from here /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:141: warning: unused parameter ‘args’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp: In instantiation of ‘boost::log_mt_posix::sources::basic_logger<CharT, FinalT, ThreadingModelT>::basic_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [16]>, CharT = char, FinalT = boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, ThreadingModelT = boost::log_mt_posix::sources::single_thread_model]’: /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/channel_feature.hpp:125: instantiated from ‘boost::log_mt_posix::sources::basic_channel_logger<BaseT, ChannelT>::basic_channel_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [16]>, BaseT = boost::log_mt_posix::sources::basic_logger<char, boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, boost::log_mt_posix::sources::single_thread_model>, ChannelT = std::basic_string<char, std::char_traits<char>, std::allocator<char> >]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/severity_feature.hpp:195: instantiated from ‘boost::log_mt_posix::sources::basic_severity_logger<BaseT, LevelT>::basic_severity_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [16]>, BaseT = boost::log_mt_posix::sources::basic_channel_logger<boost::log_mt_posix::sources::basic_logger<char, boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, boost::log_mt_posix::sources::single_thread_model>, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, LevelT = severity_level]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:564: instantiated from ‘boost::log_mt_posix::sources::basic_composite_logger<CharT, FinalT, boost::log_mt_posix::sources::single_thread_model, FeaturesT>::basic_composite_logger(const ArgsT&) [with ArgsT = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [16]>, CharT = char, FinalT = boost::log_mt_posix::sources::severity_channel_logger<severity_level, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, FeaturesT = boost::mpl::vector2<boost::log_mt_posix::sources::severity<severity_level>, boost::log_mt_posix::sources::channel<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > >]’ /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/severity_channel_logger.hpp:61: instantiated from ‘boost::log_mt_posix::sources::severity_channel_logger<LevelT, ChannelT>::severity_channel_logger(const T0&) [with T0 = boost::parameter::aux::tagged_argument<boost::log_mt_posix::keywords::tag::channel, const char [16]>, LevelT = severity_level, ChannelT = std::basic_string<char, std::char_traits<char>, std::allocator<char> >]’ /home/rbock/metafeed/sources/trunk/sources/LibLogger/tests/test.cpp:98: instantiated from here /home/rbock/metafeed/binaries/boost/1.41/include/boost/log/sources/basic_logger.hpp:141: warning: unused parameter ‘args’ [100%] Building C object tests/CMakeFiles/Test.dir/__/generated/revision.o Linking CXX executable Test [100%] Built target Test

On 03/11/2010 01:04 PM, Roland Bock wrote:
Andrey Semashev wrote:
Is there a way to have several sinks with different filter but sharing the log records?
That's an interesting idea, not very simple to implement though. The problem is that some records can pass filtering for some of the sinks and not the others, while the sinks may share the formatter. Further on, formatting of the same log record for different sinks may occur in different threads. I'll add this feature to the TODO list, I need some time to think about it.
I understand that it is difficult in all generality. Since it is an optimization, maybe it could be implemented for certain cases only? For instance restrict it to sinks which live in the same thread?
I'd like to avoid such non-obvious restrictions. I'm thinking of some kind of thread-local storage for each shared formatter. It would have to cache the record and the result of its formatting in this storage, so that it could format the record only once, no matter how many times it is invoked for it. The problem is that it's not clear when to clean the cache.
The warnings for the code attached to the original posting are attached to this mail. Compile options are:
-Wall -Wreorder -Wnon-virtual-dtor -Wno-non-template-friend -Woverloaded-virtual -Wsign-promo -Wextra -fvisibility=hidden
There is a ticket for the date_time stuff, already. Currently I have to fix that by hand with each release...
Ok, I'll look into it.
If you don't want exceptions to be thrown, you can use things like flt::has_attr, std::nothrow in filters/formatters and exception handlers.
I don't mind exceptions. The main issue from my point of view is: No critical message must be lost. If I log a message I want to be pretty sure that it is logged somehow. If the sinks fail and exceptions are thrown, currently, the message is lost. It would be unreasonable for me to add a try/catch statement to each log statement.
Maybe the message could be added to the exception? Or maybe the user could provide a callback that would be called in case of an exception?
Exception handlers allow to inject such callbacks in sinks, core and loggers. On a corner case, the callback can suppress all or some exceptions.
Not sure now, but messages must not be lost by the log system.
I think, it might be a good idea to attach the record to the exception by means of Boost.Exception.
Can you point me to the particular sections that were difficult to read?
Examples with a short explanation, why I think I had difficulties.
- core: cite: [This section contains a more detailed description of library components and features. Some of them are presented in the Tutorial section...] This may be one of the main reasons for me to have some trouble. When I wrote that mini test program, I found myself continuously jumping from tutorial to details and back again.
Ok, I'll remove that as it looks like it doesn't bring any useful information anyway.
- sink_frontends: A mini summary with mini sample code would be helpful as introduction: What do I do with sink frontends?
Construct Add filters Add backends Add attributes? <-- can I ? Hand over to core
Hmm, there are code samples for every sink frontend, and every sample contains the steps needed to setup a sink, including filters, formatters and adding to the core. Do you think that's not enough?
- utility: The init_log_to_file convenience function is mentioned in examples several times but never with a pointer to the utility section. The missing links to other sections might be a reason for me not to have really read the utility section. In fact I did not think I'd need it.
Ok, I'll add references.
I don't think adding namespaces and includes to examples in the readable documentation is a good idea. As you noted, these examples should be brief and free of such noise. However, I will reorganize the docs, so that the code samples will be taken from compilable external files.
Oh, I did not mean to include the "noise". No the examples are fine per se. But it would be very helpful to have a link to a complete/compiling sample program at the end of each tutorial section.
Will do. I wanted to play around with quickbook some more anyway. :)
- I understand that I can use my own set of severity levels. It is well documented how to filter messages using these severity levels. But I have no idea how to format messages with these severity levels? I do not want the number, I'd like a string, of course. I looked it up in the trivial.hpp, but it should be part of the documentation.
It is described in the formatters section:
All you have to do is to define operator<<. The approach is also demonstrated in examples.
OK, would be cool to have a pointer to that section from here, for instance: http://boost-log.sourceforge.net/libs/log/doc/html/log/tutorial/attributes.h...
Ok, will do.
- Maybe I missed it, but I did not find an overview of the log rotation options, including a description of the placeholders in strings.
That link seems to be damaged. It did not forward me to the real destination.
Here's the full link: http://boost-log.sourceforge.net/libs/log/doc/html/log/detailed/sink_backend...
The functions and their arguments (including named ones) are described in the Reference section. That is the formal interface description. The more expanded description with code samples in the Detailed features description. If you found inconsistencies or something missing in particular, I'll be glad to fix it.
OK, maybe it would be sufficient to add links into the reference section?
My problem is that I often found myself in the situation described above (difficulties): I am not sure if I have all the information I need. And I am not sure how to find out. Brief introductions with usage patterns in the detail-section would help a lot, I guess.
Ok, I'll try to do so. Adding links to the auto-generated reference is not a very obvious thing to do.

Andrey Semashev wrote: [snip]
- core: cite: [This section contains a more detailed description of library components and features. Some of them are presented in the Tutorial section...] This may be one of the main reasons for me to have some trouble. When I wrote that mini test program, I found myself continuously jumping from tutorial to details and back again.
Ok, I'll remove that as it looks like it doesn't bring any useful information anyway.
Hmm. The sentence is not the problem. My issue was: I found valuable/required information in both places. I therefore never knew where to look. Often enough I first looked in details and then had to go to the tutorial when I did not find what I wanted. It is very good to have the tutorial, but when I read in the details, I would prefer all information to be available in the details. It should not be necessary to go back to the tutorial. It may very well be that this topic is outdated when the other documentation things are changed. So maybe you should ignore it for now :-)
- sink_frontends: A mini summary with mini sample code would be helpful as introduction: What do I do with sink frontends?
Construct Add filters Add backends Add attributes? <-- can I ? Hand over to core
Hmm, there are code samples for every sink frontend, and every sample contains the steps needed to setup a sink, including filters, formatters and adding to the core. Do you think that's not enough?
The examples are fine if I want to take a look at a specific frontend. But if I want to know "what are frontends all about", it does not help to start reading that every sink has to be compatible with the sink interface... Thats why I suggested a mini summary, maybe in form of a list telling me what I can do with a sink in general. Regards, Roland

Andrey Semashev wrote:
On 03/10/2010 08:52 PM, Roland Bock wrote:
- none of the code examples is complete. They are missing the namespace declarations documented in the introduction, at least. While brevity is good, it would be very helpful to have complete, compiling examples in the tutorial
I don't think adding namespaces and includes to examples in the readable documentation is a good idea. As you noted, these examples should be brief and free of such noise. However, I will reorganize the docs, so that the code samples will be taken from compilable external files.
Other boost docs have extracts but also links to the full source code. I think that works well. John Bytheway

On 03/11/2010 10:41 PM, John Bytheway wrote:
Andrey Semashev wrote:
On 03/10/2010 08:52 PM, Roland Bock wrote:
- none of the code examples is complete. They are missing the namespace declarations documented in the introduction, at least. While brevity is good, it would be very helpful to have complete, compiling examples in the tutorial
I don't think adding namespaces and includes to examples in the readable documentation is a good idea. As you noted, these examples should be brief and free of such noise. However, I will reorganize the docs, so that the code samples will be taken from compilable external files.
Other boost docs have extracts but also links to the full source code. I think that works well.
Agreed, that seems to be the best solution.
participants (3)
-
Andrey Semashev
-
John Bytheway
-
Roland Bock