Линзы http4k. Работа с файлами. Журналирование #

Васильев Андрей Михайлович, 2024

Версии презентации

Обработка запроса от пользователя #

diagram

Обработку запроса от пользователя можно разделить на три логических этапа, следующих один за другим:

  1. Техническая обработка данных, пришедших от пользователя. Данные приходят в формате строк, необходимо их преобразовать к типам слоя предметной области
  2. Выполнение обработки извлечённых данных в слое предметной области: поиск значений, добавление значений, изменение значений и т.д.
  3. Формирование ответа пользователю в формате, который он ожидает

В рамках обработки HTTP-запроса может быть выполнено несколько обращений к каждому из слоёв извлечения данных и предметной области

Рекомендуется каждый этап оформлять внутри отдельного компонента, а задачей HTTP-обработчика становится их логическое объединение для достижения задачи

Работа со сложными структурами данных #

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

Рассмотрим следующие структуры данных

data class Class(val id: Int, val name: String, val teacher: String)
data class Course(val id: Int, val name: String, val clasess: List<Class>)
data class Speciality(val id: Int, val courses: List<Course>)
val speciality: Speciality = ...

diagram

Модификация структур данных #

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

val class = speciality.courses[1].classes[5]

Для редактирования потребуется выполнить несколько копирований:

val newClass = speciality.courses[1].classes[5]
    .copy(name = "Безопасность жизнедеятельности")
val newClasses = speciality.courses[1].classes.toMutableList()
    .apply { set(5, newClass) }
val newCourse = speciality.courses[1].copy(classes = newClasses)
val newCourses = speciality.courses.toMutableList()
    .apply { set(1, newCourse) }
val newSpeciality = speciality.copy(courses = newCourses)

Можно создать новую версию неизменяемых данных с нужным для нас состоянием

Сложные структуры сложны #

Сильная вложенность структур данных несёт следующие проблемы:

  • Надо корректно указать путь к данным несколько раз
    • Необходимо написать много кода
    • Возможны технические ошибки
  • Код, которому нужен доступ только к конечным данным, начинает зависеть от всех промежуточных объектов, что усложняет изменение структур данных в приложении
val class = speciality.courses[1].classes[5]

Решение с помощью функций #

Мы можем ввести функции, позволяющие решить данные задачи:

fun getName(speciality: Speciality, courseId: Int, classId: Int): String
fun setName(name: String, speciality: Speciality,
            courseId: Int, classId: Int): Speciality

Код будет зависеть только от класса Speciality и одной из указанных функций

Механизм линз #

Линза — специальный объект, который позволяет осуществлять операции чтения и записи для конкретного вложенного свойства

class ClassNameLens(
    private val courseId: Int,
    private val classId: Int,
) {
    fun getName(speciality: Speciality): String { ... }
    fun setName(name: String, speciality: Speciality): Speciality { ... }
}

val speciality: Speciality = ...
val classNameLens = ClassNameLens(1, 5)
classNameLens.getName(speciality)
val newSpeciality = classNameLens.setName("Веб-разработка", speciality)
  • Код зависит только от общего хранилища данных и линзы
  • Линзы предоставляют простой и понятный интерфейс
  • Линзы могут объединять функции по чтению и записи
  • Линзы могут выполнять преобразование данных при чтении или записи: типы данных внутри объекта могут быть, к примеру, строками, а интерфейс линзы будет предоставлять их в виде целых чисел

Линзы http4k #

В рамках библиотеки http4k линзы применяются для решения различных задач по взаимодействию со структурами данных:

  • Для чтения данных из объектов запроса
  • Для записи данный в объекты ответа
  • Для взаимодействия (чтения и записи) данных во внутренних хранилищах

Линзы библиотеки http4k определены в пакете org.http4k.lens

В рамках пакета определены два основных интерфейса:

  • Lens — односторонняя линза, позволяющая извлекать данные из сущности
    • Для работы с переменной пути доступна PathLens
  • BiDiLens — двусторонняя линза, позволяющая читывать и модифицировать данные
    • Для работы с переменной пути доступна BiDiPathLens

Однако ввиду сложности линзы для их создания используются отдельные классы-строители

Шаблон проектирования строитель #

Для создания некоторых объектов пользователю необходимо указать множество аргументов конструктора, причём каждый из них тоже может быть сложным

Чтобы упросить создание конкретного компонента в строитель выделяют:

  • логику по сбору всех аргументов для создания объекта
  • метод для создания объекта целевого класса

При использовании строителя создание объекта выглядит следующим образом:

  1. Создать объект-строитель
  2. Вызвать несколько методов, формирующих набор данных для конструктора класса
  3. Вызвать метод по созданию целевого объекта

Строители в библиотеке http4k тоже являются неизменямыми, поэтому при вызове методов создаются новые строители с изменённым состоянием

Создание линзы #

Для создания линзы необходимо указать следующие параметры:

  • Объект, для которого формируется линза (базовый строитель)
  • Настройка линзы, обычно состоит из определения типа данных для преобразования (ввести параметры строителя)
  • Терминатор (если есть обязательные или необязательные параметры) (создать линзу на основе параметров строителя)

Терминатор также создаёт объект линзы из соответствующего объекта-строителя

Классы-линзы описаны в пакете org.http4k.lens, строители описываются с помощью классов-спецификаций (наследников интерфейса LensSpec), сами линзы с помощью классов (наследников интерфейса Lens)

Поддерживается два типа линз:

  • Однонаправленные линзы, считывающие и преобразующие данные из источника
  • Двунаправленные линзы, считывающие и записывающие данные в целевой объект

Для стандартных классов http4k предлагаются двунаправленные линзы

Пример линзы http4k #

Предположим, что мы создали линзу для получения непустой строковой переменной из пути:

val parameterLens = Path.nonBlankString().of("parameter")

Для получения параметра необходимо применить лизу к хранилищу данных, к запросу:

val parameter: String = parameterLens(request)

Потенциально для установки значения можно было бы написать:

val newRequest = parameterLens("newData", request)

Запись данных может быть полезна в рамках модульных тестов

Линзы http4k #

Объект Внутренний тип данных Применимо для объектов Количество Необходимый
Параметры запроса String Request Один или много Обязательный или необязательный
Заголовок String Request / Response Один или много Обязательный или необязательный
Переменная пути String Request Один Обязательный
Поле формы String WebForm Один или много Обязательный или необязательный
Тело String Request / Response Один Обязательный

Указание объекта #

Для каждого целевого элемента предоставляется объект-строитель, настроенный на взаимодействие с данным целевым элементом

  • Параметры запроса: Query
  • Заголовок: Header
  • Переменные пути: Path
  • Поле формы: FormField
  • Тело: Body

Настройка преобразования типа данных #

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

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

Рассмотрим список преобразований типов для объекта-запроса Query

  • enum() — преобразование в перечисление
  • dateTime() — преобразование переданных данных в тип LocalDate
  • int() — преобразование в целочисленный вариант
  • nonEmptyString() — проверка строки на существование
  • nonBlankString() — проверка строки на наличие печатных символов
  • uuid() — преобразование в тип данных UUID

Данные могут быть преобразованы в любой формат с помощью метода map(), которому надо будет предоставить два метода: для преобразования из строки и преобразования в строку

Указание необходимости параметра #

Уровни необходимости параметра описываются в функциях-терминаторах в интерфейсах LensSpec и BiDiLensSpec

  • defaulted() — нужно указать название элемента, значение по умолчанию и описание
  • optional() — параметр является необязательным, необходимо указать название
  • required() — параметр является обязательным, необходимо указать название

Ключевое отличие — поведение при отсутствии целевого значения

  • defaulted() возвращает значение по умолчанию
  • optional() возвращает null-тип, но выбрасывает исключение, если параметра нет
  • required() выбрасывает исключение LensFailure

Терминатор создаёт на основе спецификации (строителя, LensSpec) нужную линзу

Линзы для параметров запроса #

Рассмотрим пример линзы, которая извлекает целое число из параметров запроса

val queryDataLens = Query.int().defaulted("data", 15)
  • Целью выступает класс Query
  • Далее указываем преобразование параметров из строки в целое число, метод int()
  • Затем указываем, что значением по умолчанию является число 15

При наличии данных будет выполнено их преобразование и получение

val request = Request(GET, Uri.of("http://example.ru"))
val requestWithQuery = request.query("data", "42")
val data = queryDataLens(requestWithQuery) // 42

При отсутствии данных линза вернёт значение по умолчанию

val request = Request(GET, Uri.of("http://example.ru"))
val data = queryDataLens(request) // 15

Данные в некорректном формате #

При отсутствии данных будет выброшено исключение

val request = Request(GET, Uri.of("http://example.ru"))
val requestWithQuery = request.query("data", "abc")
queryDataLens(requestWithQuery)
org.http4k.lens.LensFailure: query 'data' must be integer

Перехват исключения #

Выброс исключения при использовании линз должен быть обработан:

Если пользователь самостоятельно может ввести некорректные данные, то необходимо обеспечить локальную обработку данных, чтобы предоставить ему информацию по ошибке и способах её решения

Поведение разных терминаторов #

Терминатор Данные есть Данных нет Некорректные данные
defaulted Данные Значение по умолчанию Исключение
optional Данные null Исключение
required Данные Исключение Исключение

Каждый тип терминатора продуцирует линзу, способную выбросить исключение в одной или нескольких ситуациях

Проверка данных на стороне клиента (в браузере) #

  • Можно предположить, что приложение формирует корректные данные
  • В полях ввода на стороне браузера поддерживается проверка данных по типу
  • Поддерживается проверка необходимости указания данных

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

Обёртки для линз #

Мы можем преобразовать исключение к null-значению в случае появления исключения LensFailure с помощью следующих самописных функций:

import org.http4k.lens.Lens
import org.http4k.lens.LensFailure

fun <IN : Any, OUT>lensOrNull(lens: Lens<IN, OUT?>, value: IN): OUT? =
    try {
        lens.invoke(value)
    } catch (_: LensFailure) {
        null
    }

fun <IN : Any, OUT>lensOrDefault(lens: Lens<IN, OUT?>, value: IN, default: OUT): OUT =
    try {
        lens.invoke(value) ?: default
    } catch (_: LensFailure) {
        default
    }

Вариант вызова данных функций-обёрток:

lensOrNull(fromLens, request) // Значение или null

Описание линз для формы #

HTML-форма представляет собой набор полей, которые пользователь заполняет

В рамках http4k используются следующие типы данных:

  • FormField — описание требований к конкретному полю формы в форме линзы
  • Body.webForm — описание спецификации линзы для всей формы
  • WebForm — структура для хранения результатов проверки формы

Порядок использования элементов следующий:

  • Для каждого поля ввода необходимо оформить требования с использованием FormField
  • Необходимо объединить поля ввода в общую линзу для работы с формой
  • Выполнить обработку запроса и проверить результаты обработки

Пример формы #

Форма для отправки обратной связи

<form method="POST">
  <div class="mb-3">
    <label for="age" class="form-label">Возраст</label>
    <input type="number" class="form-control" id="age" name="age" required>
  </div>
  <div class="mb-3">
    <label for="name" class="form-label">Имя</label>
    <input type="password" class="form-control" id="password"
        name="password" required>
  </div>
  <div class="mb-3 form-check">
    <label for="feedback" class="form-label">Комментарии</label>
    <textarea id="feedback" name="feedback" rows="10"></textarea>
  </div>
  <button type="submit" class="btn btn-primary">Отправить</button>
</form>

Обработка формы #

val ageField = FormField.int().required("age")
val nameField = FormField.nonEmptyString().required("name")
val feedbackField = FormField.nonEmptyString().optional("feedback")
val formLens = Body.webForm(Validator.Feedback,
    ageField, nameField, feedbackField).toLens()

Обработка данных с формы

val form = formLens(request)
if (form.errors.isEmpty()) {
    val age = ageField(form)
    val feedback = feedbackField(form)
}

Можно использовать в тестах:

// Отправка пустой формы
val invalidRequest = Request(GET, "/")
    .with(Header.CONTENT_TYPE of ContentType.APPLICATION_FORM_URLENCODED)
val invalidForm = formLens(invalidRequest)
println(invalidForm.errors)
// Отправка заполненной формы
val webForm = WebForm().with(ageField of 55, nameField of "Rita"))
val validRequest = Request(GET, "/").with(formLens of webForm)
val validForm = formLens(validRequest)
val age = ageField(validForm)

Отображение результатов проверки в форме #

  • При первом показе форма должна отображать пустые поля ввода
  • В случае ошибки форма должна отображать данные, введённые пользователем и сообщения об ошибках

Удобно использовать объекты класса WebForm в обоих случаях

data class WebForm constructor(
    val fields: Map<String, List<String>> = emptyMap(),
    val errors: List<Failure> = emptyList()
)

Ограничение данного формата:

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

Для проверки наличия данных в форме можно использовать следующие конструкции:

{% if model.form.errors is not empty %}
    Присутствуют ошибки
{% endif %}
{% if model.form.fields contains "field" %}
    Данные поля field: {{ model.form.fields["field"] | first }}
{% endif %}

Использование макросов Pebble #

Для уменьшения количества повторений внутри шаблонов Pebble можно воспользоваться макросами, которые можно несколько раз вызывать внутри шаблона

{% macro input(type="text", name, placeholder="", form) %}
    <input
        type="{{ type }}"
        name="{{ name }}"
        placeholder="{{ placeholder }}"
        value="{{ form.fields[name] | first }}"
    >
{% endmacro %}

{{
    input(
        name="count",
        type="number",
        placeholder="Число работников",
        form=model.form
    )
}}

Указание типа документа #

Согласно протоколу HTTP-серверу желательно сообщать клиенту тип передаваемого документа с помощью MIME типов. Также должен поступать и клиент

Для указания типа документа используется заголовок Content-Type

В http4k для решения этой задачи можно либо воспользоваться фильтрами:

val SetHtmlContentType = Filter { next ->
    { next(it).with(CONTENT_TYPE of TEXT_HTML) }
}
val app = routes(
    return "/create" bind POST to SetHtmlContentType.then someHandler
)

Либо использовать линзы:

val renderer = PebbleTemplates().HotReload("src/main/resources")
val htmlView = Body.viewModel(renderer, TEXT_HTML).toLens()
val handler: HttpHandler {
    Response(OK).with(htmlView of SomeViewModel(42))
}

Файлы на сервере с запущенным веб-приложением #

Можно выделить следующие категории файлов:

  • Неизменяемые файлы на файловой системе:
    • Файлы приложения и библиотек, включающие ресурсы приложения, расположенные внутри исполняемых файлов
    • Файлы настроек
  • Изменяемые файлы на файловой системе:
    • Журналы работы приложения
    • Файлы, создаваемые приложением
    • Файлы, загружаемые пользователем

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

Предоставление созданных файлов клиенту #

Веб-сервер постоянно формирует новые документы и возвращает их клиенту по запросу

Иногда приложению требуется избыточное время для формирования документа:

  • При формировании документа на большом объёме информации
  • Если для формирования документа нужно много вычислительных мощностей
  • Когда формируемые документы не изменяются часто

Сигналом для формирования таких документов может быть:

  • Явный запрос по формированию данных от пользователя системы
  • Срабатывание события внутри системы, например срабатывания таймера

Такие документы разумно сначала выгрузить на файловую систему, а затем предложить клиенту ссылку, по которой можно скачать документ

Для предоставления файлов с файловой системы можно воспользоваться стандартным механизмом раздачи документов с файловой системы

Отправка файлов от клиента на сервер #

В некоторых сценариях взаимодействия между клиентом и веб-сервером необходимо загружать данные на веб-сервер:

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

В рамках протокола HTTP для отправки данных на сервер необходимо выполнить POST-запрос, в теле которого передать большой объём данных

Помимо самого тела запроса в заголовке желательно указать тип передаваемых данных с помощью заголовка Content-Type

Стандартным способом передачи файлов на сервер является их отправка с использованием HTML-форм:

  • Одним из полей ввода необходимо указать тип file
  • В качестве кодировки данных формы необходимо указать multipart/form-data

Форма для передачи файлов #

Сформируем простейшую форму, содержащую:

  • Поле для указания названия файла для загрузки
  • Элемент для указания пути к загружаемому файлу
<form action="post" enctype="multipart/form-data">
  <div class="mb-3">
    <label for="fileName" class="form-label">Название файла</label>
    <input type="text" class="form-control" id="fileName" name="fileName">
  </div>
  <div class="mb-3">
    <label for="file" class="form-label">Файл</label>
    <input type="file" class="form-control" id="file" name="file">
  </div>
 <button type="submit" class="btn btn-primary">Отправить</button>
</form>

Обработка форм с помощью http4k #

Поддержка кодировки форм поставляется в отдельной библиотеки, которую необходимо подключить к проекту в файле build.gradle:

dependencies {
    implementation("org.http4k:http4k-multipart")
}

Затем на уровне HTTP-обработчика можно извлечь данные на низком уровне с помощью класса org.http4k.core.MultipartFormBody

  • Для обработки запроса предоставляется функция from(httpMessage: HttpMessage)`
  • Затем для получения значений из созданного объекта можно получить данные
    • fun field(name: String) — получение значения поля по его названию
    • fun fields(name: String) — получение всех значений поля по названию
    • fun fieldValue(name: String) — получение значения поля в строковом виде
    • fun fieldValues(name: String) — получение всех значений в списке строк
    • fun file(name: String) — получение данных из файла по названию поля
    • fun files(name: String) — получение списка файлов, связанных с полем

Пример обработчика формы #

Сформируем обработчик для простой формы, который будет извлекать данные из формы-примера

class MultiPartFormHandler() : HttpHandler {
    override fun invoke(request: Request): Response {
        val formData = MultipartFormBody.from(request)
        val name: String? = formData.field("fileName")
        val contents: MultipartFormFile? = formData.file("file")
        return Response(FOUND).header("Location", "/done")
    }
}

Очевидно, что данный обработчик не отвечает базовым требованиям:

  • Не проверяет переданные данные
  • Не возвращает заполненную форму с данными пользователю в случае проблем с заполнением данных

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

Обработка формы с помощью линз #

Для работы с данными формами применяется другой набор классов:

  • Для описания линзы обычного поля — MultipartFormField
  • Для описания линзы поля файла — MultipartFormFile
  • Для объединения линз полей используется функция Body.multipartForm()

Сформируем линзы для обработки формы:

val fileNameField = MultipartFormField.nonBlankString().required("fileName")
val fileField = MultipartFormFile.required("file")
val formLens = Body
        .multipartForm(Validator.Feedback, fileNameField, fileField)
        .toLens()

Далее внутри HTTP-обработчика данные линцы можно будет использовать аналогично линзам для обыкновенной формы

Отслеживание работы приложения #

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

Во время работы приложения могут возникнуть разные ситуации:

  • Падение приложения
  • Увеличение времени обработки запросов от пользователя
  • Окончание свободной оперативной памяти
  • Заполнение файловой системы

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

Внешнее и внутренее состояние #

Выполним классификацию возможных причин критических ситуаций:

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

Системы мониторинга #

Задача отслеживания состояния серверов в целом решена: создан ряд продуктов, которые позволяют собрать информацию о состоянии вычислительной системы:

  • Объём используемой оперативной памяти
  • Объём используемого пространства на файловой системе
  • Объём утилизации процессорного времени
  • Количество запущенных активных процессов

Данные собираются в единую систему, которая:

  • Позволяет визуализировать текущую нагрузку на систему
  • Позволяет оповещать эксплуатанта в случае наступления события

Архитектура систем мониторинга #

diagram

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

Отслеживание состояния приложения #

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

Каждое приложение будет обладать важными характеристиками относительно задачи, которую оно решает, разработчикам стоит осмысленно подходить к метрикам

Общие метрики веб-сервера, запущенного на JVM:

  • Состояние виртуальной машины JVM (объём выделенной оперативной памяти, объём кеша, количество потоков, состояние сборщика мусора, открытые файлы)
  • Информация об обработке запроса пользователя:
    • Данные о клиенте, выполнившем запрос
    • Адрес и параметры запроса от клиента
    • Статус ответа
    • Время обработки запроса
  • Информация о времени выполнении операции над базой данных: время выполнения операции, объём переданных данных и т.д.

Журналирование работы приложения #

При отсутствии системы монитроринга можно воспользоваться локальной альтернативой — журналированием работы приложения в файлы-журналы

Журналы в данном случае являются файлами, расположенными на файловой системе

diagram

  • Файлы с журналами открываются в момент запуска приложения
  • Журналы пишутся всё время работы приложения

Необходимо выполнять ротацию журналов — удаление излишних записей из журнала, когда всё хранилище заполнено или старые данные не нужны

Журналирование в JVM #

Ввиду длинной жизни платформы JVM было предложено много решений для выполнения журналирования внутри приложения:

  • Simple Logging Facade for Java 1.x
  • Simple Logging Facade for Java 2.x
  • Apache Commons Logging
  • Logback
  • Java Util Logging

И каждый разработчик библиотеки может выбирать любое решение для записи своих журналов

Разработчики http4k решили, что они не будут использовать никакой фреймворк, но вот разработчики шаблонизатора Pebble решили использовать slf4j

Simple Logging Facade for Java 2 #

diagram

  • Конкретные компоненты приложения зависят от общего фасада slf4j
  • Внутри конкретного приложения регистрируются и настраиваются плагины для вывода журналов на нужные данному приложению пути сохранения данных

Данная библиотека является достаточно гибкой и она используется при разработке Android-приложений, для неё существуют обёртки на языке Kotlin

Формирование сообщений в SLF4J 2 #

Подключение SLF4J к приложению #

В файле build.gradle необходимо добавить зависимость от API

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.12")
}

Использование библиотеки #

  1. В конкретном классе необходимо получить доступ к объекту-журналирования
  2. В нужных местах класса добавить вызов журнала
class SomeHandler() : HttpHandler {
    private val logger = LoggerFactory.getLogger(SomeHandler::class.java)

    override fun invoke(request: Request): Response {
        logger.atInfo().log("Обрабатываем очень важный запрос")
    }
}

Журналирование обработки запросов #

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

val logger = LoggerFactory.getLogger("ru.yarsu.WebApplication")
val loggingFilter = Filter { next: HttpHandler ->
    { request: Request ->
        // Собрать данные о запросе
        // Вычислить время обработки запроса
        val result = next(request)
        // Собрать данные об ответе
        // Выполнить журналирование информации
        logger.atInfo().setMessage("Request").addKeyValue("URI", request.uri).log()
        result
    }
}
  • Для получения объекта для записи в журнал используем строковый идентификатор
  • Для добавления информации в структурированном виде используем вызов addKeyValue с передачей пары ключ-значение

Запись журналов в файл с помощью Logback #

Необходимо добавить зависимость от logback-classic в build.gradle:

dependencies {
    implementation("ch.qos.logback:logback-classic:1.5.4")
}

В момент запуска приложения Logback пытается выполнить конфигурацию автоматически:

  1. Путём загрузки класса реализующего интерфейс Configurator
  2. Путём загрузки файла logback-test.scmo из ресурсов приложения
  3. Путём загрузки файла logback.xml из ресурсов приложения

SCMO-файлы являются бинарным представлением конфигурации, оптимизированы для ускорения запуска

Пример конфигурационного файла logback.xml #

  • Сохраняем журнал в каталог logs, в файл app.log
  • Каждый отдельный файл не более 10 мегабайт
  • Весь архив не более 100 мегабайт
  • Данные кодируются в JSON для обеспечения машинной обработки
<configuration>
    <property name="HOME_LOG" value="logs/app.log"/>
    <appender name="FILE-ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${HOME_LOG}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!-- Максимальный размер архива журнала 10 МБ -->
            <maxFileSize>10MB</maxFileSize>
            <!-- Максимальный общий объём архива 100 МБ -->
            <totalSizeCap>100MB</totalSizeCap>
            <!-- Хранить не более 60 дней -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.JsonEncoder"/>
    </appender>
    <root level="debug">
        <appender-ref ref="FILE-ROLLING"/>
    </root>
</configuration>