feat(middleware-logger): write to stream + filter response + silent mode
MODIFY
packages/@integration/src/app.ts
packages/@integration/src/app.ts
import { httpListener } from '@marblejs/core';
import { bodyParser$ } from '@marblejs/middleware-body';
-import { logger$ } from './middlewares/logger.middleware';
+import { loggerDev$, loggerFile$ } from './middlewares/logger.middleware';
import { api$ } from './effects/api.effects';
const middlewares = [
- logger$,
+ loggerDev$,
+ loggerFile$,
bodyParser$,
];

MODIFY
packages/@integration/src/middlewares/logger.middleware.ts
packages/@integration/src/middlewares/logger.middleware.ts
@@ -6,4 +6,5 @@ const writePath = join(__dirname, '../../', 'access.log');
const stream = createWriteStream(writePath, { flags: 'a' });
const options = { silent: false, stream };
-export const logger$ = loggerWithOpts$(options);
+export const loggerDev$ = loggerWithOpts$();
+export const loggerFile$ = loggerWithOpts$(options);

MODIFY
packages/core/src/+internal/fp/maybe.ts
packages/core/src/+internal/fp/maybe.ts
@@ -2,6 +2,9 @@ import { Functor } from './functor';
export type Nullable<T> = T | null | undefined;
+export const isNullable = value =>
+ value === undefined || value === null;
+
export interface MaybeCases<T, U> {
some: (t: T) => U;
none: () => U;
@@ -14,41 +17,41 @@ export class Maybe<T> implements Functor<T> {
static none = <T>() => new Maybe<T>(null);
- static of = <T>(value: Nullable<T>) =>
- value
- ? Maybe.some(value)
+ static of = <T>(value: Nullable<T>): Maybe<T> =>
+ !isNullable(value)
+ ? Maybe.some(value as T)
: Maybe.none<T>()
map = <R>(f: (value: T) => R): Maybe<R> =>
- this.value
- ? Maybe.some(f(this.value))
+ !isNullable(this.value)
+ ? Maybe.some(f(this.value as T))
: Maybe.none<R>()
flatMap = <R>(f: (value: T) => Maybe<R>): Maybe<R> =>
- this.value
- ? f(this.value)
+ !isNullable(this.value)
+ ? f(this.value as T)
: Maybe.none<R>()
caseOf = <U>(cases: MaybeCases<T, U>) =>
- this.value
- ? cases.some(this.value)
+ !isNullable(this.value)
+ ? cases.some(this.value as T)
: cases.none()
valueOr = (defaultValue: T): T =>
- this.value
- ? this.value
+ !isNullable(this.value)
+ ? this.value as T
: defaultValue
valueOrCompute = <R extends T>(f: () => R): T | R =>
- this.value
- ? this.value
+ !isNullable(this.value)
+ ? this.value as T | R
: f()
valueOrThrow(error?: Error): T {
- if (!this.value) {
+ if (isNullable(this.value)) {
throw (error || new Error('No value is available'));
}
- return this.value;
+ return this.value as T;
}
}

MODIFY
packages/middleware-logger/src/logger.factory.ts
packages/middleware-logger/src/logger.factory.ts
-import { HttpRequest, HttpResponse } from '@marblejs/core';
-import { Timestamp } from 'rxjs';
import chalk from 'chalk';
-import { factorizeTime } from './time.factory';
+import { Timestamp } from 'rxjs';
+import { HttpRequest, HttpResponse } from '@marblejs/core';
+import { factorizeTime } from './logger.util';
+import { LogParams } from './logger.model';
type LogFactorizerOptions = {
colorize?: boolean;
@@ -17,9 +18,10 @@ export const factorizeLog =
(res: HttpResponse, stamp: Timestamp<HttpRequest>) =>
(opts: LogFactorizerOptions = {}) => {
const { method, url } = stamp.value;
+ const now = Date.now();
const timestamp = opts.timestamp
- ? new Date().toISOString()
+ ? new Date(now).toISOString()
: '';
const statusCode = opts.colorize
@@ -31,13 +33,7 @@ export const factorizeLog =
return prepareLogString({ timestamp, method, url, statusCode, time });
};
-export const prepareLogString = (opts: {
- timestamp?: string;
- method: string;
- url: string;
- statusCode: string;
- time: string;
-}) =>
+export const prepareLogString = (opts: LogParams) =>
!!opts.timestamp
? `${opts.timestamp} ${opts.method} ${opts.url} ${opts.statusCode} ${opts.time}`
: `${opts.method} ${opts.url} ${opts.statusCode} ${opts.time}`;

ADD
packages/middleware-logger/src/logger.handler.ts
+import { HttpRequest, HttpResponse } from '@marblejs/core';
+import { fromEvent, Timestamp } from 'rxjs';
+import { take, filter, mapTo } from 'rxjs/operators';
+import { factorizeLog } from './logger.factory';
+import { LoggerOptions } from './logger.model';
+import { writeToStream, isNotSilent, filterResponse } from './logger.util';
+
+export const loggerHandler = (res: HttpResponse, opts: LoggerOptions) => (stamp: Timestamp<HttpRequest>) =>
+ fromEvent(res, 'finish')
+ .pipe(
+ take(1),
+ mapTo(res),
+ filter(isNotSilent(opts)),
+ filter(filterResponse(opts)),
+ )
+ .subscribe(() => {
+ const { info } = console;
+ const log = factorizeLog(res, stamp);
+ const streamLog = log({ colorize: false, timestamp: true });
+ const consoleLog = log({ colorize: true });
+
+ opts.stream
+ ? writeToStream(opts.stream, streamLog)
+ : info(consoleLog);
+ });

MODIFY
packages/middleware-logger/src/logger.middleware.ts
packages/middleware-logger/src/logger.middleware.ts
-import { Middleware, HttpRequest, HttpResponse } from '@marblejs/core';
-import { Maybe } from '@marblejs/core/dist/+internal';
-import { WriteStream } from 'fs';
-import { fromEvent, Timestamp } from 'rxjs';
-import { timestamp, tap, map, take, filter, mapTo } from 'rxjs/operators';
-import { factorizeLog } from './logger.factory';
-
-export interface LoggerOptions {
- silent?: boolean;
- stream?: WriteStream;
- filter?: (req: HttpRequest) => boolean;
-}
-
-const isNotSilent = (opts: LoggerOptions) => () =>
- !opts.silent;
-
-const isRequest = (opts: LoggerOptions) => ({ value: req }: Timestamp<HttpRequest>) =>
- Maybe.of(opts.filter)
- .map(filter => filter(req))
- .valueOr(true);
-
-const writeToStream = (stream: WriteStream, chunk: string) => {
- stream.write(`${chunk}\n\n`);
-};
-
-const handleFinishEvent = (res: HttpResponse, opts: LoggerOptions) => (stamp: Timestamp<HttpRequest>) =>
- fromEvent(res, 'finish')
- .pipe(
- take(1),
- mapTo(stamp),
- filter(isNotSilent(opts)),
- filter(isRequest(opts)),
- )
- .subscribe(stamp => {
- const log = factorizeLog(res, stamp);
- const fileLog = log({ colorize: false, timestamp: true });
- const consoleLog = log({ colorize: true });
-
- if (opts.stream) { writeToStream(opts.stream, fileLog); }
-
- console.info(consoleLog);
- });
+import { Middleware } from '@marblejs/core';
+import { timestamp, tap, map } from 'rxjs/operators';
+import { LoggerOptions } from './logger.model';
+import { loggerHandler } from './logger.handler';
/**
- * @deprecated [#1] since version 1.1,
+ * @deprecated [#1] since version 1.2,
* [#2] will be deleted in version 2.0,
* [#3] use loggerWithOpts$ instead,
*/
export const logger$: Middleware = (req$, res) => {
// tslint:disable-next-line:max-line-length
- console.warn('Deprecation warning: logger$ is deprecated since v1.1 and will be removed in v2.0. Use loggerWithOpts$ instead.');
+ console.warn('Deprecation warning: logger$ is deprecated since v1.2 and will be removed in v2.0. Use loggerWithOpts$ instead.');
return loggerWithOpts$()(req$, res, null);
};
export const loggerWithOpts$ = (opts: LoggerOptions = {}): Middleware => (req$, res) =>
req$.pipe(
timestamp(),
- tap(handleFinishEvent(res, opts)),
+ tap(loggerHandler(res, opts)),
map(({ value: req }) => req),
);

ADD
packages/middleware-logger/src/logger.model.ts
+import { WriteStream } from 'fs';
+import { HttpResponse } from '@marblejs/core';
+
+export interface LoggerOptions {
+ silent?: boolean;
+ stream?: WriteStream;
+ filter?: (req: HttpResponse) => boolean;
+}
+
+export interface LogParams {
+ timestamp?: string;
+ method: string;
+ url: string;
+ statusCode: string;
+ time: string;
+}

ADD
packages/middleware-logger/src/logger.util.ts
+import { WriteStream } from 'fs';
+import { HttpResponse } from '@marblejs/core';
+import { Maybe, compose } from '@marblejs/core/dist/+internal';
+import { LoggerOptions } from './logger.model';
+
+export const isNotSilent = (opts: LoggerOptions) => () =>
+ !opts.silent;
+
+export const filterResponse = (opts: LoggerOptions) => (res: HttpResponse) =>
+ Maybe.of(opts.filter)
+ .map(filter => filter(res))
+ .valueOr(true);
+
+export const writeToStream = (stream: WriteStream, chunk: string) =>
+ stream.write(`${chunk}\n\n`);
+
+export const formatTime = (timeMs: number) =>
+ timeMs > 1000
+ ? `${timeMs / 1000}s`
+ : `${timeMs}ms`;
+
+export const getTimeDifferencIneMs = (startTime: Date): number =>
+ new Date().getTime() - startTime.getTime();
+
+export const factorizeTime = (timestamp: number) =>
+ compose(
+ formatTime,
+ getTimeDifferencIneMs
+ )(new Date(timestamp));

ADD
packages/middleware-logger/src/spec/logger.factory.spec.ts
+import { HttpResponse, HttpRequest } from '@marblejs/core';
+import { prepareLogString, factorizeLog } from '../logger.factory';
+
+describe('Logger factory', () => {
+ let loggerFactoryModule;
+ let loggerUtilModule;
+
+ beforeEach(() => {
+ jest.unmock('../logger.factory.ts');
+ loggerFactoryModule = require('../logger.factory.ts');
+
+ jest.unmock('../logger.util.ts');
+ loggerUtilModule = require('../logger.util.ts');
+ });
+
+ test('#prepareLogString stringifies log with timestamp', () => {
+ // given
+ const opts = {
+ timestamp: new Date().toISOString(),
+ method: 'GET',
+ url: '/api/v1',
+ statusCode: '200',
+ time: '300ms',
+ };
+
+ // when
+ const preparedLog = prepareLogString(opts);
+
+ // then
+ expect(preparedLog).toEqual(`${opts.timestamp} GET /api/v1 200 300ms`);
+ });
+
+ test('#prepareLogString stringifies log without timestamp', () => {
+ // given
+ const opts = {
+ method: 'GET',
+ url: '/api/v1',
+ statusCode: '200',
+ time: '300ms',
+ };
+
+ // when
+ const preparedLog = prepareLogString(opts);
+
+ // then
+ expect(preparedLog).toEqual(`GET /api/v1 200 300ms`);
+ });
+
+ test('#factorizeLog factorizes logger message', () => {
+ // given
+ const res = { statusCode: 200 } as HttpResponse;
+ const req = { method: 'GET', url: '/api/v1' } as HttpRequest;
+ const stamp = { value: req, timestamp: 1539031930521 };
+ const opts = { colorize: false, timestamp: true };
+ const date = 1540145853 * 1000;
+
+ // when
+ loggerFactoryModule.prepareLogString = jest.fn();
+ loggerUtilModule.factorizeTime = jest.fn(() => '300ms');
+ spyOn(Date, 'now').and.returnValue(date);
+ factorizeLog(res, stamp)(opts);
+
+ // then
+ expect(loggerFactoryModule.prepareLogString).toHaveBeenCalledWith({
+ method: 'GET',
+ url: '/api/v1',
+ statusCode: '200',
+ time: '300ms',
+ timestamp: '2018-10-21T18:17:33.000Z',
+ });
+ });
+});

RENAME
packages/middleware-logger/src/spec/logger-middleware.spec.ts
packages/middleware-logger/src/spec/logger.middleware.spec.ts
ADD
packages/middleware-logger/src/spec/logger.util.spec.ts
+import { join } from 'path';
+import { createWriteStream } from 'fs';
+import { HttpResponse } from '@marblejs/core';
+import { formatTime, getTimeDifferencIneMs, filterResponse, isNotSilent, writeToStream } from '../logger.util';
+
+describe('Logger util', () => {
+ test('#formatTime formats time to miliseconds or seconds', () => {
+ // given
+ const moreThanSecond = 1200;
+ const lessThanSecond = 800;
+
+ // when
+ const formattedSecond = formatTime(moreThanSecond);
+ const formattedMilisecond = formatTime(lessThanSecond);
+
+ // then
+ expect(formattedSecond).toBe('1.2s');
+ expect(formattedMilisecond).toBe('800ms');
+ });
+
+ test('#getTimeDifferenceMs returns time difference in miliseconds', () => {
+ // given
+ const startTime = new Date(1000);
+ spyOn(global, 'Date').and.returnValue({ getTime: () => 2000 });
+
+ // when
+ const timeDifference = getTimeDifferencIneMs(startTime);
+
+ // then
+ expect(timeDifference).toBe(2000 - 1000);
+ });
+
+ test('#filterResponse filters request by given predicate function', () => {
+ // given
+ const res = { status: 404 } as any as HttpResponse;
+ const predicate = res => res.status < 400;
+
+ // when
+ const resultWithPredicateFunction = filterResponse({ filter: predicate })(res);
+ const resultWithoutPredicateFunction = filterResponse({})(res);
+
+ // then
+ expect(resultWithPredicateFunction).toEqual(false);
+ expect(resultWithoutPredicateFunction).toEqual(true);
+ });
+
+ test('#isNotSilent checks if options "silent" flag is set', () => {
+ // given
+ const optsSilent = isNotSilent({ silent: true });
+ const optsNotSilent = isNotSilent({ silent: false });
+ const optsWithoutSilent = isNotSilent({});
+
+ // then
+ expect(optsSilent()).toEqual(false);
+ expect(optsNotSilent()).toEqual(true);
+ expect(optsWithoutSilent()).toEqual(true);
+ });
+
+ test('#writeToStream writes to stream passed data chunk', () => {
+ // given
+ const writePath = join(__dirname, 'test.log');
+ const stream = createWriteStream(writePath, { flags: 'a' });
+ const data = 'AAA';
+ const expectedWriteCall = 'AAA\n\n';
+
+ // when
+ jest.spyOn(stream, 'write').mockImplementation(jest.fn());
+ writeToStream(stream, data);
+
+ // then
+ expect(stream.write).toHaveBeenCalledWith(expectedWriteCall);
+ });
+});

DELETE
packages/middleware-logger/src/spec/time.factory.spec.ts
-import { formatTime, getTimeDifferenceMs } from '../time.factory';
-
-describe('TimeFactory', () => {
-
- test('#formatTime formats time to miliseconds or seconds', () => {
- // given
- const moreThanSecond = 1200;
- const lessThanSecond = 800;
-
- // when
- const formattedSecond = formatTime(moreThanSecond);
- const formattedMilisecond = formatTime(lessThanSecond);
-
- // then
- expect(formattedSecond).toBe('1.2s');
- expect(formattedMilisecond).toBe('800ms');
- });
-
- test('#getTimeDifferenceMs returns time difference in miliseconds', () => {
- // given
- const startTime = new Date(1000);
- spyOn(global, 'Date').and.returnValue({ getTime: () => 2000 });
-
- // when
- const timeDifference = getTimeDifferenceMs(startTime);
-
- // then
- expect(timeDifference).toBe(2000 - 1000);
- });
-
-});

DELETE
packages/middleware-logger/src/time.factory.ts
-import { compose } from '@marblejs/core/dist/+internal';
-
-export const formatTime = (timeMs: number) =>
- timeMs > 1000
- ? `${timeMs / 1000}s`
- : `${timeMs}ms`;
-
-export const getTimeDifferenceMs = (startTime: Date): number =>
- new Date().getTime() - startTime.getTime();
-
-export const factorizeTime = (timestamp: number) =>
- compose(
- formatTime,
- getTimeDifferenceMs
- )(new Date(timestamp));

feat(middleware-logger): write to stream + filter response + silent mode