22

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.

9
  • 6
    I 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. Commented Jul 14, 2023 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.
    – FluidCode
    Commented Jul 14, 2023 at 14:58
  • So I guess you don't have unit tests?
    – nvoigt
    Commented Jul 14, 2023 at 15:57
  • 1
    Note 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. Commented Jul 16, 2023 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.
    – Opifex
    Commented Jul 16, 2023 at 19:38

5 Answers 5

15

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.

8
  • 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? Commented Jul 14, 2023 at 15:20
  • @StaticMethod: whatever suits your requirements best, as long as it keeps the risk of getting unwanted side effects low.
    – Doc Brown
    Commented Jul 14, 2023 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.
    – FluidCode
    Commented Jul 14, 2023 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.
    – Doc Brown
    Commented Jul 14, 2023 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.
    – Dirk Boer
    Commented Jul 15, 2023 at 21:30
14

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 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.

3
  • 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 single fwrite, it would not have any relevant state. If, however, some records are written with multiple fwrite operations, then the question of whether a partial record has been written would become a relevant aspect of state.
    – supercat
    Commented Jul 14, 2023 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.
    – Martin Ba
    Commented Jul 15, 2023 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).
    – Basilevs
    Commented Jul 16, 2023 at 17:28
9

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)
5
  • 10
    Your 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.
    – Doc Brown
    Commented Jul 14, 2023 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)
    – Ben
    Commented Jul 15, 2023 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".
    – Doc Brown
    Commented Jul 15, 2023 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.
    – Basilevs
    Commented Jul 15, 2023 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.
    – Doc Brown
    Commented Jul 15, 2023 at 17:51
9

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.

4
  • Nice! And no dynamic configuration/listeners?
    – Basilevs
    Commented Jul 16, 2023 at 18:07
  • @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. Commented Jul 16, 2023 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...). Commented Jul 16, 2023 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. Commented Jul 17, 2023 at 7:10
1

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.

3
  • not sure why this is downvoted. Completely true
    – Dirk Boer
    Commented Jul 15, 2023 at 21:31
  • 2
    The 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)... Commented Jul 16, 2023 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.
    – Warbo
    Commented Jul 17, 2023 at 9:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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