Продолжаем марафон универсальных утилит на JavaScript, которые я использую от проекта к проекту независимо от стека.

import { clsx } from './string-ops';
import { getReadableTimeDiff } from './time-ops';

export const debugFactory = <TEvent, TError>({ label: header }: { label: string }) => {
  let counter = 0;
  const auxCountersState: { [key: string]: number } = {};
  const getServiceMsg = () => {
    const msgs = [];
    if (auxCountersState._unknown > 0 || auxCountersState._others > 0) {
      msgs.push('About:');
      if (auxCountersState._unknown > 0) {
        msgs.push('_unknown - Те ивенты что хотели посчитать, но не смогли идентифицировать');
      }
      if (auxCountersState._others > 0) {
        msgs.push('_others - Те ивенты что не планировали посчитать, но лог был вызван, поэтому счетчик на всякий случай заведен (просто для информации)');
      }
    }
    return msgs.join('\n');
  };
  const _auxCountersStateInc = ({ key }: { key: string | undefined }) => {
    if (typeof key === 'string') {
      if (typeof auxCountersState[key] === 'number') {
        auxCountersState[key] += 1;
      } else {
        auxCountersState[key] = 1;
      }
    } else if (typeof auxCountersState._unknown === 'number') {
      auxCountersState._unknown += 1;
    } else {
      auxCountersState._unknown = 1;
    }
  };
  let firstTs: number | undefined;
  let oldTs: number | undefined = firstTs;

  return {
    _auxCountersStateInc,
    log: ({
      evt, err, label, msgs, auxStateSelector
    }: {
      label: string;
      evt: TEvent;
      err: TError;
      msgs?: string[];
      auxStateSelector?: (e: TEvent) => string | undefined;
    }) => {
      const currentTs = new Date().getTime();
      if (typeof firstTs !== 'number') {
        firstTs = currentTs;
      }
      console.groupCollapsed(
        clsx(
          header,
          `[${counter}]`,
          typeof oldTs === 'number' ? `[+${getReadableTimeDiff({ diffInMs: currentTs - oldTs }).humanized}]` : undefined,
          label,
        )
      );
      oldTs = currentTs;
      counter += 1;

      // - NOTE: Exp
      if (typeof auxStateSelector === 'function') {
        const key = auxStateSelector(evt);
        _auxCountersStateInc({ key });
      } else {
        _auxCountersStateInc({ key: '_others' });
      }
      // -

      if (Array.isArray(msgs) && msgs?.length > 0) {
        msgs.forEach((msg) => console.log(msg));
      }
      console.dir({
        evt, err
      });
      console.log(JSON.stringify(auxCountersState, null, 2));
      console.log(`${getReadableTimeDiff({ diffInMs: currentTs - firstTs }).humanized} since first`);
      const serviceMsg = getServiceMsg();
      if (serviceMsg) { console.log(serviceMsg); }
      console.groupEnd();
    },
    counter,
  };
};

Utils

~/utils/string-ops/clsx.ts

function toVal(mix: any) {
  let k; let y; let
    str = '';

  if (typeof mix === 'string' || typeof mix === 'number') {
    str += mix;
  } else if (typeof mix === 'object') {
    if (Array.isArray(mix)) {
      const len = mix.length;
      for (k = 0; k < len; k++) {
        if (mix[k]) {
          if (y = toVal(mix[k])) {
            str && (str += ' ');
            str += y;
          }
        }
      }
    } else {
      for (y in mix) {
        if (mix[y]) {
          str && (str += ' ');
          str += y;
        }
      }
    }
  }

  return str;
}

export function clsx(...args: any) {
  let i = 0; let tmp; let x; let str = ''; const
    len = args.length;
  for (; i < len; i++) {
    if (tmp = args[i]) {
      if (x = toVal(tmp)) {
        str && (str += ' ');
        str += x;
      }
    }
  }
  return str;
}

~/utils/time-ops/getReadableTimeDiff.ts

import { getPadStart } from '~/utils/number-ops';

export const getReadableTimeDiff = ({ diffInMs: ms }: { diffInMs: number }): {
  humanized: string;
} => {
  const secs = Math.floor(Math.abs(ms) / 1000);
  const mins = Math.floor(secs / 60);
  const mscs = Math.floor(Math.abs(ms) % 1000);

  return {
    get humanized() {
      return [
        [
          getPadStart({
            value: mins, standardLength: 2
          }),
          getPadStart({
            value: secs, standardLength: 2
          }),
        ].join(':'),
        getPadStart({
          value: mscs, standardLength: 3
        }),
      ].join('.');
    }
  };
};

~/utils/number-ops/getPadStart.ts

export const getPadStart = ({
  value, standardLength
}: {
  value: number;
  standardLength: number;
}): string => String(value).padStart(standardLength, '0');

Usage example

const yourLogger = debugFactory({ label: 'YOUR LOGGER HEADER' });

yourLogger.log({ event: e, err, label: '🟢 validated.ok' });
yourLogger.log({
  event: e,
  err,
  label: '🔴 !validated.ok',
  auxStateSelector: (ev) => `ERR:${ev?.origin || 'others'}:${ev?.data?.topic || `topic-is-${typeof ev?.data?.topic}`}`,
});