Не каждому проекту нужно децентрализованное логирование. В моём случае, оказалось проще хранить логи в .json файлах формата Compact Log Event Format (CLEF). Мне нужно было простое и бесплатное решение для просмотра логов.
![Логотип разработанного приложения Логотип разработанного приложения](https://habrastorage.org/getpro/habr/upload_files/2d0/0a6/9d5/2d00a69d538f90014e093b21611f2550.png)
Готовое решение
На момент написания статьи в свободном доступе было только 1 подходящее приложение для чтения логов в формате CLEF - Compact Log Viewer, доступно в Microsoft Store.
![Compact Log Viewer Compact Log Viewer](https://habrastorage.org/getpro/habr/upload_files/03b/04e/c31/03b04ec314be87bee54bfff53eda9d1d.png)
Уже неплохо, приложение позволяло смотреть и фильтровать логи. Однако в этом решении было несколько минусов:
Каждому разработчике нужно устанавливать локально через Microsoft Store
За раз можно открыть только 1 файл
Нельзя поставить на тестовый сервер если это линукс
Часть UI занимает бесполезный график
После повторного открытия приложения загруженный ранее файл терялся, его нужно было грузить заново
Через некоторое время я устал от постоянного скачивания логов с сервера на локальную машину для их просмотра. Поэтому решил написать своё собственное приложение.
Требования к разрабатываемому приложению
К разрабатываемое приложению я установил следующие требования:
Отображать список логов
Поддерживать просмотр свойств отдельных логово
Поддерживать фильтрацию по свойствам
Поддерживать загрузку нескольких файлов
Сохранять логи между сессиями
Поддерживать запуск в среде Docker
Поддержка среды Docker позволила сделать приложение "доступным из коробки", включив его в дефолтный docker-compose файл на проектах. Также стала возможна загрузка на сервер.
Процесс разработки
Для разработки проекта был выбор между React и Angular. По личным предпочтениям был выбран React + StyledComponents.
Для UI компонентов выбрал бесплатное решение RsuiteJs.
Flow-приложения:
Перейти на страницу загрузки логов
Загрузить логи
Перенаправить на страницу просмотра логов
Реализация загрузки логов
Для форм остановился на связке Yup с Formik. Создал обёртки над компонентами форм, FileUploader
от RSuite не подходил, используя библиотеку ReactDropzone написал кастомную реализацию.
![Страница загрузки файлов Страница загрузки файлов](https://habrastorage.org/getpro/habr/upload_files/963/dfc/51f/963dfc51fa5b55f17680358f146e6535.png)
Хранение загруженных логов
Для упрощения проекта отказался от 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;
}
})
});
В итоге вышла следующая страница:
![Страница просмотра логов Страница просмотра логов](https://habrastorage.org/getpro/habr/upload_files/c57/73e/ead/c5773eead6f2a34f380750b7be098cbc.png)
Публикация приложения
Чтобы сделать проект общедоступным выложил его код в общедоступный репозиторий на 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](https://habrastorage.org/getpro/habr/upload_files/8be/8d4/e8f/8be8d4e8f35a62df52af4c2da95546dd.png)
Итоги
Разработал на React CS Logs viewer для просмотра структурированных логов, образ которого весит около 55мб, в то время как рассматриваемый выше аналог занимает 300мб. Приложение можно деплоить на тестовый сервер или поставлять в docker-compose файле вместе с кодом своего проекта, чтобы можно было смотреть логи на любой системе с установленной Docker-средой.
Буду рад вашим предложениям и замечаниям!