createLogger.js.flow 4.61 KB
// @flow

import environmentIsNode from 'detect-node';
import createGlobalThis from 'globalthis';
import stringify from 'json-stringify-safe';
import {
  sprintf,
} from 'sprintf-js';
import {
  logLevels,
} from '../constants';
import type {
  LoggerType,
  MessageContextType,
  MessageEventHandlerType,
  TranslateMessageFunctionType,
} from '../types';

const globalThis = createGlobalThis();

let domain;

if (environmentIsNode) {
  // eslint-disable-next-line global-require
  domain = require('domain');
}

const getParentDomainContext = () => {
  if (!domain) {
    return {};
  }

  const parentRoarrContexts = [];

  let currentDomain = process.domain;

  // $FlowFixMe
  if (!currentDomain || !currentDomain.parentDomain) {
    return {};
  }

  while (currentDomain && currentDomain.parentDomain) {
    currentDomain = currentDomain.parentDomain;

    if (currentDomain.roarr && currentDomain.roarr.context) {
      parentRoarrContexts.push(currentDomain.roarr.context);
    }
  }

  let domainContext = {};

  for (const parentRoarrContext of parentRoarrContexts) {
    domainContext = {
      ...domainContext,
      ...parentRoarrContext,
    };
  }

  return domainContext;
};

const getFirstParentDomainContext = () => {
  if (!domain) {
    return {};
  }

  let currentDomain = process.domain;

  // $FlowFixMe
  if (currentDomain && currentDomain.roarr && currentDomain.roarr.context) {
    return currentDomain.roarr.context;
  }

  // $FlowFixMe
  if (!currentDomain || !currentDomain.parentDomain) {
    return {};
  }

  while (currentDomain && currentDomain.parentDomain) {
    currentDomain = currentDomain.parentDomain;

    if (currentDomain.roarr && currentDomain.roarr.context) {
      return currentDomain.roarr.context;
    }
  }

  return {};
};

const createLogger = (onMessage: MessageEventHandlerType, parentContext?: MessageContextType): LoggerType => {
  // eslint-disable-next-line id-length, unicorn/prevent-abbreviations
  const log = (a, b, c, d, e, f, g, h, i, k) => {
    const time = Date.now();
    const sequence = globalThis.ROARR.sequence++;

    let context;
    let message;

    if (typeof a === 'string') {
      context = {
        ...getFirstParentDomainContext(),
        ...parentContext || {},
      };
      // eslint-disable-next-line id-length, object-property-newline
      const {...args} = {a, b, c, d, e, f, g, h, i, k};
      const values = Object.keys(args).map((key) => {
        return args[key];
      });
      // eslint-disable-next-line unicorn/no-reduce
      const hasOnlyOneParameterValued = 1 === values.reduce((accumulator, value) => {
        // eslint-disable-next-line no-return-assign, no-param-reassign
        return accumulator += typeof value === 'undefined' ? 0 : 1;
      }, 0);
      message = hasOnlyOneParameterValued ? sprintf('%s', a) : sprintf(a, b, c, d, e, f, g, h, i, k);
    } else {
      if (typeof b !== 'string') {
        throw new TypeError('Message must be a string.');
      }

      context = JSON.parse(stringify({
        ...getFirstParentDomainContext(),
        ...parentContext || {},
        ...a,
      }));

      message = sprintf(b, c, d, e, f, g, h, i, k);
    }

    onMessage({
      context,
      message,
      sequence,
      time,
      version: '1.0.0',
    });
  };

  log.child = (context: TranslateMessageFunctionType | MessageContextType): LoggerType => {
    if (typeof context === 'function') {
      return createLogger((message) => {
        if (typeof context !== 'function') {
          throw new TypeError('Unexpected state.');
        }
        onMessage(context(message));
      }, parentContext);
    }

    return createLogger(onMessage, {
      ...getFirstParentDomainContext(),
      ...parentContext,
      ...context,
    });
  };

  log.getContext = (): MessageContextType => {
    return {
      ...getFirstParentDomainContext(),
      ...parentContext || {},
    };
  };

  log.adopt = async (routine, context) => {
    if (!domain) {
      return routine();
    }

    const adoptedDomain = domain.create();

    return adoptedDomain
      .run(() => {
        // $FlowFixMe
        adoptedDomain.roarr = {
          context: {
            ...getParentDomainContext(),
            ...context,
          },
        };

        return routine();
      });
  };

  for (const logLevel of Object.keys(logLevels)) {
    // eslint-disable-next-line id-length, unicorn/prevent-abbreviations
    log[logLevel] = (a, b, c, d, e, f, g, h, i, k) => {
      return log.child({
        logLevel: logLevels[logLevel],
      })(a, b, c, d, e, f, g, h, i, k);
    };
  }

  // @see https://github.com/facebook/flow/issues/6705
  // $FlowFixMe
  return log;
};

export default createLogger;