Логирование и мониторинг

Логирование помогает отслеживать использование timeago.js в production, выявлять аномалии (слишком частые вызовы, невалидные даты) и собирать метрики производительности.


Уровни логирования

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LogEntry {
  level:     LogLevel;
  message:   string;
  timestamp: number;
  data?:     Record<string, unknown>;
}

class TimeagoLogger {
  private level: LogLevel = 'warn';

  setLevel(level: LogLevel): void {
    this.level = level;
  }

  private shouldLog(level: LogLevel): boolean {
    const ORDER: LogLevel[] = ['debug', 'info', 'warn', 'error'];
    return ORDER.indexOf(level) >= ORDER.indexOf(this.level);
  }

  log(level: LogLevel, message: string, data?: Record<string, unknown>): void {
    if (!this.shouldLog(level)) return;

    const entry: LogEntry = { level, message, timestamp: Date.now(), data };
    console[level](`[timeago] ${message}`, data ?? '');
  }

  debug(msg: string, data?: Record<string, unknown>) { this.log('debug', msg, data); }
  info(msg: string, data?: Record<string, unknown>)  { this.log('info', msg, data);  }
  warn(msg: string, data?: Record<string, unknown>)  { this.log('warn', msg, data);  }
  error(msg: string, data?: Record<string, unknown>) { this.log('error', msg, data); }
}

export const logger = new TimeagoLogger();

Обёртка с автоматическим логированием

import { format as baseFormat } from 'timeago.js';

function monitoredFormat(date: unknown, locale = 'ru'): string {
  const start = performance.now();

  logger.debug('format called', { date, locale });

  if (date == null) {
    logger.warn('format called with null/undefined date', { locale });
    return '';
  }

  const d = new Date(date as any);
  if (isNaN(d.getTime())) {
    logger.error('format called with invalid date', { date, locale });
    return '';
  }

  const result  = baseFormat(d, locale);
  const elapsed = performance.now() - start;

  if (elapsed > 5) {
    logger.warn('format took too long', { elapsed, date, locale });
  } else {
    logger.debug('format completed', { result, elapsed });
  }

  return result;
}

Счётчики вызовов

interface CallStats {
  total:   number;
  success: number;
  errors:  number;
  lastAt:  number;
}

class TimeagoStats {
  private stats: Record<string, CallStats> = {
    format: { total: 0, success: 0, errors: 0, lastAt: 0 },
    render: { total: 0, success: 0, errors: 0, lastAt: 0 },
    cancel: { total: 0, success: 0, errors: 0, lastAt: 0 },
  };

  record(fn: string, success: boolean): void {
    const s = this.stats[fn];
    if (!s) return;
    s.total++;
    s.lastAt = Date.now();
    if (success) s.success++; else s.errors++;
  }

  report(): Record<string, CallStats> {
    return { ...this.stats };
  }

  reset(): void {
    Object.values(this.stats).forEach(s => {
      s.total = s.success = s.errors = 0;
    });
  }
}

export const stats = new TimeagoStats();

Мониторинг невалидных дат

const invalidDateLog: Array<{ date: unknown; locale: string; ts: number }> = [];
const MAX_LOG = 100;

function trackInvalidDate(date: unknown, locale: string): void {
  if (invalidDateLog.length >= MAX_LOG) {
    invalidDateLog.shift(); // Кольцевой буфер
  }
  invalidDateLog.push({ date, locale, ts: Date.now() });

  logger.warn('Invalid date detected', { date, locale });
}

Интеграция с Sentry

import * as Sentry from '@sentry/browser';

function sentryFormat(date: unknown, locale = 'ru'): string {
  try {
    if (date == null) throw new TypeError('date is null');
    const d = new Date(date as any);
    if (isNaN(d.getTime())) throw new TypeError(`Invalid date: ${date}`);
    return format(d, locale);
  } catch (err) {
    Sentry.captureException(err, {
      tags:  { component: 'timeago' },
      extra: { date, locale },
    });
    return '';
  }
}

Интеграция с DataDog / StatsD

import { StatsD } from 'hot-shots'; // Или любой StatsD клиент

const statsd = new StatsD();

function metricsFormat(date: Date | string | number, locale = 'ru'): string {
  const start = Date.now();
  const result = format(date, locale);
  const elapsed = Date.now() - start;

  statsd.timing('timeago.format.duration', elapsed);
  statsd.increment('timeago.format.calls', 1, { locale });

  return result;
}

Дашборд состояния в production

// Экспортировать в window для проверки через консоль
if (process.env.NODE_ENV !== 'production') {
  (window as any).__timeago = {
    stats: () => stats.report(),
    invalidDates: () => [...invalidDateLog],
    activeElements: () => document.querySelectorAll('[datetime]').length,
  };
}

Логирование жизненного цикла render

import { render as baseRender, cancel as baseCancel } from 'timeago.js';

const activeRenders = new Map<Element, string>(); // element → locale

function trackedRender(nodes: Element | Element[], locale = 'ru'): void {
  const arr = Array.isArray(nodes) ? nodes : [nodes];

  arr.forEach(el => {
    if (activeRenders.has(el)) {
      logger.warn('render called on already-rendered element', {
        element: el.tagName,
        existingLocale: activeRenders.get(el),
        newLocale: locale,
      });
    }
    activeRenders.set(el, locale);
  });

  logger.info(`render: ${arr.length} elements, locale="${locale}"`);
  baseRender(nodes, locale);
}

function trackedCancel(nodes?: Element | Element[]): void {
  const arr = nodes ? (Array.isArray(nodes) ? nodes : [nodes]) : [];

  arr.forEach(el => activeRenders.delete(el));

  logger.info(`cancel: ${arr.length} elements`);
  baseCancel(nodes);
}

Конфигурация логирования для разных сред

if (process.env.NODE_ENV === 'development') {
  logger.setLevel('debug');
} else if (process.env.NODE_ENV === 'staging') {
  logger.setLevel('info');
} else {
  logger.setLevel('error'); // production — только критические ошибки
}

Таблица метрик для мониторинга

Метрика Пороговое значение Действие при превышении
format duration > 5 мс Warn в лог
Количество невалидных дат/мин > 10 Alert в мониторинг
Активных render элементов > 500 Предупреждение о производительности
Количество errors за час > 100 Incident