From 45305e18846c4e79bf3e97c23b39e6289aa7587d Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 30 Aug 2025 08:56:43 +0200 Subject: [PATCH 01/20] MQTTS metrics --- .../transport/mqtt/MqttTransportContext.java | 22 ++++++++++++++----- .../transport/mqtt/MqttTransportHandler.java | 8 +++++-- .../common/transport/TransportService.java | 2 +- .../service/DefaultTransportService.java | 18 ++++++++++++--- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java index 3d05e999e0..8a60168154 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java @@ -88,20 +88,30 @@ public class MqttTransportContext extends TransportContext { @Value("${transport.mqtt.proxy_enabled:false}") private boolean proxyEnabled; - private final AtomicInteger connectionsCounter = new AtomicInteger(); + private final AtomicInteger connectionsActiveCounterMQTT = new AtomicInteger(); + private final AtomicInteger connectionsActiveCounterMQTTS = new AtomicInteger(); @PostConstruct public void init() { super.init(); - transportService.createGaugeStats("openConnections", connectionsCounter); + transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTT, "protocol", "MQTT"); + transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTTS, "protocol", "MQTTS"); } - public void channelRegistered() { - connectionsCounter.incrementAndGet(); + public void channelRegistered(boolean isSSL) { + if (isSSL) { + connectionsActiveCounterMQTTS.incrementAndGet(); + } else { + connectionsActiveCounterMQTT.incrementAndGet(); + } } - public void channelUnregistered() { - connectionsCounter.decrementAndGet(); + public void channelUnregistered(boolean isSSL) { + if (isSSL) { + connectionsActiveCounterMQTTS.decrementAndGet(); + } else { + connectionsActiveCounterMQTT.decrementAndGet(); + } } public boolean checkAddress(InetSocketAddress address) { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index f753579292..bf623d67d8 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -180,16 +180,20 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.rpcAwaitingAck = new ConcurrentHashMap<>(); } + boolean isSSL() { + return sslHandler != null; + } + @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { super.channelRegistered(ctx); - context.channelRegistered(); + context.channelRegistered(isSSL()); } @Override public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { super.channelUnregistered(ctx); - context.channelUnregistered(); + context.channelUnregistered(isSSL()); } @Override diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 5c7d552855..612f331757 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -161,5 +161,5 @@ public interface TransportService { boolean hasSession(SessionInfoProto sessionInfo); - void createGaugeStats(String openConnections, AtomicInteger connectionsCounter); + void createGaugeStats(String openConnections, AtomicInteger connectionsCounter, String... tags); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 260957c990..47e341547e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -1257,9 +1257,21 @@ public class DefaultTransportService extends TransportActivityManager implements } @Override - public void createGaugeStats(String statsName, AtomicInteger number) { - statsFactory.createGauge(StatsType.TRANSPORT + "." + statsName, number); - statsMap.put(statsName, number); + public void createGaugeStats(String statsName, AtomicInteger number, String... tags) { + String key = "thingsboard" + "." + StatsType.TRANSPORT.getName() + "." + statsName; + statsFactory.createGauge(key, number, tags); + statsMap.put(statsName + TagsKey(tags), number); + } + + String TagsKey(String... tags) { + if (tags == null || tags.length < 2) return ""; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < tags.length; i += 2) { + if (i > 0) sb.append(','); + sb.append(tags[i]).append('=').append(i + 1 < tags.length ? tags[i + 1] : ""); + } + sb.append(']'); + return sb.toString(); } @Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}") From 2a926cfbdfc5a7c09363a6dec078a211af8cf8d6 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 30 Aug 2025 09:39:04 +0200 Subject: [PATCH 02/20] MQTT transport: client remote address logged on exceptionCaught --- .../transport/mqtt/MqttTransportHandler.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index bf623d67d8..aadeff0dee 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -1152,21 +1152,32 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + String clientAddr = null; + try { + InetSocketAddress remote = getAddress(ctx); + clientAddr = remote != null ? (remote.getAddress() != null ? remote.getAddress().getHostAddress() : remote.getHostString()) + ":" + remote.getPort() : "IPunknown"; + } catch (Exception ignored) { + } + if (cause instanceof IOException) { if (log.isDebugEnabled()) { - log.debug("[{}][{}][{}] IOException: {}", sessionId, + log.debug("[{}][{}][{}][{}] {}: {}", sessionId, Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), + clientAddr, + cause.getClass().getSimpleName(), cause.getMessage(), cause); } else if (log.isInfoEnabled()) { - log.info("[{}][{}][{}] IOException: {}", sessionId, + log.info("[{}][{}][{}][{}] {}: {}", sessionId, Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), + clientAddr, + cause.getClass().getSimpleName(), cause.getMessage()); } } else { - log.error("[{}] Unexpected Exception", sessionId, cause); + log.error("[{}][{}] Unexpected Exception", sessionId, clientAddr, cause); } closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN); From a643a340d8136e4da657488e34a5e840750b1791 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 5 Mar 2026 13:41:05 +0100 Subject: [PATCH 03/20] Address code review: fix naming, visibility, and lazy address resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename TagsKey → tagsKey and make it private (Java naming convention) - Make isSSL() private in MqttTransportHandler (internal use only) - Fix double space in if (isSSL) in MqttTransportContext - Extract getClientAddr() helper and move clientAddr computation inside logging guards so address resolution is skipped when logging is disabled Co-Authored-By: Claude Sonnet 4.6 --- .../transport/mqtt/MqttTransportContext.java | 4 ++-- .../transport/mqtt/MqttTransportHandler.java | 24 ++++++++++++------- .../service/DefaultTransportService.java | 4 ++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java index 8a60168154..10245c5a24 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java @@ -99,7 +99,7 @@ public class MqttTransportContext extends TransportContext { } public void channelRegistered(boolean isSSL) { - if (isSSL) { + if (isSSL) { connectionsActiveCounterMQTTS.incrementAndGet(); } else { connectionsActiveCounterMQTT.incrementAndGet(); @@ -107,7 +107,7 @@ public class MqttTransportContext extends TransportContext { } public void channelUnregistered(boolean isSSL) { - if (isSSL) { + if (isSSL) { connectionsActiveCounterMQTTS.decrementAndGet(); } else { connectionsActiveCounterMQTT.decrementAndGet(); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index aadeff0dee..d427c9b78e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -180,7 +180,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.rpcAwaitingAck = new ConcurrentHashMap<>(); } - boolean isSSL() { + private boolean isSSL() { return sslHandler != null; } @@ -258,6 +258,17 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private String getClientAddr(ChannelHandlerContext ctx) { + try { + InetSocketAddress remote = getAddress(ctx); + if (remote == null) return "unknown"; + String host = remote.getAddress() != null ? remote.getAddress().getHostAddress() : remote.getHostString(); + return host + ":" + remote.getPort(); + } catch (Exception ignored) { + return "unknown"; + } + } + InetSocketAddress getAddress(ChannelHandlerContext ctx) { var address = ctx.channel().attr(MqttTransportService.ADDRESS).get(); if (address == null) { @@ -1152,15 +1163,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - String clientAddr = null; - try { - InetSocketAddress remote = getAddress(ctx); - clientAddr = remote != null ? (remote.getAddress() != null ? remote.getAddress().getHostAddress() : remote.getHostString()) + ":" + remote.getPort() : "IPunknown"; - } catch (Exception ignored) { - } - if (cause instanceof IOException) { if (log.isDebugEnabled()) { + String clientAddr = getClientAddr(ctx); log.debug("[{}][{}][{}][{}] {}: {}", sessionId, Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), @@ -1169,6 +1174,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement cause.getMessage(), cause); } else if (log.isInfoEnabled()) { + String clientAddr = getClientAddr(ctx); log.info("[{}][{}][{}][{}] {}: {}", sessionId, Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), @@ -1177,7 +1183,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement cause.getMessage()); } } else { - log.error("[{}][{}] Unexpected Exception", sessionId, clientAddr, cause); + log.error("[{}][{}] Unexpected Exception", sessionId, getClientAddr(ctx), cause); } closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 47e341547e..e8ade498db 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -1260,10 +1260,10 @@ public class DefaultTransportService extends TransportActivityManager implements public void createGaugeStats(String statsName, AtomicInteger number, String... tags) { String key = "thingsboard" + "." + StatsType.TRANSPORT.getName() + "." + statsName; statsFactory.createGauge(key, number, tags); - statsMap.put(statsName + TagsKey(tags), number); + statsMap.put(statsName + tagsKey(tags), number); } - String TagsKey(String... tags) { + private String tagsKey(String... tags) { if (tags == null || tags.length < 2) return ""; StringBuilder sb = new StringBuilder("["); for (int i = 0; i < tags.length; i += 2) { From a2393f368c7bcaae76a0d98426987fff6a724180 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 5 Mar 2026 13:42:49 +0100 Subject: [PATCH 04/20] Keep isSSL() and getClientAddr() package-private for test accessibility Methods stubbed via Mockito spy in same-package tests must remain package-private. Revert isSSL() to package-private; getClientAddr() follows the same convention as getAddress(). Co-Authored-By: Claude Sonnet 4.6 --- .../server/transport/mqtt/MqttTransportHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index d427c9b78e..48fbad3032 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -180,7 +180,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.rpcAwaitingAck = new ConcurrentHashMap<>(); } - private boolean isSSL() { + boolean isSSL() { return sslHandler != null; } @@ -258,7 +258,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private String getClientAddr(ChannelHandlerContext ctx) { + String getClientAddr(ChannelHandlerContext ctx) { try { InetSocketAddress remote = getAddress(ctx); if (remote == null) return "unknown"; From da12ee6dc03abbe9dc9ba72fa112e8e82f0306a9 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 10 Mar 2026 12:10:26 +0100 Subject: [PATCH 05/20] Fix flaky AuditLogControllerTest: use Awaitility for async audit log assertions Audit logs are saved asynchronously via executor.submit() in AuditLogServiceImpl. Three tests were asserting log counts immediately after HTTP API calls, creating a race condition where the last audit log write may not have completed yet. Replace bare assertions with Awaitility.await().atMost(TIMEOUT, ...).untilAsserted() in testAuditLogs, testAuditLogs_byTenantIdAndEntityId, and testAuditLogs_byTenantIdAndEntityId_Sysadmin (confirmed broken: expected 2, got 1). Also replace the hardcoded 10s timeout in testAuditLogsSysAdmin with the TIMEOUT constant. Co-Authored-By: Claude Sonnet 4.6 --- .../controller/AuditLogControllerTest.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index 728dc1ec0e..56c3ad5619 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -113,17 +113,14 @@ public class AuditLogControllerTest extends AbstractControllerTest { doPost("/api/device", device, Device.class); } - List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?"); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(5, "/api/audit/logs?")).hasSize(11 + 1)); - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1)); - loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?"); - - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); - - loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?"); - - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1)); } @Test @@ -138,7 +135,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { } int expectedSize = loadedAuditLogsBefore.size() + 3; - Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test", expectedSize, getAuditLogs(100, "/api/audit/logs?").size())); } @@ -170,9 +167,8 @@ public class AuditLogControllerTest extends AbstractControllerTest { savedDevice = doPost("/api/device", savedDevice, Device.class); } - List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?"); - - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?")).hasSize(11 + 1)); } @Test @@ -188,9 +184,9 @@ public class AuditLogControllerTest extends AbstractControllerTest { tenantProfile.setName(tenantProfile.getName() + "(old)"); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/" +tenantProfile.getId().getEntityType()+ "/" + tenantProfile.getId().getId() + "?"); - - Assert.assertEquals("Audit logs count by Tenant Profile entity", 2, loadedAuditLogs.size()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(5, "/api/audit/logs/entity/" + tenantProfile.getId().getEntityType() + "/" + tenantProfile.getId().getId() + "?")) + .as("Audit logs count by Tenant Profile entity").hasSize(2)); //cleanup doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()); From feae24fac8098a3392449f30e07182cabe8d8f79 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 10 Mar 2026 12:22:07 +0100 Subject: [PATCH 06/20] Extract PAGE_SIZE constant in AuditLogControllerTest Replace magic number 5 with a named constant that explains intent: small enough to force multiple pages and verify pagination loop correctness. Co-Authored-By: Claude Sonnet 4.6 --- .../server/controller/AuditLogControllerTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index 56c3ad5619..44e9980f52 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -62,6 +62,9 @@ import static org.mockito.Mockito.verify; @DaoSqlTest public class AuditLogControllerTest extends AbstractControllerTest { + // Small enough to force multiple pages, verifying pagination loop correctness + private static final int PAGE_SIZE = 5; + private Tenant savedTenant; private User tenantAdmin; @@ -114,13 +117,13 @@ public class AuditLogControllerTest extends AbstractControllerTest { } Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(5, "/api/audit/logs?")).hasSize(11 + 1)); + assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1)); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1)); + assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1)); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1)); + assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1)); } @Test @@ -168,7 +171,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { } Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?")).hasSize(11 + 1)); + assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?")).hasSize(11 + 1)); } @Test @@ -185,7 +188,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(5, "/api/audit/logs/entity/" + tenantProfile.getId().getEntityType() + "/" + tenantProfile.getId().getId() + "?")) + assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/entity/" + tenantProfile.getId().getEntityType() + "/" + tenantProfile.getId().getId() + "?")) .as("Audit logs count by Tenant Profile entity").hasSize(2)); //cleanup From 611004327dab9b5a39b313ce109de6e50368def1 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 10 Mar 2026 12:22:55 +0100 Subject: [PATCH 07/20] Rename PAGE_SIZE to SMALL_PAGE_SIZE for clarity Co-Authored-By: Claude Sonnet 4.6 --- .../server/controller/AuditLogControllerTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index 44e9980f52..35804c1bd4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -63,7 +63,7 @@ import static org.mockito.Mockito.verify; public class AuditLogControllerTest extends AbstractControllerTest { // Small enough to force multiple pages, verifying pagination loop correctness - private static final int PAGE_SIZE = 5; + private static final int SMALL_PAGE_SIZE = 5; private Tenant savedTenant; private User tenantAdmin; @@ -117,13 +117,13 @@ public class AuditLogControllerTest extends AbstractControllerTest { } Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1)); + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1)); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1)); + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1)); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1)); + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1)); } @Test @@ -171,7 +171,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { } Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?")).hasSize(11 + 1)); + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?")).hasSize(11 + 1)); } @Test @@ -188,7 +188,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(PAGE_SIZE, "/api/audit/logs/entity/" + tenantProfile.getId().getEntityType() + "/" + tenantProfile.getId().getId() + "?")) + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/" + tenantProfile.getId().getEntityType() + "/" + tenantProfile.getId().getId() + "?")) .as("Audit logs count by Tenant Profile entity").hasSize(2)); //cleanup From 26acf60768f816cd2d6bde75b622089b3e7b6ac7 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 10 Mar 2026 13:20:40 +0100 Subject: [PATCH 08/20] Fix compilation error: use effectively final variables in lambda expressions Variables reassigned in a loop cannot be captured in lambda expressions. Introduce finalSavedDevice and finalTenantProfile as effectively final copies before use in Awaitility.await() lambdas. Co-Authored-By: Claude Sonnet 4.6 --- .../server/controller/AuditLogControllerTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index 35804c1bd4..95582cf261 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -170,8 +170,9 @@ public class AuditLogControllerTest extends AbstractControllerTest { savedDevice = doPost("/api/device", savedDevice, Device.class); } + Device finalSavedDevice = savedDevice; Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?")).hasSize(11 + 1)); + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1)); } @Test @@ -187,8 +188,9 @@ public class AuditLogControllerTest extends AbstractControllerTest { tenantProfile.setName(tenantProfile.getName() + "(old)"); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + TenantProfile finalTenantProfile = tenantProfile; Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> - assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/" + tenantProfile.getId().getEntityType() + "/" + tenantProfile.getId().getId() + "?")) + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/" + finalTenantProfile.getId().getEntityType() + "/" + finalTenantProfile.getId().getId() + "?")) .as("Audit logs count by Tenant Profile entity").hasSize(2)); //cleanup From bd181f115ed8ea536a551bfc3fd984f3527a5548 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 10 Mar 2026 17:26:15 +0200 Subject: [PATCH 09/20] UI: Hidden show on widgets button in Sys Admin users --- .../components/attribute/attribute-table.component.html | 3 ++- .../home/components/attribute/attribute-table.component.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 6a797a37aa..6d758b8c7b 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -93,7 +93,8 @@ (click)="deleteTelemetry($event)"> delete -