Skip to content

Commit f13f603

Browse files
committed
fix: clean up example-log-extension
This PR is a follow-up to the following issue based on review comments: #939 - Demonstrate how to plug in a different log (console vs memory) - Switch to @loopback/metadata for decorator implementation - Remove log level provider as it's supposed to be contributed by the app The exercise also exposes a few needs: 1. Support for optional dependency so that we can fall back a default implementation (or a better way) 2. Allow a component to bind artifacts beyond providers
1 parent 707a401 commit f13f603

File tree

13 files changed

+308
-172
lines changed

13 files changed

+308
-172
lines changed

packages/example-log-extension/README.md

Lines changed: 106 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,20 @@ Define `Binding` keys here for the component as well as any constants for the
8686
user (for this extension that'll be the logLevel `enum`).
8787

8888
```ts
89+
/**
90+
* Binding keys used by this component.
91+
*/
8992
export namespace EXAMPLE_LOG_BINDINGS {
9093
export const METADATA = 'example.log.metadata';
9194
export const APP_LOG_LEVEL = 'example.log.level';
9295
export const TIMER = 'example.log.timer';
96+
export const LOGGER = 'example.log.logger';
9397
export const LOG_ACTION = 'example.log.action';
9498
}
9599

100+
/**
101+
* Enum to define the supported log levels
102+
*/
96103
export enum LOG_LEVEL {
97104
DEBUG,
98105
INFO,
@@ -108,56 +115,93 @@ Define TypeScript type definitions / interfaces for complex types and functions
108115
```ts
109116
import {ParsedRequest, OperationArgs} from '@loopback/rest';
110117

118+
/**
119+
* A function to perform REST req/res logging action
120+
*/
111121
export interface LogFn {
112122
(
113123
req: ParsedRequest,
114124
args: OperationArgs,
125+
// tslint:disable-next-line:no-any
115126
result: any,
116127
startTime?: HighResTime,
117128
): Promise<void>;
118129

119130
startTimer(): HighResTime;
120131
}
121132

133+
/**
134+
* Log level metadata
135+
*/
122136
export type LevelMetadata = {level: number};
137+
138+
/**
139+
* High resolution time as [seconds, nanoseconds]. Used by process.hrtime().
140+
*/
123141
export type HighResTime = [number, number]; // [seconds, nanoseconds]
142+
143+
/**
144+
* Log writing function
145+
*/
146+
export type LogWriterFn = (msg: string, level: number) => void;
147+
148+
/**
149+
* Timer function for logging
150+
*/
124151
export type TimerFn = (start?: HighResTime) => HighResTime;
125152
```
126153

127154
### `src/decorators/log.decorator.ts`
128-
Extension users can use decorators to provide "hints" (or metadata) for our
129-
component. These "hints" allow the extension to modify behaviour accordingly.
155+
Extension developers can create decorators to provide "hints" (or metadata) to
156+
user artifacts such as controllers and their methods. These "hints" allow the
157+
extension to add extra processing accordingly.
130158

131159
For this extension, the decorator marks which controller methods should be
132-
logged (and optionally at which level they should be logged).
133-
`Reflector` from `@loopback/context` is used to store and retrieve the metadata
134-
by the extension.
160+
logged (and optionally at which level they should be logged). We leverage
161+
`@loopback/metadata` module to implement the decorator and inspection function.
135162

136163
```ts
137164
import {LOG_LEVEL, EXAMPLE_LOG_BINDINGS} from '../keys';
138-
import {Constructor, Reflector} from '@loopback/context';
165+
import {
166+
Constructor,
167+
MethodDecoratorFactory,
168+
MetadataInspector,
169+
} from '@loopback/context';
139170
import {LevelMetadata} from '../types';
140171

172+
/**
173+
* Mark a controller method as requiring logging (input, output & timing)
174+
* if it is set at or greater than Application LogLevel.
175+
* LOG_LEVEL.DEBUG < LOG_LEVEL.INFO < LOG_LEVEL.WARN < LOG_LEVEL.ERROR < LOG_LEVEL.OFF
176+
*
177+
* @param level The Log Level at or above it should log
178+
*/
141179
export function log(level?: number) {
142-
return function(target: Object, methodName: string): void {
143-
if (level === undefined) level = LOG_LEVEL.WARN;
144-
Reflector.defineMetadata(
145-
EXAMPLE_LOG_BINDINGS.METADATA,
146-
{level},
147-
target,
148-
methodName,
149-
);
150-
};
180+
if (level === undefined) level = LOG_LEVEL.WARN;
181+
return MethodDecoratorFactory.createDecorator<LevelMetadata>(
182+
EXAMPLE_LOG_BINDINGS.METADATA,
183+
{
184+
level,
185+
},
186+
);
151187
}
152188

189+
/**
190+
* Fetch log level stored by `@log` decorator.
191+
*
192+
* @param controllerClass Target controller
193+
* @param methodName Target method
194+
*/
153195
export function getLogMetadata(
154196
controllerClass: Constructor<{}>,
155197
methodName: string,
156198
): LevelMetadata {
157-
return Reflector.getMetadata(
158-
EXAMPLE_LOG_BINDINGS.METADATA,
159-
controllerClass.prototype,
160-
methodName,
199+
return (
200+
MetadataInspector.getMethodMetadata<LevelMetadata>(
201+
EXAMPLE_LOG_BINDINGS.METADATA,
202+
controllerClass.prototype,
203+
methodName,
204+
) || {level: LOG_LEVEL.OFF}
161205
);
162206
}
163207
```
@@ -214,23 +258,6 @@ export class TimerProvider implements Provider<TimerFn> {
214258
}
215259
```
216260

217-
### `src/providers/log-level.provider.ts`
218-
A provider can set the default binding value for `example.log.level` so it's
219-
easier to get started with the extension. User's can override the value by
220-
binding a new value or using the mixin.
221-
222-
```ts
223-
import {Provider} from '@loopback/context';
224-
import {LOG_LEVEL} from '../keys';
225-
226-
export class LogLevelProvider implements Provider<number> {
227-
constructor() {}
228-
value(): number {
229-
return LOG_LEVEL.WARN;
230-
}
231-
}
232-
```
233-
234261
### `src/providers/log-action.provider.ts`
235262
This will be the most important provider for the extension as it is responsible
236263
for actually logging the request. The extension will retrieve the metadata
@@ -245,24 +272,36 @@ import {CoreBindings} from '@loopback/core';
245272
import {OperationArgs, ParsedRequest} from '@loopback/rest';
246273
import {getLogMetadata} from '../decorators/log.decorator';
247274
import {EXAMPLE_LOG_BINDINGS, LOG_LEVEL} from '../keys';
248-
import {LogFn, TimerFn, HighResTime, LevelMetadata} from '../types';
275+
import {
276+
LogFn,
277+
TimerFn,
278+
HighResTime,
279+
LevelMetadata,
280+
LogWriterFn,
281+
} from '../types';
249282
import chalk from 'chalk';
250283

251284
export class LogActionProvider implements Provider<LogFn> {
285+
// LogWriteFn is an optional dependency and it falls back to `logToConsole`
286+
@inject(EXAMPLE_LOG_BINDINGS.LOGGER, {optional: true})
287+
private logWriter: LogWriterFn = logToConsole;
288+
289+
@inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL, {optional: true})
290+
private logLevel: number = LOG_LEVEL.WARN;
291+
252292
constructor(
253293
@inject.getter(CoreBindings.CONTROLLER_CLASS)
254294
private readonly getController: Getter<Constructor<{}>>,
255295
@inject.getter(CoreBindings.CONTROLLER_METHOD_NAME)
256296
private readonly getMethod: Getter<string>,
257-
@inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL)
258-
private readonly logLevel: number,
259297
@inject(EXAMPLE_LOG_BINDINGS.TIMER) public timer: TimerFn,
260298
) {}
261299

262300
value(): LogFn {
263301
const fn = <LogFn>((
264302
req: ParsedRequest,
265303
args: OperationArgs,
304+
// tslint:disable-next-line:no-any
266305
result: any,
267306
start?: HighResTime,
268307
) => {
@@ -279,11 +318,13 @@ export class LogActionProvider implements Provider<LogFn> {
279318
private async action(
280319
req: ParsedRequest,
281320
args: OperationArgs,
321+
// tslint:disable-next-line:no-any
282322
result: any,
283323
start?: HighResTime,
284324
): Promise<void> {
285325
const controllerClass = await this.getController();
286326
const methodName: string = await this.getMethod();
327+
287328
const metadata: LevelMetadata = getLogMetadata(controllerClass, methodName);
288329
const level: number | undefined = metadata ? metadata.level : undefined;
289330

@@ -294,36 +335,42 @@ export class LogActionProvider implements Provider<LogFn> {
294335
level !== LOG_LEVEL.OFF
295336
) {
296337
if (!args) args = [];
297-
let log = `${req.url} :: ${controllerClass.name}.`;
298-
log += `${methodName}(${args.join(', ')}) => `;
338+
let msg = `${req.url} :: ${controllerClass.name}.`;
339+
msg += `${methodName}(${args.join(', ')}) => `;
299340

300-
if (typeof result === 'object') log += JSON.stringify(result);
301-
else log += result;
341+
if (typeof result === 'object') msg += JSON.stringify(result);
342+
else msg += result;
302343

303344
if (start) {
304345
const timeDiff: HighResTime = this.timer(start);
305346
const time: number =
306347
timeDiff[0] * 1000 + Math.round(timeDiff[1] * 1e-4) / 100;
307-
log = `${time}ms: ${log}`;
348+
msg = `${time}ms: ${msg}`;
308349
}
309350

310-
switch (level) {
311-
case LOG_LEVEL.DEBUG:
312-
console.log(chalk.white(`DEBUG: ${log}`));
313-
break;
314-
case LOG_LEVEL.INFO:
315-
console.log(chalk.green(`INFO: ${log}`));
316-
break;
317-
case LOG_LEVEL.WARN:
318-
console.log(chalk.yellow(`WARN: ${log}`));
319-
break;
320-
case LOG_LEVEL.ERROR:
321-
console.log(chalk.red(`ERROR: ${log}`));
322-
break;
323-
}
351+
this.logWriter(msg, level);
324352
}
325353
}
326354
}
355+
356+
function logToConsole(msg: string, level: number) {
357+
let output;
358+
switch (level) {
359+
case LOG_LEVEL.DEBUG:
360+
output = chalk.white(`DEBUG: ${msg}`);
361+
break;
362+
case LOG_LEVEL.INFO:
363+
output = chalk.green(`INFO: ${msg}`);
364+
break;
365+
case LOG_LEVEL.WARN:
366+
output = chalk.yellow(`WARN: ${msg}`);
367+
break;
368+
case LOG_LEVEL.ERROR:
369+
output = chalk.red(`ERROR: ${msg}`);
370+
break;
371+
}
372+
if (output) console.log(output);
373+
}
327374
```
328375

329376
### `src/index.ts`
@@ -333,7 +380,6 @@ Export all the files to ensure a user can import the necessary components.
333380
export * from './decorators/log.decorator';
334381
export * from './mixins/log-level.mixin';
335382
export * from './providers/log-action.provider';
336-
export * from './providers/log-level.provider';
337383
export * from './providers/timer.provider';
338384
export * from './component';
339385
export * from './types';
@@ -347,13 +393,12 @@ they are automatically bound when a user adds the component to their application
347393
```ts
348394
import {EXAMPLE_LOG_BINDINGS} from './keys';
349395
import {Component, ProviderMap} from '@loopback/core';
350-
import {TimerProvider, LogActionProvider, LogLevelProvider} from './';
396+
import {TimerProvider, LogActionProvider} from './';
351397

352398
export class LogComponent implements Component {
353399
providers?: ProviderMap = {
354400
[EXAMPLE_LOG_BINDINGS.TIMER]: TimerProvider,
355401
[EXAMPLE_LOG_BINDINGS.LOG_ACTION]: LogActionProvider,
356-
[EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL]: LogLevelProvider,
357402
};
358403
}
359404
```

packages/example-log-extension/src/component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55

66
import {EXAMPLE_LOG_BINDINGS} from './keys';
77
import {Component, ProviderMap} from '@loopback/core';
8-
import {TimerProvider, LogActionProvider, LogLevelProvider} from './';
8+
import {TimerProvider, LogActionProvider} from './';
99

1010
export class LogComponent implements Component {
1111
providers?: ProviderMap = {
1212
[EXAMPLE_LOG_BINDINGS.TIMER]: TimerProvider,
1313
[EXAMPLE_LOG_BINDINGS.LOG_ACTION]: LogActionProvider,
14-
[EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL]: LogLevelProvider,
1514
};
1615
}

packages/example-log-extension/src/decorators/log.decorator.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {LOG_LEVEL, EXAMPLE_LOG_BINDINGS} from '../keys';
7-
import {Constructor, Reflector} from '@loopback/context';
7+
import {
8+
Constructor,
9+
MethodDecoratorFactory,
10+
MetadataInspector,
11+
} from '@loopback/context';
812
import {LevelMetadata} from '../types';
913

1014
/**
@@ -15,15 +19,13 @@ import {LevelMetadata} from '../types';
1519
* @param level The Log Level at or above it should log
1620
*/
1721
export function log(level?: number) {
18-
return function(target: Object, methodName: string): void {
19-
if (level === undefined) level = LOG_LEVEL.WARN;
20-
Reflector.defineMetadata(
21-
EXAMPLE_LOG_BINDINGS.METADATA,
22-
{level},
23-
target,
24-
methodName,
25-
);
26-
};
22+
if (level === undefined) level = LOG_LEVEL.WARN;
23+
return MethodDecoratorFactory.createDecorator<LevelMetadata>(
24+
EXAMPLE_LOG_BINDINGS.METADATA,
25+
{
26+
level,
27+
},
28+
);
2729
}
2830

2931
/**
@@ -36,9 +38,11 @@ export function getLogMetadata(
3638
controllerClass: Constructor<{}>,
3739
methodName: string,
3840
): LevelMetadata {
39-
return Reflector.getMetadata(
40-
EXAMPLE_LOG_BINDINGS.METADATA,
41-
controllerClass.prototype,
42-
methodName,
41+
return (
42+
MetadataInspector.getMethodMetadata<LevelMetadata>(
43+
EXAMPLE_LOG_BINDINGS.METADATA,
44+
controllerClass.prototype,
45+
methodName,
46+
) || {level: LOG_LEVEL.OFF}
4347
);
4448
}

packages/example-log-extension/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
export * from './decorators/log.decorator';
77
export * from './mixins/log-level.mixin';
88
export * from './providers/log-action.provider';
9-
export * from './providers/log-level.provider';
109
export * from './providers/timer.provider';
1110
export * from './component';
1211
export * from './types';

packages/example-log-extension/src/keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export namespace EXAMPLE_LOG_BINDINGS {
1010
export const METADATA = 'example.log.metadata';
1111
export const APP_LOG_LEVEL = 'example.log.level';
1212
export const TIMER = 'example.log.timer';
13+
export const LOGGER = 'example.log.logger';
1314
export const LOG_ACTION = 'example.log.action';
1415
}
1516

0 commit comments

Comments
 (0)