7

I'm writing a library that has a lot of information that may be of use in a log in the program which uses it, but I don't know the best way to expose it in such a way that the program using my library can integrate my library's logs with its own logs seemlessly (if desired).

Picking a specific logging library for my library adds to the list of dependencies for using my library as well as ties the main program to that library -- and if multiple libraries used by the main program did this, there could each have selected a different library.

I've though about having the program register a C++ stream object with the library for it to use. That seems like it would be relatively general purpose, but I also thought about just having the main program register a callback function which would be called for with the contents and metadata when data is logged. Another option would just be to store the log data in the library in some sort of list for the main program to grab whenever it wants to deal with that data, letting the main program decide when it has time to deal with the data.

I'm looking for suggestions and pros/cons of different approaches so I can decide what is best in my situation.

  • 1
    Not really an answer but I suggest you look at how the Qt toolkit does it. Google QtMessageHandler. And QMessageLogger. It looks a lot like what the other answers suggest, btw. – Teimpz Jun 8 '17 at 6:35
7

You can expose multiple methods to receive the logging from your library, and wrap all but one in adaptors to the "real" one used in the library.

E.g. you decide to internally have a std::function<void(std::string)> collection that is each logger callback. You provide:

void registerLogCallback(std::function<void(std::string)> callback); 
// Main logging implemention

and also

registerLogStream(std::ostream stream) { 
    registerLogCallback([stream](std::string message){ stream << message; }); 
}

and also

template<typename OutputIterator>
registerLogOutputIterator(OutputIterator iter) { 
    registerLogCallback([iter](std::string message){ *iter++ = message; }); 
}

and any more variations of "recieve strings from somewhere" types you care to implement adaptors for.

  • I think it would eventually also need some kind of "logging level", like debug, warning, fatal, ... So that the user can filter out what he needs. Good start nonetheless. – Teimpz Jun 8 '17 at 6:42
4

The simplest way to allow an application to be able to tap into the logging functionality is allow it to register a class/function to receive the log message. What they do with that message is entirely up to the application.

Using C/C++, you can use the following in your library:

typedef void (*LogMessageReceiver)(char const* message,
                                   void* user_data);

void registerLogMessageReceiver(LogMessageReceiver receiver,
                                void* user_data);

An application can register a function by calling registerLogMessageReceiver. with the appropriate user_data. In the logging section of your code base, you have to make sure to call that function with the appropriate message and the registered user_data.

If you don't have to worry about C, you can use a class as the receiver of messages.

struct LogMessageReceiver
{
   virtual ~LogMessageReceiver() {}
   virtual void receive(std::string const& message) = 0;
};

and add a function in the library to allow an application to register a log message receiver.

void registerLogMessageReceiver(LogMessageReceiver* receiver);

An application can register a LogMessageReceiver by calling the above function. You'll have to make some policy decisions regarding the ownership of the registered LogMessageReceiver. If the library takes ownership of the receiver, it must delete the pointer. If the library does not take ownership of the receiver, the application must take care of deleteing the receiver.

Using a class as the log message receiver allow the user_data bit to be omitted in registerLogMessageReceiver since the sub-type of LogMessageReceiver is free to hold any data that is useful for it's functioning. It does not need to be passed any additional user data in the receive function.

From there, it can get more complex depending on how sophisticated your logging mechanism is.

For example, you could have various logging levels: Concise, Normal, Verbose, or LoggingLevel1, LoggingLevel2, ..., LoggingLevelN.

In that case, you'll have to allow the application to control the level of logging they wish to use.

There are an endless set of choices once you decide to move beyond the simplistic logging mechanism. It does not make sense to delve into them here.

  • This is a good answer if pre-C++11 compilers need to be supported. If not, I'd favor Caleths' answer. std::function allows capturing which is a much safer alternative to the void* – Teimpz Jun 8 '17 at 6:40
1

Writing a library that interfaces easily with the host application's log system is easy, provided you know what that system is. When there are multiple possibilities for that, it's harder, and you're constrained by the tyranny of the lowest common denominator. You could have a compatibility layer that adapts you library to the various log systems, but now you can't rely on some useful, unique feature that only one system has. If the end user has the other system, that useful unique feature isn't there.

In the .Net world, there are (at least) two commonly used log systems, log4net and NLog. Both are similar, but not identical. I was already using log4net, and then started using NHibernate (an ORM library). Luckily, it uses log4net internally, so it was easy to add it to a project. (And here I disagree with @Daniel's answer: it's sometimes tremendously useful for NHibernate to log its activity at fine detail in the same system I'm using elsewhere, with no extra work.) If I were already invested in NLog, that means either I switch, or have a project that uses both.

At least with logging, there's usually the option of creating a custom message appender that you can configure to forward a messaged logged in one system to the other logging system. So if were already using NLog, and really wanted to continue with it, but also use NHibernate, I could grit my teeth a write a log4net appender that forwards each message to NLog. It would be straightforward to do if the APIs are similar.

The alternatives are really to pick the best system for your needs that is available and use it unreservedly, or to have some kind of adapter layer, which is subject to the lowest common denominator problem. Which is not a very satisfying answer.

  • This, plus the fact that C++ makes easy things difficult and difficult things even more difficult. – rwong May 1 at 21:16
0

I would assert that you should rethink the need to have logging coupled with your library; Especially for C++ where there is no standard logger interface.

Different applications have different policies regarding logging. A library should be policy agnostic.

The purpose of a library is to provide a service, and preferably, indicate whether a request for that service succeeded or failed; ideally with an indication of why [it failed] via errno, return code, exception... If your desire for logging is because a provided function can fail in multiple places, you might be trying to do too much in one function. Maybe not, but consider the possibility.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.