Skip to content

Commit

Permalink
implement optional assignResponse parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
flovouin authored and iamolegga committed Dec 23, 2024
1 parent 1f9fbcb commit f16a6d9
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 9 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ class MyService {
}
```

Due to the [limitation](https://github.com/pinojs/pino-http/issues/30) of the underlying `pino-http` `PinoLogger.assign` cannot extend `Request completed` logs.
By default, this does not extend `Request completed` logs. Set the `assignResponse` parameter to `true` to also enrich response logs automatically emitted by `pino-http`.

## Change pino params at runtime

Expand Down
53 changes: 52 additions & 1 deletion __tests__/assign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ describe('assign', () => {
controllers: [TestController],
providers: [TestService],
})
.forRoot()
.forRoot({ pinoHttp: { customSuccessMessage: () => 'success' } })
.run();

const wanted = logs.some((l) => l.msg === msg && l.foo === 'bar');
expect(wanted).toBeTruthy();
const responseLog = logs.find((l) => l.msg === 'success');
expect(responseLog).toBeDefined();
expect(responseLog).not.toHaveProperty('foo');
});

it('out of request context', async () => {
Expand Down Expand Up @@ -92,6 +95,54 @@ describe('assign', () => {
expect(log).toBeTruthy();
expect(log).not.toHaveProperty('foo');
});

it('response log', async () => {
const msg = Math.random().toString();

@Injectable()
class TestService {
private readonly logger = new Logger(TestService.name);

test() {
this.logger.log(msg);
return {};
}
}

@Controller('/')
class TestController {
constructor(
private readonly logger: PinoLogger,
private readonly service: TestService,
) {}

@Get()
get() {
this.logger.assign({ foo: 'bar' });
this.logger.assign({ other: 'value' });
return this.service.test();
}
}

const logs = await new TestCase(new PlatformAdapter(), {
controllers: [TestController],
providers: [TestService],
})
.forRoot({
assignResponse: true,
pinoHttp: { customSuccessMessage: () => 'success' },
})
.run();

const hasServiceLog = logs.some(
(l) => l.msg === msg && l.foo === 'bar' && l.other === 'value',
);
expect(hasServiceLog).toBeTruthy();
const hasResponseLog = logs.some(
(l) => l.msg === 'success' && l.foo === 'bar' && l.other === 'value',
);
expect(hasResponseLog).toBeTruthy();
});
});
}
});
25 changes: 19 additions & 6 deletions src/LoggerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,14 @@ export class LoggerModule implements NestModule {
forRoutes = DEFAULT_ROUTES,
pinoHttp,
useExisting,
assignResponse,
} = this.params;

const middlewares = createLoggerMiddlewares(pinoHttp || {}, useExisting);
const middlewares = createLoggerMiddlewares(
pinoHttp || {},
useExisting,
assignResponse,
);

if (exclude) {
consumer
Expand All @@ -93,9 +98,10 @@ export class LoggerModule implements NestModule {
function createLoggerMiddlewares(
params: NonNullable<Params['pinoHttp']>,
useExisting = false,
assignResponse = false,
) {
if (useExisting) {
return [bindLoggerMiddlewareFactory(useExisting)];
return [bindLoggerMiddlewareFactory(useExisting, assignResponse)];
}

const middleware = pinoHttp(
Expand All @@ -108,24 +114,31 @@ function createLoggerMiddlewares(

// FIXME: params type here is pinoHttp.Options | pino.DestinationStream
// pinoHttp has two overloads, each of them takes those types
return [middleware, bindLoggerMiddlewareFactory(useExisting)];
return [middleware, bindLoggerMiddlewareFactory(useExisting, assignResponse)];
}

function bindLoggerMiddlewareFactory(useExisting: boolean) {
function bindLoggerMiddlewareFactory(
useExisting: boolean,
assignResponse: boolean,
) {
return function bindLoggerMiddleware(
req: express.Request,
_res: express.Response,
res: express.Response,
next: express.NextFunction,
) {
let log = req.log;
let resLog = assignResponse ? res.log : undefined;

if (!useExisting && req.allLogs) {
log = req.allLogs[req.allLogs.length - 1]!;
}
if (assignResponse && !useExisting && res.allLogs) {
resLog = res.allLogs[res.allLogs.length - 1]!;
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: run requires arguments for next but should not because it can
// be called without arguments
storage.run(new Store(log), next);
storage.run(new Store(log, resLog), next);
};
}
1 change: 1 addition & 0 deletions src/PinoLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export class PinoLogger implements PinoMethods {
);
}
store.logger = store.logger.child(fields);
store.responseLogger?.setBindings(fields);
}

protected call(method: pino.Level, ...args: Parameters<LoggerFn>) {
Expand Down
7 changes: 7 additions & 0 deletions src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ export interface Params {
* {"level":30, ... "RENAME_CONTEXT_VALUE_HERE":"AppController" }
*/
renameContext?: string;

/**
* Optional parameter to also assign the response logger during calls to
* `PinoLogger.assign`. By default, `assign` does not impact response logs
* (e.g.`Request completed`).
*/
assignResponse?: boolean;
}

// for support of nestjs@8 we don't use
Expand Down
5 changes: 4 additions & 1 deletion src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { AsyncLocalStorage } from 'async_hooks';
import { Logger } from 'pino';

export class Store {
constructor(public logger: Logger) {}
constructor(
public logger: Logger,
public responseLogger?: Logger,
) {}
}

export const storage = new AsyncLocalStorage<Store>();

0 comments on commit f16a6d9

Please sign in to comment.