diff --git a/.java-version b/.java-version new file mode 100644 index 000000000..e000eb894 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17.0.9 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..7996c5394 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,57 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# syntax=docker/dockerfile:1 + +FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS jdk-download +ARG JDK_DOWNLOAD_LINK +ARG JDK_VERSION +ENV JAVA_HOME="/usr/lib/jvm/jdk-${JDK_VERSION}" + +RUN \ + set -xeuo pipefail && \ + microdnf install -y tar gzip && \ + # Install JDK from the provided archive link \ + echo "Downloading JDK from ${JDK_DOWNLOAD_LINK}" && \ + mkdir -p "${JAVA_HOME}" && \ + curl -#LfS "${JDK_DOWNLOAD_LINK}" | tar -zx --strip 1 -C "${JAVA_HOME}" + +# Use ubi9 minimal as it's more secure +FROM registry.access.redhat.com/ubi9/ubi-minimal:latest +WORKDIR /opt/trino + +ARG JDK_VERSION +ENV JAVA_HOME="/usr/lib/jvm/jdk-${JDK_VERSION}" +ENV PATH=$PATH:$JAVA_HOME/bin +COPY --from=jdk-download $JAVA_HOME $JAVA_HOME + +RUN \ + set -xeu && \ + microdnf update -y && \ + microdnf install -y tar less shadow-utils && \ + groupadd trino --gid 1000 && \ + useradd trino --uid 1000 --gid 1000 --create-home && \ + mkdir -p /usr/lib/trino && \ + chown -R "trino:trino" /usr/lib/trino /opt/trino + +COPY --chown=trino:trino gateway-ha /usr/lib/trino + +ARG TRINO_GATEWAY_VERSION +ENV GATEWAY_JAR_PATH "/usr/lib/trino/gateway-ha-${TRINO_GATEWAY_VERSION}-jar-with-dependencies.jar" + +EXPOSE 8080 +USER trino:trino +CMD java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED -jar "${GATEWAY_JAR_PATH}" "server" "/opt/trino/gateway-ha-config.yml" + +HEALTHCHECK --interval=10s --timeout=5s --start-period=10s \ + CMD /usr/lib/trino/bin/health-check diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..3d57c6da6 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,85 @@ +# Trino Gateway Docker Image + +## About the Container + +This Docker image is designed to: + +* be spun up in front of Trino clusters by mounting in a configuration file +* simplify deployment into an orchestration system + +## Quickstart + +### Dependencies + +This docker build process requires: + +* [Docker Compose V2](https://docs.docker.com/compose/) +* jq + +### Run the Trino Gateway server + +You can launch the Trino Gateway and relevant dependencies through docker for testing purposes. + +```bash +# Replace these variables to match your test requirements +# Ex: You may be locally building the docker image so something like +# `trino-gateway:5-SNAPSHOT-amd64` might be what you're expecting +TEST_GATEWAY_IMAGE="trinodb/trino-gateway:latest" +TEST_PLATFORM="amd64" + +TRINO_GATEWAY_IMAGE=${TEST_GATEWAY_IMAGE} DOCKER_DEFAULT_PLATFORM=${TEST_PLATFORM} \ + docker compose -f minimal-compose.yml \ + up --wait +``` + +This will wait until docker has spun up the services and the gateway is healthy. +If the service doesn't come up successfully you can attempt to debug it by pulling the logs: +``` +docker compose -f minimal-compose.yml logs gateway +``` + +The Trino Gateway server is now running on `localhost:8080` (the default port). + +### Verify it Runs + +Now that the gateway is up and running, here's a sample query that shows the backends configured: +```bash +curl localhost:8080/api/public/backends +``` + +Or, visit it in your browser by opening http://localhost:8080 + +## Configuration + +Configuration is expected to be mounted to the exact path of `/opt/trino/gateway-ha-config.yml`. +If it is not mounted then the gateway will fail to initialize. + +## Health Checking + +By default the container health checking is done by the [/usr/lib/trino/bin/health-check](./bin/health-check) +script which simply expects a 2XX response from the server at `/api/public/backends`. + +## Building a custom Docker image + +To build an image for a locally modified version of Trino Gateway, run the Maven +build as normal for the `gateway-ha` modules, then build the image: + +```bash +./build.sh +``` + +The Docker build process will print the ID of the image, which will also +be tagged with `trino-gateway:xxx-SNAPSHOT-yyy`, where `xxx-SNAPSHOT` is the version +number of the Trino Maven build and `-yyy` is the platform the image was built for. + +To build an image for a specific released version of Trino Gateway, +specify the `-r` option, and the build script will download +all the required artifacts: + +```bash +./build.sh -r 4 +``` + +## Getting Help + +Join the Trino community [Slack](https://trino.io/slack.html). diff --git a/docker/bin/health-check b/docker/bin/health-check new file mode 100755 index 000000000..5e2cc3eb0 --- /dev/null +++ b/docker/bin/health-check @@ -0,0 +1,75 @@ +#!/bin/bash + +set -euo pipefail + +function get_2nd_level_yaml_key() { + local parent_key=$1 + local target_key=$2 + local yaml_file_path=$3 + + # In order to get the nested key we will leverage awk to do some parsing + awk -v parent_key=${parent_key} \ + -v target_key=${target_key} \ + ' + function count_indentation(line) { + match(line, /^[[:space:]]*/); + return RLENGTH; + } + # First we search for the parent_key, once we find it we set in_block to true + match($0, "^" parent_key ":") { + in_block=1; + parent_indent_level = count_indentation($0); + next; + } \ + in_block { + # All lines we deem to be a comment will be skipped. + if (match($0, "^[[:space:]]*#")) { next; } + + # If we determine that we have left the parent_key block we will exit. + if ($0 ~ /^[^\t #]/) { exit; } + + current_indent_level = count_indentation($0); + + # Next, because we dont know the indentation levels being provided, we will attempt + # to find the indentation level of the 2nd level keys. + if ( !first_level_indentation \ + && match($0, "^[[:space:]]+.*:") \ + && current_indent_level > parent_indent_level) { + first_level_indentation = current_indent_level; + } + + # Then we will attempt to find the 2nd level target key based on: + # 1. Theres only spaces before the target_key + # 2. The indentation level is equal to the found 2nd level indentation + if (match($0, "^[[:space:]]+" target_key ":") \ + && current_indent_level == first_level_indentation) { + # If we decide that the key matches our expectations, we will print the matched value + sub(/:/, "", $2); + print $2; + } + } + ' \ + ${yaml_file_path} +} + +config=/opt/trino/gateway-ha-config.yml +scheme=http +port=8080 + +# prefer to use http even if https is enabled +if [ "$(get_2nd_level_yaml_key 'requestRouter' 'ssl' "$config")" == "true" ]; then + scheme=https +fi + +potential_port=$(get_2nd_level_yaml_key 'requestRouter' 'port' "$config") +if [ "${potential_port}" != "" ]; then + port=${potential_port} +fi + +endpoint="${scheme}://localhost:${port}/api/public/backends" + +# add --insecure to disable certificate verification in curl, in case a self-signed certificate is being used +if ! info=$(curl --fail --silent --show-error --insecure "$endpoint"); then + echo >&2 "Server is not responding to requests" + exit 1 +fi diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 000000000..b129d63f8 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +set -xeuo pipefail + +usage() { + cat <&2 +Usage: $0 [-h] [-a ] [-r ] +Builds the Trino Gateway Docker image + +-h Display help +-a Build the specified comma-separated architectures, defaults to amd64,arm64,ppc64le +-r Build the specified Trino Gateway release version, downloads all required artifacts +-j Build the Trino Gateway release with specified Temurin JDK release +EOF +} + +# Retrieve the script directory. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +cd "${SCRIPT_DIR}" || exit 2 + +SOURCE_DIR="${SCRIPT_DIR}/.." + +ARCHITECTURES=(amd64 arm64 ppc64le) +TRINO_GATEWAY_VERSION= +JDK_VERSION=$(cat "${SOURCE_DIR}/.java-version") + +while getopts ":a:h:r:j:" o; do + case "${o}" in + a) + IFS=, read -ra ARCHITECTURES <<< "$OPTARG" + ;; + r) + TRINO_GATEWAY_VERSION=${OPTARG} + ;; + h) + usage + exit 0 + ;; + j) + JDK_VERSION="${OPTARG}" + ;; + *) + usage + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + +function check_environment() { + if ! command -v jq &> /dev/null; then + echo >&2 "Please install jq" + exit 1 + fi + if ! $(docker compose version &> /dev/null); then + echo >&2 "Please install Docker Compose V2" + exit 1 + fi +} + +function temurin_jdk_link() { + JDK_VERSION="${1}" + ARCH="${2}" + + versionsUrl="https://api.adoptium.net/v3/info/release_names?heap_size=normal&image_type=jdk&os=linux&page=0&page_size=20&project=jdk&release_type=ga&semver=false&sort_method=DEFAULT&sort_order=ASC&vendor=eclipse&version=%28${JDK_VERSION}%2C%5D" + if ! result=$(curl -fLs "$versionsUrl" -H 'accept: application/json'); then + echo >&2 "Failed to fetch release names for JDK version [${JDK_VERSION}, ) from Temurin API : $result" + exit 1 + fi + + if ! RELEASE_NAME=$(echo "$result" | jq -er '.releases[]' | grep "${JDK_VERSION}" | head -n 1); then + echo >&2 "Failed to determine release name: ${RELEASE_NAME}" + exit 1 + fi + + case "${ARCH}" in + arm64) + echo "https://api.adoptium.net/v3/binary/version/${RELEASE_NAME}/linux/aarch64/jdk/hotspot/normal/eclipse?project=jdk" + ;; + amd64) + echo "https://api.adoptium.net/v3/binary/version/${RELEASE_NAME}/linux/x64/jdk/hotspot/normal/eclipse?project=jdk" + ;; + ppc64le) + echo "https://api.adoptium.net/v3/binary/version/${RELEASE_NAME}/linux/ppc64le/jdk/hotspot/normal/eclipse?project=jdk" + ;; + *) + echo "${ARCH} is not supported for Docker image" + exit 1 + ;; + esac +} + +check_environment + +if [ -n "$TRINO_GATEWAY_VERSION" ]; then + echo "๐ŸŽฃ Downloading gateway server artifact for release version ${TRINO_GATEWAY_VERSION}" + "${SOURCE_DIR}/mvnw" -C dependency:get -Dtransitive=false -Dartifact="io.trino.gateway:gateway-ha:${TRINO_GATEWAY_VERSION}:jar:jar-with-dependencies" + local_repo=$("${SOURCE_DIR}/mvnw" -B help:evaluate -Dexpression=settings.localRepository -q -DforceStdout) + trino_gateway_ha="$local_repo/io/trino/gateway/gateway-ha/${TRINO_GATEWAY_VERSION}/gateway-ha-${TRINO_GATEWAY_VERSION}-jar-with-dependencies.jar" + chmod +x "$trino_gateway_ha" +else + TRINO_GATEWAY_VERSION=$("${SOURCE_DIR}/mvnw" -f "${SOURCE_DIR}/pom.xml" --quiet help:evaluate -Dexpression=project.version -DforceStdout) + echo "๐ŸŽฏ Using currently built artifacts from the gateway-ha module and version ${TRINO_GATEWAY_VERSION}" + trino_gateway_ha="${SOURCE_DIR}/gateway-ha/target/gateway-ha-${TRINO_GATEWAY_VERSION}-jar-with-dependencies.jar" +fi + +echo "๐Ÿงฑ Preparing the image build context directory" +WORK_DIR="$(mktemp -d)" +GATEWAY_WORK_DIR="${WORK_DIR}/gateway-ha" +mkdir "${GATEWAY_WORK_DIR}" +cp "$trino_gateway_ha" "${GATEWAY_WORK_DIR}" +cp -R bin "${GATEWAY_WORK_DIR}" +cp "${SCRIPT_DIR}/Dockerfile" "${WORK_DIR}" + +TAG_PREFIX="trino-gateway:${TRINO_GATEWAY_VERSION}" + +for arch in "${ARCHITECTURES[@]}"; do + echo "๐Ÿซ™ Building the image for $arch with JDK ${JDK_VERSION}" + DOCKER_BUILDKIT=1 \ + docker build \ + "${WORK_DIR}" \ + --pull \ + --build-arg JDK_VERSION="${JDK_VERSION}" \ + --build-arg JDK_DOWNLOAD_LINK="$(temurin_jdk_link "${JDK_VERSION}" "${arch}")" \ + --build-arg TRINO_GATEWAY_VERSION="${TRINO_GATEWAY_VERSION}" \ + --platform "linux/$arch" \ + -f Dockerfile \ + -t "${TAG_PREFIX}-$arch" +done + +echo "๐Ÿงน Cleaning up the build context directory" +rm -r "${WORK_DIR}" + +echo "๐Ÿƒ Testing built images" +source container-test.sh + +for arch in "${ARCHITECTURES[@]}"; do + # TODO: remove when https://github.com/multiarch/qemu-user-static/issues/128 is fixed + if [[ "$arch" != "ppc64le" ]]; then + test_container "${TAG_PREFIX}-$arch" "linux/$arch" "${SCRIPT_DIR}/minimal-compose.yml" "gateway" "postgres" + fi + docker image inspect -f '๐Ÿš€ Built {{.RepoTags}} {{.Id}}' "${TAG_PREFIX}-$arch" +done diff --git a/docker/container-test.sh b/docker/container-test.sh new file mode 100644 index 000000000..acd74baab --- /dev/null +++ b/docker/container-test.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +function cleanup { + docker compose -f "${COMPOSE_PATH}" down +} + +function test_gateway_starts { + CONTAINER_ID= + POSTGRES_CONTAINER_ID= + trap cleanup EXIT + + # Ensure compose state is starting fresh before the test + cleanup + + local CONTAINER_NAME=$1 + local PLATFORM=$2 + local COMPOSE_PATH=$3 + local COMPOSE_GATEWAY_NAME=$4 + local COMPOSE_POSTGRES_NAME=$5 + + set +e + + # Ensure the platform specific image has been pulled locally for Postgres + DOCKER_DEFAULT_PLATFORM=${PLATFORM} \ + docker compose -f "${COMPOSE_PATH}" \ + pull ${COMPOSE_POSTGRES_NAME} + + # We need to spin up dependencies for the container + # Timeout is built into the compose file in the form of healthcheck retry limits + TRINO_GATEWAY_IMAGE=${CONTAINER_NAME} \ + DOCKER_DEFAULT_PLATFORM=${PLATFORM} \ + docker compose -f "${COMPOSE_PATH}" \ + up --wait \ + ${COMPOSE_POSTGRES_NAME} ${COMPOSE_GATEWAY_NAME} + local COMPOSE_UP_RES=$? + POSTGRES_CONTAINER_ID=$(docker compose -f "${COMPOSE_PATH}" ps -q ${COMPOSE_POSTGRES_NAME}) + CONTAINER_ID=$(docker compose -f "${COMPOSE_PATH}" ps -q ${COMPOSE_GATEWAY_NAME}) + if [ ${COMPOSE_UP_RES} -ne 0 ]; then + echo "๐Ÿšจ Took too long waiting for Trino Gateway container to become healthy" >&2 + echo "Logs from ${CONTAINER_ID} follow..." + docker logs "${CONTAINER_ID}" + + set -e + cleanup + trap - EXIT + return 1 + fi + + if ! RESULT=$(curl --fail localhost:8080/api/public/backends 2>/dev/null); then + echo "๐Ÿšจ Failed to execute a query after Trino Gateway container started" >&2 + fi + + set -e + + cleanup + trap - EXIT + + if ! [[ ${RESULT} == '[]' ]]; then + echo "๐Ÿšจ Test query didn't return expected result of 0 backends ([]): ${RESULT}" >&2 + return 1 + fi + + return 0 +} + +function test_javahome { + local CONTAINER_NAME=$1 + local PLATFORM=$2 + # Check if JAVA_HOME works + docker run --rm --platform "${PLATFORM}" "${CONTAINER_NAME}" \ + /bin/bash -c '$JAVA_HOME/bin/java -version' &>/dev/null + + [[ $? == "0" ]] +} + +function test_container { + local CONTAINER_NAME=$1 + local PLATFORM=$2 + local COMPOSE_PATH=$3 + local COMPOSE_GATEWAY_NAME=$4 + local COMPOSE_POSTGRES_NAME=$5 + echo "๐Ÿข Validating ${CONTAINER_NAME} on platform ${PLATFORM}..." + test_javahome "${CONTAINER_NAME}" "${PLATFORM}" + test_gateway_starts "${CONTAINER_NAME}" "${PLATFORM}" "${COMPOSE_PATH}" "${COMPOSE_GATEWAY_NAME}" "${COMPOSE_POSTGRES_NAME}" + echo "๐ŸŽ‰ Validated ${CONTAINER_NAME} on platform ${PLATFORM}" +} diff --git a/docker/gateway-compose.yml b/docker/gateway-compose.yml new file mode 100644 index 000000000..2121ae663 --- /dev/null +++ b/docker/gateway-compose.yml @@ -0,0 +1,22 @@ +version: "3.4" +services: + gateway: + image: ${TRINO_GATEWAY_IMAGE:-trinodb/trino-gateway:latest} + restart: always + depends_on: + postgres: + condition: service_healthy + healthcheck: + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + ports: + - "8080:8080" + volumes: + - target: /opt/trino/gateway-ha-config.yml + source: ../gateway-ha/gateway-ha-config-docker.yml + type: bind + # - target: /opt/trino/localhost.jks + # source: ../gateway-ha/target/localhost.jks + # type: bind diff --git a/docker/minimal-compose.yml b/docker/minimal-compose.yml new file mode 100644 index 000000000..4d863f16d --- /dev/null +++ b/docker/minimal-compose.yml @@ -0,0 +1,20 @@ +version: "3.4" +services: + gateway: + extends: + file: ./gateway-compose.yml + service: gateway + healthcheck: + interval: 5s + timeout: 5s + retries: 60 + start_period: 20s + postgres: + extends: + file: ./postgres-backend-compose.yml + service: postgres + healthcheck: + interval: 1s + timeout: 1s + retries: 60 + start_period: 10s diff --git a/docker/postgres-backend-compose.yml b/docker/postgres-backend-compose.yml new file mode 100644 index 000000000..4238f0efe --- /dev/null +++ b/docker/postgres-backend-compose.yml @@ -0,0 +1,22 @@ +version: "3.4" +services: + postgres: + image: ${TRINO_GATEWAY_POSTGRES_IMAGE:-postgres} + restart: always + environment: + - PGPORT=5432 + - POSTGRES_PASSWORD=P0stG&es + - POSTGRES_DB=trino_gateway_db + - POSTGRES_USER=trino_gateway_db_admin + ports: + - "5432:5432" + healthcheck: + # Need user and database name to check PostgreSQL server status + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB || exit 1"] + interval: 10s + timeout: 3s + retries: 3 + volumes: + - target: /docker-entrypoint-initdb.d/1-gateway-ha-persistence-postgres.sql + source: ../gateway-ha/src/main/resources/gateway-ha-persistence-postgres.sql + type: bind diff --git a/docker/release-docker.sh b/docker/release-docker.sh new file mode 100755 index 000000000..0c3dad85f --- /dev/null +++ b/docker/release-docker.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -eux + +VERSION=$1 +REPO=trinodb/trino-gateway +IMAGE=trino-gateway:$VERSION +TARGET=$REPO:$VERSION + +docker/build.sh -r "$VERSION" + +architectures=(amd64 arm64 ppc64le) + +for arch in "${architectures[@]}"; do + docker tag "$IMAGE-$arch" "$TARGET-$arch" + docker push "$TARGET-$arch" +done + +for name in "$TARGET" "$REPO:latest"; do + docker manifest create "$name" "${architectures[@]/#/$TARGET-}" + docker manifest push --purge "$name" +done diff --git a/docs/development.md b/docs/development.md index 8344586ef..bd6a2af7e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -37,6 +37,8 @@ or execute the following command: ### Build and run +#### Locally + This project requires Java 17. Note that higher version of Java have not been verified and may run into unexpected issues. @@ -51,6 +53,27 @@ cd gateway-ha/target/ java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED -jar gateway-ha-{{VERSION}}-jar-with-dependencies.jar server ../gateway-ha-config.yml ``` +#### In Docker + +From the root of the directory run the following commands to build the jar, docker image, and then spin it up: + +``` +./mvnw clean install + +# Note - Feel free to change the architecture and version being targeted +# Without specifying a release version with '-r' it'll default to the current snapshot +PLATFORM="amd64" +bash docker/build.sh -a ${PLATFORM} + +# This grabs the version from the pom but you can manually specify it instead +TRINO_GATEWAY_VERSION=$("./mvnw" -f "pom.xml" --quiet help:evaluate -Dexpression=project.version -DforceStdout) +TRINO_GATEWAY_IMAGE="trino-gateway:${TRINO_GATEWAY_VERSION}-${PLATFORM}" docker compose -f docker/minimal-compose.yml up -d +``` + +The [config file found here](/gateway-ha/gateway-ha-config-docker.yml) is mounted into the container. + +#### Common Run Failures + If you encounter a `Failed to connect to JDBC URL` error with the MySQL backend, this may be due to newer versions of Java disabling certain algorithms when using SSL/TLS, in particular `TLSv1` and `TLSv1.1`. This causes `Bad handshake` diff --git a/gateway-ha/gateway-ha-config-docker.yml b/gateway-ha/gateway-ha-config-docker.yml new file mode 100644 index 000000000..08e582ae4 --- /dev/null +++ b/gateway-ha/gateway-ha-config-docker.yml @@ -0,0 +1,58 @@ +routingRules: + rulesEngineEnabled: False + # rulesConfigPath: "src/main/resources/rules/routing_rules.yml" + +requestRouter: + port: 8080 + name: trinoRouter + historySize: 1000 + requestBufferSize: 8192 + +dataStore: + jdbcUrl: jdbc:postgresql://postgres:5432/trino_gateway_db + user: trino_gateway_db_admin + password: P0stG&es + driver: org.postgresql.Driver + queryHistoryHoursRetention: 24 + +backendState: + username: lb_query + ssl: false + +clusterStatsConfiguration: + useApi: true + +server: + applicationConnectors: + - type: http + port: 8090 + adminConnectors: + - type: http + port: 8091 + +# This can be adjusted based on the coordinator state +monitor: + connectionTimeout: 15 + +modules: + - io.trino.gateway.ha.module.HaGatewayProviderModule + - io.trino.gateway.ha.module.ClusterStateListenerModule + - io.trino.gateway.ha.module.ClusterStatsMonitorModule + +managedApps: + - io.trino.gateway.ha.GatewayManagedApp + - io.trino.gateway.ha.clustermonitor.ActiveClusterMonitor + +# Logging settings. +logging: + # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL. + level: INFO + + # Logger-specific levels. + loggers: + io.trino: DEBUG + + appenders: + - type: console + filterFactories: + - type: Log-filter-factory