Boost.Log: Support file name and line number

2019-03-21 06:40发布

问题:

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.

回答1:

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!):

  • http://www.boost.org/doc/libs/1_58_0/libs/log/example/doc/tutorial_fmt_custom.cpp
  • http://d.hatena.ne.jp/yamada28go/20140215/1392470561


回答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();