Хотя наш веб-сайт был простымReact приложением, на нем не было журналов на стороне клиента, только журналы API и надежная вкладка Network инструментов разработки Chromes. Этого было достаточно для отладки и устранения неполадок.

Затем настал день, когда стало важным SEO, простое React приложение должно было реализовать SSR (рендеринг на стороне сервера), и в него включился NodeJS процесс.

Получилось здорово - сайт загружается намного быстрее и его можно сканировать, но это усложняет процесс отладки. Первый вызов API происходит сейчас на сервере, если что-то пошло не так, все, что мы получаем на клиенте, - это красивое сообщение об ошибке с пустой вкладкой сети, никакой информации о том, что произошло!

У процесса SSR есть свои логи, конечно, если что-то пойдет не так, причина будет там. Я мог бы просто tail -n 100 жить с этим, но я считаю, что это очень обременительно, если в журнале содержится много информации - почему бы не смотреть на файлы журналов красиво?

Есть решения, но многие из них корпоративного уровня (читай: платные), например Graylog и Solarwinds, бесплатные существуют также, как Scalyr, но все они имеют слишком много функций, должны быть установлены и, вероятно, потребуют обучения. Бьюсь об заклад, это становится очевидным - давайте создадим наш собственный простой анализатор / визуализатор журналов как отдельное приложение! Psst, код можно найти здесь.

Задача

Для того, чтобы синтаксический анализатор журнала работал с минимальным набором функций, ему необходимо достичь нескольких целей:

  • Загружать новые записи, когда они появляются (хвостовая часть)
  • Показать журналы (компактный формат JSON, по одной строке на запись в журнале)
  • Просмотр более старых записей в файле журнала
  • Фильтровать записи по тексту

Меньшее количество целей означает, что мы можем своевременно создать эту утилиту и начать ее использовать. Чтобы еще больше ускорить разработку, давайте добавим еще одну цель - добиться всего с помощью веб-технологий.

Стек технологий

Поскольку мы хотим использовать уже знакомое, нам понадобится что-то для создания автономного приложения с использованием Javascript & Co, и это нечто Electron, фреймворк, который позволит нам создавать кроссплатформенные настольные приложения с использованием экосистемы Javascript. Имея это в виду - вот остальное:

  • React, для рисования пользовательского интерфейса
  • Redux, для управления состоянием пользовательского интерфейса
  • Стилизованные компоненты для стилизации нашего пользовательского интерфейса
  • React Json View для визуализации наших журналов JSON

Чуть не забыл упомянуть, что мы используем Typescript, чтобы избежать глупых ошибок и сделать код более читабельным после того, как я все об этом забыл.

Архитектура

Приложение Electron состоит из двух процессов:

  • main - это процесс, запускающий приложение и имеющий доступ к Node определенным функциям. Более-менее конфигурация в зависимости от обстоятельств.
  • renderer - процесс, который отвечает за все, что касается пользовательского интерфейса, в основном встроенный Chromium (ура, больше никаких несовместимостей браузеров при написании для Интернета!). Имеет доступ к WEB API и чувствует себя так, как будто пишет любой другой код для Интернета.

Помимо этого разделения, больше ничего не происходит, за исключением того, что нам нужно будет общаться между процессами, мы коснемся этого при достижении целей.

Ниже представлены оба файла процесса во всей их красе. main - это в основном конфигурация, а renderer выглядит как React приложение.

Задача I. Чтение и отслеживание файла

Начнем с простого чтения и показа пользователю содержимого файла. На первый взгляд кажется, что мы можем просто сделать это в процессе рендеринга, прочитав файл, в котором нам понадобится его содержимое.

Изучив Electron документы о том, как открыть диалог «открыть файл», мы видим, что мы можем сделать это только в main процессе, но можем ли мы использовать его в renderer?

Ответ на этот вопрос - да, но нет. Мы могли бы использовать модуль remote, но у него есть несколько проблем:

  • Это медленно
  • Это создает угрозу безопасности
  • Он будет объявлен устаревшим в v12 из Electron и полностью удален в v14 (ссылка)

Теперь это заставляет нас выполнять чтение файла в main процессе, но, как оказалось, это не проблема, Electron предоставляет модули IPC, которые мы можем и должны использовать:

  • ipcMain - используется в main процессе для добавления прослушивателей событий для сообщений, поступающих из нашего renderer процесса.
  • ipcRenderer - используется в процессе renderer для добавления прослушивателей событий и отправки сообщений на main

Учитывая это описание - как мы можем отправлять события на renderer из основного? Для этого нам нужно использовать объект thewindow, так как окон может быть несколько, но только одно главное. rendered знает свое основное, но обратное может быть неоднозначным.

Вооружившись этими знаниями, последовательность операций будет выглядеть следующим образом:

  • renderer запускает событие в главное окно, поэтому открывается диалоговое окно выбора файла.
  • main прослушивает это событие, открывает диалоговое окно и читает выбранный файл. Как только весь файл будет прочитан, отправляет его содержимое в renderer
  • renderer прослушивает событие, связанное с содержимым файла, после его получения сохраняет все в сокращенном виде, а затем выполняет магическую визуализацию, чтобы показать это пользователю.

В коде при вызове конца пользовательского интерфейса все просто. Настройка слушателей может быть объединена в одну функцию, которая затем может быть вызвана с помощью auseEffect в корне приложения React без каких-либо зависимостей, так что это происходит только один раз.

// Helper function
export const invokeFileSelect = () => {
  ipcRenderer.send(PROCESS_EVENTS.FILE_SELECT);
};
...
// Adding a click handler on the button
onClick={() => {
  invokeFileSelect();
}
// Listening for the response, storting everything in redux
ipcRenderer.on(PROCESS_EVENTS.TRANSMIT_CURRENT_LOG,
  (event, args: ILogFile) => {
    store.dispatch(setCurrentLogAction(args));
  }
);

А на Node слушатели также могут быть объединены в одну функцию и вызваны непосредственно в main, передавая созданный объект window (он нужен нам для отправки ответа).

ipcMain.on(PROCESS_EVENTS.FILE_SELECT, async () => {
  const files = await dialog.showOpenDialog(window, {
    properties: ["openFile"],
    filters: [{ name: "LOG", extensions: ["log"] }],
  });
// Multi selection is not allowed
!files.canceled && fileService.readLastLog(window, files.filePaths[0]);
});
...
window.webContents.send(PROCESS_EVENTS.TRANSMIT_CURRENT_LOG, {
  logs,
  lineNumber: totalLineCount,
  filePath,
  totalLineCount,
} as ILogFile);

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

Чтение может выполняться с помощью встроенного модуля readline, не путать с пакетом readline npm вместе с fs, инкапсулированным в красивый объект.

Следует отметить одно: я использовал watchFile, потому что watch не работал, когда файл журнала был добавлен сервером, но работал нормально, если я редактировал его вручную. Я использую окна, так что это может быть виновником, или, может быть, это как-то связано с тем, как winston добавляет файл.

Задача II: Визуализация журналов

Теперь, когда у нас есть данные в Redux хранилище, нам нужно как-то их визуализировать. Поскольку мы используем React - для этого нам нужен компонент. К нашему счастью, в npm уже существует такой компонент под названием react-json-view.

Все, что нам нужно сделать, это создать компонент-контейнер, который подключен к хранилищу Redux, и передать в него необходимые данные. Он даже поставляется с поддержкой тем из коробки!

Задача III: просмотреть записи журнала в файле журнала.

Мы отправляем весь файл журнала в Redux в виде массива, что означает, что для достижения текущей цели нам нужно создать переменную состояния activeIndex, которая указывает на активную запись журнала. Добавьте несколько кнопок, действий redux и контейнеров, и вуаля! Не вдаваясь в подробности, поскольку в этом нет ничего необычного, полную информацию о реализации можно найти здесь.

Цель IV: фильтрация

Как это должно работать? Я предположил, что пользователь вводит какой-то текст, а затем мы фильтруем записи журнала по этому значению. Если мы введем «возраст», мы получим все те записи, в которых где-то есть «возраст». Для лучшего взаимодействия с пользователем мы должны показывать, сколько совпадений содержится в каждой записи журнала, и иметь возможность быстро переключаться между ними.

Самостоятельная фильтрация не представляет угрозы, в нашем хранилище Redux есть все записи и можно быстро получить значение фильтра, но как мы можем показать вхождения и выполнить действие перехода?

Обычно такого рода вещи могут быть выполнены с помощью useRef в React, но в этом случае мы не можем передать их, поскольку наш JSON обрабатывается черным ящиком. Есть возможность форкнуть и добавить эту функциональность, но это займет много времени - почему бы не поискать в DOM после рендеринга?

Мы создаем единую ссылку и передаем ее в контейнер. После этого в useEffect мы используем его, чтобы получить все элементы и проверить, содержит ли их textContent значение нашего фильтра. Элемент, который мы хотим найти, не будет иметь дочерних HTML, если мы опустим эту проверку, у нас будет полноценное дерево с элементом, который действительно содержит это значение.

Свойства эффектов - это ref контейнера, флаг isUiLocked, который переключается, когда пользователь делает что-то (выбирает новый файл, изменяет значение фильтра), требующее обработки. Если элемент соответствует нашему фильтру, мы меняем его текст и цвет фона. Теперь о реализации прыжкового поведения.

Взглянув на иерархию компонентов, мы видим, что элементы управления, которые будут иметь кнопки для появления next/previous, находятся на том же уровне, что и jsonContainer.

export const Root: React.FC = () => {
  return (
    <ThemeProvider theme={LogDefaultTheme}>
      <GlobalStyles />
      <Provider store={store}>
        <AppContainer>
          <TitleBar />
          <ContentContainer>
            <Controls /> 
            <LogContainer>
              <Options /> {/* Scroll call origin */}
              <JsonContainer /> {/* Finds the HTML elements*/}
            </LogContainer>
          </ContentContainer>
        </AppContainer>
      </Provider>
    </ThemeProvider>
  );
};

Один из вариантов - переместить ссылку вверх по иерархии, а затем передать ее в контейнер JSON вместе с обратным вызовом для получения совпадающих элементов, но есть и другой вариант.

Почему бы не хранить их в нашем магазине Redux? Магазин должен быть сериализуемым, мы не можем сериализовать ссылки на элементы HTML. Это та же история, что и с размещением туда функций - они потеряют окружение, захваченное закрытием, но есть уловка, которую мы можем использовать.

export const refStore: { [key: string]: any } = {};

В том же файле, в котором мы создаем и экспортируем наш магазин, мы можем экспортировать refStore, который в основном является глобальным объектом. Однако этот подход не вызовет изменения состояния, поскольку состояние остается прежним, мы можем исправить это, добавив свойство, которое сигнализирует об изменении.

case GlobalActionTypes.SET_FILTER_ELEMENTS:
  /*
  As the chaning of refStore does not change the store itself we need
  to add a variable to the store to signal it has changed.
  */
  refStore[REF_KEY.FILTER_ELEMENTS] = action.payload.elements;
  return {
    ...state,
    filterElementsUpdate: Math.random() * 10000,
};

Если компоненту требуются элементы из refStore, он просто должен иметь filterElementsUpdate в качестве свойства состояния и реагировать на них useEffect.

Цель V: Бонус

У написания собственного решения есть еще одно преимущество - мы можем внедрить несколько сырых мемов, что сделает все приложение более интересным в использовании.

Заключение

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

Думаете, что-то можно было сделать лучше? Я весь во внимании!

Не стесняйтесь делать с кодом как хотите, если вы его пропустили, его можно найти здесь.