From 819193de8b3bf2f6be93de1253b4eb9da9ce6671 Mon Sep 17 00:00:00 2001 From: Alejandro Revilla Date: Wed, 12 Jun 2024 17:56:21 -0300 Subject: [PATCH] Initial take at Structured Audit Log support --- jpos/src/main/java/module-info.java | 10 ++ .../main/java/org/jpos/log/AuditLogEvent.java | 38 +++++ .../main/java/org/jpos/log/LogRenderer.java | 48 ++++++ .../org/jpos/log/LogRendererRegistry.java | 138 ++++++++++++++++++ .../main/java/org/jpos/log/evt/Deploy.java | 33 +++++ .../main/java/org/jpos/log/evt/LogEvt.java | 37 +++++ .../java/org/jpos/log/evt/LogMessage.java | 6 + .../src/main/java/org/jpos/log/evt/Start.java | 36 +++++ jpos/src/main/java/org/jpos/log/evt/Stop.java | 37 +++++ .../main/java/org/jpos/log/evt/UnDeploy.java | 25 ++++ .../json/AuditLogEventJsonLogRenderer.java | 37 +++++ .../render/json/LogEventJsonLogRenderer.java | 73 +++++++++ .../render/json/LoggeableJsonLogRenderer.java | 21 +++ .../ByteArrayMarkdownLogRenderer.java | 23 +++ .../markdown/ContextMarkdownRenderer.java | 37 +++++ .../markdown/ElementMarkdownLogRenderer.java | 33 +++++ .../markdown/LogEventMarkdownRenderer.java | 32 ++++ .../LoggeableMarkdownLogRenderer.java | 21 +++ .../markdown/ObjectMarkdownLogRenderer.java | 18 +++ .../markdown/ProfilerMarkdownRenderer.java | 66 +++++++++ .../markdown/StringMarkdownLogRenderer.java | 17 +++ .../ThrowableMarkdownLogRenderer.java | 26 ++++ ...nManagerTraceArrayMarkdownLogRenderer.java | 24 +++ ...actionManagerTraceMarkdownLogRenderer.java | 19 +++ .../log/render/txt/ObjectTxtLogRenderer.java | 19 +++ .../render/xml/LogEventXmlLogRenderer.java | 73 +++++++++ .../render/xml/LoggeableXmlLogRenderer.java | 19 +++ jpos/src/main/java/org/jpos/q2/Q2.java | 68 ++++++--- .../java/org/jpos/q2/qbean/LoggerAdaptor.java | 17 ++- .../jpos/transaction/TransactionManager.java | 52 ++++--- jpos/src/main/java/org/jpos/util/Caller.java | 2 +- .../java/org/jpos/util/JsonLogWriter.java | 46 ++++++ .../main/java/org/jpos/util/LogListener.java | 4 +- .../main/java/org/jpos/util/Loggeable.java | 11 +- jpos/src/main/java/org/jpos/util/Logger.java | 4 +- .../java/org/jpos/util/MarkdownLogEvent.java | 26 ++++ .../java/org/jpos/util/RotateLogListener.java | 12 +- .../java/org/jpos/util/SimpleLogListener.java | 46 ++---- .../main/java/org/jpos/util/TxtLogWriter.java | 57 ++++++++ .../main/java/org/jpos/util/XmlLogWriter.java | 47 ++++++ .../services/org.jpos.log.LogRenderer | 20 +++ .../org/jpos/util/DailyLogListenerTest.java | 2 + .../org/jpos/util/RotateLogListenerTest.java | 8 +- .../org/jpos/util/SimpleLogListenerTest.java | 29 +--- 44 files changed, 1308 insertions(+), 109 deletions(-) create mode 100644 jpos/src/main/java/org/jpos/log/AuditLogEvent.java create mode 100644 jpos/src/main/java/org/jpos/log/LogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/LogRendererRegistry.java create mode 100644 jpos/src/main/java/org/jpos/log/evt/Deploy.java create mode 100644 jpos/src/main/java/org/jpos/log/evt/LogEvt.java create mode 100644 jpos/src/main/java/org/jpos/log/evt/LogMessage.java create mode 100644 jpos/src/main/java/org/jpos/log/evt/Start.java create mode 100644 jpos/src/main/java/org/jpos/log/evt/Stop.java create mode 100644 jpos/src/main/java/org/jpos/log/evt/UnDeploy.java create mode 100644 jpos/src/main/java/org/jpos/log/render/json/AuditLogEventJsonLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/json/LogEventJsonLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/json/LoggeableJsonLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/ByteArrayMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/ContextMarkdownRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/ElementMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/LogEventMarkdownRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/LoggeableMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/ObjectMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/ProfilerMarkdownRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/StringMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/ThrowableMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceArrayMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceMarkdownLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/txt/ObjectTxtLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/xml/LogEventXmlLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/log/render/xml/LoggeableXmlLogRenderer.java create mode 100644 jpos/src/main/java/org/jpos/util/JsonLogWriter.java create mode 100644 jpos/src/main/java/org/jpos/util/MarkdownLogEvent.java create mode 100644 jpos/src/main/java/org/jpos/util/TxtLogWriter.java create mode 100644 jpos/src/main/java/org/jpos/util/XmlLogWriter.java create mode 100644 jpos/src/main/resources/META-INF/services/org.jpos.log.LogRenderer diff --git a/jpos/src/main/java/module-info.java b/jpos/src/main/java/module-info.java index 19d63f3224..a7fc66761f 100644 --- a/jpos/src/main/java/module-info.java +++ b/jpos/src/main/java/module-info.java @@ -17,6 +17,10 @@ requires micrometer.core; requires micrometer.registry.prometheus; requires org.apache.sshd; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.datatype.jsr310; + requires com.fasterxml.jackson.dataformat.xml; // requires net.i2p.crypto.eddsa; exports org.jpos.iso.packager; @@ -45,6 +49,12 @@ exports org.jpos.core; exports org.jpos.core.handlers.exception; exports org.jpos.rc; + exports org.jpos.log; + exports org.jpos.log.render.xml; + exports org.jpos.log.render.json; + exports org.jpos.log.render.markdown; + exports org.jpos.log.evt; uses org.jpos.core.EnvironmentProvider; + uses org.jpos.log.LogRenderer; } diff --git a/jpos/src/main/java/org/jpos/log/AuditLogEvent.java b/jpos/src/main/java/org/jpos/log/AuditLogEvent.java new file mode 100644 index 0000000000..852cfdf47e --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/AuditLogEvent.java @@ -0,0 +1,38 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2023 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.log; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.jpos.log.evt.*; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "t" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = Start.class, name = "start"), + @JsonSubTypes.Type(value = Stop.class, name = "stop"), + @JsonSubTypes.Type(value = Deploy.class, name = "deploy"), + @JsonSubTypes.Type(value = UnDeploy.class, name = "undeploy"), + @JsonSubTypes.Type(value = LogMessage.class, name = "msg") +}) + +public sealed interface AuditLogEvent permits LogMessage, Deploy, UnDeploy, Start, Stop { } diff --git a/jpos/src/main/java/org/jpos/log/LogRenderer.java b/jpos/src/main/java/org/jpos/log/LogRenderer.java new file mode 100644 index 0000000000..035a637466 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/LogRenderer.java @@ -0,0 +1,48 @@ +package org.jpos.log; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +public interface LogRenderer { + void render (T obj, PrintStream ps, String indent); + Class clazz(); + Type type(); + + default void render (T obj, PrintStream ps) { + render (obj, ps, ""); + } + + default String render (T obj, String indent) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + render (obj, ps, indent); + return baos.toString(); + } + + default String render (T obj) { + return render (obj, ""); + } + + default String indent (String indent, String s) { + if (s == null || s.isEmpty() || indent==null || indent.isEmpty()) { + return s; + } + String[] lines = s.split("\n", -1); // Preserve trailing empty strings + StringBuilder indentedString = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + indentedString.append(indent).append(lines[i]); + if (i < lines.length - 1) { + indentedString.append("\n"); + } + } + return indentedString.toString(); + } + + + enum Type { + XML, + JSON, + TXT, + MARKDOWN + } +} diff --git a/jpos/src/main/java/org/jpos/log/LogRendererRegistry.java b/jpos/src/main/java/org/jpos/log/LogRendererRegistry.java new file mode 100644 index 0000000000..fbc12e35d0 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/LogRendererRegistry.java @@ -0,0 +1,138 @@ +package org.jpos.log; + +import java.io.PrintStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A registry for managing {@link LogRenderer} instances associated with specific class types and renderer types. + * This class allows for the registration, retrieval, and management of {@link LogRenderer} instances dynamically, + * using a thread-safe approach to ensure proper operation in multi-threaded environments. + */ +public class LogRendererRegistry { + private static final Map> renderers = Collections.synchronizedMap( + new LinkedHashMap<>() + ); + static { + for (LogRenderer r : ServiceLoader.load(LogRenderer.class)) { + register (r); + } + } + + /** + * Registers a {@link LogRenderer} in the registry with a key generated from the renderer's class and type. + * @param renderer The renderer to register. Must not be null. + * @throws NullPointerException if the renderer is null. + */ + public static void register (LogRenderer renderer) { + Objects.requireNonNull(renderer); + renderers.put(new LogRendererRegistry.Key(renderer.clazz(), renderer.type()), renderer); + } + + /** + * Dumps the current state of the registry to the specified {@link PrintStream}. + * @param ps The {@link PrintStream} to which the dump will be written, e.g.: System.out + */ + public static void dump (PrintStream ps) { + ps.println (LogRendererRegistry.class); + for (Map.Entry> entry : renderers.entrySet()) { + ps.println(" " + entry.getKey() + ": " + entry.getValue().getClass()); + } + ps.println (); + } + + /** + * Retrieves a {@link LogRenderer} that matches the specified class and type. If no direct match is found, + * it attempts to find a renderer for any superclass or implemented interfaces. If no specific renderer is found, + * it defaults to a renderer for {@link Object}, if present for the given type. + * + * @param clazz The class for which a renderer is required. + * @param type The type of the renderer. + * @return The matching {@link LogRenderer}, or a default renderer if no specific match is found. + */ + @SuppressWarnings("unchecked") + public static LogRenderer getRenderer(Class clazz, LogRenderer.Type type) { + LogRenderer renderer = getRendererForClass(clazz, type); + boolean needsCache = false; + if (renderer == null) { + needsCache = true; + renderer = getRendererForInterface(clazz, type); + } + if (renderer == null) { + renderer = (LogRenderer) renderers.get(new LogRendererRegistry.Key(Object.class, type)); + } + if (renderer != null && needsCache) + renderers.put(new LogRendererRegistry.Key(clazz, renderer.type(), true), renderer); + return renderer; + } + + /** + * Recursively searches for a renderer for the given class and type, considering superclasses. + * + * @param clazz The class for which a renderer is needed. + * @param type The type of the renderer. + * @return A matching renderer, or null if none is found. + */ + @SuppressWarnings("unchecked") + private static LogRenderer getRendererForClass (Class clazz, LogRenderer.Type type) { + LogRenderer renderer = (LogRenderer) renderers.get(new LogRendererRegistry.Key(clazz, type)); + if (renderer == null && clazz.getSuperclass() != Object.class) { + renderer = getRendererForClass(clazz.getSuperclass(), type); + } + return renderer; + } + + /** + * Searches for a renderer among the interfaces implemented by the specified class. + * + * @param clazz The class whose interfaces will be checked for a matching renderer. + * @param type The type of the renderer. + * @return A matching renderer, or null if none is found. + */ + @SuppressWarnings("unchecked") + private static LogRenderer getRendererForInterface (Class clazz, LogRenderer.Type type) { + for (Class i : clazz.getInterfaces()) { + LogRenderer renderer = (LogRenderer) renderers.get(new LogRendererRegistry.Key(i, type)); + if (renderer != null) + return renderer; + } + return null; + } + + /** + * A private key class to encapsulate the combination of class type and renderer type. + * This key is used to uniquely identify renderers within the registry. + */ + private static class Key { + private final Class clazz; + private final LogRenderer.Type type; + private final boolean cache; + + public Key(Class clazz, LogRenderer.Type type) { + this (clazz, type, false); + } + public Key(Class clazz, LogRenderer.Type type, boolean cache) { + this.clazz = clazz; + this.type = type; + this.cache = cache; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LogRendererRegistry.Key key = (LogRendererRegistry.Key) o; + return Objects.equals(clazz, key.clazz) && type == key.type; + } + + @Override + public int hashCode() { + return Objects.hash(clazz, type); + } + + @Override + public String toString() { + return "Key{" + clazz + ", type=" + type + (cache ? ", cached" : "") + '}'; + } + } +} diff --git a/jpos/src/main/java/org/jpos/log/evt/Deploy.java b/jpos/src/main/java/org/jpos/log/evt/Deploy.java new file mode 100644 index 0000000000..78d9d688f4 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/evt/Deploy.java @@ -0,0 +1,33 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2023 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.log.evt; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.jpos.log.AuditLogEvent; + +/** + * Represents a deployment configuration with a specific path and an enabled status. + * + *

This record is used as part of the audit log events in the system to track deployment actions.

+ * + * @param path the path where the deployment is located + * @param enabled a boolean flag indicating whether the deployment is enabled + */ + +public record Deploy(String path, @JacksonXmlProperty(isAttribute = true) boolean enabled) implements AuditLogEvent { } diff --git a/jpos/src/main/java/org/jpos/log/evt/LogEvt.java b/jpos/src/main/java/org/jpos/log/evt/LogEvt.java new file mode 100644 index 0000000000..82e9b72d2e --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/evt/LogEvt.java @@ -0,0 +1,37 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2010 Alejandro P. Revilla + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.log.evt; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import org.jpos.log.AuditLogEvent; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@JacksonXmlRootElement(localName = "log") +public record LogEvt( + @JacksonXmlProperty(isAttribute = true) Instant ts, + @JacksonXmlProperty(isAttribute = true) @JsonProperty("trace-id") UUID traceId, + @JacksonXmlProperty(isAttribute = true) String realm, + @JacksonXmlProperty(isAttribute = true) String tag, + @JacksonXmlProperty(isAttribute = true) Long elapsed, + @JsonProperty("evt") @JacksonXmlElementWrapper(useWrapping = false) List events) { } \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/log/evt/LogMessage.java b/jpos/src/main/java/org/jpos/log/evt/LogMessage.java new file mode 100644 index 0000000000..7f82a28650 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/evt/LogMessage.java @@ -0,0 +1,6 @@ +package org.jpos.log.evt; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jpos.log.AuditLogEvent; + +public record LogMessage(@JsonProperty("m") String msg) implements AuditLogEvent { } diff --git a/jpos/src/main/java/org/jpos/log/evt/Start.java b/jpos/src/main/java/org/jpos/log/evt/Start.java new file mode 100644 index 0000000000..30446d992d --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/evt/Start.java @@ -0,0 +1,36 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2023 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.log.evt; + +import org.jpos.log.AuditLogEvent; + +import java.util.UUID; + +/** + * Represents the starting log entry for an auditing process in the system. This record encapsulates + * all the essential details needed for initializing audit logs in a structured and consistent format. + * + * @param q2 The identifier of the Q2 system instance from which the log is originating. + * @param version The version of the Q2 system, detailing the specific build or release version. + * @param appVersion The version of the application that is running within the Q2 system, + * providing context about the application's release state. + * @param deploy Absolute path to Q2's deploy directory. + * @param env The name of the environment in which the application is running. + */ +public record Start(UUID q2, String version, String appVersion, String deploy, String env) implements AuditLogEvent { } diff --git a/jpos/src/main/java/org/jpos/log/evt/Stop.java b/jpos/src/main/java/org/jpos/log/evt/Stop.java new file mode 100644 index 0000000000..7ede688e4f --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/evt/Stop.java @@ -0,0 +1,37 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2023 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.log.evt; + +import org.jpos.log.AuditLogEvent; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +/** + * Represents the stopping log entry, marking the completion of a log instance (end of file/run). + * + * @param ts The timestamp marking the exact time this log event was created, represented as an {@link Instant}. + * @param id The unique identifier of the Q2 instance, corresponding to the {@link UUID} initialized at the + * start of the process. This ID links the stop event directly with its corresponding start event. + * @param uptime The duration between the {@link Start} event and this {@link Stop} event for the specific log instance, + * given as a {@link Duration}. This measures the total time taken for the event, providing insights into + * performance and operational efficiency. + */ +public record Stop(Instant ts, UUID id, Duration uptime) implements AuditLogEvent { } diff --git a/jpos/src/main/java/org/jpos/log/evt/UnDeploy.java b/jpos/src/main/java/org/jpos/log/evt/UnDeploy.java new file mode 100644 index 0000000000..8ee994de51 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/evt/UnDeploy.java @@ -0,0 +1,25 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2023 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.log.evt; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.jpos.log.AuditLogEvent; + +public record UnDeploy(String path, @JacksonXmlProperty(isAttribute = true) boolean start) implements AuditLogEvent { } + diff --git a/jpos/src/main/java/org/jpos/log/render/json/AuditLogEventJsonLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/json/AuditLogEventJsonLogRenderer.java new file mode 100644 index 0000000000..e8dafadb79 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/json/AuditLogEventJsonLogRenderer.java @@ -0,0 +1,37 @@ +package org.jpos.log.render.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.jpos.log.AuditLogEvent; +import org.jpos.log.LogRenderer; + +import java.io.PrintStream; + +public final class AuditLogEventJsonLogRenderer implements LogRenderer { + private final ObjectMapper mapper = new ObjectMapper(); + + public AuditLogEventJsonLogRenderer () { + mapper.registerModule(new JavaTimeModule()); + mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + + @Override + public void render(AuditLogEvent evt, PrintStream ps, String indent) { + try { + ps.print (mapper.writeValueAsString(evt)); + } catch (JsonProcessingException e) { + ps.print (kv("exception", e.toString())); + } + } + public Class clazz() { + return AuditLogEvent.class; + } + public Type type() { + return Type.JSON; + } + + private String kv (String k, String v) { + return "{\"%s\":\"%s\"}".formatted(k,v); + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/json/LogEventJsonLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/json/LogEventJsonLogRenderer.java new file mode 100644 index 0000000000..1610bece13 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/json/LogEventJsonLogRenderer.java @@ -0,0 +1,73 @@ +package org.jpos.log.render.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.jpos.log.AuditLogEvent; + +import org.jpos.log.LogRenderer; +import org.jpos.log.evt.LogEvt; +import org.jpos.log.evt.LogMessage; +import org.jpos.util.LogEvent; +import org.jpos.util.Loggeable; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.Duration; +import java.util.List; +import java.util.UUID; + +public final class LogEventJsonLogRenderer implements LogRenderer { + private final ObjectMapper mapper = new ObjectMapper(); + + public LogEventJsonLogRenderer() { + mapper.registerModule(new JavaTimeModule()); + mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + @Override + public void render(LogEvent evt, PrintStream ps, String indent) { + List events = evt.getPayLoad() + .stream() + .map (obj -> obj instanceof AuditLogEvent ? (AuditLogEvent) obj : new LogMessage(dump(obj))) + .toList(); + long elapsed = Duration.between(evt.getCreatedAt(), evt.getDumpedAt()).toMillis(); + LogEvt ev = new LogEvt ( + evt.getDumpedAt(), + UUID.randomUUID(), + evt.getRealm(), + evt.getTag(), + elapsed == 0L ? null : elapsed, + events + ); + try { + ps.println (mapper.writeValueAsString(ev)); + } catch (JsonProcessingException e) { + ps.print (kv("exception", e.toString())); + } + } + public Class clazz() { + return LogEvent.class; + } + public Type type() { + return Type.JSON; + } + + private String kv (String k, String v) { + return "{\"%s\":\"%s\"}".formatted(k,v); + } + + private String dump (Object obj) { + if (obj instanceof Loggeable loggeable) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + loggeable.dump(ps, ""); + return baos.toString(); + } + return obj.toString(); + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/json/LoggeableJsonLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/json/LoggeableJsonLogRenderer.java new file mode 100644 index 0000000000..38b785e3d6 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/json/LoggeableJsonLogRenderer.java @@ -0,0 +1,21 @@ +package org.jpos.log.render.json; + +import org.jpos.log.LogRenderer; +import org.jpos.util.Loggeable; + +import java.io.PrintStream; + +public final class LoggeableJsonLogRenderer implements LogRenderer { + @Override + public void render(Loggeable obj, PrintStream ps, String indent) { + ps.println("{\"" + obj.getClass().getSimpleName()+"\": \""); + obj.dump (ps, indent); + ps.println("\"}"); + } + public Class clazz() { + return Loggeable.class; + } + public Type type() { + return Type.JSON; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/ByteArrayMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/ByteArrayMarkdownLogRenderer.java new file mode 100644 index 0000000000..5c6af6ca95 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/ByteArrayMarkdownLogRenderer.java @@ -0,0 +1,23 @@ +package org.jpos.log.render.markdown; + +import org.jpos.iso.ISOUtil; +import org.jpos.log.LogRenderer; + +import java.io.PrintStream; + +public final class ByteArrayMarkdownLogRenderer implements LogRenderer { + @Override + public void render(byte[] b, PrintStream ps, String indent) { + if (b.length > 16) { + ps.printf ("```%n%s%n```%n", indent(indent, ISOUtil.hexdump(b))); + } else { + ps.printf ("`%s`%n", indent(indent,ISOUtil.hexString(b))); + } + } + public Class clazz() { + return byte[].class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/ContextMarkdownRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/ContextMarkdownRenderer.java new file mode 100644 index 0000000000..814d979ff0 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/ContextMarkdownRenderer.java @@ -0,0 +1,37 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import org.jpos.log.LogRendererRegistry; +import org.jpos.transaction.Context; + +import java.io.PrintStream; +import java.util.Map; + +public final class ContextMarkdownRenderer implements LogRenderer { + @Override + public void render(Context ctx, PrintStream ps, String indent) { + Map map = ctx.getMapClone(); + map.forEach((key, value) -> formatEntry(key.toString(), value, ps)); + } + + public Class clazz() { + return Context.class; + } + public Type type() { + return Type.MARKDOWN; + } + + private void formatEntry (String key, Object value, PrintStream ps) { + LogRenderer renderer = LogRendererRegistry.getRenderer(value.getClass(), Type.MARKDOWN); + if (renderer != null) { + ps.printf ("### %s%n", key); + // ps.printf ("> %s%n%n", Caller.shortClassName(renderer.getClass().getCanonicalName())); + renderer.render (value, ps, ""); + } else { + ps.printf ("No renderer could be found for class %s%n", value.getClass()); + ps.printf ("### %s%n```%n", key); + ps.println(value); + ps.println("```"); + } + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/ElementMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/ElementMarkdownLogRenderer.java new file mode 100644 index 0000000000..5e780a483d --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/ElementMarkdownLogRenderer.java @@ -0,0 +1,33 @@ +package org.jpos.log.render.markdown; + +import org.jdom2.Element; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; +import org.jpos.log.LogRenderer; +import java.io.IOException; +import java.io.PrintStream; + +public final class ElementMarkdownLogRenderer implements LogRenderer { + final XMLOutputter out = new XMLOutputter(Format.getPrettyFormat()); + + public ElementMarkdownLogRenderer() { + out.getFormat().setLineSeparator("\n"); + } + @Override + public void render(Element o, PrintStream ps, String indent) { + ps.println("```xml"); + + try { + out.output(o, ps); + } catch (IOException ex) { + ex.printStackTrace(ps); + } + ps.println("```"); + } + public Class clazz() { + return Element.class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/LogEventMarkdownRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/LogEventMarkdownRenderer.java new file mode 100644 index 0000000000..d674717066 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/LogEventMarkdownRenderer.java @@ -0,0 +1,32 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import org.jpos.log.LogRendererRegistry; +import org.jpos.util.LogEvent; + +import java.io.PrintStream; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public final class LogEventMarkdownRenderer implements LogRenderer { + @Override + public void render(LogEvent evt, PrintStream ps, String indent) { + ps.printf ("## %s %s %s%s%n", + LocalDateTime.ofInstant(evt.getDumpedAt(), ZoneId.systemDefault()), + evt.getRealm(), + evt.getTag(), + evt.hasException() ? " (*)" : "" + ); + + indent = indent + " "; + for (Object obj : evt.getPayLoad()) { + LogRendererRegistry.getRenderer(obj.getClass(), Type.MARKDOWN).render(obj, ps, indent); + } + } + public Class clazz() { + return LogEvent.class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/LoggeableMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/LoggeableMarkdownLogRenderer.java new file mode 100644 index 0000000000..74ec8cdce8 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/LoggeableMarkdownLogRenderer.java @@ -0,0 +1,21 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import org.jpos.util.Loggeable; + +import java.io.PrintStream; + +public final class LoggeableMarkdownLogRenderer implements LogRenderer { + @Override + public void render(Loggeable obj, PrintStream ps, String indent) { + ps.println("```xml"); + obj.dump (ps, indent); + ps.println("```"); + } + public Class clazz() { + return Loggeable.class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/ObjectMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/ObjectMarkdownLogRenderer.java new file mode 100644 index 0000000000..a1062cdeaa --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/ObjectMarkdownLogRenderer.java @@ -0,0 +1,18 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import java.io.PrintStream; + +public final class ObjectMarkdownLogRenderer implements LogRenderer { + @Override + public void render(Object obj, PrintStream ps, String indent) { + // ps.printf ("> %s%n%n%s%n", obj.getClass().getCanonicalName(), obj); + ps.printf ("%s%n", indent(indent,obj.toString())); + } + public Class clazz() { + return Object.class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/ProfilerMarkdownRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/ProfilerMarkdownRenderer.java new file mode 100644 index 0000000000..05dcbcd116 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/ProfilerMarkdownRenderer.java @@ -0,0 +1,66 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import org.jpos.util.Profiler; + +import java.io.PrintStream; +import java.util.Set; + +import static java.lang.StringTemplate.STR; +import static java.util.FormatProcessor.FMT; + +public final class ProfilerMarkdownRenderer implements LogRenderer { + public ProfilerMarkdownRenderer() { + } + + @Override + public void render(Profiler prof, PrintStream ps, String indent) { + var events = prof.getEvents(); + int width = maxLength(events.keySet()); + // String fmt = prettyPrint ? STR."%-\{"%d".formatted(maxLength(events.keySet()))}" : "%"; + final String fmt = STR."| %-\{width}s | %10.10s | %10.10s |%n"; + ps.print (row(fmt, "Checkpoint", "Elapsed", "Total")); + ps.print( + row(fmt, "-".repeat(width), "---------:", "-------:") + ); + StringBuilder graph = new StringBuilder(); + events.forEach((key, v) -> { + ps.print( + row(fmt, v.getEventName(), toMillis(v.getDurationInNanos()), toMillis(v.getTotalDurationInNanos())) + ); + graph.append (" \"%s\" : %s%n".formatted(key, toMillis(v.getDurationInNanos()))); + }); + ps.println(); + ps.println ("```mermaid"); + ps.println ("pie title Profiler"); + ps.println (graph); + ps.println ("```"); + + } + public Class clazz() { + return Profiler.class; + } + + public Type type() { + return Type.MARKDOWN; + } + private String row (String fmt, String c1, String c2, String c3) { + return fmt.formatted(c1, c2, c3); + } + private String toString(Profiler.Entry e, String fmt) { + return FMT.""" + \{fmt}s |\{toMillis(e.getDurationInNanos())} | \{toMillis(e.getTotalDurationInNanos())} | + """.formatted(e.getEventName()); + } + + private String toMillis(long nanos) { + return FMT."%d\{nanos / Profiler.TO_MILLIS}.%03d\{nanos % Profiler.TO_MILLIS % 1000}"; + } + + private int maxLength (Set keys) { + return keys.stream() + .mapToInt(String::length) + .max() + .orElse(0); // R + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/StringMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/StringMarkdownLogRenderer.java new file mode 100644 index 0000000000..17861aff0e --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/StringMarkdownLogRenderer.java @@ -0,0 +1,17 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import java.io.PrintStream; + +public final class StringMarkdownLogRenderer implements LogRenderer { + @Override + public void render(String s, PrintStream ps, String indent) { + ps.println (indent(indent, s)); + } + public Class clazz() { + return String.class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/ThrowableMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/ThrowableMarkdownLogRenderer.java new file mode 100644 index 0000000000..8207ea74c5 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/ThrowableMarkdownLogRenderer.java @@ -0,0 +1,26 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +public final class ThrowableMarkdownLogRenderer implements LogRenderer { + @Override + public void render(Throwable t, PrintStream ps, String indent) { + ps.println(stackTrace(indent+" ", t)); + } + public Class clazz() { + return Throwable.class; + } + public Type type() { + return Type.MARKDOWN; + } + + private String stackTrace(String indent, Throwable t) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + t.printStackTrace(ps); + return indent(indent, baos.toString()); + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceArrayMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceArrayMarkdownLogRenderer.java new file mode 100644 index 0000000000..0d1c5c71d7 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceArrayMarkdownLogRenderer.java @@ -0,0 +1,24 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import static org.jpos.transaction.TransactionManager.Trace; + +import java.io.PrintStream; + +public final class TransactionManagerTraceArrayMarkdownLogRenderer implements LogRenderer { + @Override + public void render(Trace[] traces, PrintStream ps, String indent) { + ps.println ("```mermaid"); + ps.println ("gitGraph"); + for (int i=0; i clazz() { + return Trace[].class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceMarkdownLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceMarkdownLogRenderer.java new file mode 100644 index 0000000000..9115f8bf1a --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/markdown/TransactionManagerTraceMarkdownLogRenderer.java @@ -0,0 +1,19 @@ +package org.jpos.log.render.markdown; + +import org.jpos.log.LogRenderer; +import org.jpos.transaction.TransactionManager; + +import java.io.PrintStream; + +public final class TransactionManagerTraceMarkdownLogRenderer implements LogRenderer { + @Override + public void render(TransactionManager.Trace t, PrintStream ps, String indent) { + ps.println (indent(indent, t.toString())); + } + public Class clazz() { + return TransactionManager.Trace.class; + } + public Type type() { + return Type.MARKDOWN; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/txt/ObjectTxtLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/txt/ObjectTxtLogRenderer.java new file mode 100644 index 0000000000..634b61392a --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/txt/ObjectTxtLogRenderer.java @@ -0,0 +1,19 @@ +package org.jpos.log.render.txt; + +import org.jpos.log.LogRenderer; +import org.jpos.util.Loggeable; + +import java.io.PrintStream; + +public final class ObjectTxtLogRenderer implements LogRenderer { + @Override + public void render(Object obj, PrintStream ps, String indent) { + ps.printf ("%s%s%n", indent, obj.toString().replaceAll("\\r\\n|\\r|\\n", ("\\\\n"))); + } + public Class clazz() { + return Object.class; + } + public Type type() { + return Type.TXT; + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/xml/LogEventXmlLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/xml/LogEventXmlLogRenderer.java new file mode 100644 index 0000000000..05e94d4234 --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/xml/LogEventXmlLogRenderer.java @@ -0,0 +1,73 @@ +package org.jpos.log.render.xml; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.jpos.log.AuditLogEvent; + +import org.jpos.log.LogRenderer; +import org.jpos.log.evt.LogEvt; +import org.jpos.log.evt.LogMessage; +import org.jpos.util.LogEvent; +import org.jpos.util.Loggeable; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.Duration; +import java.util.List; +import java.util.UUID; + +public final class LogEventXmlLogRenderer implements LogRenderer { + private final XmlMapper mapper = new XmlMapper(); + + public LogEventXmlLogRenderer() { + mapper.registerModule(new JavaTimeModule()); + mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + } + + @Override + public void render(LogEvent evt, PrintStream ps, String indent) { + List events = evt.getPayLoad() + .stream() + .map (obj -> obj instanceof AuditLogEvent ? (AuditLogEvent) obj : new LogMessage(dump(obj))) + .toList(); + long elapsed = Duration.between(evt.getCreatedAt(), evt.getDumpedAt()).toMillis(); + LogEvt ev = new LogEvt ( + evt.getDumpedAt(), + UUID.randomUUID(), + evt.getRealm(), + evt.getTag(), + elapsed == 0L ? null : elapsed, + events + ); + try { + ps.println (mapper.writeValueAsString(ev)); + } catch (JsonProcessingException e) { + ps.print (kv("exception", e.toString())); + } + } + public Class clazz() { + return LogEvent.class; + } + public Type type() { + return Type.XML; + } + + private String kv (String k, String v) { + return "{\"%s\":\"%s\"}".formatted(k,v); + } + + private String dump (Object obj) { + if (obj instanceof Loggeable loggeable) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + loggeable.dump(ps, ""); + return baos.toString(); + } + return obj.toString(); + } +} diff --git a/jpos/src/main/java/org/jpos/log/render/xml/LoggeableXmlLogRenderer.java b/jpos/src/main/java/org/jpos/log/render/xml/LoggeableXmlLogRenderer.java new file mode 100644 index 0000000000..aa43ba702b --- /dev/null +++ b/jpos/src/main/java/org/jpos/log/render/xml/LoggeableXmlLogRenderer.java @@ -0,0 +1,19 @@ +package org.jpos.log.render.xml; + +import org.jpos.log.LogRenderer; +import org.jpos.util.Loggeable; + +import java.io.PrintStream; + +public final class LoggeableXmlLogRenderer implements LogRenderer { + @Override + public void render(Loggeable obj, PrintStream ps, String indent) { + obj.dump (ps, indent); + } + public Class clazz() { + return Loggeable.class; + } + public Type type() { + return Type.XML; + } +} diff --git a/jpos/src/main/java/org/jpos/q2/Q2.java b/jpos/src/main/java/org/jpos/q2/Q2.java index e583a01224..400c23b5a8 100644 --- a/jpos/src/main/java/org/jpos/q2/Q2.java +++ b/jpos/src/main/java/org/jpos/q2/Q2.java @@ -43,6 +43,10 @@ import org.jpos.core.Environment; import org.jpos.iso.ISOException; import org.jpos.iso.ISOUtil; +import org.jpos.log.evt.Start; +import org.jpos.log.evt.Stop; +import org.jpos.log.evt.Deploy; +import org.jpos.log.evt.UnDeploy; import org.jpos.metrics.PrometheusService; import org.jpos.q2.install.ModuleUtils; // import org.jpos.q2.ssh.SshService; @@ -321,10 +325,12 @@ public boolean ready (long millis) { return ready(); } public void shutdown (boolean join) { + if (log != null) + log.info (auditStop(Duration.between(startTime, Instant.now()))); shutdown.countDown(); unregisterQ2(); if (q2Thread != null) { - log.info ("shutting down"); + // log.info ("shutting down"); q2Thread.interrupt (); if (join) { try { @@ -436,6 +442,7 @@ public void run () { log.info ("shutting down (hook/" + shutdownHookDelay + ")"); if (shutdownHookDelay > 0) ISOUtil.sleep(shutdownHookDelay); + log.info (auditStop(Duration.between(startTime, Instant.now()))); shuttingDown = true; shutdown.countDown(); if (q2Thread != null) { @@ -453,6 +460,7 @@ public void run () { // exception. } } + log.info (auditStop(Duration.between(startTime, Instant.now()))); } } ); @@ -522,10 +530,10 @@ private long persist (File f, ObjectName name) { } private void undeploy (File f) { - QEntry qentry = (QEntry) dirMap.get (f); + QEntry qentry = dirMap.get (f); try { if (log != null) - log.trace ("undeploying:" + f.getCanonicalPath()); + log.info (new UnDeploy(f.getCanonicalPath(), true)); if (qentry.isQBean()) { Object obj = qentry.getObject (); @@ -533,7 +541,7 @@ private void undeploy (File f) { factory.destroyQBean (this, name, obj); } if (log != null) - log.info ("undeployed:" + f.getCanonicalPath()); + log.info (new UnDeploy(f.getCanonicalPath(), false)); } catch (Exception e) { getLog().warn ("undeploy", e); @@ -557,7 +565,10 @@ private boolean register (File f) { private boolean deploy (File f) { LogEvent evt = log != null ? log.createInfo() : null; + boolean enabled = false; + String filePath = ""; try { + filePath = f.getCanonicalPath(); QEntry qentry = dirMap.get (f); SAXBuilder builder = createSAXBuilder(); Document doc; @@ -579,19 +590,16 @@ private boolean deploy (File f) { return false; } } - if (QFactory.isEnabled(rootElement)) { - if (evt != null) { - evt.addMessage("deploy: " + f.getCanonicalPath()); - } + enabled = QFactory.isEnabled(rootElement); + if (evt != null) + evt.addMessage(new Deploy(f.getCanonicalPath(), enabled)); + if (enabled) { Object obj = factory.instantiate (this, factory.expandEnvProperties(rootElement)); qentry.setObject (obj); - ObjectInstance instance = factory.createQBean ( this, doc.getRootElement(), obj ); qentry.setInstance (instance); - } else if (evt != null) { - evt.addMessage("deploy ignored (enabled='" + QFactory.getEnabledAttribute(rootElement) + "'): " + f.getCanonicalPath()); } } catch (InstanceAlreadyExistsException e) { @@ -621,8 +629,10 @@ private boolean deploy (File f) { // This will also save deploy error repeats... return false; } finally { - if (evt != null) + if (evt != null) { + // evt.addMessage(new Deploy(Instant.now(), filePath, enabled)); Logger.log(evt); + } } return true ; } @@ -653,11 +663,11 @@ private void initSystemLogger () { getLog().warn ("init-system-logger", e); } } - Environment env = Environment.getEnvironment(); - getLog().info("Q2 started, deployDir=" + deployDir.getAbsolutePath() + ", environment=" + env.getName()); - if (env.getErrorString() != null) - getLog().error(env.getErrorString()); - +// Environment env = Environment.getEnvironment(); +// getLog().info("Q2 started, deployDir=" + deployDir.getAbsolutePath() + ", environment=" + env.getName()); +// if (env.getErrorString() != null) +// getLog().error(env.getErrorString()); + getLog().info (auditStart()); } public Log getLog () { if (log == null) { @@ -766,8 +776,6 @@ private void parseCmdLine (String[] args, boolean environmentOnly) { helpFormatter.printHelp ("Q2", options); System.exit (0); } - - if (line.hasOption ("c")) { cli = new CLI(this, line.getOptionValue("c"), line.hasOption("i")); } else if (line.hasOption ("i")) @@ -1251,4 +1259,26 @@ public String[] environmentArgs (String[] args) { Arrays.stream(ISOUtil.commaDecode(envArgs)), Arrays.stream(args)) .toArray(String[]::new) : args); } + + private Start auditStart() { + Environment env = Environment.getEnvironment(); + String envName = env.getName(); + if (env.getErrorString() != null) + envName = envName + " (" + env.getErrorString() + ")"; + return new Start( + getQ2().getInstanceId(), + getVersion(), + getAppVersionString(), + getDeployDir().getAbsolutePath(), + envName + ); + } + + private Stop auditStop(Duration dur) { + return new Stop( + Instant.now(), + getInstanceId(), + dur + ); + } } diff --git a/jpos/src/main/java/org/jpos/q2/qbean/LoggerAdaptor.java b/jpos/src/main/java/org/jpos/q2/qbean/LoggerAdaptor.java index b7af8398e9..96a14de989 100644 --- a/jpos/src/main/java/org/jpos/q2/qbean/LoggerAdaptor.java +++ b/jpos/src/main/java/org/jpos/q2/qbean/LoggerAdaptor.java @@ -22,9 +22,7 @@ import org.jpos.core.ConfigurationException; import org.jpos.q2.QBeanSupport; import org.jpos.q2.QFactory; -import org.jpos.util.LogEventOutputStream; -import org.jpos.util.LogListener; -import org.jpos.util.Logger; +import org.jpos.util.*; import java.io.IOException; import java.io.PrintStream; @@ -76,7 +74,20 @@ private void addListener (Element e) String clazz = e.getAttributeValue("class"); LogListener listener = factory.newInstance(clazz); factory.setConfiguration(listener, e); + attemptToAddWriter (e.getChild("writer"), listener); logger.addListener(listener); } } + + private void attemptToAddWriter (Element e, LogListener listener) throws ConfigurationException { + if (e != null) { + QFactory factory = getServer().getFactory(); + if (QFactory.isEnabled(e)) { + String clazz = e.getAttributeValue("class"); + LogEventWriter writer = factory.newInstance(clazz); + factory.setConfiguration(writer, e); + listener.setLogEventWriter (writer); + } + } + } } diff --git a/jpos/src/main/java/org/jpos/transaction/TransactionManager.java b/jpos/src/main/java/org/jpos/transaction/TransactionManager.java index d9fbdce350..c31b9db773 100644 --- a/jpos/src/main/java/org/jpos/transaction/TransactionManager.java +++ b/jpos/src/main/java/org/jpos/transaction/TransactionManager.java @@ -18,7 +18,9 @@ package org.jpos.transaction; -import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.binder.BaseUnits; @@ -115,6 +117,7 @@ public class TransactionManager private Gauge activeSessionsGauge; private Counter transactionCounter; + private boolean freezeLog; @Override public void initService () throws ConfigurationException { @@ -410,6 +413,7 @@ public void setConfiguration (Configuration cfg) throws ConfigurationException { } catch (Exception e) { throw new ConfigurationException (e); } + freezeLog = cfg.getBoolean("freeze-log", true); } public void addListener (TransactionStatusListener l) { synchronized (statusListeners) { @@ -480,7 +484,7 @@ public void dump (PrintStream ps, String indent) { if (recover && p instanceof ContextRecovery cr) { context = recover (cr, id, context, pp, true); if (evt != null) - evt.addMessage (" commit-recover: " + getName(p)); + evt.addMessage (Trace.of("commit-recover", getName(p))); } if (hasStatusListeners) notifyStatusListeners ( @@ -488,7 +492,7 @@ session, TransactionStatusEvent.State.COMMITING, id, getName(p), context ); commitOrAbort (p, id, context, pp, this::commit); if (evt != null) { - evt.addMessage (" commit: " + getName(p)); + evt.addMessage (Trace.of("commit", getName(p))); if (prof != null) prof.checkPoint (" commit: " + getName(p)); } @@ -503,7 +507,7 @@ session, TransactionStatusEvent.State.COMMITING, id, getName(p), context if (recover && p instanceof ContextRecovery cr) { context = recover (cr, id, context, pp, true); if (evt != null) - evt.addMessage (" abort-recover: " + getName(p)); + evt.addMessage (Trace.of("abort-recover", getName(p))); } if (hasStatusListeners) notifyStatusListeners ( @@ -512,7 +516,7 @@ session, TransactionStatusEvent.State.ABORTING, id, getName(p), context commitOrAbort (p, id, context, pp, this::abort); if (evt != null) { - evt.addMessage (" abort: " + getName(p)); + evt.addMessage (Trace.of("abort", getName(p))); if (prof != null) prof.checkPoint (" abort: " + getName(p)); } @@ -528,7 +532,7 @@ session, TransactionStatusEvent.State.ABORTING, id, getName(p), context return ((AbortParticipant)p).prepareForAbort (id, context); } } catch (Throwable t) { - getLog().warn ("PREPARE-FOR-ABORT: " + Long.toString (id), t); + getLog().warn ("PREPARE-FOR-ABORT: " + id, t); } finally { getParams(p).timers.prepareForAbortTimer.record (c.elapsed(), TimeUnit.MILLISECONDS); if (metrics != null) @@ -544,7 +548,7 @@ session, TransactionStatusEvent.State.ABORTING, id, getName(p), context setThreadName(id, "prepare", p); return p.prepare (id, context); } catch (Throwable t) { - getLog().warn ("PREPARE: " + Long.toString (id), t); + getLog().warn ("PREPARE: " + id, t); } finally { getParams(p).timers.prepareTimer.record (c.elapsed(), TimeUnit.MILLISECONDS); if (metrics != null) { @@ -561,7 +565,7 @@ session, TransactionStatusEvent.State.ABORTING, id, getName(p), context setThreadName(id, "commit", p); p.commit(id, context); } catch (Throwable t) { - getLog().warn ("COMMIT: " + Long.toString (id), t); + getLog().warn ("COMMIT: " + id, t); } finally { getParams(p).timers.commitTimer.record (c.elapsed(), TimeUnit.MILLISECONDS); if (metrics != null) @@ -616,7 +620,7 @@ session, TransactionStatusEvent.State.PREPARING_FOR_ABORT, id, getName(p), conte action = prepareOrAbort (p, id, context, pp, this::prepareForAbort); if (evt != null && p instanceof AbortParticipant) { - evt.addMessage("prepareForAbort: " + getName(p)); + evt.addMessage(Trace.of("prepareForAbort", getName(p))); if (prof != null) prof.checkPoint ("prepareForAbort: " + getName(p)); } @@ -640,14 +644,14 @@ session, TransactionStatusEvent.State.PREPARING, id, getName(p), context retry = (action & RETRY) == RETRY; if (evt != null) { - evt.addMessage (" prepare: " - + getName(p) - + (abort ? " ABORTED" : " PREPARED") + evt.addMessage (Trace.of("prepare", getName(p), + (abort ? " ABORTED" : " PREPARED") + (timeout ? " TIMEOUT" : "") + (maxTime ? " MAX_TIMEOUT" : "") + (retry ? " RETRY" : "") + ((action & READONLY) == READONLY ? " READONLY" : "") - + ((action & NO_JOIN) == NO_JOIN ? " NO_JOIN" : "")); + + ((action & NO_JOIN) == NO_JOIN ? " NO_JOIN" : "")) + ); if (prof != null) prof.checkPoint ("prepare: " + getName(p)); } @@ -770,9 +774,7 @@ public TransactionParticipant createParticipant (Element e) throws ConfigurationException { QFactory factory = getFactory(); - TransactionParticipant participant = - factory.newInstance (QFactory.getAttributeValue (e, "class") - ); + TransactionParticipant participant = factory.newInstance (QFactory.getAttributeValue (e, "class")); factory.setLogger (participant, e); QFactory.invoke (participant, "setTransactionManager", this, TransactionManager.class); factory.setConfiguration (participant, e); @@ -980,8 +982,8 @@ protected synchronized void checkRetryTask () { * @param prof profiler (may be null) * @return FrozenLogEvent */ - protected FrozenLogEvent freeze(Serializable context, LogEvent evt, Profiler prof) { - return new FrozenLogEvent(evt); + protected LogEvent freeze(Serializable context, LogEvent evt, Profiler prof) { + return freezeLog ? new FrozenLogEvent(evt) : evt; } public class RetryTask implements Runnable { @@ -1084,6 +1086,8 @@ public static T getContext() { public static Long getId() { return tlId.get(); } + + private void notifyStatusListeners (int session, TransactionStatusEvent.State state, long id, String info, Serializable context) { @@ -1194,6 +1198,18 @@ private record Timers ( io.micrometer.core.instrument.Timer abortTimer, io.micrometer.core.instrument.Timer snapshotTimer) { } + public record Trace (String phase, String message, String info) { + @Override + public String toString() { + return "%15s: %s%s".formatted(phase, message, info); + } + public static Trace of (String phase, String message) { + return new Trace (phase, message, ""); + } + public static Trace of (String phase, String message, String info) { + return new Trace (phase, message, info); + } + } private Set getSet (Element e) { return e != null ? new HashSet<>(Arrays.asList(ISOUtil.commaDecode(e.getTextTrim()))) : Collections.emptySet(); diff --git a/jpos/src/main/java/org/jpos/util/Caller.java b/jpos/src/main/java/org/jpos/util/Caller.java index e58919a3ef..d9863aae98 100644 --- a/jpos/src/main/java/org/jpos/util/Caller.java +++ b/jpos/src/main/java/org/jpos/util/Caller.java @@ -41,7 +41,7 @@ public static String info(int pos) { } return sb.append(st.getMethodName()) .append(':') - .append(Integer.toString(st.getLineNumber())) + .append(st.getLineNumber()) .toString(); } public static String shortClassName(String clazz) { diff --git a/jpos/src/main/java/org/jpos/util/JsonLogWriter.java b/jpos/src/main/java/org/jpos/util/JsonLogWriter.java new file mode 100644 index 0000000000..be46716dcd --- /dev/null +++ b/jpos/src/main/java/org/jpos/util/JsonLogWriter.java @@ -0,0 +1,46 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2022 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.util; + +import org.jpos.log.LogRenderer; +import org.jpos.log.LogRendererRegistry; + +import java.io.PrintStream; + +public class JsonLogWriter implements LogEventWriter { + private PrintStream ps; + private final LogRenderer renderer = LogRendererRegistry.getRenderer(LogEvent.class, LogRenderer.Type.JSON); + + @Override + public void write(LogEvent ev) { + renderer.render(ev, ps, ""); + } + + @Override + public void setPrintStream(PrintStream ps) { + this.ps = ps; + } + + @Override + public void close() { + if (ps != System.out && ps != System.err) { + ps.close(); + } + } +} diff --git a/jpos/src/main/java/org/jpos/util/LogListener.java b/jpos/src/main/java/org/jpos/util/LogListener.java index fcf2b46365..50c68c8ff5 100644 --- a/jpos/src/main/java/org/jpos/util/LogListener.java +++ b/jpos/src/main/java/org/jpos/util/LogListener.java @@ -27,5 +27,7 @@ */ public interface LogListener extends EventListener { LogEvent log(LogEvent ev); + default void setLogEventWriter (LogEventWriter w) { + // do nothing, for backward compatibility + } } - diff --git a/jpos/src/main/java/org/jpos/util/Loggeable.java b/jpos/src/main/java/org/jpos/util/Loggeable.java index 8fff25669e..0d4a4838e5 100644 --- a/jpos/src/main/java/org/jpos/util/Loggeable.java +++ b/jpos/src/main/java/org/jpos/util/Loggeable.java @@ -18,6 +18,9 @@ package org.jpos.util; +import org.jpos.log.LogRenderer; +import org.jpos.log.LogRendererRegistry; + import java.io.PrintStream; /** @@ -26,5 +29,11 @@ */ public interface Loggeable { void dump(PrintStream p, String indent); + default void dump(PrintStream p, String indent, LogRenderer.Type type) { + var renderer = LogRendererRegistry.getRenderer(this.getClass(), type); + if (renderer != null) + renderer.render (this, p, indent); + else + dump (p, indent); + } } - diff --git a/jpos/src/main/java/org/jpos/util/Logger.java b/jpos/src/main/java/org/jpos/util/Logger.java index 7955816ec7..f99b28436a 100644 --- a/jpos/src/main/java/org/jpos/util/Logger.java +++ b/jpos/src/main/java/org/jpos/util/Logger.java @@ -94,10 +94,10 @@ public static void log (LogEvent evt) { l = getLogger(Q2.LOGGER_NAME); } if (l != null && l.hasListeners ()) { - Iterator i = l.listeners.iterator(); + Iterator i = l.listeners.iterator(); while (i.hasNext() && evt != null) { try { - evt = ((LogListener) i.next()).log(evt); + evt = i.next().log(evt); } catch (ConcurrentModificationException e) { break; } catch (Throwable t) { diff --git a/jpos/src/main/java/org/jpos/util/MarkdownLogEvent.java b/jpos/src/main/java/org/jpos/util/MarkdownLogEvent.java new file mode 100644 index 0000000000..f62d305ac0 --- /dev/null +++ b/jpos/src/main/java/org/jpos/util/MarkdownLogEvent.java @@ -0,0 +1,26 @@ +package org.jpos.util; + +import java.io.PrintStream; +import java.io.Serializable; + +public class MarkdownLogEvent extends LogEvent { + private String frozen; + + public MarkdownLogEvent(String frozen) { + this.frozen = frozen; + } + public MarkdownLogEvent (LogEvent evt) { + super(evt.getSource(), evt.getTag(), evt.getRealm()); + frozen = evt.toString(); + } + @Override + public void dump (PrintStream ps, String indent) { + ps.print (frozen); + } + + @Override + public String toString () { + return frozen; + } +} + diff --git a/jpos/src/main/java/org/jpos/util/RotateLogListener.java b/jpos/src/main/java/org/jpos/util/RotateLogListener.java index b85f02acca..09ab1d8c01 100644 --- a/jpos/src/main/java/org/jpos/util/RotateLogListener.java +++ b/jpos/src/main/java/org/jpos/util/RotateLogListener.java @@ -22,6 +22,7 @@ import org.jpos.core.Configurable; import org.jpos.core.Configuration; import org.jpos.core.ConfigurationException; +import org.jpos.core.XmlConfigurable; import java.io.File; import java.io.FileOutputStream; @@ -29,7 +30,6 @@ import java.io.PrintStream; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.Date; import java.util.Timer; import java.util.TimerTask; import java.time.Instant; @@ -43,7 +43,7 @@ * @since jPOS 1.2 */ public class RotateLogListener extends SimpleLogListener - implements AutoCloseable, Configurable, Destroyable + implements AutoCloseable, Configurable, XmlConfigurable, Destroyable { FileOutputStream f; String logName = null; @@ -56,6 +56,7 @@ public class RotateLogListener extends SimpleLogListener Rotate rotate; public static final int CHECK_INTERVAL = 100; public static final long DEFAULT_MAXSIZE = 10000000; + private boolean hasWriter; ScheduleTimer timer = null; RotationAlgo rotationAlgo = null; @@ -144,7 +145,8 @@ public void setConfiguration (Configuration cfg) @Override public void setConfiguration(Element e) throws ConfigurationException { - super.setConfiguration(e); + super.setConfiguration (e); // just in case parent has something to do in the future + hasWriter = e != null && e.getChild("writer") != null; runPostConfiguration(); } @@ -161,7 +163,7 @@ protected synchronized void openLogFile() throws IOException { f.close(); f = new FileOutputStream (logName, true); setPrintStream (new PrintStream(f)); - if (writer == null) { + if (!hasWriter) { p.println(""); p.println(""); } @@ -248,6 +250,8 @@ public void run() { } } } + + @Override public void destroy () { if (rotate != null) rotate.cancel(); diff --git a/jpos/src/main/java/org/jpos/util/SimpleLogListener.java b/jpos/src/main/java/org/jpos/util/SimpleLogListener.java index 6ff321dfdb..58ac45350b 100644 --- a/jpos/src/main/java/org/jpos/util/SimpleLogListener.java +++ b/jpos/src/main/java/org/jpos/util/SimpleLogListener.java @@ -19,13 +19,10 @@ package org.jpos.util; import org.jdom2.Element; -import org.jpos.core.Configurable; import org.jpos.core.ConfigurationException; import org.jpos.core.XmlConfigurable; -import org.jpos.q2.SimpleConfigurationFactory; import java.io.PrintStream; -import java.util.function.Consumer; /** * @author Alejandro P. Revilla @@ -33,13 +30,13 @@ * @see org.jpos.core.Configurable * @since jPOS 1.2 */ -public class SimpleLogListener implements LogListener, XmlConfigurable, Consumer { +public class SimpleLogListener implements LogListener, XmlConfigurable, Destroyable { LogEventWriter writer = null; PrintStream p; public SimpleLogListener () { super(); - p = System.out; + setPrintStream(System.out); } public SimpleLogListener (PrintStream p) { this (); @@ -75,39 +72,20 @@ public synchronized LogEvent log (LogEvent ev) { } @Override - public void accept(LogEventWriter writer) { - if (p != null) { - writer.setPrintStream(p); - } + public void setLogEventWriter (LogEventWriter writer) { this.writer = writer; + if (p != null) + writer.setPrintStream(p); } @Override public void setConfiguration(Element e) throws ConfigurationException { - Element ew = e.getChild("writer"); - LogEventWriter writer; - if (ew != null) { - String clazz = ew.getAttributeValue("class"); - if (clazz != null) { - try { - writer = (LogEventWriter) Class.forName(clazz).newInstance(); - } catch (Exception ex) { - throw new ConfigurationException(ex); - } - if (writer != null) { - if (writer instanceof Configurable) { - SimpleConfigurationFactory factory = new SimpleConfigurationFactory(); - ((Configurable) writer).setConfiguration(factory.getConfiguration(ew)); - } - if (writer instanceof XmlConfigurable) { - ((XmlConfigurable) writer).setConfiguration(ew); - } - accept(writer); - } - } else { - throw new ConfigurationException("The writer configuration requires a class attribute"); - } - } + // nothing to do for now } -} + @Override + public void destroy () { + if (writer != null) + writer.close(); + } +} diff --git a/jpos/src/main/java/org/jpos/util/TxtLogWriter.java b/jpos/src/main/java/org/jpos/util/TxtLogWriter.java new file mode 100644 index 0000000000..832bfdc52e --- /dev/null +++ b/jpos/src/main/java/org/jpos/util/TxtLogWriter.java @@ -0,0 +1,57 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2022 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.util; + +import org.jpos.log.LogRenderer; +import org.jpos.log.LogRendererRegistry; + +import java.io.PrintStream; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public class TxtLogWriter implements LogEventWriter { + private PrintStream ps; + private final LogRenderer renderer = LogRendererRegistry.getRenderer(LogEvent.class, LogRenderer.Type.TXT); + private Instant start; + + @Override + public void write(LogEvent ev) { + renderer.render(ev, ps, LocalDateTime.ofInstant(start, ZoneId.systemDefault()) + " "); + } + + @Override + public void setPrintStream(PrintStream ps) { + this.ps = ps; + start = Instant.now(); + // ps.printf ("# Log Start %s (%d)%n", LocalDateTime.ofInstant(start, ZoneId.systemDefault()), ps.hashCode()); + } + + @Override + public void close() { +// Instant now = Instant.now(); +// ps.printf ("# Log End %s (%s)%n", +// LocalDateTime.ofInstant(now, ZoneId.systemDefault()), +// ISODate.formatDuration(Duration.between(start,now)) +// ); + if (ps != System.out && ps != System.err) { + ps.close(); + } + } +} diff --git a/jpos/src/main/java/org/jpos/util/XmlLogWriter.java b/jpos/src/main/java/org/jpos/util/XmlLogWriter.java new file mode 100644 index 0000000000..fc60d01aa2 --- /dev/null +++ b/jpos/src/main/java/org/jpos/util/XmlLogWriter.java @@ -0,0 +1,47 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2022 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.util; + +import org.jpos.log.LogRenderer; +import org.jpos.log.LogRendererRegistry; + +import java.io.PrintStream; + +public class XmlLogWriter implements LogEventWriter { + private PrintStream ps; + private final LogRenderer renderer = LogRendererRegistry.getRenderer(LogEvent.class, LogRenderer.Type.XML); + + @Override + public void write(LogEvent ev) { + renderer.render(ev, ps, ""); + } + + @Override + public void setPrintStream(PrintStream ps) { + this.ps = ps; + } + + @Override + public void close() { + if (ps != System.out && ps != System.err) { + ps.close(); + } + } +} + diff --git a/jpos/src/main/resources/META-INF/services/org.jpos.log.LogRenderer b/jpos/src/main/resources/META-INF/services/org.jpos.log.LogRenderer new file mode 100644 index 0000000000..f39b80c715 --- /dev/null +++ b/jpos/src/main/resources/META-INF/services/org.jpos.log.LogRenderer @@ -0,0 +1,20 @@ +org.jpos.log.render.json.LogEventJsonLogRenderer +org.jpos.log.render.json.LoggeableJsonLogRenderer +org.jpos.log.render.json.AuditLogEventJsonLogRenderer + +org.jpos.log.render.xml.LoggeableXmlLogRenderer +org.jpos.log.render.xml.LogEventXmlLogRenderer + +org.jpos.log.render.markdown.ProfilerMarkdownRenderer +org.jpos.log.render.markdown.ContextMarkdownRenderer +org.jpos.log.render.markdown.LoggeableMarkdownLogRenderer +org.jpos.log.render.markdown.LogEventMarkdownRenderer +org.jpos.log.render.markdown.StringMarkdownLogRenderer +org.jpos.log.render.markdown.ObjectMarkdownLogRenderer +org.jpos.log.render.markdown.ByteArrayMarkdownLogRenderer +org.jpos.log.render.markdown.ThrowableMarkdownLogRenderer +org.jpos.log.render.markdown.ElementMarkdownLogRenderer +org.jpos.log.render.markdown.TransactionManagerTraceMarkdownLogRenderer +org.jpos.log.render.markdown.TransactionManagerTraceArrayMarkdownLogRenderer + +org.jpos.log.render.txt.ObjectTxtLogRenderer diff --git a/jpos/src/test/java/org/jpos/util/DailyLogListenerTest.java b/jpos/src/test/java/org/jpos/util/DailyLogListenerTest.java index b4ac150d80..c35d8a163b 100644 --- a/jpos/src/test/java/org/jpos/util/DailyLogListenerTest.java +++ b/jpos/src/test/java/org/jpos/util/DailyLogListenerTest.java @@ -36,6 +36,7 @@ import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; +import org.jdom2.Element; import org.jpos.core.Configuration; import org.jpos.core.ConfigurationException; import org.jpos.core.SimpleConfiguration; @@ -465,6 +466,7 @@ private DailyLogListener createCompressingDailyLogListenerWithIsoDateFormat(Stri configuration.setProperty("maxsize", "1000000"); logRotationTestDirectory.allowNewFileCreation(); listener.setConfiguration(new SimpleConfiguration(configuration)); + listener.setConfiguration((Element) null); return listener; } diff --git a/jpos/src/test/java/org/jpos/util/RotateLogListenerTest.java b/jpos/src/test/java/org/jpos/util/RotateLogListenerTest.java index 9fbed1c566..c8fc2a9e05 100644 --- a/jpos/src/test/java/org/jpos/util/RotateLogListenerTest.java +++ b/jpos/src/test/java/org/jpos/util/RotateLogListenerTest.java @@ -26,11 +26,13 @@ import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Properties; +import org.jdom2.Element; import org.jpos.core.Configuration; import org.jpos.core.ConfigurationException; import org.jpos.core.SimpleConfiguration; @@ -185,6 +187,7 @@ public void testLogRotationWorks() throws Exception { // when: a rotation is executed listener.logRotate(); + DirectoryStream stream = Files.newDirectoryStream(logRotationTestDirectory.getDirectory()); // then: new events should end up in the current file and old events in the archived file listener.log(new LogEvent("Message 2")); @@ -194,6 +197,8 @@ public void testLogRotationWorks() throws Exception { assertTrue(currentLogFileContents.contains(""), "Logger element should not have been closed in the current file"); + + String archivedLogFile1Contents = getStringFromFile(logRotationTestDirectory.getFile(logFileName + ".1")); assertTrue(archivedLogFile1Contents.contains("Message 1"), "Archived log file should contain the first message"); assertFalse(archivedLogFile1Contents.contains("Message 2"), "Archived log file should not contain the second message"); @@ -203,6 +208,7 @@ public void testLogRotationWorks() throws Exception { // when: another rotation is executed listener.logRotate(); + // then: new events should end up in the current file and old events in the archived files listener.log(new LogEvent("Message 3")); @@ -243,7 +249,6 @@ public void testLogRotateAbortsWhenCreatingNewFileFails() throws Exception { logRotationTestDirectory.allowNewFileCreation(); String logFileContents = getStringFromFile(logRotationTestDirectory.getFile(logFileName)); - System.out.println("logFileContents = " + logFileContents); assertTrue(logFileContents.contains("Message 1"), "Log file should contain first message"); assertTrue(logFileContents.contains("Message 2"), "Log file should contain second message"); assertFalse(logFileContents.contains(""), "Logger element should not have been closed"); @@ -320,6 +325,7 @@ private RotateLogListener createRotateLogListenerWithIsoDateFormat( } logRotationTestDirectory.allowNewFileCreation(); listener.setConfiguration(new SimpleConfiguration(configuration)); + listener.setConfiguration((Element) null); // we need to call this in order to simulate LoggerAdaptor behavior and run postConfiguration LogWriter stuff return listener; } } diff --git a/jpos/src/test/java/org/jpos/util/SimpleLogListenerTest.java b/jpos/src/test/java/org/jpos/util/SimpleLogListenerTest.java index c4ec27e8ad..6942e3d741 100644 --- a/jpos/src/test/java/org/jpos/util/SimpleLogListenerTest.java +++ b/jpos/src/test/java/org/jpos/util/SimpleLogListenerTest.java @@ -86,33 +86,12 @@ void testSetConfigurationShouldNotCreateAndSetWriterIfNotPresent() throws Config simpleLogListener.setConfiguration(root); assertNull(simpleLogListener.writer); } - - @Test - void testSetConfigurationShouldThrowConfigurationExceptionOnNewInstanceFailure() { - SimpleLogListener simpleLogListener = new SimpleLogListener(); - Element root = new Element("root"); - Element we = new Element("writer"); - Element prop = new Element("property"); - we.setAttribute("class", "org.jpos.util.FakeLogEventWriter"); - we.addContent(prop); - root.addContent(we); - assertThrows(ConfigurationException.class, () -> simpleLogListener.setConfiguration(root)); - } - - @Test - void testSetConfigurationShouldThrowConfigurationExceptionWhenWriterClassAttributeMissing() { - SimpleLogListener simpleLogListener = new SimpleLogListener(); - Element root = new Element("root"); - Element we = new Element("writer"); - root.addContent(we); - assertThrows(ConfigurationException.class, () -> simpleLogListener.setConfiguration(root)); - } - + @Test void testShouldClosePrintStreamOnNonNullWriter() { SimpleLogListener simpleLogListener = new SimpleLogListener(); LogEventWriter logEventWriter = mock(LogEventWriter.class); - simpleLogListener.accept(logEventWriter); + simpleLogListener.setLogEventWriter(logEventWriter); simpleLogListener.setPrintStream(new PrintStream(System.out)); assertNotNull(simpleLogListener.p); simpleLogListener.close(); @@ -125,7 +104,7 @@ void testSetPrintStreamShouldSetPrintStreamOnNonNullWriter() { SimpleLogListener simpleLogListener = new SimpleLogListener(); LogEventWriter logEventWriter = mock(LogEventWriter.class); PrintStream printStream = new PrintStream(System.out); - simpleLogListener.accept(logEventWriter); + simpleLogListener.setLogEventWriter(logEventWriter); simpleLogListener.setPrintStream(printStream); verify(logEventWriter).setPrintStream(printStream); } @@ -134,7 +113,7 @@ void testSetPrintStreamShouldSetPrintStreamOnNonNullWriter() { void testShouldLogUsingWriter() { SimpleLogListener simpleLogListener = new SimpleLogListener(); LogEventWriter writer = mock(LogEventWriter.class); - simpleLogListener.accept(writer); + simpleLogListener.setLogEventWriter(writer); LogEvent ev = new LogEvent(); simpleLogListener.log(ev); verify(writer).write(ev);