Over and over again we're told, "globals are bad" and with good reason. However, I'm working with a logger that needs to be accessible everywhere in the program. Why shouldn't I create a global logging object? The alternative seems to be to pass the logger object into every object I instantiate and that seems very repetitive and non-productive.
Loggers, even if they have some global state internally, are rarely objects which change the core behaviour of a program (at least when the program itself does not behave in some crazy way like exchanging the logger object in the middle of some execution, quering the logger's state, or by postprocessing its own logging output for deciding about further actions).
Hence keeping a logger in a global variable isn't necessarily that bad - the risk of unwanted side effects is usually quite low. What you achieve by this design is a program which can only use one kind of logger in all of its modules throughout it's whole execution time. For many small-to-mid-size programs, this is fine.
Note that this design still gives you the option of easily changing the type of logger between different execution contexts, like development, test or production.
For larger program systems or programs with special requirements, however, this can start to make trouble. For example, when you have different teams working on different modules, with different logging requirements for their parts. Or, when you start to introduce multithreading, and you want to pipe the logging output of different modules into different log files to make this threadsafe, the most simple way could be to inject a differently initialized logger (with a different target file) into the objects related to each thread.
So, in short, it depends - you have to decide by yourself whether your program fits into the former or the latter category. The crucial point is often not to miss the point when a formerly small-and-simple program evolves into a large-and-complex system. But this means you will usually have to redesign certain parts either, and that's the time when you should replace a global logging variable by something more flexible.
While there are certainly cases where doing dependency injection for a logger make sense, most frameworks and utilities I've worked with do not do this.
It probably boils down to one aspect of a "global logger": It is NOT a
Take, for example, the lowliest and simplest form of logging: Dumping info on stdout. (e.g., see
Console.WriteLine in C#, or
printfin C, or
say in perl).
Here, you are clearly using a global "resource" (the output stream), but in the context you are using it, it is not really a variable. You might just interact with it using a global/static function call.
This often stays true, no matter what your logging "framework" is.
So, what you should not do, is just define a logging object as a "classic" global variable, that can be changed and/or configured from anywhere in your program.
What you can do, is define (or use something that defines) a clear interface on how to access one or more (global, or "global") logging resources in a way to log to them.
This shows that there is no silver bullet - if it works for you, then great, do it. Here are a few things why you might still pass the logger instead of having a static global dependency:
- Passing it into the objects clearly shows that the object has a dependency on the logger (no hidden dependency, DIP)
- Passing in a logger enables you to switch out the implementation more easily: Maybe you want a different logger for tests, production, etc (this would be possible with a global logger that knows its context, but more difficult)
Disclaimer: I am obsessed with logging. I have, in 17 years as a professional developer, written no less than 4 logging libraries, in 3 different companies and 2 different languages. Reader beware.
On reading advice
Advice is often dispensed in a pithy form. It's easy to remember, especially if it comes with a pun, easy to communicate, etc...
But you cannot just read the pithy form and truly understand the advice. You need to dig deeper and understand the motivations.
Why not globals?
So, why do people say that globals should be avoided? What is bad with globals? What's problem there?
Global variables impede Locality of Reasoning by creating coupling between far away parts of the code. This in turn prevent a "Divide and Conquer" approach in understanding, or testing, the code, as it makes it harder to isolate independent parts of the code, as any piece of code touching a given global is intrinsically all tied up together.
The issue, thus, is not "globals", it's the fact that global variables enable Action at a Distance, preventing the achievement of High Cohesion, Low Coupling (Modularity) which makes working with software easier.
Not all global variables are created equal.
Formally, the key point is whether a global creates a link between two otherwise decoupled parts of the program by creating a "hidden" communication channel. A write-only variable, for example, creates no such communication channel.
Pragmatically, however, one needs to push further. A running process is typically composed of an application part resting on an runtime part, which provides the application with a host of services... including memory allocation, garbage collection, metrics collection, and, yes, logs collection.
Distinguishing the two matters because arguably logging is not a write-only operation -- the logs end up somewhere, which can be read. Of important, however, is that metrics/logs collections should be write-only for the application part of the program.
And therefore, from the application point of view, a logger is write-only. As such, it does not create a link between decoupled parts of the program, and all is good.
Should a logger be global?
Functionally a global logger is a non-problem. Logging is not part of the purpose of the application and should not influence the function of the application. Therefore, functionally speaking, nobody cares.
From a non-functional requirements perspective, however, it may be best to not have a fully global logger, in particular:
- Activating/deactivating logging only on some threads could be nice to have.
- Avoiding contention between different logging threads is nice to have.
Yet, at the same time, still from a non-functional requirements perspective, it is best for a logging system to be as unobtrusive as possible. Passing loggers explicitly, or instantiating loggers in every single class, is NOT great. It's damn annoying, it's clutter, it detracts from the function of the code.
In consideration of the non-functional requirements, my advice is thus:
- A global core: the part doing all the work, really.
- Thread-local queues: to avoid contention.
- Thread-local tidbits of configuration, possibly, if you wish for per-thread configuration. I've never bothered, to be honest.
It's the design I've settled on for my 3rd (C++) and 4th (Rust) logging systems, and it works beautifully.
Logging is often a good candidate for dynamic scoping. This is where the binding is looked up by traversing the stack, rather than being passed around explicitly; allowing overrides like
(let ((log my-other-logger)) (my-thing-which-may-log))
Dynamic scope is useful for things that the code's caller knows better than the code's implementer, and which are otherwise irrelevant to the code's specific functionality (similar to "aspects"; things which are relevant should usually be arguments). Whether something is "relevant" isn't always clear, but if something would mostly get passed along as-is between calls (like we often find with dependency injection) then dynamic scope can (a) avoid such boilerplate, and (b) allows variables to be introduced/removed without changing the code's API.
Shameless plug: I wrote a blog post on dynamic scope, and how it's related to environment variables.