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.
-
6I don't think this question can actually be answered unless you say what your "good reasons" are. Most objections I see regarding globals relate to global variables, i.e. global data that can be modified, usually with no designed API. Among other things, all classes and all defined functions in a program are "global" in your sense. So I think the premise needs more justification.– preferred_anonJul 14 at 13:35
-
Who told you that globals are bad? Do they know that every running program has a Context? This seems like the usual story of the rule of caution that becomes a rule applied in an obtuse manner.– FluidCodeJul 14 at 14:58
-
So I guess you don't have unit tests?– nvoigtJul 14 at 15:57
-
1Note that it's not an either/or decision; you could support both a global logger object (or a static API), for ease-of-use in the general case, and passing custom logger object, for when non-default behavior is desired strongly enough to justify the complication of having to pass an object around.– Jeremy FriesnerJul 16 at 13:34
-
@preferred_anon OP is talking about a logger. Not a logging function. An object is a variable, so a global logger-object would be a global variable.– OpifexJul 16 at 19:38
5 Answers
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.
-
Let's assume I'm working on a large program and one team wants objects to log with one format and another team wants to log with another format, but both want to log with the same file. I could have the object's constructor instantiate a logger or have a static logger for the class, but then I have the same question of do I make the file path variable and other shared configs global or do I pass it into every class/object? Jul 14 at 15:20
-
@StaticMethod: whatever suits your requirements best, as long as it keeps the risk of getting unwanted side effects low. Jul 14 at 15:36
-
I would say that different modules can have different logging requirements if they are running on separate microservices. One deployed service should have a clear requirement that the sys admin can manage. It is the person who executes the deployment who has to care for disk space and all the other details, not the developer. Jul 14 at 17:22
-
@FluidCode: your comment seems to be off-track, since making the configuration of a logger available to an admin at run time is 100% orthogonal to the decision "global variable vs DI". Or in other words, this has nothing to do with the question. Jul 14 at 18:41
-
This is also perfectly fine for enormous programs. Logging should just not have side effects or state that can be read. As simple as that. Console.WriteLine is a logger. Unity’s Debug.log is a logger. There might be very special cases and parts where you want to inject your logger but also in large applications it is 99% unnecesary boiler plate. Jul 15 at 21:30
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 variable
.
Take, for example, the lowliest and simplest form of logging: Dumping info on stdout. (e.g., see Console.WriteLine
in C#, or printf
in 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.
-
Some loggers encapsulate state that may be relevant to programs using them; others don't. Note that it's possible for logger state to be relevant even if it's not observable. If, for example, a logger uses a an
fwrite
function which is guaranteed to be atomic with regard to invocations on other threads, and it always writes compelte stand-alone records using a singlefwrite
, it would not have any relevant state. If, however, some records are written with multiplefwrite
operations, then the question of whether a partial record has been written would become a relevant aspect of state.– supercatJul 14 at 18:05 -
@supercat - that potentially encapsulated state is exactly why I wrote "do not use just a global logging object thingy, but use a clear interface to do (global) logging" / Whether that clear/clean interface is via a static method, global function, singleton member function, .... is then secondary, I'd say. Jul 15 at 6:58
-
@MartinBa most logging frameworks allow custom appenders (read - listeners) which in turn allow for arbitrary side-effects. However you want to skin the cat - modern frameworks are global and often stateful (variable).– BasilevsJul 16 at 17:28
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)
-
10Your overall answer is ok, but your second bullet point is, to be honest, plain wrong. Using different loggers for tests and production will be equally simple, regardless if one initializes a global variable with a specific logger at the program's start, or if one uses DI for this. What really becomes difficult is to use different loggers for different objects within the same execution context. Jul 14 at 12:45
-
1@DocBrown Such as having logging functionality in the testing process itself, while treating logs emitted from the unit-under-test differently (those logs may well be the functionality some of the test cases cover)– BenJul 15 at 3:26
-
@Ben: the logs from the "testing process itself" will be created by some logger of your testing framework, which is unlikely to be the same logger object in ones own code. This completely irrelevant to "global variable vs DI". Jul 15 at 7:25
-
1@DocBrown logging initialization on the start of the program precludes isolated tests, as different units would dereference different loggers (due to mutability of global logger) depending on their initialization timings. Also, using global logger for tests makes it harder to isolate logging events from different units in integration tests.– BasilevsJul 15 at 14:50
-
1@Basilevs: exactly what I wrote in my answer - when your program starts to become larger, your testing requirements increase, and then there will be a point where a global variable for a logger starts to make trouble. Jul 15 at 17:51
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.
Different globals?
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?
Maybe.
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.
Putting altogether
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.
-
-
@Basilevs: Those are fairly orthogonal, though it's easier to reconfigure dynamically with a global registry. The 3rd library I did had listeners, so it could be setup to log to file or to a distant server. The 4th doesn't, I don't need any yet... or for the foreseeable future. Jul 16 at 18:18
-
2"Logging is not part of the purpose of the application and should not influence the function of the application." Logging may be part of the requirements. I worked for a company that produced safety critical systems, and lawyers aborted at least one lawsuit when they showed the other side a transcript of the log-file (Our former customer claimed that we hadn't detected certain events; the log-file showed that there operator had been overriding warnings for 2 weeks...). Jul 16 at 23:29
-
3@SimonCrase: In my previous company the logging system I had made was used, in part, for "audit" logs which were tracked for regulatory purposes. Despite being called audit "logs", I always considered it a mistake to reuse the logging system for them: having been built to be unobtrusive, the system would discard logs if the queue got too full (with QoS, but still). If there are requirements for certain events/metrics to be tracked, I would argue a dedicated system is probably a better choice: different trade-offs need to be made. Jul 17 at 7:10
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.
Examples of using dynamic scope for logging can be found in Scala's Console
and Racket's ports
Shameless plug: I wrote a blog post on dynamic scope, and how it's related to environment variables.
-
-
2The most efficient way to implement dynamic scoping is NOT to walk up the stack, but instead to have a "global" stack (for each dynamic thingy) for which the top can be consulted in O(1)... Jul 16 at 17:05
-
@MatthieuM. sure, as an implementation detail (although it gets tricky with concurrency). I don't like that as a mental model though, since it relies on semantics of mutability, time, etc. which we may not want/have in our surface language: e.g. dynamic scope works perfectly well in a pure-functional setting, pushing/popping a stack in-place has no meaning.– WarboJul 17 at 9:25