diff --git a/core/src/main/java/io/jstach/rainbowgum/LogAppender.java b/core/src/main/java/io/jstach/rainbowgum/LogAppender.java index fd1d172c..38def54c 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogAppender.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogAppender.java @@ -9,7 +9,6 @@ import io.jstach.rainbowgum.LogAppender.AbstractLogAppender; import io.jstach.rainbowgum.LogAppender.ThreadSafeLogAppender; import io.jstach.rainbowgum.LogEncoder.Buffer; -import io.jstach.rainbowgum.LogProperties.Property; /** * Appenders are guaranteed to be written synchronously much like an actor in actor @@ -68,20 +67,22 @@ public interface AppenderProvider { /** * Creates a builder to create an appender provider. + * @param name name of appender. * @return builder. */ - public static Builder builder() { - return LogAppender.builder(); + public static Builder builder(String name) { + return LogAppender.builder(name); } } /** * Creates a builder. + * @param name appender name. * @return builder. */ - public static Builder builder() { - return new Builder(); + public static Builder builder(String name) { + return new Builder(name); } /** @@ -108,22 +109,23 @@ public static LogAppender of(List appenders) { */ public static final class Builder { - protected LogOutput output = LogOutput.ofStandardOut(); + protected @Nullable LogOutput output; protected @Nullable LogEncoder encoder; - // private final String name; + private final String name; - private Builder() { + private Builder(String name) { + this.name = name; } - //// /** - //// * Name of the appender. - //// * @return name. - //// */ - //// public String name() { - //// return this.name; - // } + /** + * Name of the appender. + * @return name. + */ + public String name() { + return this.name; + } /** * Sets output. If not set defaults to {@link LogOutput#ofStandardOut()}. @@ -172,29 +174,15 @@ public Builder encoder(LogEncoder encoder) { * @return an appender factory. */ public AppenderProvider build() { - var output = this.output; - var encoder = this.encoder; + /* + * We need to capture parameters since appender creation needs to be lazy. + */ + AppenderConfig a = new AppenderConfig(name, output, encoder); return config -> { - return logAppender(config, output, encoder); + return DefaultAppenderRegistry.appender(a, config); }; } - private static final Property defaultsAppenderBufferProperty = Property.builder() - .map(s -> Boolean.parseBoolean(s)) - .orElse(false) - .build(LogProperties.concatKey("defaults.appender.buffer")); - - private static LogAppender logAppender(LogConfig config, LogOutput output, @Nullable LogEncoder encoder) { - var formatterRegistry = config.formatterRegistry(); - var properties = config.properties(); - if (encoder == null) { - encoder = LogEncoder.of(formatterRegistry.formatterForOutputType(output.type())); - } - - return defaultsAppenderBufferProperty.get(properties).value() ? new BufferLogAppender(output, encoder) - : new DefaultLogAppender(output, encoder); - } - } @Override diff --git a/core/src/main/java/io/jstach/rainbowgum/LogAppenderRegistry.java b/core/src/main/java/io/jstach/rainbowgum/LogAppenderRegistry.java index e25e9766..06965240 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogAppenderRegistry.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogAppenderRegistry.java @@ -41,8 +41,33 @@ public static LogAppenderRegistry of() { } +record AppenderConfig(String name, @Nullable LogOutput output, @Nullable LogEncoder encoder) { + + AppenderConfig { + validateName(name); + } + + private static String validateName(String name) { + if (name.isBlank()) { + throw new IllegalStateException("Appender name cannot be null. name=" + name); + } + if (name.contains(" ") || name.contains("\t") || name.contains("\n") || name.contains("\r")) { + throw new IllegalStateException("Appender name cannot have whitespace"); + } + if (name.contains(LogProperties.SEP)) { + throw new IllegalStateException("Appender name cannot have '" + LogProperties.SEP + "'"); + } + return name; + } +} + final class DefaultAppenderRegistry implements LogAppenderRegistry { + /* + * The shit in here is a mess because auto configuration of appenders based on + * properties is complicated particularly because we want to support Spring Boots + * configuration OOB. + */ private final Map providers = new ConcurrentHashMap<>(); static final Property fileProperty = Property.builder().toURI().build(LogProperties.FILE_PROPERTY); @@ -58,9 +83,7 @@ static List defaultAppenders(LogConfig config) { List appenderNames = appendersProperty.get(config.properties()).value(List.of()); for (String appenderName : appenderNames) { - var outputProperty = outputProperty(LogAppender.APPENDER_OUTPUT_PROPERTY, appenderName, config); - var encoderProperty = encoderProperty(LogAppender.APPENDER_ENCODER_PROPERTY, appenderName, config); - appenders.add(appender(appenderName, outputProperty, encoderProperty, config)); + appenders.add(appender(appenderName, config)); } if (appenders.isEmpty()) { var consoleAppender = defaultConsoleAppender(config); @@ -70,7 +93,10 @@ static List defaultAppenders(LogConfig config) { } private static LogAppender defaultConsoleAppender(LogConfig config) { - var consoleAppender = LogAppender.builder().output(LogOutput.ofStandardOut()).build().provide(config); + var consoleAppender = LogAppender.builder(LogAppender.CONSOLE_APPENDER_NAME) + .output(LogOutput.ofStandardOut()) + .build() + .provide(config); return consoleAppender; } @@ -79,28 +105,71 @@ static Optional fileAppender(LogConfig config) { var outputProperty = outputProperty(LogAppender.APPENDER_OUTPUT_PROPERTY, name, config); var encoderProperty = encoderProperty(LogAppender.APPENDER_ENCODER_PROPERTY, name, config); return fileProperty // - .map(u -> appender(name, outputProperty, encoderProperty, config)) + .map(u -> appender(name, config, outputProperty, encoderProperty)) .get(config.properties()) .optional(); } + private static final Property defaultsAppenderBufferProperty = Property.builder() + .map(s -> Boolean.parseBoolean(s)) + .orElse(false) + .build(LogProperties.concatKey("defaults.appender.buffer")); + + static LogAppender appender( // + String name, LogConfig config) { + var builder = new AppenderConfig(name, null, null); + var outputProperty = outputProperty(LogAppender.APPENDER_OUTPUT_PROPERTY, name, config); + var encoderProperty = encoderProperty(LogAppender.APPENDER_ENCODER_PROPERTY, name, config); + return appender(builder, config, outputProperty, encoderProperty); + } + + static LogAppender appender( // + AppenderConfig appenderConfig, // + LogConfig config) { + String name = appenderConfig.name(); + var outputProperty = outputProperty(LogAppender.APPENDER_OUTPUT_PROPERTY, name, config); + var encoderProperty = encoderProperty(LogAppender.APPENDER_ENCODER_PROPERTY, name, config); + return appender(appenderConfig, config, outputProperty, encoderProperty); + } + static LogAppender appender( // - String name, Property outputProperty, Property encoderProperty, LogConfig config) { + AppenderConfig builder, // + LogConfig config, // + Property outputProperty, Property encoderProperty) { + + @Nullable + LogOutput output = builder.output(); + @Nullable + LogEncoder encoder = builder.encoder(); + var properties = config.properties(); + + if (output == null) { + output = outputProperty.get(properties).value(); + } + + if (encoder == null) { + var _output = output; + encoder = encoderProperty.get(properties).value(() -> { + var formatterRegistry = config.formatterRegistry(); + return LogEncoder.of(formatterRegistry.formatterForOutputType(_output.type())); + }); + } + + return defaultsAppenderBufferProperty.get(properties).value() ? new BufferLogAppender(output, encoder) + : new DefaultLogAppender(output, encoder); + } + + static LogAppender appender( // + String name, // + LogConfig config, // + Property outputProperty, Property encoderProperty) { var appenderRegistry = config.appenderRegistry(); var appender = appenderRegistry.appender(name).map(a -> a.provide(config)).orElse(null); if (appender != null) { return appender; } - var builder = LogAppender.builder(); - LogOutput output = outputProperty.get(config.properties()).valueOrNull(); - if (output != null) { - builder.output(output); - } - LogEncoder encoder = encoderProperty.get(config.properties()).valueOrNull(); - if (encoder != null) { - builder.encoder(encoder); - } - return builder.build().provide(config); + var builder = new AppenderConfig(name, null, null); + return appender(builder, config, outputProperty, encoderProperty); } diff --git a/core/src/main/java/io/jstach/rainbowgum/LogEncoder.java b/core/src/main/java/io/jstach/rainbowgum/LogEncoder.java index cac7bd16..2e14325d 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogEncoder.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogEncoder.java @@ -1,6 +1,5 @@ package io.jstach.rainbowgum; -import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.nio.CharBuffer; diff --git a/core/src/main/java/io/jstach/rainbowgum/LogOutputRegistry.java b/core/src/main/java/io/jstach/rainbowgum/LogOutputRegistry.java index ad79004a..782bd781 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogOutputRegistry.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogOutputRegistry.java @@ -74,15 +74,28 @@ public Optional output(String name) { return Optional.ofNullable(outputs.get(name)); } + LogOutput provide(String name, LogProperties properties) throws IOException { + var o = output(name).orElse(null); + if (o != null) { + return o; + } + return output(URI.create(name + ":///"), name, properties); + } + @Override public LogOutput output(URI uri, String name, LogProperties properties) throws IOException { String scheme = uri.getScheme(); String path = uri.getPath(); OutputProvider customProvider; if (scheme == null && path != null) { - @SuppressWarnings("resource") - FileOutputStream fos = new FileOutputStream(path); - return LogOutput.of(uri, fos.getChannel()); + if (name.equals(LogAppender.FILE_APPENDER_NAME)) { + @SuppressWarnings("resource") + FileOutputStream fos = new FileOutputStream(path); + return LogOutput.of(uri, fos.getChannel()); + } + else { + return provide(name, properties); + } } else if (NAMED_OUTPUT_SCHEME.equals(scheme)) { String host = uri.getHost(); @@ -90,7 +103,7 @@ else if (NAMED_OUTPUT_SCHEME.equals(scheme)) { name = host; } String _name = name; - return output(name).orElseThrow(() -> new IOException("Output for name: " + _name + " not found.")); + return output(_name).orElseThrow(() -> new IOException("Output for name: " + _name + " not found.")); } else if (LogOutput.STDOUT_SCHEME.equals(scheme)) { return LogOutput.ofStandardOut(); diff --git a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java index 4def69b7..51693a6e 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java @@ -7,14 +7,18 @@ import java.util.Arrays; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.jdt.annotation.Nullable; @@ -395,8 +399,8 @@ private static RuntimeException throwPropertyError(String key, Exception e) { throw new RuntimeException("Error for property. key: " + key, e); } - private static RuntimeException throwMissingError(String key) { - throw new NoSuchElementException("Property missing. key: " + key); + private static RuntimeException throwMissingError(List keys) { + throw new NoSuchElementException("Property missing. key: " + keys); } /** @@ -480,6 +484,155 @@ static String interpolateKey(String name, Map parameters) { return name; } + /** + * Property prefix parameter pattern. + */ + static final Pattern PARAMETER_PATTERN = Pattern.compile("\\{(.*?)\\}"); + + /** + * Extract key parameters with {@link #PARAMETER_PATTERN}. + * @param key also known as property name. + * @return parameters in key. + */ + static Set keyParameters(String key) { + Set tokens = new LinkedHashSet<>(); + Matcher matcher = PARAMETER_PATTERN.matcher(key); + while (matcher.find()) { + tokens.add(matcher.group(1)); + } + return tokens; + } + + private static void validateKeyParameters(String key, Set parameters) { + var keyParameters = keyParameters(key); + if (!parameters.equals(keyParameters)) { + throw new IllegalArgumentException("Keyed parameter mismatch. key is parameters: " + keyParameters + + " provided parameters: " + parameters); + } + } + + /** + * A lazily retrieved value. + * + * @param value type. + */ + sealed interface Value { + + /** + * Maps a property value to a new property value. + * @param new value type. + * @param mapper function. + * @return new property value. + */ + public Value map(PropertyFunction mapper); + + /** + * Gets the value. + * @return value or null. + */ + public @Nullable T valueOrNull(); + + /** + * Gets a value if there is if not uses the fallback. + * @param fallback maybe null. + * @return value. + */ + default @Nullable T valueOrNull(@Nullable T fallback) { + var v = valueOrNull(); + if (v != null) { + return v; + } + return fallback; + } + + /** + * Gets the value and will fail with {@link NoSuchElementException} if there is no + * value. + * @return value. + * @throws NoSuchElementException if there is no value. + */ + public T value() throws NoSuchElementException; + + /** + * Append description of value. + * @param sb appended to. + */ + public void describe(StringBuilder sb); + + } + + // record ValueValue(@Nullable T valueOrNull) implements Value{ + // + // @Override + // public Value map( + // PropertyFunction mapper) { + // // TODO Auto-generated method stub + // return null; + // } + // + // @Override + // public T value() + // throws NoSuchElementException { + // var t = valueOrNull(); + // if (t == null) { + // throw new NoSuchElementException(); + // } + // return t; + // } + // + // } + + /** + * Multiple properties tried in order. + * + * @param property type. + * @param propertyValues property values + */ + record PropertiesValue(List> propertyValues) implements Value { + + @Override + public Value map(PropertyFunction mapper) { + + List> values = new ArrayList<>(); + for (var v : propertyValues) { + values.add(v.map(mapper)); + } + return new PropertiesValue<>(values); + } + + @Override + public @Nullable T valueOrNull() { + for (var pv : propertyValues) { + var v = pv.valueOrNull(); + if (v != null) { + return v; + } + } + return null; + } + + @Override + public T value() throws NoSuchElementException { + var t = valueOrNull(); + if (t != null) { + return t; + } + StringBuilder error = new StringBuilder(); + error.append("Property not found for: "); + describe(error); + throw new NoSuchElementException(error.toString()); + } + + @Override + public void describe(StringBuilder sb) { + for (var v : propertyValues) { + v.describe(sb); + sb.append(", "); + } + } + + } + /** * A property value is a property and its lazily retrieved value. * @@ -487,7 +640,7 @@ static String interpolateKey(String name, Map parameters) { * @param property backing property. * @param properties the log properties instance. */ - record PropertyValue(Property property, LogProperties properties) { + record PropertyValue(Property property, LogProperties properties) implements Value { /** * Maps a property value to a new property value. * @param new value type. @@ -503,7 +656,7 @@ public PropertyValue map(PropertyFunctionnull. */ public @Nullable T valueOrNull() { - return property.propertyGetter.valueOrNull(properties, property.key); + return property.propertyGetter.valueOrNull(properties, property.keys); } /** @@ -512,7 +665,11 @@ public PropertyValue map(PropertyFunction optional() { * @throws NoSuchElementException if there is no value. */ public T value() throws NoSuchElementException { - return property.propertyGetter.value(properties, property.key); + return property.propertyGetter.value(properties, property.keys); } /** @@ -542,18 +699,68 @@ public T value() throws NoSuchElementException { * null. */ public T value(@Nullable T fallback) throws NoSuchElementException { - return property.propertyGetter.value(properties, property.key, fallback); + return property.propertyGetter.value(properties, property.keys, fallback); + } + + /** + * Gets a value if there if not uses the fallback if not null otherwise throws an + * exception. + * @param fallback maybe null. + * @return value. + * @throws NoSuchElementException if no property and fallback is + * null. + */ + public T value(@SuppressWarnings("exports") Supplier fallback) + throws NoSuchElementException { + return property.propertyGetter.value(properties, property.keys, fallback); + } + + /** + * Set a property if its not null. + * @param consumer first parameter is first key and second parameter is non null + * value. + */ + public void set(BiConsumer consumer) { + T value = valueOrNull(); + if (value != null) { + consumer.accept(property.key(), value); + } + } + + public void describe(StringBuilder b) { + b.append(property); } } /** * A property description. * - * @param property type. * @param propertyGetter getter to retrieve property value from {@link LogProperties}. - * @param key property key. + * @param keys property keys to try in order + * @param property type. */ - record Property(PropertyGetter propertyGetter, String key) { + record Property(PropertyGetter propertyGetter, List keys) { + + /** + * Property Constructor. + * @param propertyGetter getter to retrieve property value from + * {@link LogProperties}. + * @param keys property keys to try in order + */ + public Property { + Objects.requireNonNull(propertyGetter); + if (keys.isEmpty()) { + throw new IllegalArgumentException("should have at least one key"); + } + } + + /** + * Gets the first key. + * @return key. + */ + public String key() { + return keys.get(0); + } /** * Gets a property value. @@ -571,7 +778,19 @@ public PropertyValue get(LogProperties properties) { * @return property. */ public Property map(PropertyFunction mapper) { - return new Property<>(propertyGetter.map(mapper), key); + return new Property<>(propertyGetter.map(mapper), keys); + } + + /** + * Set a property if its not null. + * @param value value to set. + * @param consumer first parameter is first key and second parameter is non null + * value. + */ + public void set(T value, BiConsumer consumer) { + if (value != null) { + consumer.accept(key(), value); + } } /** @@ -599,6 +818,23 @@ sealed interface PropertyGetter { @Nullable T valueOrNull(LogProperties props, String key); + /** + * Value or null. + * @param props log properties. + * @param keys key to use in order. + * @return value or null. + */ + @Nullable + default T valueOrNull(LogProperties props, List keys) { + for (String key : keys) { + var v = valueOrNull(props, key); + if (v != null) { + return v; + } + } + return null; + } + /** * Value or fallback if property is missing. * @param props log properties. @@ -631,11 +867,24 @@ default String fullyQualifiedKey(String key) { * @throws NoSuchElementException if no value is found for key. */ default T value(LogProperties props, String key) throws NoSuchElementException { - var t = valueOrNull(props, key); - if (t == null) { - throw findRoot(this).throwMissing(props, key); + return value(props, List.of(key)); + } + + /** + * Uses the key to get a property and fail if missing. + * @param props log properties. + * @param keys keys to try in order. + * @return value. + * @throws NoSuchElementException if no value is found for key. + */ + default T value(LogProperties props, List keys) throws NoSuchElementException { + for (String key : keys) { + var v = valueOrNull(props, key); + if (v != null) { + return v; + } } - return t; + throw findRoot(this).throwMissing(props, keys); } /** @@ -648,12 +897,46 @@ default T value(LogProperties props, String key) throws NoSuchElementException { * null. */ default T value(LogProperties props, String key, @Nullable T fallback) throws NoSuchElementException { - var t = valueOrNull(props, key); + return value(props, List.of(key), fallback); + } + + /** + * Value or fallback or exception if property is missing and fallback is null. + * @param props log properties. + * @param keys key to use. + * @param fallback to use can be null. + * @return value or null. + * @throws NoSuchElementException if property is missing and fallback is + * null. + */ + default T value(LogProperties props, List keys, @Nullable T fallback) throws NoSuchElementException { + var t = valueOrNull(props, keys); if (t == null) { t = fallback; } if (t == null) { - throw findRoot(this).throwMissing(props, key); + throw findRoot(this).throwMissing(props, keys); + } + return t; + } + + /** + * Value or fallback or exception if property is missing and fallback is null. + * @param props log properties. + * @param keys key to use. + * @param fallback to use can be null returned. + * @return value or null. + * @throws NoSuchElementException if property is missing and fallback is + * null. + */ + default T value(LogProperties props, List keys, + @SuppressWarnings("exports") Supplier fallback) throws NoSuchElementException { + var t = valueOrNull(props, keys); + if (t == null) { + t = fallback.get(); + } + if (t == null) { + throw findRoot(this).throwMissing(props, keys); } return t; } @@ -671,14 +954,20 @@ default Property property(String key) { * Creates a Property from the given key and this getter. * @param key key. * @return property. + * @throws IllegalArgumentException if the key is malformed. */ - default Property build(String key) { + default Property build(String key) throws IllegalArgumentException { String fqn = fullyQualifiedKey(key); if (!fqn.startsWith(LogProperties.ROOT_PREFIX)) { throw new IllegalArgumentException( "Property key should start with: '" + LogProperties.ROOT_PREFIX + "'. key = " + key); } - return new Property<>(this, key); + if (fqn.endsWith(".") || fqn.startsWith(".")) { + throw new IllegalArgumentException( + "Property key should not start or end with '" + LogProperties.SEP + "'"); + } + validateKeyParameters(key, Set.of()); + return new Property<>(this, List.of(key)); } /** @@ -689,7 +978,9 @@ default Property build(String key) { * @return property. */ default Property buildWithName(String key, String name) { - String fqn = LogProperties.interpolateKey(key, Map.of(NAME, name)); + var parameters = Map.of(NAME, name); + validateKeyParameters(key, parameters.keySet()); + String fqn = LogProperties.interpolateKey(key, parameters); return build(fqn); } @@ -740,8 +1031,13 @@ RuntimeException throwError(LogProperties props, String key, Exception e) { throw LogProperties.throwPropertyError(props.description(fullyQualifiedKey(key)), e); } - NoSuchElementException throwMissing(LogProperties props, String key) { - throw LogProperties.throwMissingError(props.description(fullyQualifiedKey(key))); + NoSuchElementException throwMissing(LogProperties props, List keys) { + List resolvedKeys = describeKeys(props, keys); + throw LogProperties.throwMissingError(resolvedKeys); + } + + List describeKeys(LogProperties props, List keys) { + return keys.stream().map(k -> props.description(fullyQualifiedKey(k))).toList(); } /** diff --git a/core/src/main/java/io/jstach/rainbowgum/LogRouter.java b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java index ef86861e..de8ff387 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogRouter.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java @@ -200,11 +200,12 @@ protected Builder self() { /** * Adds an appender by giving an appender builder to a consumer. + * @param name appender name. * @param consumer consumer. * @return this builder. */ - public Builder appender(Consumer consumer) { - var builder = LogAppender.builder(); + public Builder appender(String name, Consumer consumer) { + var builder = LogAppender.builder(name); consumer.accept(builder); this.appenders.add(builder.build()); return this; diff --git a/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java index d0e136bf..0e4ff178 100644 --- a/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java +++ b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java @@ -36,18 +36,18 @@ public Optional provide(LogConfig config) { Property bufferSize = Property.builder() .map(Integer::parseInt) .orElse(1024) - .build("custom.async.bufferSize"); + .build("logging.custom.async.bufferSize"); Property output = Property.builder() .map(URI::create) .map(u -> config.output(u, "")) .orElse(LogOutput.ofStandardOut()) - .build("custom.output"); + .build("logging.custom.output"); var gum = RainbowGum.builder() // .route(r -> { r.publisher(PublisherProvider.async().bufferSize(bufferSize.get(config.properties()).value()).build()); - r.appender(a -> { + r.appender("console", a -> { a.output(output.get(config.properties()).value()); }); r.level(Level.INFO); diff --git a/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java b/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java index 64a7d4a4..221346e0 100644 --- a/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java +++ b/core/src/test/java/io/jstach/rainbowgum/RainbowGumTest.java @@ -58,7 +58,8 @@ void testFormatterBuilder() throws Exception { .newline() // .build(); - var sysout = LogAppender.builder() // + var sysout = LogAppender.builder("sysout") // + .output(LogOutput.ofStandardOut()) .formatter(formatter) .build(); diff --git a/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQOutput.java b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQOutput.java index c5d24587..616306e7 100644 --- a/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQOutput.java +++ b/rainbowgum-rabbitmq/src/main/java/io/jstach/rainbowgum/rabbitmq/RabbitMQOutput.java @@ -52,9 +52,15 @@ public class RabbitMQOutput implements LogOutput { private final String exchangeType; - final static String DEFAULT_EXCHANGE = "logging"; - - final static String DEFAULT_EXCHANGE_TYPE = "topic"; + /** + * Default exchange. + */ + public final static String DEFAULT_EXCHANGE = "logging"; + + /** + * Default exchange type for declaration. + */ + public final static String DEFAULT_EXCHANGE_TYPE = "topic"; RabbitMQOutput(URI uri, ConnectionFactory connectionFactory, @Nullable String appId, String exchange, Function routingKeyFunction, String connectionName, boolean declareExchange, diff --git a/rainbowgum-slf4j/src/test/java/io/jstach/rainbowgum/slf4j/RainbowGumLoggerFactoryTest.java b/rainbowgum-slf4j/src/test/java/io/jstach/rainbowgum/slf4j/RainbowGumLoggerFactoryTest.java index 15303a18..f91ddaf9 100644 --- a/rainbowgum-slf4j/src/test/java/io/jstach/rainbowgum/slf4j/RainbowGumLoggerFactoryTest.java +++ b/rainbowgum-slf4j/src/test/java/io/jstach/rainbowgum/slf4j/RainbowGumLoggerFactoryTest.java @@ -20,7 +20,7 @@ void testGetLogger() { var gum = RainbowGum.builder().route(route -> { route.level(System.Logger.Level.WARNING, "ignore"); route.level(System.Logger.Level.INFO); - route.appender(a -> { + route.appender("list", a -> { a.formatter((output, event) -> { event.formattedMessage(output); output.append(" {"); diff --git a/rainbowgum/src/test/java/snippets/RainbowGumProviderExample.java b/rainbowgum/src/test/java/snippets/RainbowGumProviderExample.java index de119525..193d2b5f 100644 --- a/rainbowgum/src/test/java/snippets/RainbowGumProviderExample.java +++ b/rainbowgum/src/test/java/snippets/RainbowGumProviderExample.java @@ -20,18 +20,18 @@ public Optional provide(LogConfig config) { Property bufferSize = Property.builder() .map(Integer::parseInt) .orElse(1024) - .build("custom.async.bufferSize"); + .build("logging.custom.async.bufferSize"); Property output = Property.builder() .map(URI::create) .map(u -> config.output(u, "")) .orElse(LogOutput.ofStandardOut()) - .build("custom.output"); + .build("logging.custom.output"); var gum = RainbowGum.builder() // .route(r -> { r.publisher(PublisherProvider.async().bufferSize(bufferSize.get(config.properties()).value()).build()); - r.appender(a -> { + r.appender("console", a -> { a.output(output.get(config.properties()).value()); }); r.level(Level.INFO); diff --git a/test/rainbowgum-test-rabbitmq/amqp b/test/rainbowgum-test-rabbitmq/amqp new file mode 100644 index 00000000..b254593c --- /dev/null +++ b/test/rainbowgum-test-rabbitmq/amqp @@ -0,0 +1 @@ +21:18:02.511 [main] INFO io.jstach.rainbowgum.test.rabbitmq.RabbitMQSetup - hello diff --git a/test/rainbowgum-test-rabbitmq/src/test/java/io/jstach/rainbowgum/test/rabbitmq/RabbitMQSetupTest.java b/test/rainbowgum-test-rabbitmq/src/test/java/io/jstach/rainbowgum/test/rabbitmq/RabbitMQSetupTest.java index d016c050..1ed6b275 100644 --- a/test/rainbowgum-test-rabbitmq/src/test/java/io/jstach/rainbowgum/test/rabbitmq/RabbitMQSetupTest.java +++ b/test/rainbowgum-test-rabbitmq/src/test/java/io/jstach/rainbowgum/test/rabbitmq/RabbitMQSetupTest.java @@ -1,8 +1,18 @@ package io.jstach.rainbowgum.test.rabbitmq; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -10,16 +20,43 @@ import org.testcontainers.containers.RabbitMQContainer; import org.testcontainers.utility.DockerImageName; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; + +import io.jstach.rainbowgum.LogAppender; import io.jstach.rainbowgum.LogProperties; +import io.jstach.rainbowgum.LogProperties.Property; +import io.jstach.rainbowgum.rabbitmq.RabbitMQOutput; import io.jstach.rainbowgum.rabbitmq.RabbitMQOutputBuilder; class RabbitMQSetupTest { static RabbitMQContainer rabbit = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine")); + static MyConsumer consumer; + @BeforeAll public static void beforeAll() { rabbit.start(); + ConnectionFactory factory = new ConnectionFactory(); + try { + factory.setUri(rabbit.getAmqpUrl()); + Channel channel = factory.newConnection().createChannel(); + String queue = "rainbowgum"; + String exchange = RabbitMQOutput.DEFAULT_EXCHANGE; + channel.exchangeDeclare(exchange, RabbitMQOutput.DEFAULT_EXCHANGE_TYPE); + channel.queueDeclare(queue, false, false, false, Map.of()); + channel.queueBind(queue, exchange, "#"); + consumer = new MyConsumer(channel); + channel.basicConsume(queue, true, consumer); + } + catch (Exception e) { + fail(e); + } + } @AfterAll @@ -27,8 +64,37 @@ public static void afterAll() { rabbit.stop(); } + static class MyConsumer extends DefaultConsumer { + + BlockingQueue messages = new ArrayBlockingQueue<>(100); + + public MyConsumer(Channel channel) { + super(channel); + } + + @Override + public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) + throws IOException { + var message = new Message(new String(body, StandardCharsets.UTF_8)); + System.out.println(message); + messages.add(message); + + } + + public List blockAndDrain() throws InterruptedException { + List r = new ArrayList<>(); + r.add(messages.poll(5, TimeUnit.SECONDS)); + messages.drainTo(r); + return r; + } + + record Message(String body) { + } + + } + @Test - void testMain() { + void testMain() throws InterruptedException { RabbitMQOutputBuilder b = new RabbitMQOutputBuilder("amqp"); // b.host(rabbit.getHost()); // b.port(rabbit.getAmqpPort()); @@ -36,12 +102,14 @@ void testMain() { b.declareExchange(true); Map properties = new LinkedHashMap<>(); properties.put(LogProperties.APPENDERS_PROPERTY, "amqp"); - properties.put(LogProperties.interpolateKey(LogProperties.APPENDER_PREFIX, Map.of("name", "amqp")), "amqp:///"); + Property.builder().buildWithName(LogAppender.APPENDER_OUTPUT_PROPERTY, "amqp").set("amqp", properties::put); b.toProperties(properties::put); System.out.println(properties); System.out.println(rabbit.getAmqpUrl()); // LoggerFactoryFriend.reset(); RabbitMQSetup.run(properties); + var messages = consumer.blockAndDrain(); + assertEquals(1, messages.size()); } }