0

I've been writing logging code for our product, and have an architectural problem. We have two command-line executables, written in C++ for Window, call them Foo and Bar, that depend on a DLL which we'll name Core. I want to log from within Core.

The question is, where should those log entries end up? If I run Foo, I want to see them in Foo.log, and if I run Bar, they should be in Bar.log. What if I run both Foo and Bar at the same time, what then? (I think I already sorted the case when multiple copies of Foo or Bar are run, effectively locking the log file).

One thought is that Core can keep a list of "all the loggers I need to invoke when someone makes a logging request". This implies there is a whole new API to write and that logging in DLLs is written different to logging in exes or static libraries. That's not ideal. I may not even know where the code ends up if it's in a lib!

I've looked at log4cplus, and boost logging but can't get any traction on how this would work with those components either, so am a bit stuck for ideas. This is surely a solved problem though?!

Julian Gold
  • 1,246
  • 2
  • 19
  • 40
  • In your `DllMain`, handle `DLL_PROCESS_ATTACH`, figure [what process you are in](https://stackoverflow.com/q/6286959/11683), and set up the logger accordingly. – GSerg May 14 '19 at 08:06
  • I don't see any problem here. There is nothing preventing you from specifying where logs should be written to when creating a logging context in each executable. – user7860670 May 14 '19 at 08:09
  • @VTT in my DLL I have a single logger logger = LogManager::GetLogger("core"). If Foo and Bar both run, they will both try to configure the logger to write to their log file (hence a race condition). – Julian Gold May 14 '19 at 08:27
  • `GetLogger` should take an argument specifying where logs should be stored then. Also there is no race condition because calling that from `Foo` and from `Bar` (of from different instances of `Foo`) will yield different objects. – user7860670 May 14 '19 at 08:29
  • @GSerg DLL_PROCESS_ATTACH is called once per attaching process (ie for both Foo and Bar), or once when the DLL itself is loaded? And how do I figure out if it's Foo or Bar (or some other application?) – Julian Gold May 14 '19 at 08:29
  • You should definitely not add anything in `DllMain` (because it is restricted and normally should not execute any custom code). And there is no need to figure out anything - just supply appropriate arguments explicitly. – user7860670 May 14 '19 at 08:32
  • 1
    Dll itself does not load, it is loaded into a process, and dllmain is called each time. Click the link above. – GSerg May 14 '19 at 08:32
  • @GSerg thanks, I think I may have misunderstood something fundamental. If Foo and Bar both use Core.dll, if Core has a variable x, then is there one copy of x that Foo and Bar share, or do they have their own x's? – Julian Gold May 14 '19 at 08:53
  • 2
    Since `Foo` and `Bar` are different processes each will have its own `x`. – user7860670 May 14 '19 at 08:57
  • We use modularized architecture for our products - that is there is main executable which loads "plugins". This executable is responsible for initialization of (among other stuff) logging. So each dynamic library (plugin) has list of resources and among them logger. Has some benefits and some drawbacks, however is quite adaptable. Just create interface executable must provide for logging. – rAndom69 May 14 '19 at 17:37

1 Answers1

0

Function pointers is the key, IMO.

I usually implement loggers on the very top level, in the .exe code, and on startup I pass the function pointer all the way down. Here’s an example.

enum struct eLogLevel : uint8_t
{
    Error,
    Warning,
    Info,
    Debug
};
// C function pointer to write messages to arbitrary destination. Messages are better to be UTF8.
using pfnLogMessage = void( *)( eLogLevel lvl, const char* msg );

// DLL API, call this once, immediately after loading your DLL. Keep the level + function pointer in static variables. 
// Unless messing with DLL segments, static variables are per-process, i.e. two processes will have different copy of these variables.
// Good idea to also call on shutdown, passing nullptr, before closing the underlying implementation stream.
void initializeLogging( eLogLevel maxLevelToLog, pfnLogMessage fnLogMessage );

// Invoke that function pointer to actually log stuff.
void logMessage( eLogLevel lvl, const CStringA& msg );

// Convenience wrappers to format and write various messages.
// Moar examples: https://github.com/Const-me/vis_avs_dx/blob/master/avs_dx/DxVisuals/Utils/logger.h
inline void logMessageV( eLogLevel lvl, const char* pszFormat, va_list args )
{
    CStringA str;
    str.FormatV( pszFormat, args );
    logMessage( lvl, str );
}

#define LOG_MESSAGE_FORMAT( lvl ) va_list args; va_start( args, pszFormat ); logMessageV( lvl, pszFormat, args ); va_end( args );

inline void logError( const char* pszFormat, ... )
{
    LOG_MESSAGE_FORMAT( eLogLevel::Error );
}
Soonts
  • 20,079
  • 9
  • 57
  • 130
  • This works fine when I have a single instance of a single app talking to the logging DLL. But when I have (either/or) multiple instances of multiple applications talking to the logging DLL, it breaks. – Julian Gold May 20 '19 at 10:34
  • @JulianGold The approach doesn’t use global resources, it’s up to the caller process to implement that function pointer. It doesn’t break unless you mess with DLL segments, i.e. keep global variables in the `SHARED` read/write section of the DLL. What likely happens, you are trying to open same file for writing from multiple processes. If you want to log multiple processes in the same file, and you don’t log too many stuff i.e. performance is not an issue, you can open/create the file with `FILE_SHARE_WRITE` flag, and use named mutex to synchronize. – Soonts May 20 '19 at 17:34