From ea34cadd6bd897b20835bf65d5a2bcbb12823f37 Mon Sep 17 00:00:00 2001 From: Adam Gent Date: Mon, 8 Jan 2024 22:05:59 -0500 Subject: [PATCH] Pull out JSON into its own module --- .../io/jstach/rainbowgum/LogProperties.java | 99 ++++++- .../LogConfigurable.java} | 24 +- .../rainbowgum/annotation/package-info.java | 6 + .../io/jstach/rainbowgum/json/JsonBuffer.java | 97 ------- core/src/main/java/module-info.java | 2 +- pom.xml | 6 + .../jstach/rainbowgum/apt/BuilderModel.java | 12 +- .../rainbowgum/apt/ConfigProcessor.java | 25 +- .../rainbowgum/apt/prism/package-info.java | 9 +- .../io/jstach/rainbowgum/apt/Example.java | 2 - rainbowgum-json/pom.xml | 33 +++ .../io/jstach/rainbowgum/json/Grisu3.java | 0 .../io/jstach/rainbowgum/json/JsonBuffer.java | 244 ++++++++++++++++++ .../jstach/rainbowgum/json/RawJsonWriter.java | 0 .../rainbowgum/json/encoder}/GelfEncoder.java | 44 +++- .../rainbowgum/json/encoder/package-info.java | 5 + .../jstach/rainbowgum/json/package-info.java | 0 .../src/main/java/module-info.java | 11 + .../rainbowgum/rabbitmq/RabbitMQOutput.java | 10 +- .../rainbowgum/test/config/ExampleConfig.java | 8 +- 20 files changed, 493 insertions(+), 144 deletions(-) rename core/src/main/java/io/jstach/rainbowgum/{ConfigObject.java => annotation/LogConfigurable.java} (66%) create mode 100644 core/src/main/java/io/jstach/rainbowgum/annotation/package-info.java delete mode 100644 core/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java create mode 100644 rainbowgum-json/pom.xml rename {core => rainbowgum-json}/src/main/java/io/jstach/rainbowgum/json/Grisu3.java (100%) create mode 100644 rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java rename {core => rainbowgum-json}/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java (100%) rename {core/src/main/java/io/jstach/rainbowgum/json => rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder}/GelfEncoder.java (72%) create mode 100644 rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/package-info.java rename {core => rainbowgum-json}/src/main/java/io/jstach/rainbowgum/json/package-info.java (100%) create mode 100644 rainbowgum-json/src/main/java/module-info.java diff --git a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java index 7cf135d1..8b7f8a91 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogProperties.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogProperties.java @@ -3,6 +3,7 @@ import java.lang.System.Logger.Level; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedHashMap; @@ -11,6 +12,7 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -99,6 +101,11 @@ public interface LogProperties { */ static final String OUTPUT_PREFIX = ROOT_PREFIX + "output.{name}."; + /** + * Logging output prefix for configuration. + */ + static final String ENCODER_PREFIX = ROOT_PREFIX + "encoder.{name}."; + /** * Analogous to {@link System#getProperty(String)}. * @param key property key. @@ -184,14 +191,88 @@ public static LogProperties of(List logProperties) { * @see LogOutput */ public static LogProperties of(URI uri) { - var m = parseUriQuery(uri.getRawQuery(), true); + Map m = new LinkedHashMap<>(); + parseUriQuery(uri.getRawQuery(), (k, v) -> { + if (v != null) { + m.put(k, v); + } + }); return new MapProperties(uri.toString(), m); } - private static Map parseUriQuery(String query, boolean decode) { + /** + * Parse a {@linkplain URI#getRawQuery() URI query} in + * application/x-www-form-urlencoded format useful for parsing properties values + * that have key values embedded in them. This parser unlike form encoding + * uses %20 for space as the data is coming from a URI. + * @param query raw query component of URI. + * @param consumer accept will be called for each entry and if key (left hand) does + * not have a "=" following it the second parameter on the consumer + * accept will be passed null. + */ + public static void parseUriQuery(String query, + @SuppressWarnings("exports") BiConsumer consumer) { + parseUriQuery(query, true, consumer); + } + + /** + * Parses a URI query formatted string to a Map. + * @param query raw query component of URI. + * @return decoded key values with last key winning over previous equal keys. + * @see #parseUriQuery(String, BiConsumer) + */ + public static Map parseMap(String query) { + Map m = new LinkedHashMap<>(); + parseUriQuery(query, (k, v) -> { + if (v != null) { + m.put(k, v); + } + }); + return m; + } + + /** + * Parses a URI query for a multi value map. + * @param query raw query component of URI. + * @return decoded key values with multiple keys grouped together in order found. + */ + public static Map> parseMultiMap(String query) { + Map> m = new LinkedHashMap<>(); + BiConsumer f = (k, v) -> { + m.computeIfAbsent(k, _k -> new ArrayList()).add(v); + }; + parseUriQuery(query, true, f); + return m; + } - Map kvs = new LinkedHashMap<>(); - String[] pairs = query.split("&"); + /** + * Parses a list of strings from a string that is + * percent encoded for escaping (application/x-www-form-urlencoded) where the + * separator can be either "&" or ",". + *

+ * An example would be using ampersand:


+	 *  "a&b%20B&c" -> ["a","b B","c"]
+	 *   
and comma:

+	 *  "a,b,c" -> ["a","b","c"]
+	 *   
+ * @param query the comma or ampersand delimited string that is in URI query format. + * @return list of strings + */ + public static List parseList(String query) { + List list = new ArrayList<>(); + parseUriQuery(query, true, "[&,]", (k, v) -> list.add(k)); + return list; + } + + private static void parseUriQuery(String query, boolean decode, BiConsumer consumer) { + parseUriQuery(query, decode, "&", consumer); + } + + private static void parseUriQuery(String query, boolean decode, String sep, + BiConsumer consumer) { + String[] pairs = query.split(sep); for (String pair : pairs) { int idx = pair.indexOf("="); String key; @@ -201,7 +282,7 @@ private static Map parseUriQuery(String query, boolean decode) { } else if (idx < 0) { key = pair; - value = ""; + value = null; } else { key = pair.substring(0, idx); @@ -209,16 +290,16 @@ else if (idx < 0) { } if (decode) { key = PercentCodec.decode(key, StandardCharsets.UTF_8); - value = PercentCodec.decode(key, StandardCharsets.UTF_8); + if (value != null) { + value = PercentCodec.decode(value, StandardCharsets.UTF_8); + } } if (key.isBlank()) { continue; } - kvs.put(key, value); - + consumer.accept(key, value); } - return kvs; } /** diff --git a/core/src/main/java/io/jstach/rainbowgum/ConfigObject.java b/core/src/main/java/io/jstach/rainbowgum/annotation/LogConfigurable.java similarity index 66% rename from core/src/main/java/io/jstach/rainbowgum/ConfigObject.java rename to core/src/main/java/io/jstach/rainbowgum/annotation/LogConfigurable.java index 579dce98..aa2d7574 100644 --- a/core/src/main/java/io/jstach/rainbowgum/ConfigObject.java +++ b/core/src/main/java/io/jstach/rainbowgum/annotation/LogConfigurable.java @@ -1,4 +1,4 @@ -package io.jstach.rainbowgum; +package io.jstach.rainbowgum.annotation; import static java.lang.annotation.RetentionPolicy.CLASS; @@ -8,12 +8,13 @@ import java.lang.annotation.Target; /** - * Used to generate Rainbow Gum config objects + * Used to generate Rainbow Gum config builder objects that once built will call the + * method annotated with the properties from the generated builder on build. */ @Retention(CLASS) @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD }) @Documented -public @interface ConfigObject { +public @interface LogConfigurable { /** * Name of builder. @@ -61,4 +62,21 @@ } + /** + * Use to set static defaults to parameters. + */ + @Retention(CLASS) + @Target({ ElementType.PARAMETER }) + @Documented + public @interface ConvertParameter { + + /** + * Static method on target type to call to convert the parameter. The method must + * be return a type and have a single argument. + * @return static method name. + */ + String value(); + + } + } diff --git a/core/src/main/java/io/jstach/rainbowgum/annotation/package-info.java b/core/src/main/java/io/jstach/rainbowgum/annotation/package-info.java new file mode 100644 index 00000000..1b3bb0ef --- /dev/null +++ b/core/src/main/java/io/jstach/rainbowgum/annotation/package-info.java @@ -0,0 +1,6 @@ +/** + * RainbowGum annotations used for static code generation. + * @see io.jstach.rainbowgum.annotation.LogConfigurable + */ +@org.eclipse.jdt.annotation.NonNullByDefault +package io.jstach.rainbowgum.annotation; \ No newline at end of file diff --git a/core/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java b/core/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java deleted file mode 100644 index 37e49eaa..00000000 --- a/core/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.jstach.rainbowgum.json; - -import io.jstach.rainbowgum.LogEncoder.Buffer; - -import static io.jstach.rainbowgum.json.RawJsonWriter.COMMA; -import static io.jstach.rainbowgum.json.RawJsonWriter.SEMI; - -import org.eclipse.jdt.annotation.Nullable; - -import io.jstach.rainbowgum.LogEvent; -import io.jstach.rainbowgum.LogOutput; - -class JsonBuffer implements Buffer { - - private final RawJsonWriter jsonWriter = new RawJsonWriter(1024 * 8); - - private final StringBuilder formattedMessageBuilder = new StringBuilder(); - - private final boolean prettyprint; - - public static final int EXTENDED_F = 0x00000002; - - public JsonBuffer(boolean prettyprint) { - super(); - this.prettyprint = prettyprint; - } - - @Override - public void drain(LogOutput output, LogEvent event) { - jsonWriter.write(output, event); - clear(); - } - - @Override - public void clear() { - jsonWriter.reset(); - formattedMessageBuilder.setLength(0); - } - - public RawJsonWriter getJsonWriter() { - return jsonWriter; - } - - public StringBuilder getFormattedMessageBuilder() { - return formattedMessageBuilder; - } - - public final int write(String k, @Nullable String v, int index) { - return write(k, v, index, 0); - } - - public final int write(String k, @Nullable String v, int index, int flag) { - if (v == null) - return index; - _writeStartField(k, index, flag); - jsonWriter.writeString(v); - _writeEndField(flag); - return index + 1; - - } - - public final int writeDouble(String k, double v, int index, int flag) { - _writeStartField(k, index, flag); - jsonWriter.writeDouble(v); - _writeEndField(flag); - return index + 1; - } - - public final int writeInt(String k, int v, int index, int flag) { - _writeStartField(k, index, flag); - jsonWriter.writeInt(v); - _writeEndField(flag); - return index + 1; - } - - private final void _writeStartField(String k, int index, int flag) { - if (index > 0) { - jsonWriter.writeByte(COMMA); - } - if (prettyprint) { - jsonWriter.writeAscii("\n"); - } - if (prettyprint) { - jsonWriter.writeAscii(" "); - } - if ((flag & EXTENDED_F) == EXTENDED_F) { - jsonWriter.writeAsciiString("_"); - } - jsonWriter.writeAsciiString(k); - jsonWriter.writeByte(SEMI); - } - - private final void _writeEndField(int flag) { - - } - -} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index df76744d..ae140237 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -9,8 +9,8 @@ module io.jstach.rainbowgum { exports io.jstach.rainbowgum; + exports io.jstach.rainbowgum.annotation; exports io.jstach.rainbowgum.format; - exports io.jstach.rainbowgum.json; exports io.jstach.rainbowgum.output; exports io.jstach.rainbowgum.spi; diff --git a/pom.xml b/pom.xml index 6418e8e5..cdabe143 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,11 @@ rainbowgum-jansi ${project.version} + + ${project.groupId} + rainbowgum-json + ${project.version} + ${project.groupId} rainbowgum-disruptor @@ -788,5 +793,6 @@ etc rainbowgum-rabbitmq rainbowgum-config-apt + rainbowgum-json diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/BuilderModel.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/BuilderModel.java index 9966fb01..79ad01bb 100644 --- a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/BuilderModel.java +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/BuilderModel.java @@ -2,9 +2,12 @@ import java.util.List; +import org.eclipse.jdt.annotation.Nullable; + import io.jstach.jstache.JStache; import io.jstach.jstache.JStacheConfig; import io.jstach.jstache.JStacheType; +import io.jstach.rainbowgum.apt.BuilderModel.PropertyModel; @JStacheConfig(type = JStacheType.STACHE) @JStache(path = "io/jstach/rainbowgum/apt/ConfigBuilder.java") @@ -37,13 +40,17 @@ public List prefixParameters() { return properties.stream().filter(p -> p.kind == PropertyKind.NAME_PARAMETER).toList(); } + record Converter(String methodName) { + } + record PropertyModel(PropertyKind kind, // String name, // String type, // String typeWithAnnotation, // String defaultValue, // boolean required, // - String javadoc) { + String javadoc, // + @Nullable Converter converter) { private static final String INTEGER_TYPE = "java.lang.Integer"; @@ -62,6 +69,9 @@ public String propertyLiteral() { } public String convertMethod() { + if (converter != null) { + return ".map(_v -> " + converter.methodName + "(_v))"; + } return switch (type) { case INTEGER_TYPE -> ".toInt()"; case STRING_TYPE -> ""; diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ConfigProcessor.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ConfigProcessor.java index 61cc6aef..a8318e97 100644 --- a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ConfigProcessor.java +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/ConfigProcessor.java @@ -51,7 +51,8 @@ import org.eclipse.jdt.annotation.Nullable; import io.jstach.rainbowgum.apt.BuilderModel.PropertyModel; -import io.jstach.rainbowgum.apt.prism.ConfigObjectPrism; +import io.jstach.rainbowgum.apt.prism.LogConfigurablePrism; +import io.jstach.rainbowgum.apt.prism.ConvertParameterPrism; import io.jstach.rainbowgum.apt.prism.DefaultParameterPrism; import io.jstach.rainbowgum.apt.prism.PrefixParameterPrism; import io.jstach.svc.ServiceProvider; @@ -59,12 +60,12 @@ /** * Creates ConfigBuilders from static factory methods. */ -@SupportedAnnotationTypes({ ConfigObjectPrism.PRISM_ANNOTATION_TYPE, PrefixParameterPrism.PRISM_ANNOTATION_TYPE, +@SupportedAnnotationTypes({ LogConfigurablePrism.PRISM_ANNOTATION_TYPE, PrefixParameterPrism.PRISM_ANNOTATION_TYPE, DefaultParameterPrism.PRISM_ANNOTATION_TYPE }) @ServiceProvider(value = Processor.class) public class ConfigProcessor extends AbstractProcessor { - private static final String CONFIG_BEAN_CLASS = ConfigObjectPrism.PRISM_ANNOTATION_TYPE; + private static final String CONFIG_BEAN_CLASS = LogConfigurablePrism.PRISM_ANNOTATION_TYPE; /** * No-Arg constructor for Service Loader. @@ -89,7 +90,7 @@ public boolean process(Set annotations, RoundEnvironment continue; } ExecutableElement ee = (ExecutableElement) annotatedElement; - ConfigObjectPrism prism = ConfigObjectPrism.getInstanceOn(annotatedElement); + LogConfigurablePrism prism = LogConfigurablePrism.getInstanceOn(annotatedElement); model(h, prism, ee); } } @@ -102,7 +103,7 @@ public SourceVersion getSupportedSourceVersion() { } @Nullable - private BuilderModel model(Helper h, ConfigObjectPrism prism, ExecutableElement ee) { + private BuilderModel model(Helper h, LogConfigurablePrism prism, ExecutableElement ee) { TypeElement enclosingType = (TypeElement) ee.getEnclosingElement(); String builderName = prism.name(); @@ -179,13 +180,14 @@ private PropertyModel propertyModel(ExecutableElement ee, VariableElement p, Hel String typeWithAnnotation = ToStringTypeVisitor.toCodeSafeString(p.asType()); String defaultValue = "null"; var defaultParameter = DefaultParameterPrism.getInstanceOn(p); + TypeElement enclosingType = (TypeElement) ee.getEnclosingElement(); + String fqnEnclosing = h.getFullyQualifiedClassName(enclosingType.asType()); if (defaultParameter != null) { - TypeElement enclosingType = (TypeElement) ee.getEnclosingElement(); String field = defaultParameter.value(); if (field.isBlank()) { field = "DEFAULT_" + name; } - defaultValue = h.getFullyQualifiedClassName(enclosingType.asType()) + "." + field; + defaultValue = fqnEnclosing + "." + field; } boolean required = !h.isNullable(p.asType()); @@ -204,8 +206,13 @@ private PropertyModel propertyModel(ExecutableElement ee, VariableElement p, Hel if (javadoc == null) { javadoc = ""; } - var prop = new BuilderModel.PropertyModel(kind, name, type, typeWithAnnotation, defaultValue, required, - javadoc); + BuilderModel.Converter c = null; + ConvertParameterPrism converterParameterPrism = ConvertParameterPrism.getInstanceOn(p); + if (converterParameterPrism != null) { + c = new BuilderModel.Converter(fqnEnclosing + "." + converterParameterPrism.value()); + } + var prop = new BuilderModel.PropertyModel(kind, name, type, typeWithAnnotation, defaultValue, required, javadoc, + c); return prop; } diff --git a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/prism/package-info.java b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/prism/package-info.java index ab4dbf62..7125da7d 100644 --- a/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/prism/package-info.java +++ b/rainbowgum-config-apt/src/main/java/io/jstach/rainbowgum/apt/prism/package-info.java @@ -2,9 +2,12 @@ * Prism are generated in this package. This is not public API. */ @io.jstach.prism.GeneratePrisms({ - @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.ConfigObject.class, publicAccess = true), - @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.ConfigObject.PrefixParameter.class, + @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.annotation.LogConfigurable.class, publicAccess = true), - @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.ConfigObject.DefaultParameter.class, + @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.annotation.LogConfigurable.PrefixParameter.class, + publicAccess = true), + @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.annotation.LogConfigurable.DefaultParameter.class, + publicAccess = true), + @io.jstach.prism.GeneratePrism(value = io.jstach.rainbowgum.annotation.LogConfigurable.ConvertParameter.class, publicAccess = true) }) package io.jstach.rainbowgum.apt.prism; \ No newline at end of file diff --git a/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/Example.java b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/Example.java index ef89cf06..654b8d75 100644 --- a/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/Example.java +++ b/rainbowgum-config-apt/src/test/java/io/jstach/rainbowgum/apt/Example.java @@ -1,7 +1,5 @@ package io.jstach.rainbowgum.apt; -import io.jstach.rainbowgum.ConfigObject; - public record Example(Integer count) { } diff --git a/rainbowgum-json/pom.xml b/rainbowgum-json/pom.xml new file mode 100644 index 00000000..79a88635 --- /dev/null +++ b/rainbowgum-json/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + io.jstach.rainbowgum + rainbowgum-maven-parent + 0.1.3-SNAPSHOT + + + ../ + ${basedir}/.. + + rainbowgum-json + + + ${project.groupId} + rainbowgum-core + + + io.jstach.rainbowgum + rainbowgum-config-apt + true + provided + + + io.jstach.pistachio + pistachio-svc + + + io.jstach.pistachio + pistachio-svc-apt + + + \ No newline at end of file diff --git a/core/src/main/java/io/jstach/rainbowgum/json/Grisu3.java b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/Grisu3.java similarity index 100% rename from core/src/main/java/io/jstach/rainbowgum/json/Grisu3.java rename to rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/Grisu3.java diff --git a/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java new file mode 100644 index 00000000..349e704f --- /dev/null +++ b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/JsonBuffer.java @@ -0,0 +1,244 @@ +package io.jstach.rainbowgum.json; + +import io.jstach.rainbowgum.LogEncoder.Buffer; + +import static io.jstach.rainbowgum.json.RawJsonWriter.COMMA; +import static io.jstach.rainbowgum.json.RawJsonWriter.SEMI; + +import org.eclipse.jdt.annotation.Nullable; + +import io.jstach.rainbowgum.LogEvent; +import io.jstach.rainbowgum.LogOutput; + +/** + * A buffer designed for encoding JSON efficiently. + */ +public final class JsonBuffer implements Buffer { + + private final RawJsonWriter jsonWriter = new RawJsonWriter(1024 * 8); + + private final StringBuilder formattedMessageBuilder = new StringBuilder(); + + private final boolean prettyprint; + + private final ExtendedFieldPrefix extendedFieldPrefix; + + /** + * A flag to indicate this field is extended which means it will be prefixed with + * {@link ExtendedFieldPrefix}. + */ + public static final int EXTENDED_F = 0x00000002; + + protected static final byte DEFAULT_EXTENDED_FIELD_PREFIX = '_'; + + /** + * Create a json buffer. + * @param prettyprint whether or not to pretty print the JSON. + * @param extendedFieldPrefix prefix for extended fields. + */ + public JsonBuffer(boolean prettyprint, ExtendedFieldPrefix extendedFieldPrefix) { + super(); + this.prettyprint = prettyprint; + this.extendedFieldPrefix = extendedFieldPrefix; + } + + @Override + public void drain(LogOutput output, LogEvent event) { + jsonWriter.write(output, event); + clear(); + } + + @Override + public void clear() { + jsonWriter.reset(); + formattedMessageBuilder.setLength(0); + } + + /** + * Reusable String buffer for formatted messages. + * @return buffer. + */ + public StringBuilder getFormattedMessageBuilder() { + return formattedMessageBuilder; + } + + /** + * JSON tokens. + */ + public enum JSONToken { + + /** + * Helper for writing JSON object start: { + */ + OBJECT_START(RawJsonWriter.OBJECT_START), + + /** + * Helper for writing JSON object end: } + */ + OBJECT_END(RawJsonWriter.OBJECT_END), + + /** + * Helper for writing JSON array start: [ + */ + ARRAY_START(RawJsonWriter.ARRAY_START), + + /** + * Helper for writing JSON array end: ] + */ + ARRAY_END(RawJsonWriter.ARRAY_END), + + /** + * Helper for writing comma separator: , + */ + COMMA(RawJsonWriter.COMMA), + + /** + * Helper for writing semicolon: : + */ + SEMI(RawJsonWriter.SEMI), + + /** + * Helper for writing JSON quote: " + */ + QUOTE(RawJsonWriter.QUOTE), + + /** + * Helper for writing JSON escape: \\ + */ + ESCAPE(RawJsonWriter.ESCAPE); + + final byte raw; + + private JSONToken(byte raw) { + this.raw = (byte) raw; + } + + } + + private static final byte UNDERSCORE_PREFIX = '_'; + + private static final byte AT_PREFIX = '@'; + + private static byte LF = '\n'; + + private static byte SPACE = ' '; + + /** + * Extended fields are just fields that have some special prefix for things like GELF + * and ECS. + */ + public enum ExtendedFieldPrefix { + + /** + * "_" Underscore prefix + */ + UNDERSCORE(UNDERSCORE_PREFIX), + /** + * "@" At symbol prefix + */ + AT(AT_PREFIX); + + private final byte raw; + + private ExtendedFieldPrefix(byte raw) { + this.raw = raw; + } + + } + + /** + * Writes a JSON token. + * @param token token not null. + */ + public final void write(JSONToken token) { + jsonWriter.writeByte(token.raw); + } + + /** + * Efficiently writes a line feed. + */ + public final void writeLineFeed() { + jsonWriter.writeByte(LF); + } + + /** + * Writes a string field. + * @param k field name + * @param v value + * @param index the current index for comma determination + * @return index + 1 + */ + public final int write(String k, @Nullable String v, int index) { + return write(k, v, index, 0); + } + + /** + * Writes a string field. + * @param k field name + * @param v value + * @param index the current index for comma determination + * @param flag see {@link #EXTENDED_F} + * @return index + 1 + */ + public final int write(String k, @Nullable String v, int index, int flag) { + if (v == null) + return index; + _writeStartField(k, index, flag); + jsonWriter.writeString(v); + _writeEndField(flag); + return index + 1; + + } + + /** + * Writes a double field. + * @param k field name + * @param v value + * @param index the current index for comma determination + * @param flag see {@link #EXTENDED_F} + * @return index + 1 + */ + public final int writeDouble(String k, double v, int index, int flag) { + _writeStartField(k, index, flag); + jsonWriter.writeDouble(v); + _writeEndField(flag); + return index + 1; + } + + /** + * Writes a string field. + * @param k field name + * @param v value + * @param index the current index for comma determination + * @param flag see {@link #EXTENDED_F} + * @return index + 1 + */ + public final int writeInt(String k, int v, int index, int flag) { + _writeStartField(k, index, flag); + jsonWriter.writeInt(v); + _writeEndField(flag); + return index + 1; + } + + private final void _writeStartField(String k, int index, int flag) { + if (index > 0) { + jsonWriter.writeByte(COMMA); + } + if (prettyprint) { + jsonWriter.writeByte(LF); + } + if (prettyprint) { + jsonWriter.writeByte(SPACE); + } + if ((flag & EXTENDED_F) == EXTENDED_F) { + jsonWriter.writeByte(extendedFieldPrefix.raw); + } + jsonWriter.writeAsciiString(k); + jsonWriter.writeByte(SEMI); + } + + private static final void _writeEndField(int flag) { + // ignore for now. + } + +} diff --git a/core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java similarity index 100% rename from core/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java rename to rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/RawJsonWriter.java diff --git a/core/src/main/java/io/jstach/rainbowgum/json/GelfEncoder.java b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/GelfEncoder.java similarity index 72% rename from core/src/main/java/io/jstach/rainbowgum/json/GelfEncoder.java rename to rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/GelfEncoder.java index e872e1de..49bbb845 100644 --- a/core/src/main/java/io/jstach/rainbowgum/json/GelfEncoder.java +++ b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/GelfEncoder.java @@ -1,14 +1,14 @@ -package io.jstach.rainbowgum.json; +package io.jstach.rainbowgum.json.encoder; import static io.jstach.rainbowgum.json.JsonBuffer.EXTENDED_F; -import static io.jstach.rainbowgum.json.RawJsonWriter.OBJECT_END; -import static io.jstach.rainbowgum.json.RawJsonWriter.OBJECT_START; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.System.Logger.Level; import java.time.Instant; import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Objects; import org.eclipse.jdt.annotation.Nullable; @@ -16,8 +16,16 @@ import io.jstach.rainbowgum.LogEncoder; import io.jstach.rainbowgum.LogEvent; import io.jstach.rainbowgum.LogFormatter.LevelFormatter; +import io.jstach.rainbowgum.LogProperties; +import io.jstach.rainbowgum.annotation.LogConfigurable; +import io.jstach.rainbowgum.json.JsonBuffer; +import io.jstach.rainbowgum.json.JsonBuffer.ExtendedFieldPrefix; +import io.jstach.rainbowgum.json.JsonBuffer.JSONToken; -class GelfEncoder extends LogEncoder.AbstractEncoder { +/** + * A JSON encoder in GELF format. + */ +public class GelfEncoder extends LogEncoder.AbstractEncoder { private final String host; @@ -34,15 +42,31 @@ class GelfEncoder extends LogEncoder.AbstractEncoder { this.prettyprint = prettyprint; } + @LogConfigurable(prefix = LogProperties.ENCODER_PREFIX) + public static GelfEncoder of(@LogConfigurable.PrefixParameter String name, String host, // + /* + * @LogConfigurable.ConvertParameter("convertHeaders") @Nullable Map + */ + String headers, @Nullable Boolean prettyPrint) { + prettyPrint = prettyPrint == null ? false : prettyPrint; + host = Objects.requireNonNull(host); + var _headers = KeyValues.of(convertHeaders(headers)); + return new GelfEncoder(host, _headers, prettyPrint); + } + + static Map convertHeaders(String headers) { + return LogProperties.parseMap(headers); + } + @Override protected JsonBuffer doBuffer() { - return new JsonBuffer(this.prettyprint); + return new JsonBuffer(this.prettyprint, ExtendedFieldPrefix.UNDERSCORE); } @Override protected void doEncode(LogEvent event, JsonBuffer buffer) { buffer.clear(); - var raw = buffer.getJsonWriter(); var formattedMessage = buffer.getFormattedMessageBuilder(); final String host = this.host; event.formattedMessage(formattedMessage); @@ -60,7 +84,7 @@ protected void doEncode(LogEvent event, JsonBuffer buffer) { fullMessage = sw.toString(); } int level = levelToSyslogLevel(event.level()); - raw.writeByte(OBJECT_START); + buffer.write(JSONToken.OBJECT_START); int index = 0; index = buffer.write("host", host, index); index = buffer.write("short_message", shortMessage, index); @@ -107,10 +131,10 @@ protected void doEncode(LogEvent event, JsonBuffer buffer) { index = buffer.write("version", "1.1", index); if (index > 0 && prettyprint) { - raw.writeAscii("\n"); + buffer.writeLineFeed(); } - raw.writeByte(OBJECT_END); - raw.writeAscii("\n"); + buffer.write(JSONToken.OBJECT_END); + buffer.writeLineFeed(); } private int levelToSyslogLevel(Level level) { diff --git a/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/package-info.java b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/package-info.java new file mode 100644 index 00000000..78f08b1d --- /dev/null +++ b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/encoder/package-info.java @@ -0,0 +1,5 @@ +/** + * Common JSON encoders. + */ +@org.eclipse.jdt.annotation.NonNullByDefault +package io.jstach.rainbowgum.json.encoder; \ No newline at end of file diff --git a/core/src/main/java/io/jstach/rainbowgum/json/package-info.java b/rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/package-info.java similarity index 100% rename from core/src/main/java/io/jstach/rainbowgum/json/package-info.java rename to rainbowgum-json/src/main/java/io/jstach/rainbowgum/json/package-info.java diff --git a/rainbowgum-json/src/main/java/module-info.java b/rainbowgum-json/src/main/java/module-info.java new file mode 100644 index 00000000..6d8547ed --- /dev/null +++ b/rainbowgum-json/src/main/java/module-info.java @@ -0,0 +1,11 @@ +/** + * Provides JSON formatters and encoders. + * This module does not require external JSON libraries but instead + * provides a zero dependency JSON writer. + */ +module io.jstach.rainbowgum.json { + exports io.jstach.rainbowgum.json; + requires transitive io.jstach.rainbowgum; + + requires static org.eclipse.jdt.annotation; +} \ No newline at end of file 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 4d222681..c5d24587 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 @@ -17,12 +17,12 @@ import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; -import io.jstach.rainbowgum.ConfigObject; import io.jstach.rainbowgum.LogConfig; import io.jstach.rainbowgum.LogEvent; import io.jstach.rainbowgum.LogOutput; import io.jstach.rainbowgum.LogProperties; import io.jstach.rainbowgum.MetaLog; +import io.jstach.rainbowgum.annotation.LogConfigurable; /** * RabbitMQ Output that will write publish messages to a given exchange with a given @@ -70,11 +70,11 @@ public class RabbitMQOutput implements LogOutput { this.exchangeType = exchangeType; } - @ConfigObject(prefix = LogProperties.OUTPUT_PREFIX) + @LogConfigurable(prefix = LogProperties.OUTPUT_PREFIX) static RabbitMQOutput of( // - @ConfigObject.PrefixParameter String name, // + @LogConfigurable.PrefixParameter String name, // @Nullable URI uri, // - @ConfigObject.DefaultParameter("DEFAULT_EXCHANGE") String exchange, // + @LogConfigurable.DefaultParameter("DEFAULT_EXCHANGE") String exchange, // @Nullable String routingKey, // @Nullable Boolean declareExchange, // @Nullable String host, // @@ -83,7 +83,7 @@ static RabbitMQOutput of( // @Nullable Integer port, // @Nullable String appId, // @Nullable String connectionName, // - @ConfigObject.DefaultParameter("DEFAULT_EXCHANGE_TYPE") @Nullable String exchangeType, // + @LogConfigurable.DefaultParameter("DEFAULT_EXCHANGE_TYPE") @Nullable String exchangeType, // @Nullable String virtualHost) { connectionName = connectionName == null ? "rainbowgumOutput" : connectionName; declareExchange = declareExchange == null ? false : declareExchange; diff --git a/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/ExampleConfig.java b/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/ExampleConfig.java index 655f4c79..a8584eca 100644 --- a/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/ExampleConfig.java +++ b/test/rainbowgum-test-config/src/main/java/io/jstach/rainbowgum/test/config/ExampleConfig.java @@ -4,7 +4,7 @@ import org.eclipse.jdt.annotation.Nullable; -import io.jstach.rainbowgum.ConfigObject; +import io.jstach.rainbowgum.annotation.LogConfigurable; public record ExampleConfig(String name, Integer count, @Nullable URI uri) { @@ -15,10 +15,10 @@ public record ExampleConfig(String name, Integer count, @Nullable URI uri) { * @param uri parameter uri. * @return config */ - @ConfigObject(name = "ExampleConfigBuilder", prefix = "logging.example.{name}.") + @LogConfigurable(name = "ExampleConfigBuilder", prefix = "logging.example.{name}.") public static ExampleConfig of( // - @ConfigObject.PrefixParameter String name, // - @ConfigObject.DefaultParameter("DEFAULT_COUNT") Integer count, // + @LogConfigurable.PrefixParameter String name, // + @LogConfigurable.DefaultParameter("DEFAULT_COUNT") Integer count, // @Nullable URI uri) { return new ExampleConfig(name, count, uri); }