Pino (Logging)

main.ts

// bufferLogs: true is a feature that temporarily stores (buffers) 
// all log messages that occur during the application's bootstrap process 
// until the logger is fully configured. Here's why this is important
// prevent logging loss
const app = await NestFactory.create(AppModule, { bufferLogs: true });
// Uses dependency injection to get the logger instance (app.get(Logger))
app.useLogger(app.get(Logger));
app.module.ts
@Module({
  imports: [
   // Declare logger module in global level and customize the setting
    LoggerModule.forRoot({
      pinoHttp: pinoOptionConfig(),
    })
  ],
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
pino.config.ts
import { Options } from 'pino-http';
import pino from 'pino';
import * as apm from 'elastic-apm-node/start';
const LOG_FILE_BASE_PATH = process.env.LOG_FILE_BASE_PATH ?? '/tmp';
const LOG_FILE_ROTATE_SIZE = process.env.LOG_FILE_ROTATE_SIZE ?? '50M';
const LOG_FILE_MAX_FILE_HISTORY = process.env.LOG_FILE_MAX_FILE_HISTORY ?? 5;

type TransportOptions =
  | pino.TransportSingleOptions<Record<string, any>>
  | pino.TransportMultiOptions<Record<string, any>>
  | pino.TransportPipelineOptions<Record<string, any>>;

const targetOptions = {
  translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l o',
  ignore: 'res,context,filename',
  singleLine: true,
};

export function pinoOptionConfig(): Options {
  const logLevel = process.env.BOT_BUILDER_BACKEND_LOG_LEVEL || 'info';
  return {
   // min level of log
   // Only logs at this level 
   // or higher will be output (e.g., if set to 'info', debug logs won't show)
    level: logLevel,
   // append the additional field
    mixin: (mergeObject, level) => {
      return { 'level-label': pino.levels.labels[level].toUpperCase(), ...injectApmTraceId() };
    },
    // time of each log
    timestamp: pino.stdTimeFunctions.isoTime,
    // autom log http res and req
    autoLogging: true,
    quietReqLogger: false,
    // the output setting
    transport: configTransport(logLevel),
    // format specific log, such as request
    serializers: {
      req: formatRequestLog,
    },
  };
}

const injectApmTraceId = () => {
  const apmTransaction = {};
  if (apm?.isStarted()) {
    const apmTx = apm.currentTransaction;
    if (apmTx) {
      const apmCurrentTransaction = {};
      apmCurrentTransaction['trace.id'] = apmTx.ids['trace.id'];
      apmCurrentTransaction['transaction.id'] = apmTx.ids['transaction.id'];
      const span = apm.currentSpan;
      if (span) {
        apmCurrentTransaction['span.id'] = span.ids['span.id'];
      }
      apmTransaction['apmTransaction'] = apmCurrentTransaction;
    }
  }
  return apmTransaction;
};

const formatRequestLog = (req: pino.SerializedRequest) => {
  const requestLog: { api: string; gravitee: Record<string, string> | null } = {
    api: `[${req.id}] [${req.method}] [${req.url}]`,
    gravitee: null,
  };
  if (req.headers['x-gravitee-request-id']) {
    requestLog.gravitee = {
      'x-gravitee-request-id': req.headers['x-gravitee-request-id'],
      'x-gravitee-transaction-id': req.headers['x-gravitee-transaction-id'],
    };
  }
  return requestLog;
};

const configTransport = (logLevel: string): TransportOptions => {
  return {
    // console log output setting
    targets: [
      {
        target: 'pino-pretty',
        level: logLevel,
        options: {
          ...targetOptions,
        },
      },
      // file log output setting
      {
        target: require.resolve('./pino-transport-file-rotating'),
        level: logLevel,
        options: {
          size: LOG_FILE_ROTATE_SIZE,
          maxFiles: Number(LOG_FILE_MAX_FILE_HISTORY),
          path: LOG_FILE_BASE_PATH,
        },
      },
    ],
  };
};

Last updated

Was this helpful?