diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties index 88fbcd0fa4..d2a7cf1947 100644 --- a/gradle/projects.main.properties +++ b/gradle/projects.main.properties @@ -36,6 +36,7 @@ aggregated-license-report=aggregated-license-report polaris-immutables=tools/immutables polaris-container-spec-helper=tools/container-spec-helper polaris-version=tools/version +polaris-misc-types=tools/misc-types polaris-config-docs-annotations=tools/config-docs/annotations polaris-config-docs-generator=tools/config-docs/generator diff --git a/tools/misc-types/build.gradle.kts b/tools/misc-types/build.gradle.kts new file mode 100644 index 0000000000..17203d74ea --- /dev/null +++ b/tools/misc-types/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +plugins { + alias(libs.plugins.jandex) + id("polaris-client") +} + +description = + "Misc types used in configurations and converters for microprofile-config & Jackson, exposes no runtime dependencies" + +dependencies { + compileOnly(libs.smallrye.config.core) + compileOnly(platform(libs.quarkus.bom)) + compileOnly("io.quarkus:quarkus-core") + + compileOnly(platform(libs.jackson.bom)) + compileOnly("com.fasterxml.jackson.core:jackson-databind") + + testImplementation(libs.smallrye.config.core) + + testImplementation(platform(libs.jackson.bom)) + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") + + testCompileOnly(project(":polaris-immutables")) + testAnnotationProcessor(project(":polaris-immutables", configuration = "processor")) +} diff --git a/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySize.java b/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySize.java new file mode 100644 index 0000000000..7548498884 --- /dev/null +++ b/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySize.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.polaris.misc.types.memorysize; + +import static com.fasterxml.jackson.annotation.JsonFormat.*; +import static java.lang.String.format; +import static java.util.Locale.ROOT; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.Nonnull; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; +import org.eclipse.microprofile.config.spi.Converter; + +/** + * Type representing a memory size in bytes, using 1024 as the multiplier for kilo, mega, etc. + * + *

String representations, for both {@link #valueOf(String) parsing} and {@link #toString() + * generating}, support memory size suffixes like {@code K} for "kilo", {@code M} for "mega". + * + *

(De)serialization support for Eclipse Microprofile Config / smallrye-config provided via a + * {@link Converter} implementation, let smallrye-config discover converters automatically (default + * in Quarkus). + * + *

(De)serialization support for Jackson provided via a Jackson module, provided via the Java + * service loader mechanism. Use {@link ObjectMapper#findAndRegisterModules()} for manually created + * object mappers. + * + *

Jackson serialization supports both {@link Shape#STRING string} (default) and {@link + * Shape#NUMBER integer} representations via {@link JsonFormat @JsonFormat}{@code (shape = + * JsonFormat.}{@link Shape Shape}{@code .NUMBER)}. Number/int serialization always represents the + * number of bytes. + * + *

Note that, although unlikely in practice, memory sizes may exceed {@link Long#MAX_VALUE} and + * calls to {@link #asLong()} the result in an {@link ArithmeticException}. + */ +public abstract class MemorySize { + private static final Pattern MEMORY_SIZE_PATTERN = + Pattern.compile("^(\\d+)([BbKkMmGgTtPpEeZzYy]?)$"); + private static final BigInteger KILO_BYTES = BigInteger.valueOf(1024); + private static final Map MEMORY_SIZE_MULTIPLIERS; + private static final char[] SUFFIXES = new char[] {'B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'}; + + static { + MEMORY_SIZE_MULTIPLIERS = new HashMap<>(); + MEMORY_SIZE_MULTIPLIERS.put("K", KILO_BYTES); + MEMORY_SIZE_MULTIPLIERS.put("M", KILO_BYTES.pow(2)); + MEMORY_SIZE_MULTIPLIERS.put("G", KILO_BYTES.pow(3)); + MEMORY_SIZE_MULTIPLIERS.put("T", KILO_BYTES.pow(4)); + MEMORY_SIZE_MULTIPLIERS.put("P", KILO_BYTES.pow(5)); + MEMORY_SIZE_MULTIPLIERS.put("E", KILO_BYTES.pow(6)); + MEMORY_SIZE_MULTIPLIERS.put("Z", KILO_BYTES.pow(7)); + MEMORY_SIZE_MULTIPLIERS.put("Y", KILO_BYTES.pow(8)); + } + + static final class MemorySizeLong extends MemorySize { + private final long bytes; + + MemorySizeLong(long bytes) { + this.bytes = bytes; + } + + @Override + public long asLong() { + return bytes; + } + + @Nonnull + @Override + public BigInteger asBigInteger() { + return BigInteger.valueOf(bytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof MemorySize)) { + return false; + } + + if (o instanceof MemorySizeLong) { + var l = (MemorySizeLong) o; + return bytes == l.bytes; + } + + var that = (MemorySize) o; + return asBigInteger().equals(that.asBigInteger()); + } + + @Override + public int hashCode() { + return Long.hashCode(bytes); + } + + @Override + public String toString() { + var mask = 1024 - 1; + var s = 0; + var v = bytes; + + while (v > 0 && (v & mask) == 0L) { + v >>= 10; + s++; + } + + return Long.toString(v) + SUFFIXES[s]; + } + } + + static final class MemorySizeBig extends MemorySize { + private final BigInteger bytes; + + MemorySizeBig(@Nonnull BigInteger bytes) { + this.bytes = bytes; + } + + @Override + public long asLong() { + return bytes.longValueExact(); + } + + @Nonnull + @Override + public BigInteger asBigInteger() { + return bytes; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof MemorySize)) { + return false; + } + + MemorySize that = (MemorySize) o; + return bytes.equals(that.asBigInteger()); + } + + @Override + public int hashCode() { + return bytes.hashCode(); + } + + @Override + public String toString() { + var s = 0; + var v = bytes; + + while (v.signum() > 0 && v.remainder(KILO_BYTES).signum() == 0) { + v = v.divide(KILO_BYTES); + s++; + } + + return v.toString() + SUFFIXES[s]; + } + } + + public static MemorySize ofBytes(long bytes) { + return new MemorySizeLong(bytes); + } + + public static MemorySize ofKilo(int kb) { + return new MemorySizeLong(1024L * kb); + } + + public static MemorySize ofMega(int mb) { + return new MemorySizeLong(1024L * 1024L * mb); + } + + public static MemorySize ofGiga(int gb) { + return new MemorySizeLong(1024L * 1024L * 1024L * gb); + } + + /** + * Convert data size configuration value respecting the following format (shown in regular + * expression) "[0-9]+[BbKkMmGgTtPpEeZzYy]?" If the value contain no suffix, the size is treated + * as bytes. + * + * @param value - value to convert. + * @return {@link MemorySize} - a memory size represented by the given value + */ + public static MemorySize valueOf(String value) { + value = value.trim(); + if (value.isEmpty()) { + return null; + } + var matcher = MEMORY_SIZE_PATTERN.matcher(value); + if (matcher.find()) { + var number = new BigInteger(matcher.group(1)); + var scale = matcher.group(2).toUpperCase(ROOT); + var multiplier = MEMORY_SIZE_MULTIPLIERS.get(scale); + if (multiplier != null) { + number = number.multiply(multiplier); + } + try { + return new MemorySizeLong(number.longValueExact()); + } catch (ArithmeticException e) { + return new MemorySizeBig(number); + } + } + + throw new IllegalArgumentException( + format( + "value %s not in correct format (regular expression): [0-9]+[BbKkMmGgTtPpEeZzYy]?", + value)); + } + + @Nonnull + public abstract BigInteger asBigInteger(); + + /** + * Memory size as a {@code long} value. May throw an {@link ArithmeticException} if the value is + * bigger than {@link Long#MAX_VALUE}. + */ + public abstract long asLong(); +} diff --git a/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySizeConfigConverter.java b/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySizeConfigConverter.java new file mode 100644 index 0000000000..975214e9cb --- /dev/null +++ b/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySizeConfigConverter.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.polaris.misc.types.memorysize; + +import org.eclipse.microprofile.config.spi.Converter; + +public class MemorySizeConfigConverter implements Converter { + + @Override + public MemorySize convert(String value) { + return MemorySize.valueOf(value); + } +} diff --git a/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySizeJackson.java b/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySizeJackson.java new file mode 100644 index 0000000000..bc8158501c --- /dev/null +++ b/tools/misc-types/src/main/java/org/apache/polaris/misc/types/memorysize/MemorySizeJackson.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.polaris.misc.types.memorysize; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import java.io.IOException; + +public class MemorySizeJackson extends SimpleModule { + public MemorySizeJackson() { + addDeserializer(MemorySize.class, new MemorySizeDeserializer()); + addSerializer(MemorySize.class, MemorySizeSerializer.AS_STRING); + } + + private static class MemorySizeDeserializer extends JsonDeserializer { + @Override + public MemorySize deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + switch (p.currentToken()) { + case VALUE_NUMBER_INT: + var bigInt = p.getBigIntegerValue(); + try { + return new MemorySize.MemorySizeLong(bigInt.longValueExact()); + } catch (ArithmeticException e) { + return new MemorySize.MemorySizeBig(bigInt); + } + case VALUE_STRING: + return MemorySize.valueOf(p.getText()); + default: + throw new IllegalArgumentException( + "Unsupported token " + p.currentToken() + " for " + MemorySize.class.getName()); + } + } + } + + private static class MemorySizeSerializer extends JsonSerializer + implements ContextualSerializer { + final boolean asInt; + + static final MemorySizeSerializer AS_STRING = new MemorySizeSerializer(false); + static final MemorySizeSerializer AS_INT = new MemorySizeSerializer(true); + + private MemorySizeSerializer(boolean asInt) { + this.asInt = asInt; + } + + @Override + public void serialize(MemorySize value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + if (asInt) { + if (value instanceof MemorySize.MemorySizeBig) { + generator.writeNumber(value.asBigInteger()); + } else { + generator.writeNumber(value.asLong()); + } + } else { + generator.writeString(value.toString()); + } + } + + @Override + public JsonSerializer createContextual(SerializerProvider provider, BeanProperty property) { + + if (property != null) { + var propertyFormat = property.findPropertyFormat(provider.getConfig(), handledType()); + if (propertyFormat != null) { + var shape = propertyFormat.getShape(); + switch (shape) { + case NUMBER: + case NUMBER_INT: + return AS_INT; + case STRING: + case ANY: + case NATURAL: + return AS_STRING; + default: + throw new IllegalStateException( + "Shape " + + shape + + " not supported for " + + MemorySize.class.getName() + + " serialization"); + } + } + } + + return null; + } + } +} diff --git a/tools/misc-types/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/tools/misc-types/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 0000000000..b29379a03b --- /dev/null +++ b/tools/misc-types/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +org.apache.polaris.misc.types.memorysize.MemorySizeJackson diff --git a/tools/misc-types/src/test/java/org/apache/polaris/misc/types/memorysize/TestMemorySize.java b/tools/misc-types/src/test/java/org/apache/polaris/misc/types/memorysize/TestMemorySize.java new file mode 100644 index 0000000000..fac2dc4851 --- /dev/null +++ b/tools/misc-types/src/test/java/org/apache/polaris/misc/types/memorysize/TestMemorySize.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.polaris.misc.types.memorysize; + +import static java.util.Locale.ROOT; +import static java.util.Objects.requireNonNull; +import static org.assertj.core.groups.Tuple.tuple; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfigBuilder; +import jakarta.annotation.Nullable; +import java.math.BigInteger; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.apache.polaris.immutables.PolarisImmutable; +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions; +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(SoftAssertionsExtension.class) +public class TestMemorySize { + @InjectSoftAssertions SoftAssertions soft; + + @ParameterizedTest + @MethodSource + public void parseString(String in, String expected) { + soft.assertThat(requireNonNull(MemorySize.valueOf(in)).toString()).isEqualTo(expected); + } + + static Stream parseString() { + return Stream.of( + arguments("0G", "0B"), + arguments("1024M", "1G"), + arguments(String.valueOf(1024 * 1024), "1M"), + arguments(String.valueOf(4 * 1024 * 1024), "4M"), + arguments(String.valueOf(1024 * 1024 * 1024), "1G")); + } + + @ParameterizedTest + @MethodSource + public void parse(String s, BigInteger expected) { + var parsed = requireNonNull(MemorySize.valueOf(s)); + soft.assertThat(parsed.asBigInteger()).isEqualTo(expected); + if (Character.isDigit(s.charAt(s.length() - 1))) { + soft.assertThat(parsed.toString()).isEqualTo(s.toUpperCase(ROOT) + 'B'); + } else { + soft.assertThat(parsed.toString()).isEqualTo(s.toUpperCase(ROOT)); + } + try { + var l = expected.longValueExact(); + soft.assertThat(parsed.asLong()).isEqualTo(l); + soft.assertThat(parsed).isInstanceOf(MemorySize.MemorySizeLong.class); + } catch (ArithmeticException e) { + soft.assertThat(parsed).isInstanceOf(MemorySize.MemorySizeBig.class); + } + } + + static Stream parse() { + return Stream.of( + tuple("", BigInteger.ONE), + tuple("B", BigInteger.ONE), + tuple("K", BigInteger.valueOf(1024)), + tuple("M", BigInteger.valueOf(1024).pow(2)), + tuple("G", BigInteger.valueOf(1024).pow(3)), + tuple("T", BigInteger.valueOf(1024).pow(4)), + tuple("P", BigInteger.valueOf(1024).pow(5)), + tuple("E", BigInteger.valueOf(1024).pow(6)), + tuple("Z", BigInteger.valueOf(1024).pow(7)), + tuple("Y", BigInteger.valueOf(1024).pow(8))) + .flatMap( + t -> + Stream.of( + tuple(t.toList().get(0).toString().toLowerCase(ROOT), t.toList().get(1)), t)) + .flatMap( + t -> { + var suffix = t.toList().get(0).toString(); + var mult = (BigInteger) t.toList().get(1); + return Stream.of( + arguments("1" + suffix, mult), + arguments("5" + suffix, mult.multiply(BigInteger.valueOf(5))), + arguments("32" + suffix, mult.multiply(BigInteger.valueOf(32))), + arguments("1023" + suffix, mult.multiply(BigInteger.valueOf(1023)))); + }); + } + + @ParameterizedTest + @MethodSource("parse") + public void serdeConfig(String input, BigInteger expected) { + var configMap = + Map.of( + "memory-size.implicit", input, + "memory-size.optional-present", input); + var config = + new SmallRyeConfigBuilder() + .withMapping(MemorySizeConfig.class) + .addDiscoveredConverters() + .withSources(new PropertiesConfigSource(configMap, "configMap")) + .build() + .getConfigMapping(MemorySizeConfig.class); + var value = new MemorySize.MemorySizeBig(expected); + soft.assertThat(config) + .extracting( + MemorySizeConfig::implicit, + MemorySizeConfig::optionalEmpty, + MemorySizeConfig::optionalPresent) + .containsExactly(value, Optional.empty(), Optional.of(value)); + } + + @ConfigMapping(prefix = "memory-size") + interface MemorySizeConfig { + MemorySize implicit(); + + Optional optionalEmpty(); + + Optional optionalPresent(); + } + + @ParameterizedTest + @MethodSource("parse") + public void serdeJackson(@SuppressWarnings("unused") String input, BigInteger expected) + throws Exception { + var value = new MemorySize.MemorySizeBig(expected); + + var immutable = + ImmutableMemorySizeJson.builder() + .implicit(value) + .implicitInt(value) + .optionalPresent(value) + .optionalPresentInt(value) + .build(); + + var mapper = new ObjectMapper().findAndRegisterModules(); + var json = mapper.writeValueAsString(immutable); + var nodes = mapper.readValue(json, JsonNode.class); + var deser = mapper.readValue(json, MemorySizeJson.class); + + soft.assertThat(deser).isEqualTo(immutable); + + soft.assertThat(nodes.get("implicit").asText()).isEqualTo(value.toString()); + soft.assertThat(nodes.get("implicitInt").bigIntegerValue()).isEqualTo(value.asBigInteger()); + soft.assertThat(nodes.get("optionalPresent").asText()).isEqualTo(value.toString()); + soft.assertThat(nodes.get("optionalPresentInt").bigIntegerValue()) + .isEqualTo(value.asBigInteger()); + } + + @PolarisImmutable + @JsonSerialize(as = ImmutableMemorySizeJson.class) + @JsonDeserialize(as = ImmutableMemorySizeJson.class) + interface MemorySizeJson { + MemorySize implicit(); + + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + MemorySize implicitInt(); + + @Nullable + MemorySize implicitNull(); + + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + @Nullable + MemorySize implicitIntNull(); + + Optional optionalEmpty(); + + Optional optionalPresent(); + + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + Optional optionalPresentInt(); + } +} diff --git a/tools/misc-types/src/test/resources/logback-test.xml b/tools/misc-types/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..4a4d9a629d --- /dev/null +++ b/tools/misc-types/src/test/resources/logback-test.xml @@ -0,0 +1,32 @@ + + + + + + + %date{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + +