How to Log and stay Modular

Started by
55 comments, last by ApochPiQ 12 years, 7 months ago
Okay so it seems like just creating a function that logs stuff either to a file or to the console should be fine as it is the simplest and there is really no need for anything more that that. If I need something more, like a logger that sends stuff through the network then I could just take the log data and send it with a different thing. After all, I shouldn't make something that does everything.
Advertisement

How exactly are you going to provide a logging hook to a library by simply a free/static function?

The OP specifically asked how to do logging in a modular fashion. Discounting the utility of that requirement, at the very least you'll need some static variable that the user of the library can re-assign/populate.




Function pointers. Delegates. Whatever your language of choice supports. Doesn't matter.

I guess maybe Java is at a disadvantage here, but I did get the impression we're talking about C++, so function pointer it is. If you're fixated on passing an object, it can still be a static member function if absolutely necessary, but I still honestly don't see any benefit whatsoever to passing an object for this.


Or you have your logging itself be provided by a module (statically linked lib) by itself, which exposes a free function/set of free functions; this seems like the obvious choice to me, and indeed has long been my preference (and that of many programmers smarter than me with whom I have worked over the years).


And for the record, I didn't intend to refer to "overhead" in the sense of runtime cost; although there is some cost, it's probably negligible next to the cost of actually doing logging in the first place. My point isn't that free functions are preferable because they're "faster" or "more efficient" or some vacuous nonsense like that; it's about mental overhead.

Most of the time, you get something like this:

Logger log;
log.Write("Something happened!");


Oh boy! Now it takes two lines of code to do one thing.

Worse, we get suggestions like earlier in the thread. This is a pattern I have seen in actual code and it always makes me want to vomit:

#define LOG(x) do { Logger log; log.Write(x); } while(false)

Now you have a macro to generate two lines of code to do one thing.


I repeat my former plea: someone please explain to me why this is considered a reasonable thing to do, ever.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]


[quote name='Telastyn' timestamp='1315702542' post='4860181']
How exactly are you going to provide a logging hook to a library by simply a free/static function?

The OP specifically asked how to do logging in a modular fashion. Discounting the utility of that requirement, at the very least you'll need some static variable that the user of the library can re-assign/populate.


Function pointers. Delegates. Whatever your language of choice supports. Doesn't matter.
[/quote]

Cool, just making sure that's what you meant.
So, what's the final color of bike shed?

Most of the time, you get something like this:
Logger log;
log.Write("Something happened!");
Sorry, I've just jumped in at the end of the thread... but what is this?

[font="Courier New"]log.Write[/font] is saying "write this line to this log output", right? Which means the constructor of [font="Courier New"]Logger[/font] should take an argument that says what that output channel is, like L[font="Courier New"]ogger log("c:/foobar.txt");[/font] ?

Without that constructor argument, then [font="Courier New"]Logger[/font] is a monostate, which puts it in the same bucket as singletons and globals, but in the more-obscure-than-singleton corner!

So this particular example seems to be a complete straw-man, as it's conceptually identical to a free-function implementation, except with stupid monostate-syntax...
Do people really use monostate log implementations?
IMHO the point of using a log object would be in configuring it's output/destination via the constructor, like above, instead of having a global log output/destination.

...and in C++, the delegate-to-free-function being recommended could likely be implemented as an object... wink.gif

And for the record, I didn't intend to refer to "overhead" in the sense of runtime cost; although there is some cost, it's probably negligible next to the cost of actually doing logging in the first place. My point isn't that free functions are preferable because they're "faster" or "more efficient" or some vacuous nonsense like that; it's about mental overhead.

Ok, good. I was worried for a second. Although if you're getting mental overhead from creating a global logger object and using it, You're Doing It Wrong. It's not supposed to be tricky, complex, ugly, etc.


Most of the time, you get something like this:

Logger log;
log.Write("Something happened!");


I hope not. I'm not talking about creating a new Logger object whenever you want to log something. I'm talking about having a global Logger object that the one can access from anywhere and use to log.


This is a pattern I have seen in actual code and it always makes me want to vomit:

#define LOG(x) do { Logger log; log.Write(x); } while(false)

I agree, whoever does that should be fired.


I repeat my former plea: someone please explain to me why this is considered a reasonable thing to do, ever.

Ever? Really? I'm in agreement with you 95% of the time. Most of the time you won't need anything more than some free function to do the logging. And in the OP's case, I think a free function will provide him with all the functionality he needs and it will a clean and great solution. But there are a few times where it's convenient to make a logger object that can store its state and encapsulate its information. For example, let's say you want to provide the user with the ability to choose where the log gets outputted to. It's easy and clean with an object, because you can simply do globalLogger = Logger("new/output/file.txt");, and none of the other code has to know that there was any kind of change at all. You can also tell the user where it's currently logging by simply doing globalLogger.getLogFilePath(), or something like that. The idea is that the data is encapsulated. You can provide this same kind of functionality with free functions, but IMHO using an object as a logger is a cleaner solution in this case because it keeps all the data encapsulated. And no, I wouldn't add any atrocious boiler plate code, like in your example. That'd just be ridiculous.


So, what's the final color of bike shed?

Neon pink witha neon green roof, why? :)
[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

[quote name='ApochPiQ' timestamp='1315725950' post='4860254']
This is a pattern I have seen in actual code and it always makes me want to vomit:

#define LOG(x) do { Logger log; log.Write(x); } while(false)

I agree, whoever does that should be fired.[/quote]
Ever used std::clog or std::cout?

[quote name='Cornstalks' timestamp='1315752689' post='4860345']
[quote name='ApochPiQ' timestamp='1315725950' post='4860254']
This is a pattern I have seen in actual code and it always makes me want to vomit:

#define LOG(x) do { Logger log; log.Write(x); } while(false)

I agree, whoever does that should be fired.[/quote]
Ever used std::clog or std::cout?
[/quote]
Yeah, and I wish the industry would use and support them more. I'm not saying we should reinvent the wheel unnecessarily. Just discussing some options that can be used when a different wheel/tool is needed.
[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]
Sure, if your logger needs to store state, then by all means use a lightweight object to do so; that's fine by me. (I'm skeptical that 99% of the loggers written would ever need to store any state, and that last 1% better be darn convincing, but I wouldn't rule it out entirely.)

What I'm arguing against is people who think they need state who simply don't. My monostate logger example is really just an extreme stripped-down version of what I see most people doing; they think there is state but there really isn't, and so their code boils down to a verbose version of what I flippantly dismissed as evil in my earlier post.



My preferred solution is to invert the relationship between the code doing the logging (call it the "client") and the logging system itself (call it the "provider"). Most solutions involve the client telling the provider where to log, etc. at the point of invocation (or nearby if you're using constructors and logging objects). To me this seems brittle; if you suddenly want all of your graphics-related logging to go to foo.txt instead of bar.txt, have fun updating your 500 constructor calls across the project!

Instead, I like to abstract the notion of a "channel" into a simple enum. Everything related to Foo goes into the Foo channel. There is also often a useful case where a severity can be attached; every Foo-related Error gets a channel of Foo and a severity of Error. Then, all call sites to the logging provider look the same: they provide a channel, a severity, and some content. Period. There is no need for state or other cruft; hence my tendency towards a simple free function.

The real power comes when you look at the provider itself. The obvious approach is to have your Log() function simply wrap std ostreams or whatever else. What gets interesting is when you make the Log() function just a dispatcher; all it does is forward the log request, possibly based on its channel and severity, to a "back end" implementation. The back end then takes care of doing the heavy lifting: writing to a network log, serializing to disk, printing something in the debugger if applicable, etc.

Last but not least, you write a couple of free functions for pushing and popping back ends onto a simple stack (only the topmost "active" back end is invoked by the provider dispatch layer). Add some RAII objects (which is totally legit!) to handle scope-controlled back end manipulation, and poof, you're done.

Now you have a system which minimizes mental overhead. If all you want to do is write to the logs, you write a single function call. If you want to change the log temporarily to reroute it, you create a single RAII wrapper on the stack. If you want to change the way all of your logging is handled across the program, which might be millions of lines of code, all you do is hook in a new provider back end at the appropriate point.

This gives you complete flexibility and control over all aspects of your logging, while pretty much ensuring that anything you want to do requires minimal effort.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]


Okay so it seems like just creating a function that logs stuff either to a file or to the console should be fine as it is the simplest and there is really no need for anything more that that. If I need something more, like a logger that sends stuff through the network then I could just take the log data and send it with a different thing. After all, I shouldn't make something that does everything.

Yes. Is the purpose of your log to keep track of your code path? Then the standard output should do just fine. std::cout, printf, or System.out.println, whatever fancies you the most.

If you want more complex behavior, such as sending it over to a network for further processing, then yes, you'd need more functionalities. Even in such cases, I advise you that you still do not need to make such complex objects that you pass around wherever you need logging.

This topic is closed to new replies.

Advertisement