How-to - Logging in AU2?

I actually like the logger and DI from Aurelia 1.

I’m using TypeScript.

How do I get ahold of a (default console would be fine) logger instance in Aurelia 2, both in a component and in a general utility file?

I tried several variationsAurealia.register(LoggerConfiguration.create(console) and creating a component with a constructor like constructor(private log: ILogger) but no luck with that or the @ILogger decorator I found.

Ideally, in my module I’d like to have access to a logger directly, e.g. import { DefaultLogger as log} from "somewhere" then just use log.debug() in my code rather than needing to prefix *this*.log.xxx() every time - is that a possibility?

1 Like

What is it that a global logger instance gives you and simply console.log cannot?

I’m thinking if you need some specific functionality just write a custom logger module for it and import like a 3rd-party library, no need to go through dependency injection and this.

1 Like

@ShawnTalbert

In Aurelia 2 we re-architected things in such a way that shared globals are completely avoided. This enables a number of otherwise hard-if-not-impossible-to-achieve use cases, but has the drawback of there being slightly more boilerplate for certain basic use cases.

Before going alpha, we will take a moment to think about some backward-compatible globals to make available such as Container.instance and getLogger, which would be initialized based on the first app startup. This is not set in stone yet.

As for now, one way to do this would be to create a separate file e.g. common.ts (to avoid exporting from your startup.ts):

common.ts

import { DI, LoggerConfiguration, ILogger } from '@aurelia/kernel'; 

export const container = DI.createContainer();
container.register(LoggerConfiguration.create(console));
export const log = container.get(ILogger);

And then in your startup.ts:

import { container } from './common';
import { Aurelia } from '@aurelia/runtime';
import { JitHtmlBrowserConfiguration } from '@aurelia/jit-html-browser';
// Don't mind the import locations being all over the place atm,
// this will get simpler..

new Aurelia(container)
  .register(
    JitHtmlBrowserConfiguration,
    // ...
  )
  .app({
      host: document.querySelector('app'),
      component: App,
  })
  .start();

Then in your consuming files:
stuff.ts

import { log } from './common';

log.info('...');

Many thanks @fkleuver, I’ll try that out.

Not sure why @khuongduybui interpreted my ask as ‘global’ variables. I wasn’t asking for a global variable at all. Just something module scoped or constructor injected. You kindly show a way to achieve that through common.ts.

@fkleuver Could you shed some light on why Aurelia.register(LoggerConfiguration.create(console) in startup.ts doesn’t work? By doesn’t work, I mean I was hoping the call above to allow automatic constructor injection of an ILogger in my components. Must I create my own DI container and use it at Aurelia init time? I thought Aurelia.register() was a call against the ‘default’ container and hoped it would suffice.

@khuongduybui - I consider it a best practice to avoid console.log (among other things, using console directly assumes a specific concrete implementation of a logger and it’s a dependency on a global variable, not to mention all the additional features that rich logging libraries provide).

@fkleuver - I’ve used the Aurelia1 logger in other projects (not Aurelia) with great success for years. Curious what motivated rewriting it for AU2?

Could you shed some light on why Aurelia.register(LoggerConfiguration.create(console) in startup.ts doesn’t work?

It should work. Are you not getting a logger when you try to inject it like so?

export class App {
  constructor(@ILogger private logger: ILogger) {}
}

Must I create my own DI container and use it at Aurelia init time? I thought Aurelia.register() was a call against the ‘default’ container and hoped it would suffice.

There is no ‘default’ container. The constructor of the Aurelia class is:

constructor(container: IContainer = DI.createContainer()) {

My example just so happens to be a straight forward way to keep a hold of the root container for your app. You can also peel it out of the Aurelia instance via the .container property.

I consider it a best practice to avoid console.log (… and it’s a dependency on a global variable

This kind of answers your other question:

I’ve used the Aurelia1 logger in other projects (not Aurelia) with great success for years. Curious what motivated rewriting it for AU2?

Many of the problems in Aurelia 1 weren’t directly visible to users, but manifested in maintenance problems that the core team suffered from and made it harder for us to keep evolving and improving the framework. This is part of why Aurelia 1 has so many open issues and PRs that remained unresolved for years: many things are too hard to test, and we have no reliable means to prevent regressions.

Relying on mutable module-scoped variables is one of the major contributing factors, which is why (at least in non-legacy mode) we’re getting rid of module-scoped container and logger. There will be a legacy package that keeps these in to ease migration for existing apps, but we would recommend a slight change in approach for greenfield projects.

We’ll make sure to explain this in detail with plenty of examples as we get closer to release. It’s not something I can be spending too much time on right now.

But in summary, these types of changes make Aurelia 2 easier for us to maintain, test and improve upon, which allowed us to address almost all feature requests of Aurelia 1 and add even more powerful api’s and features that will save users more time in building ambitious applications.

It should work. Are you not getting a logger when you try to inject it like so?

export class App {
  constructor(@ILogger private logger: ILogger) {}
}

I just discovered that the above does work (combined with this in my startup file):

 Aurelia
.register(RouterConfiguration,LoggerConfiguration.create(console))
.app(MyApp)
.start()

…but I didn’t notice that the default logging level was set so high - and hence my debug/info test calls never surfaced.

Can I presume that the constructor injection technique shown above is the suggested best practice for AU2 (rather than what you showed in common.ts whereby I can import {log} and have it available throughout my module?)

But in summary, these types of changes make Aurelia 2 easier for us to maintain, test and improve

I’m all for this! The more testable and maintainable it is the more likely I will understand it and/or contribute without entirely reverse engineering from source.

In principle yes, and this is rooted in the IoC principle and testability. This is a choice that’s ultimately for users to make: in a huge enterprise app with a big budget some might want to mock the logger and verify warning/error calls at least.
In most projects, I don’t think this matters, and injecting the logger would be more out of consistency than anything else.

Something I’ve done in a production app with au2 a few months ago is add methods like this to some complex component trees where a lot of debug tracing may be needed:

export class SomeComponent {
  public constructor(
    @ILogger private readonly logger: ILogger,
  ) {}

  public getThing(id: number): Thing {
    this.debug('getThing', `id: ${id}`);
    // ...
  }

  private debug(method: string, message: string): void {
    this.logger.debug(`[SomeComponent.${method}] - ${message}`);
  }
}

etc. Just to keep things a bit tidy.

We might change and tweak a few APIs based on alpha feedback later on, it’s not set in stone yet.

I’m very familiar with constructor injection from other platforms, where it is difficult to mock via the module system. However, with javascript modules there is the idea of mocking at the module level. Jest has some support for mocking entire modules built in.

Do you feel that mocking the module (in our sample, common.ts) to be harder than mocking through a constructor injected logger?
Or is the motivation more architectural around keeping all dependencies internal to the class (i.e a class shouldn’t directly depend on anything external to itself) or something else?

Constructor injection is great for keeping a class’s dependencies clear (except for that pain of needing to go throughthis everywhere, and that it leaks implementation details).
However, what if we’re writing a module that is just simple functions or other constructs other than classes?

Thanks for taking the time to discuss some of this. Although we’re using a logger as our example, it is useful for me to zero in on some emerging AU2 best practices for dependency injection and module-scoped techniques in general.

Just because you can, doesn’t mean you should.

Modules are global singletons, so mocking (mutating) them means you now have to keep track of exactly where and when you mutate them and make sure you revert those mutations at the right time.
With OOP-based mocking, you discard the object and the GC will clean it all up for you.

And I wouldn’t call (native) javascript modules very mockable by design. Jest seems to have a fair bit of complexity and makes some serious performance and compatibility trade-offs (which I have already discussed at length in other topics) to pull this off.

With classes and proper IoC, you get the ability natively for free, without the need for any particular tool, that works in nodejs as well as in the browser (which I believe jest still doesn’t support).

I might do a dedicated post at a later point in time with more details and examples of what makes jest a ticking timebomb for larger projects. But no, I don’t think using jest is a good idea, nor are the patterns/habits it promotes.

As for the general (architectural) motivation behind using classes instead of modules, it is important to be able to support multiple simultaneous instances of the same thing at the same time, with potentially different dependencies. This is not possible with just modules.

This is not exactly only related to Au2 but more in general. Module mocking, while doable in JavaScript, is a source of weird side-effects. I’ve come to see this a lot when working with cross-combined functions from various files. Also Jest does a very poor job when it comes to mocking ES6 imported functions.

The trouble with modules is that they behave kind of as singletons when used in various places. E.g think of a testfile where you need to mock a function per test with different means. Whereas a class constructor can be shaped according to your needs in easy ways.

EDIT: ups too late, I see that @fkleuver already added an answer with additional details

EDIT 2: in reply to some statements from Fred, I can definitely agree on the performance degradation with Jest while doing module mockings. For the large IDE I’m building we’ve experienced severe slowdowns, which partially also come by having to resolve the huge dependency of Electron. More then often we’ve now also restrained to a similar approach as he’s shown with the private debug sample, just that it encapsulates the external function to be mocked so that one now can more easily mock the private instead.

EDIT 3: To back up the Jest problems, here’s a sample issue and there are many more of these

Thanks both of you. Good to hear there’s still a few folks that don’t consider OO as utterly evil and that module-level mocking isn’t necessarily what all the cool kids are doing.

My main pet peeve with constructor injection (and this is platform independent) is that it reveals innards of the class that it otherwise would not need to.

For example the following class reveals to the consumer than it depends upon an IFoo. That could be an implementation detail that I may not want to reveal if it were not for support of testing/mocking. If we decide class A should use an IBar instead of IFoo to achieve it’s internal goals, we have to also change the public API (the constructor).

class A {
  constructor(private f: IFoo){}
}

Still, I feel the constructor injection approach to be the simplest approach to understand.

You can still hide that by simply injecting a container. Then you can register your mocks with the container and you get the best of both worlds.

Except please don’t do this. The idea of exposing the innards is exactly why the principle is called Inversion Of Control. The consumer feeds the service and thus has the control. Aka the service tells it needs a specific dependency in order to fullfill it’s task. It shouldn’t necessarily know anything about it’s environment.

I agree.

Good to hear there’s still a few folks that don’t consider OO as utterly evil

The ones that do consider OO as utterly evil, generally don’t understand the principles of OO in the first place. They keep going on and on about inheritance and stuff, when you obviously can (and should) do OO while keeping inheritance to a minimum. But I guess I’m preaching to the choir here.

Aye, problem is this definition of Inversion of Control is in direct conflict with encapsulation and info hiding. Not expecting us to solve this issue here as it’s been around a long time.

Per @fkleuver point, I have used the ‘inject a container’ approach, but that still ends up with every class needing access to a container and adds coupling to the container API. If you view traditional constructor injection as a push model (direct IoC) then I think of the ‘container’ approach as a pull model (each instance pulls it’s dependencies by asking for them).

I don’t think there’s a clear winner here, and tradeoffs for each.

Since DI has been around since the beginning for Aurelia, what approach do people most often use? traditional constructor injection that exposes all the internal dependencies of the class directly via the constructor?

1 Like

Highly recommend the accepted answer here based on your intial statement https://stackoverflow.com/questions/31121611/dependency-inversion-principle-solid-vs-encapsulation-pillars-of-oop/37730333

Sry for the OT, leaving you guys to play on with Logging and stuff :wink:

An inevitable side-effect of splitting things up into smaller pieces is that somewhere you have to bring the pieces together again.
Whether that be via dependency injection, service location or module imports/exports. The glue code just sits in a different location.

I am of the belief that the constructor / the point where you create an object, is generally the correct location to be bringing all the pieces together. But sometimes it just isn’t. That’s a trade-off without hard rules that everyone needs to make for themselves.

1 Like

Thanks for the stackoverflow link. I actually disagree with a couple of the answers there but this is a discussion that I’ve seen for decades so I agree with your thought to leave the OT alone at this point. :slight_smile:

I also agree with @fkleuver - in terms of dependency management in the bigger picture the different approaches seem to just shift the problem around.

Thanks for both your thoughts!