[Перевод] Сверхпростое логгирование в Javascript — два декоратора, и готово

ew1vcdon5l1qxt8dhjrickcyzlo.jpeg

Вам еще не надоело писать logger.info('ServiceName.methodName.') и logger.info('ServiceName.methodName -> done.') на каждый чих? Может вы, так же как и я, неоднократно задумывались о том, чтобы это дело автоматизировать? В данной статье рассказ пойдет о class-logger, как об одном из вариантов решения проблемы с помощью всего лишь двух декораторов.


Зачем оно вообще надо?

Не все перфекционисты — инженеры, но все инженеры — перфекционисты (ну, почти). Нам нравятся красивые лаконичный абстракции. Мы видим красоту в наборе кракозябликов, которые человек неподготовленный и прочитать не сможет. Нам нравится чувствовать себя богами, создавая микро-вселенные, которые живут по нашим правилам. Предположительно, все эти качества берут начало из нашей необъятной лени. Нет, инженер не боится работы как класса, но люто ненавидит рутинные повторяющиеся действия, которые руки так и тянутся автоматизировать.

Написав несколько тысяч строк кода, предназначенного только лишь для логгирования, обычно мы приходим к созданиям каких-то своих паттернов и стандартов написания сообщений. К сожалению, нам все еще приходится применять эти паттерны вручную. Главное «зачем» class-logger — предоставить декларативный конфигурируемый способ простого логгирования шаблонных сообщений при создании класса и вызове его методов.


Шашки наголо!

Не бродя вокруг да около, перейдем сразу к коду.

import { LogClass, Log } from 'class-logger'

@LogClass()
class ServiceCats {
  @Log()
  eat(food: string) {
    return 'purr'
  }
}

Этот сервис создаст три записи в логе:


  • Перед его созданием со списком аргументов, переданных в конструктор.
  • Перед вызовом eat со списком аргументов.
  • После вызова eat со списком аргументов и результатом выполнения.

In words of code:

// Логгирует перед созданием ServiceCats
// `ServiceCats.construct. Args: [].`
const serviceCats = new ServiceCats()

// Логгирует перед вызовом `eat`
// `ServiceCats.eat. Args: [milk].`
serviceCats.eat('milk')
// Логгирует после вызова `eat`
// `ServiceCats.eat -> done. Args: [milk]. Res: purr.`

Потыкать «вживую».

Полный список событий, которые можно залоггировать:


  • Перед созданием класса.
  • Перед вызовом синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.
  • После вызова синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.
  • Ошибки вызова синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.


Функциональное свойство — стрелочная функция присвоенная свойству (class ServiceCats { private meow = () => null }). Наиболее часто используется для сохранения контекста выполнения (this).


Нам обещали «конфигурируемый» способ логгирования

class-logger три уровня иерархии конфигурации:


  • Глобальный
  • Класс
  • Метод

При вызове каждого метода, все три уровня мержатся вместе. Библиотека предоставляет разумный глобальный конфиг по умолчанию, так что ее можно использовать и без какой-либо предварительной настройки.


Глобальный конфиг

Он создается для всего приложения. Его можно переопределить с помощью setConfig.

import { setConfig } from 'class-logger'

setConfig({
  log: console.info,
})


Конфиг класса

Он свой для каждого класса и применяется для всех методов этого класса. Может переопределять глобальный конфиг.

import { LogClass } from 'class-logger'

setConfig({
  log: console.info,
})

@LogClass({
  // Переопределяет глобальный конфиг для этого сервиса
  log: console.debug,
})
class ServiceCats {}


Конфиг метода

Работает только для самого метода. Имеет приоритет над конфигом класса и глобальным конфигом.

import { LogClass } from 'class-logger'

setConfig({
  log: console.info,
})

@LogClass({
  // Переопределяет глобальный конфиг для этого сервиса
  log: console.debug,
})
class ServiceCats {
  private energy = 100

  @Log({
    // Переопределяет конфиг класса только для этого метода
    log: console.warn,
  })
  eat(food: string) {
    return 'purr'
  }

  // Этот метод использует `console.debug` предоставленный конфигом класса
  sleep() {
    this.energy += 100
  }
}

Потыкать «вживую»


Что можно конфигурировать?

Мы рассмотрели как изменять конфиг, но все еще не разобрали, что именно в нем можно изменить.


Здесь можно найти много примеров конфигурационных объектов для разных случаев.

Ссылка на интерфейс, если вы понимаете TypeScript лучше. чем русский :)

Объект конфигурации имеет следующие свойства:


log

Это функция, которая занимается, собственно, логгированием. Ей на вход приходит отформатированное сообщения в виде строки. Используется для логгирования следующих событий:


  • Перед созданием класса.
  • Перед вызовом синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.
  • После вызова синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.

По умолчанию console.log.


logError

Это функция, которая тоже занимается логгированием, но уже только сообщений об ошибке. Ей также на вход приходит отформатированное сообщения в виде строки. Используется для логгирования следующих событий:


  • Ошибки вызова синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.

По умолчанию console.error.


formatter

Это объект с двумя методами: start и end. Эти методы создают то самое отформативанное сообщение.

start создает сообщения для следующих событий:


  • Перед созданием класса.
  • Перед вызовом синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.

end создает сообщения для следующих событий:


  • После вызова синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.
  • Ошибки вызова синхронных и асинхронных методов и функциональных свойств, как статических, так и не статических.

По умолчанию new ClassLoggerFormatterService().


include

Конфиг того, что должно быть включено в конечное сообщение.


args

Это может быть как булево значение, так и объект.

Если это boolean, то он задает включать ли список аргументов (помните Args: [milk]?) во все сообщения (start и end).

Если это объект, то у него должно быть два булевых свойства: start и end. start задает включать ли список аргументов для start сообщений, end — для end сообщений.

По умолчанию true.


construct

Это boolean, который контролирует логгировать ли создание класса или нет.

По умолчанию true.


result

Это boolean, который контролирует логгировать возвращаемое из метода значение. Стоит заметить, что возвращаемое значение — это не только то, что метод возвращает при успешном выполнении, но и ошибка, которую он бросает в случае неудачного выполнения. Помните Res: purr? Если этот флаг будет false, то не будет никакого Res: purr.

По умолчанию true.


classInstance

Это может быть как булево значение, так и объект. Концепция та же, что и include.args.

Добавляет сериализованное представление инстанса вашего класса в сообщения. Другими словами, если у вашего класса есть какие-то свойства, то они будут сериализованны в JSON и добавлены в логи.

Не все свойства подлежат сериализации. class-logger использует следующую логику:


  • Берем все собственные (те, которые не в прототипе) свойства интсанса.
    • Почему? Очень редко прототип меняет динамически, так что нет большого смысла логгировать его содержимое.
  • Выкидываем из них все типа function.
    • Почему? Чаще всего свойства типа function — это просто стрелочные функции, которые не сделали обычными методами ради сохранения контекста (this). Они редко имеют динамическую природу, так что и логгировать их нет смысла.
  • Выкидываем из них все объекты, которые не являются простыми объектами.
    • Что за простые объекты? ClassLoggerFormatterService считает объект простым, если его прототип — это Object.prototype.
    • Почему? Часто в качестве свойств выступают инстансы других классов-сервисов. Наши логи распухли бы самым безобразным образом, если бы мы из стали сериализовать.
  • Сериализовать в JSON строку все, что осталось.
class ServiceA {}

@LogClass({
  include: {
    classInstance: true,
  },
})
class Test {
  private serviceA = new ServiceA()
  private prop1 = 42
  private prop2 = { test: 42 }
  private method1 = () => null

  @Log()
  public method2() {
    return 42
  }
}

// Логгирует перед созданием `Test`
// 'Test.construct. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}.'
const test = new Test()

// Логгирует перед вызовом `method2`
// 'Test.method2. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}.'
test.method2()
// Логгирует после вызова `method2`
// 'Test.method2 -> done. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}. Res: 42.'

По умолчанию false.


Берем полный контроль над форматированием сообщений

Что же делать, если вам нравится идея, но ваши представление о прекрасном настаивает на другом формате строки сообщений? Вы можете взять полный контроль над форматированием сообщений, передав свой собственный форматтер.

Вы можете написать свой собственный форматтер с нуля. Безусловно. Но этот вариант не будет освещен в рамках этой (если вам интересна именно эта опцию, загляните в раздел «Formatting» в README).

Самый быстрый и простой способ переопределить форматирование — это унаследовать свой форматтер от форматтера по умолчанию — ClassLoggerFormatterService.

ClassLoggerFormatterService имеет следующие protected методы, которые создают небольшие блоки финальных сообщений:


  • base
    • Возвращает имя класса и имя метода. Пример: ServiceCats.eat.
  • operation
    • Возвращает -> done или -> error в зависимости от того, успешно ли выполнился метод.
  • args
    • Возвращает сериализованный список аргументов. Пример: . Args: [milk]. Он использует fast-safe-stringify для сериализации объектов под капотом.
  • classInstance
    • Возвращает сериализованный инстанс класса. Пример: . Class instance: {"prop1":42,"prop2":{"test":42}}. Если вы включили include.classInstance в конфиге, но по какой-то причине сам инстанс в момент логгирования не доступен (например, для статических методов или перед созданием класса), возвращает N/A.
  • result
    • Возвращает сериализованный результат выполнения или сериализованную ошибку. Использует fast-safe-stringify для объектов под капотом. Сериализованная ошибка включает в себя следующие свойства:
    • Имя класса (функции), которая была использована для создания ошибки (error.constructor.name).
    • Код (error.code).
    • Сообщение (error.message).
    • Имя (error.name).
    • Стек трейс (error.stack).
  • final
    • Возвращает .. Просто ..

Сообщение start состоит из:


  • base
  • args
  • classInstance
  • final

Сообщение end состоит:


  • base
  • operation
  • args
  • classInstance
  • result
  • final

Вы можете переопределить только необходимые вам базовые методы.

Давайте рассмотрим, как можно добавить timestamp ко всем сообщениям.


Я не говорю, что это надо делать в реальных проектах. pino, winston и большинство других логгеров способны делать это самостоятельно. Этот пример служит в исключительно образовательных целях.

import {
  ClassLoggerFormatterService,
  IClassLoggerFormatterStartData,
  setConfig,
} from 'class-logger'

class ClassLoggerTimestampFormatterService extends ClassLoggerFormatterService {
  protected base(data: IClassLoggerFormatterStartData) {
    const baseSuper = super.base(data)
    const timestamp = Date.now()
    const baseWithTimestamp = `${timestamp}:${baseSuper}`
    return baseWithTimestamp
  }
}

setConfig({
  formatter: new ClassLoggerTimestampFormatterService(),
})

Потыкать «вживую»


Заключение

Не забывайте следовать инструкциям по установке и ознакомьтесь с требованиями перед тем как использовать эту библиотеку на вашем проекте.

Надеюсь, вы не потратили время зря, и статья была вам хоть чуточку полезна. Просьба пинать и критиковать. Будем учиться кодить лучше вместе.

© Habrahabr.ru