Search code examples
c++boost-log

What synchronization is required for Boost Log?


I have a native C++ library that is used from a WPF and C++\CLI application. I need to implementing logging in the native library that can be merged with the logging in the WPF application.

I'm trying to use Boost LogV2 to create an 'observable sink' where I can register a listener to propagate the native log messages.

One of my main 'concerns' is the comment from Boost sample: 'We skip the actual synchronization code for brevity'. I'm not sure what, if any synchronization I should be doing.

Also, would there be any issues with having multiple log sources using the same sink? I want to have different loggers with different attributes added to the logger (ie which module the log message was raised from).

The log listener

class ILogListener
{
public:
  virtual ~ILogListener() = default;
  virtual void onMessageLogged(const std::string& message) = 0;

protected:
  ILogListener() {};

private:
  // Disabling default copy constructor and default 
  // assignment operator.
  ILogListener(const ILogListener& copy) = delete;
  ILogListener& operator=(const ILogListener& copy) = delete;
};

class MyLogListener : public ILogListener
{
  void onMessageLogged(const std::string& message) override
  {
    std::cout << message << "\n";
  }
};

The Log Sink

class ObservableLogSink : public sinks::basic_sink_backend< sinks::concurrent_feeding >
{
public:
  // The function is called for every log record to be written to log
  void consume(logging::record_view const& rec)
  {
    auto message = rec[expr::smessage];
    auto message_value = message.get();

    notifyListeners(message_value);
  }

  template <typename container, typename T>
  void addIfNotInContainer(container& c, T item)
  {
    typename container::iterator iter = find(c.begin(), c.end(), item);

    // Only add the listener if we don't have it in the container
    if (iter == c.end())
    {
      c.push_back(item);
    }
  }

  void notifyListeners(const std::string& message)
  {
    for (std::vector<ILogListener*>::iterator begin = listeners_.begin(), end = listeners_.end(); begin != end; ++begin)
    {
      (*begin)->onMessageLogged(message);
    }
  }

  void registerListener(ILogListener* listener)
  {
    addIfNotInContainer(listeners_, listener);
  }

  void removeListener(ILogListener* listener)
  {
    std::vector<ILogListener*>::iterator iter = find(listeners_.begin(), listeners_.end(), listener);

    // Only delete the listener if we have it in the container
    if (iter != listeners_.end())
    {
      listeners_.erase(iter);
    }
  }

private:
  std::vector<ILogListener*> listeners_;
};

The Logger

class Logger
{
public:
  Logger() : sink_backend_(new ObservableLogSink())
  {
    init();
  }
  ~Logger(){}

  void registerListener(ILogListener* listener)
  {
    sink_backend_->registerListener(listener);
  }
  void removeListener(ILogListener* listener)
  {
    // TODO
  }

  void init()
  {
    logging::add_common_attributes();

    boost::shared_ptr< logging::core > core = logging::core::get();

    boost::shared_ptr<sinks::unlocked_sink<ObservableLogSink>> sink(new sinks::unlocked_sink<ObservableLogSink>(sink_backend_));

    core->add_sink(sink);
  }

  template <typename ... args >
  void log(args ... to_print)
  {

    logging::record rec = main_log_.open_record();
    if (rec)
    {
      logging::record_ostream strm(rec);

      print(strm, to_print...);

      render_log_.push_record(boost::move(rec));
    }
  }

private:
  boost::log::sources::logger_mt main_log_;

  boost::shared_ptr<ObservableLogSink> sink_backend_;

  template <typename T >
  void print(logging::record_ostream& stream, T only)
  {
    stream << only;
    stream.flush();
  }

  template <typename T, typename ... args >
  void print(logging::record_ostream& stream, T current, args... next)
  {
    stream << current << ' ';
    print(stream, next...);
  }
};

In Action

int main()
{
  Logger logger;

  std::unique_ptr<ILogListener> listener(new MyLogListener);

  logger.registerListener(listener.get());
  logger.log("Hello", "from", "logging");
}

Solution

  • If you use unlocked_sink frontend, as you do in your code and is done in the example, then the backend can be invoked concurrently from multiple threads that may emit log records. The backend itself must implement measures to protect its data from concurrent access. Usually, you need a mutex for that. There are cases when you don't need synchronization, such as when the backend simply forwards log records to a system call or another synchronized API; unlocked_sink is primarily intended for such cases.

    However, if your backend has data that needs protection, a better approach would be to use synchronous_sink frontend that does thread synchronization for you. To express the requirement that your backend needs external synchronization you should derive from sinks::basic_sink_backend< sinks::synchronized_feeding >. This way the user won't be able to use unlocked_sink frontend with your backend.

    Also, you might want to enable formatting for your backend, if you want your observer to receive formatted messages instead of just the message text. You can do this by adding formatted_records to the backend requirements.

    class ObservableLogSink :
        public sinks::basic_sink_backend<
            sinks::combine_requirements<
                sinks::synchronized_feeding,
                sinks::formatted_records
            >::type
        >
    {
    public:
        // Define types for characters and strings consumed by this backend
        typedef char char_type;
        typedef std::basic_string< char_type > string_type;
    
        // The function is called for every log record to be written to log
        void consume(logging::record_view const& rec,
            string_type const& formatted_message)
        {
            // Pass formatted_message to the observers
        }
    };
    

    You can then configure formatters for your sink similar to how you do for other sinks.