Skip to content
This repository was archived by the owner on Feb 2, 2018. It is now read-only.

Commit 0264fbf

Browse files
committed
feat: logger mixin extension
Initial implementation for a logger mixin that allows applications to bind a Logger automatically.
1 parent 29bff47 commit 0264fbf

File tree

6 files changed

+452
-0
lines changed

6 files changed

+452
-0
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
export * from './decorators/txIdFromHeader.decorator';
7+
export * from './mixins/logger.mixin';
8+
export * from './types';

src/mixins/README.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Mixins
2+
3+
This directory contains source files for the mixins exported by this extension.
4+
5+
## Overview
6+
7+
Sometimes it's helpful to write partial classes and then combining them together to build more powerful classes. This pattern is called Mixins (mixing in partial classes) and is supported by LoopBack 4.
8+
9+
LoopBack 4 supports mixins at an `Application` level. Your partial class can then be mixed into the `Application` class. A mixin class can modify or override existing methods of the class or add new ones! It is also possible to mixin multiple classes together as needed.
10+
11+
### High level example
12+
```ts
13+
class MyApplication extends MyMixinClass(Application) {
14+
// Your code
15+
};
16+
17+
// Multiple Classes mixed together
18+
class MyApp extends MyMixinClass(MyMixinClass2(Application)) {
19+
// Your code
20+
}
21+
```
22+
23+
## Getting Started
24+
25+
For hello-extensions we write a simple Mixin that allows the `Application` class to bind a `Logger` class from ApplicationOptions, Components, or `.logger()` method that is mixed in. `Logger` instances are bound to the key `loggers.${Logger.name}`. Once a Logger has been bound, the user can retrieve it by using [Dependency Injection](http://loopback.io/doc/en/lb4/Dependency-injection.html) and the key for the `Logger`.
26+
27+
### What is a Logger?
28+
> A Logger class is provides a mechanism for logging messages of varying priority by providing an implementation for `Logger.info()` & `Logger.error()`. An example of a Logger is `console` which has `console.log()` and `console.error()`.
29+
30+
#### An example Logger
31+
```ts
32+
class ColorLogger implements Logger {
33+
log(...args: LogArgs) {
34+
console.log('log :', ...args);
35+
}
36+
37+
error(...args: LogArgs) {
38+
const data = args.join(' ');
39+
// log in red color
40+
console.log('\x1b[31m error: ' + data + '\x1b[0m');
41+
}
42+
}
43+
```
44+
45+
## LoggerMixin
46+
A complete & functional implementation can be found in `logger.mixin.ts`. *Here are some key things to keep in mind when writing your own Mixin*.
47+
48+
### constructor()
49+
A Mixin constructor must take an array of any type as it's argument. This would represent `ApplicationOptions` for our base class `Application` as well as any properties we would like for our Mixin.
50+
51+
It is also important for the constructor to call `super(args)` so `Application` continues to work as expected.
52+
```ts
53+
constructor(...args: any[]) {
54+
super(args);
55+
}
56+
```
57+
58+
### Binding via `ApplicationOptions`
59+
As mentioned earlier, since our `args` represents `ApplicationOptions`, we can make it possible for users to pass in their `Logger` implementations in a `loggers` array on `ApplicationOptions`. We can then read the array and automatically bind these for the user.
60+
61+
#### Example user experience
62+
```ts
63+
class MyApp extends LoggerMixin(Application){
64+
constructor(...args: any[]) {
65+
super(...args);
66+
}
67+
};
68+
69+
const app = new MyApp({
70+
loggers: [ColorLogger]
71+
});
72+
```
73+
74+
#### Example Implementation
75+
To implement this, we would check `this.options` to see if it has a `loggers` array and if so, bind it by calling the `.logger()` method. (More on that below).
76+
```ts
77+
if (this.options.loggers) {
78+
for (const logger of this.options.loggers) {
79+
this.logger(logger);
80+
}
81+
}
82+
```
83+
84+
### Binding via `.logger()`
85+
As mentioned earlier, we can add a new function to our `Application` class called `.logger()` into which a user would pass in their `Logger` implementation so we can bind it to the `loggers.*` key for them. We just add this new method on our partial Mixin class.
86+
```ts
87+
logger(logClass: Logger) {
88+
const loggerKey = `loggers.${logClass.name}`;
89+
this.bind(loggerKey).toClass(logClass);
90+
}
91+
```
92+
93+
### Binding a `Logger` from a `Component`
94+
Our base class of `Application` already has a method that binds components. We can modify this method to continue binding a `Component` as usual but also binding any `Logger` instances provided by that `Component`. When modifying behavior of an existing method, we can ensure existing behavior by calling the `super.method()`. In our case the method is `.component()`.
95+
```ts
96+
component(component: Constructor<any>) {
97+
super.component(component); // ensures existing behavior from Application
98+
this.mountComponentLoggers(component);
99+
}
100+
```
101+
102+
We have now modified `.component()` to do it's thing and then call our method `mountComponentLoggers()`. In this method is where we check for `Logger` implementations declared by the component in a `loggers` array by retrieving the instance of the `Component`. Then if `loggers` array exists, we bind the `Logger` instances as normal (by leveraging our `.logger()` method).
103+
104+
```ts
105+
mountComponentLoggers(component: Constructor<any>) {
106+
const componentKey = `components.${component.name}`;
107+
const compInstance = this.getSync(componentKey);
108+
109+
if (compInstance.loggers) {
110+
for (const logger of compInstance.loggers) {
111+
this.logger(logger);
112+
}
113+
}
114+
}
115+
```
116+
117+
118+
## Examples for using LoggerMixin
119+
```
120+
// Using the app's .logger() function.
121+
class LoggingApplication extends LoggerMixin(Application) {
122+
constructor(...args: any[]) {
123+
super(...args);
124+
this.logger(ColorLogger);
125+
}
126+
}
127+
128+
// Binding a Logger provided by a component
129+
class LoggingComponent implements Component{
130+
loggers: [ColorLogger];
131+
}
132+
133+
const app = new LoggingApplication({
134+
components: [LoggingComponent] // Logger from MyComponent will be bound to loggers.ColorLogger
135+
});
136+
```

src/mixins/logger.mixin.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright IBM Corp. 2017. All Rights Reserved.
2+
// Node module: loopback-next-extension-starter
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
// tslint:disable:no-any
7+
8+
import {Constructor} from '@loopback/context';
9+
import {Logger} from '../types';
10+
11+
/**
12+
* A mixin class for Application that creates a .logger()
13+
* function to register a Logger automatically. Also overrides
14+
* component function to allow it to register Logger's automatically.
15+
*
16+
* ```ts
17+
*
18+
* class MyApplication extends LoggerMixin(Application) {}
19+
* ```
20+
*/
21+
export function LoggerMixin<T extends Constructor<any>>(superClass: T) {
22+
return class extends superClass {
23+
// A mixin class has to take in a type any[] argument!
24+
constructor(...args: any[]) {
25+
super(...args);
26+
if (!this.options) this.options = {};
27+
28+
if (this.options.loggers) {
29+
for (const logger of this.options.loggers) {
30+
this.logger(logger);
31+
}
32+
}
33+
}
34+
35+
/**
36+
* Add a Logger to this application.
37+
*
38+
* @param Logger The Logger to add.
39+
*
40+
* ```ts
41+
*
42+
* class Logger {
43+
* log(...args: any) {
44+
* console.log(...args);
45+
* }
46+
* };
47+
*
48+
* app.logger(Logger);
49+
* ```
50+
*/
51+
logger(logClass: Constructor<Logger>) {
52+
const loggerKey = `loggers.${logClass.name}`;
53+
this.bind(loggerKey).toClass(logClass);
54+
}
55+
56+
/**
57+
* Add a component to this application. Also mounts
58+
* all the components Loggers.
59+
*
60+
* @param component The component to add.
61+
*
62+
* ```ts
63+
*
64+
* export class ProductComponent {
65+
* controllers = [ProductController];
66+
* loggers = [ProductLogger];
67+
* providers = {
68+
* [PRODUCT_PROVIDER]: ProductProvider,
69+
* };
70+
* };
71+
*
72+
* app.component(ProductComponent);
73+
* ```
74+
*/
75+
component(component: Constructor<any>) {
76+
super.component(component);
77+
this.mountComponentLoggers(component);
78+
}
79+
80+
/**
81+
* Get an instance of a component and mount all it's
82+
* loggers. This function is intended to be used internally
83+
* by component()
84+
*
85+
* @param component The component to mount Logger's of
86+
*/
87+
mountComponentLoggers(component: Constructor<any>) {
88+
const componentKey = `components.${component.name}`;
89+
const compInstance = this.getSync(componentKey);
90+
91+
if (compInstance.loggers) {
92+
for (const logger of compInstance.loggers) {
93+
this.logger(logger);
94+
}
95+
}
96+
}
97+
};
98+
}

src/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright IBM Corp. 2013,2017. All Rights Reserved.
2+
// Node module: loopback-next-extension-starter
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
// Types and interfaces exposed by the extension go here
7+
8+
// tslint:disable-next-line:no-any
9+
export type LogArgs = any[];
10+
11+
// A traditional Logger interface would have `.warn()` and `.info()` methods &
12+
// potentially others but they have been omitted to keep this example simple.
13+
export interface Logger {
14+
log(...args: LogArgs): void;
15+
error(...args: LogArgs): void;
16+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright IBM Corp. 2013,2017. All Rights Reserved.
2+
// Node module: loopback-next-extension-starter
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {Application, inject} from '@loopback/core';
7+
import {RestComponent, RestServer, get, param} from '@loopback/rest';
8+
import {sinon, Client, createClientForHandler} from '@loopback/testlab';
9+
import {LoggerMixin, Logger, LogArgs} from '../../..';
10+
11+
describe('logger.mixin (acceptance)', () => {
12+
// tslint:disable-next-line:no-any
13+
let app: any;
14+
let server: RestServer;
15+
// tslint:disable-next-line:no-any
16+
let spy: any;
17+
18+
beforeEach(createApp);
19+
beforeEach(createLogger);
20+
beforeEach(createController);
21+
beforeEach(getServerFromApp);
22+
beforeEach(() => {
23+
spy = sinon.spy(console, 'log');
24+
});
25+
26+
afterEach(() => {
27+
spy.restore();
28+
});
29+
30+
it('.log() logs request information', async () => {
31+
const client: Client = createClientForHandler(server.handleHttp);
32+
await client.get('/?name=John').expect(200, 'Hi John');
33+
sinon.assert.calledWith(spy, sinon.match('log: hello() called with: John'));
34+
});
35+
36+
it('.error() logs request information', async () => {
37+
const client: Client = createClientForHandler(server.handleHttp);
38+
await client.get('/error?name=John').expect(200, 'Hi John');
39+
sinon.assert.calledWith(
40+
spy,
41+
sinon.match('error: hello() called with: John'),
42+
);
43+
});
44+
45+
function createApp() {
46+
class LoggerApplication extends LoggerMixin(Application) {
47+
// tslint:disable-next-line:no-any
48+
constructor(...args: any[]) {
49+
super({
50+
components: [RestComponent],
51+
});
52+
}
53+
}
54+
55+
app = new LoggerApplication();
56+
}
57+
58+
function createLogger() {
59+
class ColorLogger implements Logger {
60+
log(...args: LogArgs) {
61+
const data = 'log: ' + args.join(' ');
62+
console.log(data);
63+
}
64+
65+
error(...args: LogArgs) {
66+
const data = args.join(' ');
67+
// log in red color
68+
console.log('\x1b[31m error: ' + data + '\x1b[0m');
69+
}
70+
}
71+
72+
app.logger(ColorLogger);
73+
}
74+
75+
function createController() {
76+
class MyController {
77+
constructor(@inject('loggers.ColorLogger') protected log: Logger) {}
78+
79+
@get('/')
80+
@param.query.string('name')
81+
hello(name: string) {
82+
this.log.log('hello() called with:', name);
83+
return `Hi ${name}`;
84+
}
85+
86+
@get('/error')
87+
@param.query.string('name')
88+
helloError(name: string) {
89+
this.log.error('hello() called with:', name);
90+
return `Hi ${name}`;
91+
}
92+
}
93+
94+
app.controller(MyController);
95+
}
96+
97+
async function getServerFromApp() {
98+
server = await app.getServer(RestServer);
99+
}
100+
});

0 commit comments

Comments
 (0)