From ae7389b5203e2c3b3c7ea28d25baf6b149020cb3 Mon Sep 17 00:00:00 2001 From: ar-agt Date: Sat, 13 Jun 2026 09:32:46 -0300 Subject: [PATCH 1/3] feat(saf): add Micrometer metrics instrumentation --- modules/saf/build.gradle | 4 +- modules/saf/src/main/java/module-info.java | 1 + .../saf/src/main/java/org/jpos/saf/SAF.java | 27 +- .../main/java/org/jpos/saf/SAFMetrics.java | 103 ++++++++ .../java/org/jpos/saf/SAFMetricsTest.java | 105 ++++++++ .../src/test/java/org/jpos/saf/SAFTest.java | 231 ++++++++++++++++++ 6 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 modules/saf/src/main/java/org/jpos/saf/SAFMetrics.java create mode 100644 modules/saf/src/test/java/org/jpos/saf/SAFMetricsTest.java create mode 100644 modules/saf/src/test/java/org/jpos/saf/SAFTest.java diff --git a/modules/saf/build.gradle b/modules/saf/build.gradle index 8dbab3b5a8..c19c13a931 100644 --- a/modules/saf/build.gradle +++ b/modules/saf/build.gradle @@ -2,5 +2,7 @@ description = 'jPOS-EE :: Store and Forward (SAF) Module' dependencies { api project(':modules:core') -} + testImplementation testlibs.bundles.junit + testRuntimeOnly testlibs.bundles.junit.platform +} diff --git a/modules/saf/src/main/java/module-info.java b/modules/saf/src/main/java/module-info.java index c771fd0f3b..c3400af448 100644 --- a/modules/saf/src/main/java/module-info.java +++ b/modules/saf/src/main/java/module-info.java @@ -1,4 +1,5 @@ module org.jpos.saf { + requires micrometer.core; requires org.jpos.jpos; exports org.jpos.saf; diff --git a/modules/saf/src/main/java/org/jpos/saf/SAF.java b/modules/saf/src/main/java/org/jpos/saf/SAF.java index 47cc0d55bb..e913fd8496 100644 --- a/modules/saf/src/main/java/org/jpos/saf/SAF.java +++ b/modules/saf/src/main/java/org/jpos/saf/SAF.java @@ -18,6 +18,7 @@ package org.jpos.saf; +import io.micrometer.core.instrument.MeterRegistry; import java.io.PrintStream; import java.util.Date; import java.io.Serializable; @@ -53,11 +54,16 @@ public class SAF extends QBeanSupport implements Runnable, Loggeable { String queue; String head; String delayQueue; + SAFMetrics metrics; public void initService() { queue = getName(); head = getName() + ".head"; delayQueue = getName() + ".delayed"; + MeterRegistry registry = getServer() != null ? getServer().getMeterRegistry() : null; + metrics = new SAFMetrics(registry, getName()); + if (psp instanceof JDBMSpace) + metrics.registerQueueDepthGauge(() -> ((JDBMSpace) psp).size(queue)); NameRegistrar.register(getName(), this); } @@ -189,6 +195,10 @@ public String getStatus() { return sb.toString(); } + SAFMetrics getSAFMetrics() { + return metrics; + } + private boolean latchMsg() { Entry entry = (Entry) psp.rdp(head); if (entry == null) { @@ -215,7 +225,9 @@ private void autoCommitOff() { } private Entry send(Entry entry) { + String mti = getMTI(entry); if (shouldIgnore(entry)) { + metrics.sendExpired(mti, isMaxRetransmission(entry) ? "max-retransmissions" : "expired"); LogEvent evt = getLog().createLogEvent("saf-warning"); if (isMaxRetransmission(entry)) evt.addMessage("max retransmission count (" + maxRetransmissions + ") has been reached."); @@ -227,6 +239,7 @@ private Entry send(Entry entry) { Logger.log(evt); return null; } + long start = System.nanoTime(); try { ISOMsg resp = mux.request(entry.msg, waitForResponse); if (resp == null) { @@ -239,6 +252,7 @@ private Entry send(Entry entry) { if (retryResponseCodes != null && retryResponseCodes.indexOf(rc) >= 0) { // this result code requires retransmission, so we don't increase // the retransmission counter. The request may expire though + metrics.sendRetried(mti, rc); LogEvent evt = createLogEvent("info", entry, resp); evt.addMessage("response code '" + resp.getString(39) @@ -255,6 +269,7 @@ private Entry send(Entry entry) { + "' is in valid-response-codes list (" + validResponseCodes + ")"); Logger.log(evt); + metrics.sendSucceeded(mti, rc); // GOOD - Message was sent if (entry.responseKey != null) { if (entry.wipePreviousResponse) @@ -272,6 +287,7 @@ private Entry send(Entry entry) { + "' not in valid-response-codes list (" + validResponseCodes + ")"); Logger.log(evt); + metrics.sendDiscarded(mti, "not-in-valid-codes"); } } if (entry.count == 1 && flagRetransmissions.indexOf(entry.msg.getMTI()) >= 0) @@ -282,10 +298,20 @@ private Entry send(Entry entry) { evt.addMessage("--- stack trace ---"); evt.addMessage(e); Logger.log(evt); + } finally { + metrics.sendCompleted(System.nanoTime() - start); } return entry; } + private String getMTI(Entry entry) { + try { + return entry != null && entry.msg != null ? entry.msg.getMTI() : "unknown"; + } catch (ISOException e) { + return "unknown"; + } + } + private LogEvent createLogEvent(String type, Entry entry, ISOMsg resp) { LogEvent evt = getLog().createLogEvent(type); evt.addMessage(" Message timestamp: " + new Date(entry.time)); @@ -344,4 +370,3 @@ public Entry(ISOMsg msg, String responseKey, long responseTimeout, boolean wipeP } } } - diff --git a/modules/saf/src/main/java/org/jpos/saf/SAFMetrics.java b/modules/saf/src/main/java/org/jpos/saf/SAFMetrics.java new file mode 100644 index 0000000000..c25d1b00fb --- /dev/null +++ b/modules/saf/src/main/java/org/jpos/saf/SAFMetrics.java @@ -0,0 +1,103 @@ +/* + * 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 . + */ + +package org.jpos.saf; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; + +import java.util.function.Supplier; + +final class SAFMetrics { + private static final String QUEUE_SIZE = "jpos.saf.queue.size"; + private static final String SEND_DURATION = "jpos.saf.send.duration"; + private static final String SEND_SUCCESS = "jpos.saf.send.success"; + private static final String SEND_RETRIED = "jpos.saf.send.retried"; + private static final String SEND_EXPIRED = "jpos.saf.send.expired"; + private static final String SEND_DISCARDED = "jpos.saf.send.discarded"; + + private final MeterRegistry registry; + private final String name; + + SAFMetrics(MeterRegistry registry, String name) { + this.registry = registry; + this.name = name; + } + + boolean isEnabled() { + return registry != null; + } + + void registerQueueDepthGauge(Supplier supplier) { + if (registry == null) + return; + Gauge.builder(QUEUE_SIZE, supplier) + .tags(tags()) + .description("Current SAF queue depth") + .register(registry); + } + + void sendCompleted(long elapsedNanos) { + if (registry == null) + return; + Timer.builder(SEND_DURATION) + .tags(tags()) + .description("Time spent waiting for SAF send responses") + .publishPercentileHistogram() + .publishPercentiles(0.5, 0.95, 0.99) + .register(registry) + .record(Math.max(0L, elapsedNanos), java.util.concurrent.TimeUnit.NANOSECONDS); + } + + void sendSucceeded(String mti, String rc) { + if (registry == null) + return; + counter(SEND_SUCCESS, Tags.of("mti", mti, "rc", rc)).increment(); + } + + void sendRetried(String mti, String rc) { + if (registry == null) + return; + counter(SEND_RETRIED, Tags.of("mti", mti, "rc", rc)).increment(); + } + + void sendExpired(String mti, String reason) { + if (registry == null) + return; + counter(SEND_EXPIRED, Tags.of("mti", mti, "reason", reason)).increment(); + } + + void sendDiscarded(String mti, String reason) { + if (registry == null) + return; + counter(SEND_DISCARDED, Tags.of("mti", mti, "reason", reason)).increment(); + } + + private Counter counter(String name, Tags tags) { + return Counter.builder(name) + .tags(tags().and(tags)) + .register(registry); + } + + private Tags tags() { + return Tags.of("saf", name); + } +} diff --git a/modules/saf/src/test/java/org/jpos/saf/SAFMetricsTest.java b/modules/saf/src/test/java/org/jpos/saf/SAFMetricsTest.java new file mode 100644 index 0000000000..dca00a9fcc --- /dev/null +++ b/modules/saf/src/test/java/org/jpos/saf/SAFMetricsTest.java @@ -0,0 +1,105 @@ +/* + * 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 . + */ + +package org.jpos.saf; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SAFMetricsTest { + @Test + void noopWhenRegistryIsNull() { + SAFMetrics metrics = new SAFMetrics(null, "saf"); + assertFalse(metrics.isEnabled()); + + metrics.registerQueueDepthGauge(() -> 3); + metrics.sendCompleted(1_000L); + metrics.sendSucceeded("0200", "00"); + metrics.sendRetried("0200", "91"); + metrics.sendExpired("0200", "expired"); + metrics.sendDiscarded("0200", "not-in-valid-codes"); + } + + @Test + void registersQueueDepthGauge() { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAFMetrics metrics = new SAFMetrics(registry, "saf"); + + metrics.registerQueueDepthGauge(() -> 7); + + Gauge gauge = registry.find("jpos.saf.queue.size").tag("saf", "saf").gauge(); + assertNotNull(gauge); + assertEquals(7.0, gauge.value(), 0.0001); + } + + @Test + void recordsSendDuration() { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAFMetrics metrics = new SAFMetrics(registry, "saf"); + + metrics.sendCompleted(TimeUnit.MILLISECONDS.toNanos(25)); + + Timer timer = registry.find("jpos.saf.send.duration").tag("saf", "saf").timer(); + assertNotNull(timer); + assertEquals(1L, timer.count()); + assertTrue(timer.totalTime(TimeUnit.MILLISECONDS) >= 25.0); + } + + @Test + void recordsOutcomeCountersWithTags() { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAFMetrics metrics = new SAFMetrics(registry, "saf"); + + metrics.sendSucceeded("0200", "00"); + metrics.sendRetried("0200", "91"); + metrics.sendExpired("0200", "expired"); + metrics.sendDiscarded("0200", "not-in-valid-codes"); + + Counter success = registry.find("jpos.saf.send.success") + .tags("saf", "saf", "mti", "0200", "rc", "00") + .counter(); + Counter retried = registry.find("jpos.saf.send.retried") + .tags("saf", "saf", "mti", "0200", "rc", "91") + .counter(); + Counter expired = registry.find("jpos.saf.send.expired") + .tags("saf", "saf", "mti", "0200", "reason", "expired") + .counter(); + Counter discarded = registry.find("jpos.saf.send.discarded") + .tags("saf", "saf", "mti", "0200", "reason", "not-in-valid-codes") + .counter(); + + assertNotNull(success); + assertNotNull(retried); + assertNotNull(expired); + assertNotNull(discarded); + assertEquals(1.0, success.count(), 0.0001); + assertEquals(1.0, retried.count(), 0.0001); + assertEquals(1.0, expired.count(), 0.0001); + assertEquals(1.0, discarded.count(), 0.0001); + } +} diff --git a/modules/saf/src/test/java/org/jpos/saf/SAFTest.java b/modules/saf/src/test/java/org/jpos/saf/SAFTest.java new file mode 100644 index 0000000000..d790f43fd0 --- /dev/null +++ b/modules/saf/src/test/java/org/jpos/saf/SAFTest.java @@ -0,0 +1,231 @@ +/* + * 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 . + */ + +package org.jpos.saf; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.jpos.iso.ISOException; +import org.jpos.iso.ISOMsg; +import org.jpos.iso.ISOResponseListener; +import org.jpos.iso.MUX; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SAFTest { + @Test + void initServiceCreatesMetricsAccessorWithoutServer() { + SAF saf = new SAF(); + saf.setName("test-saf"); + + saf.initService(); + + assertNotNull(saf.getSAFMetrics()); + } + + @Test + void sendCountsSuccessfulResponses() throws Exception { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAF saf = newSAF(registry, new StaticMux(response("0210", "00"))); + SAF.Entry entry = new SAF.Entry(request("0200")); + + SAF.Entry result = invokeSend(saf, entry); + + assertNull(result); + assertCounter(registry, "jpos.saf.send.success", "saf", "test-saf", "mti", "0200", "rc", "00"); + assertTimer(registry); + } + + @Test + void sendCountsRetryResponses() throws Exception { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAF saf = newSAF(registry, new StaticMux(response("0210", "91"))); + saf.retryResponseCodes = "91"; + SAF.Entry entry = new SAF.Entry(request("0200")); + + SAF.Entry result = invokeSend(saf, entry); + + assertSame(entry, result); + assertCounter(registry, "jpos.saf.send.retried", "saf", "test-saf", "mti", "0200", "rc", "91"); + assertTimer(registry); + } + + @Test + void sendCountsDiscardedResponsesOutsideValidCodes() throws Exception { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAF saf = newSAF(registry, new StaticMux(response("0210", "05"))); + SAF.Entry entry = new SAF.Entry(request("0200")); + + SAF.Entry result = invokeSend(saf, entry); + + assertSame(entry, result); + assertCounter(registry, "jpos.saf.send.discarded", "saf", "test-saf", "mti", "0200", "reason", "not-in-valid-codes"); + assertTimer(registry); + } + + @Test + void sendCountsExpiredMessages() throws Exception { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAF saf = newSAF(registry, new StaticMux(null)); + saf.expiration = 1L; + SAF.Entry entry = new SAF.Entry(request("0200")); + entry.time = System.currentTimeMillis() - 5_000L; + + SAF.Entry result = invokeSend(saf, entry); + + assertNull(result); + assertCounter(registry, "jpos.saf.send.expired", "saf", "test-saf", "mti", "0200", "reason", "expired"); + } + + @Test + void sendCountsMaxRetransmissionMessages() throws Exception { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAF saf = newSAF(registry, new StaticMux(null)); + saf.maxRetransmissions = 1; + SAF.Entry entry = new SAF.Entry(request("0200")); + entry.count = 2; + + SAF.Entry result = invokeSend(saf, entry); + + assertNull(result); + assertCounter(registry, "jpos.saf.send.expired", "saf", "test-saf", "mti", "0200", "reason", "max-retransmissions"); + } + + @Test + void sendRecordsDurationOnTimeoutAndIsoException() throws Exception { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAF timeoutSaf = newSAF(registry, new StaticMux(null)); + + SAF.Entry timeoutResult = invokeSend(timeoutSaf, new SAF.Entry(request("0200"))); + assertNotNull(timeoutResult); + assertEquals(1L, registry.find("jpos.saf.send.duration").timer().count()); + + SimpleMeterRegistry exceptionRegistry = new SimpleMeterRegistry(); + SAF exceptionSaf = newSAF(exceptionRegistry, new ThrowingMux()); + + SAF.Entry exceptionResult = invokeSend(exceptionSaf, new SAF.Entry(request("0200"))); + assertNotNull(exceptionResult); + assertEquals(1L, exceptionRegistry.find("jpos.saf.send.duration").timer().count()); + } + + private static SAF newSAF(SimpleMeterRegistry registry, MUX mux) { + SAF saf = new SAF(); + saf.setName("test-saf"); + saf.metrics = new SAFMetrics(registry, saf.getName()); + saf.mux = mux; + saf.validResponseCodes = "00"; + saf.waitForResponse = 1_000L; + saf.flagRetransmissions = ""; + return saf; + } + + private static SAF.Entry invokeSend(SAF saf, SAF.Entry entry) throws Exception { + Method method = SAF.class.getDeclaredMethod("send", SAF.Entry.class); + method.setAccessible(true); + try { + return (SAF.Entry) method.invoke(saf, entry); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception ex) + throw ex; + throw e; + } + } + + private static ISOMsg request(String mti) { + return new ISOMsg(mti); + } + + private static ISOMsg response(String mti, String rc) throws ISOException { + ISOMsg response = new ISOMsg(mti); + response.set(39, rc); + return response; + } + + private static void assertCounter(SimpleMeterRegistry registry, String name, String... tags) { + Counter counter = registry.find(name).tags(tags).counter(); + assertNotNull(counter); + assertEquals(1.0, counter.count(), 0.0001); + } + + private static void assertTimer(SimpleMeterRegistry registry) { + Timer timer = registry.find("jpos.saf.send.duration").tag("saf", "test-saf").timer(); + assertNotNull(timer); + assertEquals(1L, timer.count()); + assertTrue(timer.totalTime(TimeUnit.NANOSECONDS) >= 0L); + } + + private static final class StaticMux implements MUX { + private final ISOMsg response; + + private StaticMux(ISOMsg response) { + this.response = response; + } + + @Override + public ISOMsg request(ISOMsg m, long timeout) { + return response; + } + + @Override + public void request(ISOMsg m, long timeout, ISOResponseListener r, Object handBack) { + throw new UnsupportedOperationException(); + } + + @Override + public void send(ISOMsg m) throws IOException, ISOException { + } + + @Override + public boolean isConnected() { + return true; + } + } + + private static final class ThrowingMux implements MUX { + @Override + public ISOMsg request(ISOMsg m, long timeout) throws ISOException { + throw new ISOException("boom"); + } + + @Override + public void request(ISOMsg m, long timeout, ISOResponseListener r, Object handBack) { + throw new UnsupportedOperationException(); + } + + @Override + public void send(ISOMsg m) throws IOException, ISOException { + } + + @Override + public boolean isConnected() { + return true; + } + } +} From 1715c315f35ff61f6198068acef53c9c96e63c77 Mon Sep 17 00:00:00 2001 From: ar-agt Date: Sat, 13 Jun 2026 11:52:09 -0300 Subject: [PATCH 2/3] docs(saf): document Micrometer metrics --- doc/src/asciidoc/module_saf.adoc | 39 +++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/doc/src/asciidoc/module_saf.adoc b/doc/src/asciidoc/module_saf.adoc index f878ad11a7..8dc276acc1 100644 --- a/doc/src/asciidoc/module_saf.adoc +++ b/doc/src/asciidoc/module_saf.adoc @@ -136,4 +136,41 @@ The module integrates with the jPOS `Logger` and `LogEvent` utilities to provide * Logs transmission attempts and responses. * Reports expired or ignored messages. -* Provides status information using the `getStatus()` method. \ No newline at end of file +* Provides status information using the `getStatus()` method. + +==== Metrics + +When Q2 has a Micrometer `MeterRegistry` configured, the SAF module registers +the following meters tagged with `saf=`: + +* `jpos.saf.queue.size` ++ +Current queue depth gauge. This is registered when SAF uses a `JDBMSpace` +backing store. + +* `jpos.saf.send.duration` ++ +Timer for SAF send attempts, with percentile histogram publishing enabled and +percentiles `0.5`, `0.95`, and `0.99`. + +* `jpos.saf.send.success` ++ +Counter incremented on valid responses, tagged with `mti` and `rc`. + +* `jpos.saf.send.retried` ++ +Counter incremented when the response code is in `retry-response-codes`, +tagged with `mti` and `rc`. + +* `jpos.saf.send.expired` ++ +Counter incremented when a queued message is discarded because it expired or +reached the maximum retransmission count, tagged with `mti` and `reason`. + +* `jpos.saf.send.discarded` ++ +Counter incremented when a response code is outside `valid-response-codes`, +tagged with `mti` and `reason`. + +If no Micrometer registry is configured, SAF metrics remain disabled and SAF +continues operating without meter registration. From a542e3e546f20b2b2652a1dd51b81c4752ebcdc3 Mon Sep 17 00:00:00 2001 From: ar-agt Date: Sat, 13 Jun 2026 21:52:10 -0300 Subject: [PATCH 3/3] fix(saf): record both expiration causes --- doc/src/asciidoc/module_saf.adoc | 2 ++ modules/saf/src/main/java/org/jpos/saf/SAF.java | 5 ++++- .../saf/src/test/java/org/jpos/saf/SAFTest.java | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/doc/src/asciidoc/module_saf.adoc b/doc/src/asciidoc/module_saf.adoc index 8dc276acc1..29c0f470ed 100644 --- a/doc/src/asciidoc/module_saf.adoc +++ b/doc/src/asciidoc/module_saf.adoc @@ -166,6 +166,8 @@ tagged with `mti` and `rc`. + Counter incremented when a queued message is discarded because it expired or reached the maximum retransmission count, tagged with `mti` and `reason`. +When both conditions apply to the same entry, both reason-tagged counters are +incremented. * `jpos.saf.send.discarded` + diff --git a/modules/saf/src/main/java/org/jpos/saf/SAF.java b/modules/saf/src/main/java/org/jpos/saf/SAF.java index e913fd8496..3329cd90c7 100644 --- a/modules/saf/src/main/java/org/jpos/saf/SAF.java +++ b/modules/saf/src/main/java/org/jpos/saf/SAF.java @@ -227,7 +227,10 @@ private void autoCommitOff() { private Entry send(Entry entry) { String mti = getMTI(entry); if (shouldIgnore(entry)) { - metrics.sendExpired(mti, isMaxRetransmission(entry) ? "max-retransmissions" : "expired"); + if (isMaxRetransmission(entry)) + metrics.sendExpired(mti, "max-retransmissions"); + if (isExpired(entry)) + metrics.sendExpired(mti, "expired"); LogEvent evt = getLog().createLogEvent("saf-warning"); if (isMaxRetransmission(entry)) evt.addMessage("max retransmission count (" + maxRetransmissions + ") has been reached."); diff --git a/modules/saf/src/test/java/org/jpos/saf/SAFTest.java b/modules/saf/src/test/java/org/jpos/saf/SAFTest.java index d790f43fd0..7fcecdaf36 100644 --- a/modules/saf/src/test/java/org/jpos/saf/SAFTest.java +++ b/modules/saf/src/test/java/org/jpos/saf/SAFTest.java @@ -117,6 +117,23 @@ void sendCountsMaxRetransmissionMessages() throws Exception { assertCounter(registry, "jpos.saf.send.expired", "saf", "test-saf", "mti", "0200", "reason", "max-retransmissions"); } + @Test + void sendCountsBothIgnoreReasonsWhenEntryIsExpiredAndMaxRetransmissionsExceeded() throws Exception { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + SAF saf = newSAF(registry, new StaticMux(null)); + saf.expiration = 1L; + saf.maxRetransmissions = 1; + SAF.Entry entry = new SAF.Entry(request("0200")); + entry.time = System.currentTimeMillis() - 5_000L; + entry.count = 2; + + SAF.Entry result = invokeSend(saf, entry); + + assertNull(result); + assertCounter(registry, "jpos.saf.send.expired", "saf", "test-saf", "mti", "0200", "reason", "expired"); + assertCounter(registry, "jpos.saf.send.expired", "saf", "test-saf", "mti", "0200", "reason", "max-retransmissions"); + } + @Test void sendRecordsDurationOnTimeoutAndIsoException() throws Exception { SimpleMeterRegistry registry = new SimpleMeterRegistry();