Boost.Log: Support file name and line number
Asked Answered
R

2

7

I am trying to make my team go away from log4cxx and try to use Boost.Log v2 instead. Our current log4cxx pattern is rather simple:

log4cxx::helpers::Properties prop;
prop.setProperty("log4j.rootLogger","DEBUG, A1");
prop.setProperty("log4j.appender.A1","org.apache.log4j.ConsoleAppender");
prop.setProperty("log4j.appender.A1.layout","org.apache.log4j.PatternLayout");
prop.setProperty("log4j.appender.A1.layout.ConversionPattern","%d{ABSOLUTE} %-5p [%c] %m%n");
log4cxx::PropertyConfigurator::configure(prop);

However I failed to find a solution to support filename and line number printing. I've found so far an old post, but there is no clear solution (no accepted solution). I've looked into using those BOOST_LOG_NAMED_SCOPE but they feel very ugly as it make it impossible to print the proper line number when multiple of those are used within the same function.

I've also found a simpler direct solution, here. But that also feel ugly, since it will print the fullpath, instead of just the basename (it is not configurable). So the fullpath and line number are always printed on a fixed location (before expr::smessage).

I've found also this post, which looks like an old solution.

And finally the most promising solution, I found was here. However it does not even compile for me.

So my question is simply: how can I use Boost.Log v2 to print filename (not fullpath) and line number with minimal formatter flexibility (no solution with MACRO and __FILE__ / __LINE__ accepted, please). I'd appreciate a solution which does not involve BOOST_LOG_NAMED_SCOPE, as described here:

If instead you want to see line numbers of particular log records then the best way is to define a custom macro which you will use to write logs. In that macro, you can add the file name and line number as attributes to the record (you can use manipulators for that). Note that in this case you won't be able to use these attributes in filters, but you probably don't need that.

Recognition answered 1/7, 2015 at 6:53 Comment(0)
R
15

I finally found a simple solution based on add_value. Here is the full source code:

#include <ostream>
#include <fstream>
#include <boost/log/core.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>
#include <boost/log/utility/manipulators/add_value.hpp>
#include <boost/filesystem.hpp>

namespace logging = boost::log;
namespace src = boost::log::sources;
namespace expr = boost::log::expressions;
namespace sinks = boost::log::sinks;

void my_formatter(logging::record_view const& rec, logging::formatting_ostream& strm)
{
    // Get the LineID attribute value and put it into the stream
    strm << logging::extract< unsigned int >("LineID", rec) << ": ";
    strm << logging::extract< int >("Line", rec) << ": ";
    logging::value_ref< std::string > fullpath = logging::extract< std::string >("File", rec);
    strm << boost::filesystem::path(fullpath.get()).filename().string() << ": ";

    // The same for the severity level.
    // The simplified syntax is possible if attribute keywords are used.
    strm << "<" << rec[logging::trivial::severity] << "> ";

    // Finally, put the record message to the stream
    strm << rec[expr::smessage];
}

void init()
{
    typedef sinks::synchronous_sink< sinks::text_ostream_backend > text_sink;
    boost::shared_ptr< text_sink > sink = boost::make_shared< text_sink >();

    sink->locked_backend()->add_stream(
        boost::make_shared< std::ofstream >("sample.log"));

    sink->set_formatter(&my_formatter);

    logging::core::get()->add_sink(sink);
}

#define MY_GLOBAL_LOGGER(log_,sv) BOOST_LOG_SEV( log_, sv) \
  << boost::log::add_value("Line", __LINE__)      \
  << boost::log::add_value("File", __FILE__)       \
  << boost::log::add_value("Function", BOOST_CURRENT_FUNCTION)

int main(int, char*[])
{
    init();
    logging::add_common_attributes();

    using namespace logging::trivial;
    src::severity_logger< severity_level > lg;

    MY_GLOBAL_LOGGER(lg,debug) << "Keep";
    MY_GLOBAL_LOGGER(lg,info) << "It";
    MY_GLOBAL_LOGGER(lg,warning) << "Simple";
    MY_GLOBAL_LOGGER(lg,error) << "Stupid";

    return 0;
}

On my Linux box, I compiled it using:

$ c++ -otutorial_fmt_custom -DBOOST_LOG_DYN_LINK tutorial_fmt_custom.cpp -lboost_log -lboost_log_setup -lboost_thread -lpthread -lboost_filesystem

If you run and check the log file generated here is what you get:

$ ./tutorial_fmt_custom && cat sample.log 
1: 61: tutorial_fmt_custom.cpp: <debug> Keep
2: 62: tutorial_fmt_custom.cpp: <info> It
3: 63: tutorial_fmt_custom.cpp: <warning> Simple
4: 64: tutorial_fmt_custom.cpp: <error> Stupid

My solution is based on two inputs (thanks!):

Recognition answered 1/7, 2015 at 12:7 Comment(3)
bear in mind that your solution will output filename as long as you compile your project using file names. Try to compile using ../test/tutorial_fmt_custom.cpp then you will get the same in log file instead of just file name.Featly
I am trying the approach for using add_value that you suggest, which looks much nicer than the other approaches I have seen. But I cannot get it to work. I think you left out the declaration+definition of your attributes and keywords. Can you provide them in your answer? Also, the page for the second link is not in English. Can you point me to an English translation? (I tried Google Translate, but it did not work.) Thanks!Buffet
The compilation needed -lboost_system for meKarafuto
F
2

In order to output filename and line you will need to register Scopes:

logging::core::get()->add_global_attribute("Scopes", attributes::named_scope());

and add custom formatter:

void my_formatter(logging::record_view const& rec, logging::formatting_ostream& strm)
{
    const auto cont = logging::extract< attributes::named_scope::value_type >("Scopes", rec);
    if(cont.empty())
        return;

    auto it = cont->begin();
    boost::filesystem::path path(it->file_name.c_str());
    strm << path.filename().string() << ":" << it->line << "\t" << rec[expr::smessage];
}

Note: In order for this to work the scopes container needs to be cleared before each logging: if(!attributes::named_scope::get_scopes().empty()) attributes::named_scope::pop_scope();

Featly answered 1/7, 2015 at 7:29 Comment(2)
Could you please add an example on how to use this so that it prints the correct line number (where BOOST_LOG_SET is, rather than line where BOOST_LOG_NAMED_SCOPE is). Thanks.Recognition
@Recognition Note cont->begin() there. You need to clear the container before each logging and you are probably accessing always first the same entry. See updated answer.Featly

© 2022 - 2024 — McMap. All rights reserved.