Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions jpos/src/dist/deploy/00_logger.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,10 @@
<logger name="Q2" class="org.jpos.q2.qbean.LoggerAdaptor" realm="system">
<property name="redirect" value="stdout, stderr" />

<log-listener class="org.jpos.util.SimpleLogListener" enabled="${log.simple:true}" />

<log-listener class="org.jpos.util.SimpleLogListener" enabled="${log.xml:false}">
<writer class="org.jpos.util.XmlLogWriter" enabled="true" />
</log-listener>

<log-listener class="org.jpos.util.SimpleLogListener" enabled="${log.json:false}">
<writer class="org.jpos.util.JsonLogWriter" enabled="true" />
<log-listener class="org.jpos.util.SimpleLogListener">
<writer class="org.jpos.util.JsonlLogWriter" enabled="true" />
</log-listener>

<log-listener class="org.jpos.util.SimpleLogListener" enabled="${log.markdown:false}">
<writer class="org.jpos.util.MarkdownLogWriter" enabled="true" />
</log-listener>

<!-- <log-listener class="org.jpos.util.SimpleLogListener" enabled="true">-->
<!-- <writer class="org.jpos.util.TxtLogWriter" enabled="true" />-->
<!-- </log-listener>-->

<log-listener class="org.jpos.util.DailyLogListener" enabled="true">
<property name="window" value="86400" /> <!-- optional, default one day -->
<!--needed-->
Expand Down
18 changes: 18 additions & 0 deletions jpos/src/main/java/org/jpos/iso/BaseChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ public void send (ISOMsg m)
m.setPackager (p);
m = applyOutgoingFilters (m, evt);
evt.addMessage (m);
applyTags (evt, m);
m.setDirection(ISOMsg.OUTGOING); // filter may have dropped this info
m.setPackager (p); // and could have dropped packager as well
byte[] b = pack(m);
Expand Down Expand Up @@ -848,6 +849,7 @@ else if (len > 0 && len <= getMaxPacketLength()) {
unpack (m, b);
m.setDirection(ISOMsg.INCOMING);
evt.addMessage (m);
applyTags (evt, m);
m = applyIncomingFilters (m, header, b, evt);
m.setDirection(ISOMsg.INCOMING);
cnt[RX]++;
Expand Down Expand Up @@ -1262,4 +1264,20 @@ protected void incrementMsgOutCounter(ISOMsg m) throws ISOException {
isoMsgMetrics.recordMessage(m, MeterInfo.ISOMSG_OUT);
}
}
private void applyTags (LogEvent evt, ISOMsg m) {
if (m.hasField(3)) {
String f3 = m.getString(3);
if (f3 != null && f3.length() >= 2)
evt.withTag("pcode", f3.substring(0, 2));
}
if (m.hasField(41))
evt.withTag("tid", m.getString(41));
if (m.hasField(42))
evt.withTag("mid", m.getString(42));
try {
String mti = m.getMTI();
if (mti != null)
evt.withTag("mti", mti);
} catch (ISOException ignored) { }
}
}
2 changes: 1 addition & 1 deletion jpos/src/main/java/org/jpos/log/AuditLogEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@
@JsonSubTypes.Type(value = Txn.class, name = "txn")
})

public sealed interface AuditLogEvent permits Connect, Deploy, DeployActivity, Disconnect, License, Listen, LogMessage, SessionEnd, SessionStart, Shutdown, Start, Stop, SysInfo, ThrowableAuditLogEvent, Txn, UnDeploy, Warning { }
public interface AuditLogEvent { }
9 changes: 5 additions & 4 deletions jpos/src/main/java/org/jpos/log/evt/LogEvt.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@

package org.jpos.log.evt;

import com.fasterxml.jackson.annotation.JsonInclude;
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.Map;

@JacksonXmlRootElement(localName = "log")
public record LogEvt(
@JacksonXmlProperty(isAttribute = true) Instant ts,
@JacksonXmlProperty(isAttribute = true) @JsonProperty("trace-id") String traceId,
@JacksonXmlProperty(isAttribute = true) String realm,
@JacksonXmlProperty(isAttribute = true) String tag,
@JacksonXmlProperty(isAttribute = true) String kind,
@JacksonXmlProperty(isAttribute = true) Long lifespan,
@JsonProperty("evt") @JacksonXmlElementWrapper(useWrapping = false) List<AuditLogEvent> events) { }
@JsonInclude(JsonInclude.Include.NON_EMPTY) Map<String,String> tags,
@JsonProperty("payload") @JacksonXmlElementWrapper(useWrapping = false) List<AuditLogEvent> events) { }
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class LogEventJsonLogRenderer implements LogRenderer<LogEvent> {
private final ObjectMapper mapper = new ObjectMapper();
Expand Down Expand Up @@ -64,10 +66,9 @@ public void render(LogEvent evt, PrintStream ps, String indent) {
long elapsed = Duration.between(evt.getCreatedAt(), evt.getDumpedAt()).toMillis();
LogEvt ev = new LogEvt (
evt.getDumpedAt(),
evt.getTraceId(),
evt.getRealm(),
evt.getTag(),
elapsed == 0L ? null : elapsed,
buildTags(evt),
events
);
try {
Expand All @@ -83,6 +84,16 @@ public Type type() {
return Type.JSON;
}

private Map<String,String> buildTags(LogEvent evt) {
evt.getTraceId(); // ensure trace-id is generated
Map<String,String> tags = new LinkedHashMap<>();
String realm = evt.getRealm();
if (realm != null && !realm.isEmpty())
tags.put("realm", realm);
tags.putAll(evt.getTags());
return tags;
}

private String dump (Object obj) {
if (obj instanceof Loggeable loggeable) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class LogEventXmlLogRenderer implements LogRenderer<LogEvent> {
private final XmlMapper mapper = new XmlMapper();
Expand Down Expand Up @@ -65,10 +67,9 @@ public void render(LogEvent evt, PrintStream ps, String indent) {
long elapsed = Duration.between(evt.getCreatedAt(), evt.getDumpedAt()).toMillis();
LogEvt ev = new LogEvt (
evt.getDumpedAt(),
evt.getTraceId(),
evt.getRealm(),
evt.getTag(),
elapsed == 0L ? null : elapsed,
buildTags(evt),
events
);
try {
Expand All @@ -84,6 +85,16 @@ public Type type() {
return Type.XML;
}

private Map<String,String> buildTags(LogEvent evt) {
evt.getTraceId(); // ensure trace-id is generated
Map<String,String> tags = new LinkedHashMap<>();
String realm = evt.getRealm();
if (realm != null && !realm.isEmpty())
tags.put("realm", realm);
tags.putAll(evt.getTags());
return tags;
}

private String kv (String k, String v) {
return "{\"%s\":\"%s\"}".formatted(k,v);
}
Expand Down
1 change: 1 addition & 0 deletions jpos/src/main/java/org/jpos/util/JsonLogWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.io.PrintStream;

@Deprecated(forRemoval = false)
public class JsonLogWriter implements LogEventWriter {
private PrintStream ps;
private final LogRenderer<LogEvent> renderer = LogRendererRegistry.getRenderer(LogEvent.class, LogRenderer.Type.JSON);
Expand Down
186 changes: 186 additions & 0 deletions jpos/src/main/java/org/jpos/util/JsonlLogWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2026 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 <http://www.gnu.org/licenses/>.
*/

package org.jpos.util;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.jpos.core.Configurable;
import org.jpos.core.Configuration;
import org.jpos.core.ConfigurationException;
import org.jpos.iso.ISOMsg;
import org.jpos.iso.ISOUtil;
import org.jpos.log.AuditLogEvent;
import org.jpos.log.evt.LogEvt;
import org.jpos.log.evt.LogMessage;
import org.jpos.log.evt.ThrowableAuditLogEvent;
import org.jpos.log.render.ThrowableSerializer;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.time.Duration;
import java.util.*;

/**
* JSONL (one JSON object per line) LogEventWriter with built-in PCI protection.
*
* <p>When serializing ISOMsg objects in LogEvent payloads, sensitive fields are
* masked or wiped inline — no upstream {@link ProtectedLogListener} required.</p>
*
* <p>Configuration properties (same convention as {@link ProtectedLogListener}):</p>
* <ul>
* <li>{@code protect} — space-separated field numbers to mask via {@link ISOUtil#protect(String)} (default: {@code "2"})</li>
* <li>{@code wipe} — space-separated field numbers to replace with [WIPED] (default: {@code "35 45 48 52 55"})</li>
* </ul>
*
* <p>Output is suitable for {@code jq}, Filebeat, and Elasticsearch ingestion.</p>
*
* @since 3.0.0
*/
public class JsonlLogWriter extends BaseLogEventWriter implements Configurable {
private static final String WIPED = "[WIPED]";
private static final Set<Integer> DEFAULT_SAFE_FIELDS = Set.of(
3, 4, 7, 11, 12, 13, 18, 22, 24, 25, 32, 37, 38, 39, 41, 42, 49, 90
);

private ObjectMapper mapper;
private String host;
private Set<Integer> protectFields = Set.of(2);
private Set<Integer> wipeFields = Set.of(35, 45, 48, 52, 55);

public JsonlLogWriter() {
initMapper();
try {
host = InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
host = "unknown";
}
}

@Override
public void setConfiguration(Configuration cfg) throws ConfigurationException {
String protect = cfg.get("protect", null);
if (protect != null) {
protectFields = toIntSet(protect);
}
String wipe = cfg.get("wipe", null);
if (wipe != null) {
wipeFields = toIntSet(wipe);
}
initMapper();
}

@Override
public void write(LogEvent ev) {
if (p == null || ev == null)
return;
try {
List<AuditLogEvent> events;
synchronized (ev.getPayLoad()) {
events = ev.getPayLoad()
.stream()
.map(this::toAuditLogEvent)
.toList();
}
long elapsed = Duration.between(ev.getCreatedAt(), ev.getDumpedAt()).toMillis();
LogEvt logEvt = new LogEvt(
ev.getDumpedAt(),
ev.getTag(),
elapsed == 0L ? null : elapsed,
buildTags(ev),
events
);
p.println(mapper.writeValueAsString(logEvt));
p.flush();
} catch (JsonProcessingException e) {
p.println("{\"error\":\"" + e.getMessage().replace("\"", "'") + "\"}");
p.flush();
}
}

private AuditLogEvent toAuditLogEvent(Object obj) {
return switch (obj) {
case AuditLogEvent ale -> ale;
case ISOMsg m -> new LogMessage(protectAndDump(m));
case Throwable t -> new ThrowableAuditLogEvent(t);
default -> new LogMessage(dump(obj));
};
}

private Map<String,String> buildTags(LogEvent ev) {
ev.getTraceId(); // ensure trace-id is generated
Map<String,String> tags = new LinkedHashMap<>();
String realm = ev.getRealm();
if (realm != null && !realm.isEmpty())
tags.put("realm", realm);
if (host != null)
tags.put("host", host);
tags.putAll(ev.getTags());
return tags;
}

private String protectAndDump(ISOMsg original) {
ISOMsg m = (ISOMsg) original.clone();
for (int field : protectFields) {
String v = m.getString(field);
if (v != null) {
m.set(field, ISOUtil.protect(v));
}
}
for (int field : wipeFields) {
if (m.hasField(field)) {
m.set(field, WIPED);
}
}
return dump(m);
}

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().trim();
}
return obj.toString();
}

private void initMapper() {
mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SimpleModule module = new SimpleModule();
module.addSerializer(Throwable.class, new ThrowableSerializer());
mapper.registerModule(module);
}

private static Set<Integer> toIntSet(String spaceSeparated) {
if (spaceSeparated == null || spaceSeparated.isBlank())
return Set.of();
Set<Integer> result = new HashSet<>();
for (String token : spaceSeparated.trim().split("\\s+")) {
result.add(Integer.parseInt(token));
}
return Collections.unmodifiableSet(result);
}
}
Loading
Loading