Не каждому проекту нужно децентрализованное логирование. В моём случае, оказалось проще хранить логи в .json файлах формата Compact Log Event Format (CLEF). Мне нужно было простое и бесплатное решение для просмотра логов.

Логотип разработанного приложения
Логотип разработанного приложения

Готовое решение

На момент написания статьи в свободном доступе было только 1 подходящее приложение для чтения логов в формате CLEF - Compact Log Viewer, доступно в Microsoft Store.

Compact Log Viewer
Compact Log Viewer

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

  1. Каждому разработчике нужно устанавливать локально через Microsoft Store

  2. За раз можно открыть только 1 файл

  3. Нельзя поставить на тестовый сервер если это линукс

  4. Часть UI занимает бесполезный график

  5. После повторного открытия приложения загруженный ранее файл терялся, его нужно было грузить заново

Через некоторое время я устал от постоянного скачивания логов с сервера на локальную машину для их просмотра. Поэтому решил написать своё собственное приложение.

Требования к разрабатываемому приложению

К разрабатываемое приложению я установил следующие требования:

  1. Отображать список логов

  2. Поддерживать просмотр свойств отдельных логово

  3. Поддерживать фильтрацию по свойствам

  4. Поддерживать загрузку нескольких файлов

  5. Сохранять логи между сессиями

  6. Поддерживать запуск в среде Docker

Поддержка среды Docker позволила сделать приложение "доступным из коробки", включив его в дефолтный docker-compose файл на проектах. Также стала возможна загрузка на сервер.

Процесс разработки

Для разработки проекта был выбор между React и Angular. По личным предпочтениям был выбран React + StyledComponents.

Для UI компонентов выбрал бесплатное решение RsuiteJs.

Flow-приложения:

  1. Перейти на страницу загрузки логов

  2. Загрузить логи

  3. Перенаправить на страницу просмотра логов

Реализация загрузки логов

Для форм остановился на связке Yup с Formik. Создал обёртки над компонентами форм, FileUploader от RSuite не подходил, используя библиотеку ReactDropzone написал кастомную реализацию.

Страница загрузки файлов
Страница загрузки файлов

Хранение загруженных логов

Для упрощения проекта отказался от backend и NoSql базы данных. Логи записываю в хранилище браузера используя Localforage. Добавил AppState и AppStorage контексты.

//прослойка над хранилищем, контролирует согласованность данных за счет storageVersion
export interface AppStorage {
  loadedLogFilesInfo?: FileInfo[];
  csLogs?: CsLog[];
  storageVersion: number;
}
//Основной контекст приложения
export interface AppState {
  loadedLogFilesInfo?: FileInfo[];
  csLogs?: CsLog[];
}

export function BaseLayout(): ReactElement {
  //кастомная реализация светлой и темной темы 
  const theme = useTheme();

  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <AppStorageContextProvider>
        <AppStateContextProvider>
          <RSuiteProvider theme={theme}>
            <AppLayout />
          </RSuiteProvider>
        </AppStateContextProvider>
      </AppStorageContextProvider>
    </ErrorBoundary>
  );
}

Для дальнейшего отображения логов в списке нужно установить что принимать за уникальный Id каждой записи в файле. Рассмотрев представленный ниже пример логов видно, что поля id нет. Можно попробовать использовать "@t", однако гарантии уникальности время лога не даёт.

{"@t":"2024-03-06T07:43:20.5672449Z","@mt":"Starting up","@l":"Information","EnvironmentName":"Staging"}
{"@t":"2024-03-06T07:43:23.3405140Z","@mt":"Executing ViewResult, running view {ViewName}.","@l":"Warning","@tr":"f3e14fda062a0ae0d641782c77ee0617","@sp":"0aee0c652548be62","ViewName":"Index","EventId":{"Id":1,"Name":"ViewResultExecuting"},"SourceContext":"Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor","ActionId":"9ea02b27-cd15-4432-82b1-7f9768a48e4b","ActionName":"DealerServiceSystem.Web.Controllers.CheckListCategoryController.Index (DealerServiceSystem.Web)","RequestId":"40000bc6-0002-f200-b63f-84710c7967bb","EnvironmentName":"Staging"}
{"@t":"2024-03-06T07:43:23.7431244Z","@mt":"Executed ViewResult - view {ViewName} executed in {ElapsedMilliseconds}ms.","@l":"Debug","@tr":"f3e14fda062a0ae0d641782c77ee0617","@sp":"0aee0c652548be62","ViewName":"Index","ElapsedMilliseconds":404.2372,"EventId":{"Id":4,"Name":"ViewResultExecuted"},"SourceContext":"Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor","ActionId":"9ea02b27-cd15-4432-82b1-7f9768a48e4b","RequestId":"40000bc6-0002-f200-b63f-84710c7967bb","EnvironmentName":"Staging"}
{"@t":"2024-03-06T07:43:23.7466340Z","@mt":"Executed action in {ElapsedMilliseconds}ms","@l":"Warning","@tr":"f3e14fda062a0ae0d641782c77ee0617","@sp":"0aee0c652548be62","ElapsedMilliseconds":536.3229,"EventId":{"Id":105,"Name":"ActionExecuted"},"SourceContext":"Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker","RequestId":"40000bc6-0002-f200-b63f-84710c7967bb","EnvironmentName":"Staging"}

Решил эту проблему добавив "искусственный" id, сгенерированный через crypto-randomuuid, вышли следующие структуры:

export interface CsLog {
  id: string;
  data: CsLogData;
}

export interface CsLogData {
  [key: string]: any;
  '@t': Date;
  '@mt': string;
  '@l'?: 'Verbose' | 'Debug' | 'Information' | 'Warning' | 'Error' | 'Fatal';
}

Реализация просмотра и фильтрации логов

Реализовал пагинацию в отдельном компоненте, используется следующим образом:

const [paginatedLogs, setPaginatedLogs] = useState<CsLog[] | null>(null)

<StyledPagination values={filteredLogs} onChange={setPaginatedLogs} />

Для фильтра по логам использовал SearchJs, синтаксис простейшего фильтра { "data.<property_name>": "value" }, реализация:

searchjs.matchArray(appState.csLogs, JSON.parse(filter))

Валидацию фильтра сделал в лоб, пытаюсь парсить json, ошибка - неверный формат:

export const logsViewPageFormSchema: yup.Schema<LogsViewPageFormData> = yup.object({
  filter: yup.string().test('json', 'Filter must have JSON format', (value) => {
    if (value === undefined) {
      return true;
    }

    try {
      JSON.parse(value);

      return true;
    } catch (error) {
      return false;
    }
  })
});

В итоге вышла следующая страница:

Страница просмотра логов
Страница просмотра логов

Публикация приложения

Чтобы сделать проект общедоступным выложил его код в общедоступный репозиторий на GitHub по этой ссылке.

Настроил workflow на билд Docker Image-а и паблиш в репозитории DockerHub ссылка. Приложение можно запустить используя следующий docker-compose файл, проект будет доступен на по ссылке http://localhost:8080.

version: "3.8"
services:
  client:
    image: "migiki/cs-logs-viewer:latest"
    container_name: cs-logs-viewer
    ports:
      - "8080:80"

Также добавил MkDocs документацию к проекту, доступна по ссылке

Документация CS Logs viewer
Документация CS Logs viewer

Итоги

Разработал на React CS Logs viewer для просмотра структурированных логов, образ которого весит около 55мб, в то время как рассматриваемый выше аналог занимает 300мб. Приложение можно деплоить на тестовый сервер или поставлять в docker-compose файле вместе с кодом своего проекта, чтобы можно было смотреть логи на любой системе с установленной Docker-средой.

Буду рад вашим предложениям и замечаниям!

Комментарии (12)


  1. radioxoma
    07.04.2024 21:01
    +2

    Зачётная иконка. Напомнило почему-то про Log Lady из Твин Пикса.


    1. SStefS Автор
      07.04.2024 21:01

      Иконку сгенерировал с ИИ. Правда был водяной знак, перерисовал вручную.


  1. SpiderEkb
    07.04.2024 21:01
    +1

    А не думали сделать UDTF чтобы читать логи при помощи SQL запроса? У нас есть такое - очень удобно.

    Продуктовые логи сразу пишем в БД, но есть еще системные (joblog) логи заданий - вот для них есть UDTF.


    1. SStefS Автор
      07.04.2024 21:01

      Да, сразу смотрел на эту реализацию, по примеру как у Seq. Для более простого старта пошёл на компромисс.

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


      1. SpiderEkb
        07.04.2024 21:01
        +2

        Смотреть логи скулем универсально и удобно с точки зрения фильтрации

        Поскокльку у нас вся работа крутится вокруг БД, то все логи ведем в виде таблиц бд. Там есть уровни логирования (как минимум три уровня - только ошибки, ошибки + бизнес ссобщения (то, что может быть интересно бизнесу), ошибки, бизнес и трейсы (это уже отладочная информацимя). Есть настройки - глубина хранения логов для разных уровней логирования, уровень логирования...

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

        Поскульку наша платформа работает "как большие машины" (т.е. все работает в рамках задания - job), для каждого задания системой ведется joblog (туда всякие системные вещи пишутся - системные исключений и т.п.). Это хранится в каком-то своем формате и смотреть можно или системными командами в виде текста, или есть системная UDTF JOBLOG_INFO, которая позволяет делать это скулем

        SELECT ORDINAL_POSITION,
               MESSAGE_TYPE,
               MESSAGE_TIMESTAMP,
               FROM_LIBRARY,
               FROM_PROGRAM,
               MESSAGE_TEXT,
               MESSAGE_SECOND_LEVEL_TEXT
        FROM TABLE(QSYS2.JOBLOG_INFO(JOB_NAME => '316557/QUSER/QZDASOINIT'));

        Ну и получаем примерно такое

        1	INFORMATIONAL	2024-04-06 09:32:40.412299	QSYS	QWTPIIPP	Задание 400332/QUSER/QZDASOINIT запущено 06.04.24 в 09:32:40 в подсистеме QUSRWRK в библиотеке QSYS. Задание появилось в системе 06.04.24 в 09:32:40.	
        2	INFORMATIONAL	2024-04-08 11:47:52.655109	QSYS	QWTCHGJB	ACGDTA для 400332/QUSER/QZDASOINIT не занесен в журнал, код причины 1.	&N Причина . . . . :   Данные Задание учета ресурсов задания 400332/QUSER/QZDASOINIT не были занесены в журнал учета ресурсов системы с именем QSYS/QACGJRN. &P -- Коды причин и их значения приведены ниже: &P -- 1 - Системное значение уровня учета ресурсов (QACGLVL) указывает, что выполнять учет ресурсов этого уровня при входе задания в систему не нужно. &P -- 2 - Журнал QSYS/QACGJRN не в состоянии принимать данные.  Данные учета ресурсов были отправлены в протокол хронологии (QHST) в тексте сообщения CPF1303. Действия по исправлению см. в сообщении CPF1302 протокола хронологии (QHST). &P -- 3 - Журнал учета ресурсов QSYS/QACGJRN был выделен другому заданию.  Данные учета ресурсов были отправлены в протокол хронологии (QHST) в виде текста сообщения CPF1303.
        3	INFORMATIONAL	2024-04-08 13:39:45.479460	QSYS	QWTCHGJB	ACGDTA для 400332/QUSER/QZDASOINIT не занесен в журнал, код причины 1.	&N Причина . . . . :   Данные Задание учета ресурсов задания 400332/QUSER/QZDASOINIT не были занесены в журнал учета ресурсов системы с именем QSYS/QACGJRN. &P -- Коды причин и их значения приведены ниже: &P -- 1 - Системное значение уровня учета ресурсов (QACGLVL) указывает, что выполнять учет ресурсов этого уровня при входе задания в систему не нужно. &P -- 2 - Журнал QSYS/QACGJRN не в состоянии принимать данные.  Данные учета ресурсов были отправлены в протокол хронологии (QHST) в тексте сообщения CPF1303. Действия по исправлению см. в сообщении CPF1302 протокола хронологии (QHST). &P -- 3 - Журнал учета ресурсов QSYS/QACGJRN был выделен другому заданию.  Данные учета ресурсов были отправлены в протокол хронологии (QHST) в виде текста сообщения CPF1303.
        4	INFORMATIONAL	2024-04-08 13:39:45.479800	QSYS	QZBSSECR	К серверу подключен пользователь *** с клиента **.**.**.**.	&N Причина . . . . :   В данный момент с этим заданием сервера связан пользовательский профайл *** с клиента **.**.**.**.  Имя клиента - это имя удаленной системы TCP/IP, IP-адрес этой системы в десятичной записи с точками, адрес IPv6 либо имя локального хоста.
        5	COMPLETION	2024-04-08 13:39:46.070314	QSYS	QWTCHGJB	Задание 400332/QUSER/QZDASOINIT изменено пользователем Y2KU.	
        6	INFORMATIONAL	2024-04-08 13:39:46.102125	QSYS	QZDASRV	Следующие специальные регистры были заданы: CLIENT_ACCTNG: Windows 10;SSL=false;admin_user=true, CLIENT_APPLNAME: IBM i Access Client Solutions - Run SQL Scripts, CLIENT_PROGRAMID: file:/C:/Users/*****/IBM/ClientSolutions/acsbundle.jar | Версия: 1.1.8.8 | ИД компоновки: 1380 | 2021-09-13 13:47:59, CLIENT_USERID: *****, CLIENT_WRKSTNNAME: ******	&N Причина . . . . :   Это сообщение отправлено когда любой из клиентских специальных регистров был обновлен.

        В общем и целом это все достаточоно удобно.


  1. Pijng
    07.04.2024 21:01

    Спасибо за статью! Интересно смотреть как коллеги по цеху задачу с продуктовыми логами решают.

    А в рамках одного лог файла свойства у каждой записи по шейпу совпадают или могут быть разными? Если совпадают – не думали прям на клиенте генерить «форму» для фильтра свойств? Или нужна более сложная фильтрация по логам?

    Недавно в целом чем-то схожее решение разработали у себя. Буду наблюдать за гитхабом у вас для референсов :)


    1. SStefS Автор
      07.04.2024 21:01

      Спасибо за отзыв!

      Форма объявлена в интерфейсе:

      export interface CsLogData {
        //в логе могут быть любые свойства
        [key: string]: any;
        //Минимально нужные данные для одного лога
        '@t': Date;
        '@mt': string;
        '@l'?: 'Verbose' | 'Debug' | 'Information' | 'Warning' | 'Error' | 'Fatal';
      }

      Форма может быть любой, минимально требуемые параметры: Время, Сообщение, Уровень. Более гибкий вариант.

      Генерировать форму отличная идея! В OpenSearch есть подобная система, автоматически сканируются параметры логов, можно по готовому списку фильтровать.


  1. denaspireone
    07.04.2024 21:01
    +1

    Я просто оставлю это здесь https://docs.victoriametrics.com/victorialogs/querying/#web-ui
    14,9 Мб, а еще это база данных для логов

    PS: vim = 4.6Mb


    1. SStefS Автор
      07.04.2024 21:01

      Крутой проект! Как-то не попадался в поисковике. Может сможете подсказать, логи нужно грузить через агрегатор на подобии logstash?


      1. denaspireone
        07.04.2024 21:01

        1. SStefS Автор
          07.04.2024 21:01

          Из документации выглядит скорее как децентрализованное логирование, а не просмотр json файлов напрямую, возможно пропустил что-то в документации.


        1. SStefS Автор
          07.04.2024 21:01

          Как замена Opensearch Dashboard отличная тула!