diff --git a/.gitignore b/.gitignore index 777a52efc7..6c5003fc02 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ rebuild-docker.sh */.run/** .run/** .run -.claude/ +.claude diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 7614a0149d..2f9c0cd302 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,6 +14,12 @@ -- limitations under the License. -- +-- CALCULATED FIELD ADDITIONAL INFO ADDITION START + +ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar; + +-- CALCULATED FIELD ADDITIONAL INFO ADDITION END + -- RULE CHAIN NOTES MIGRATION START ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS notes varchar(1000000); 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..95582cf261 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 SMALL_PAGE_SIZE = 5; + private Tenant savedTenant; private User tenantAdmin; @@ -113,17 +116,14 @@ public class AuditLogControllerTest extends AbstractControllerTest { doPost("/api/device", device, Device.class); } - List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?"); - - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); - - loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?"); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1)); - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1)); - 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(SMALL_PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1)); } @Test @@ -138,7 +138,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 +170,9 @@ 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()); + Device finalSavedDevice = savedDevice; + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1)); } @Test @@ -188,9 +188,10 @@ 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()); + TenantProfile finalTenantProfile = tenantProfile; + Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> + 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 doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()); diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index d9ec187e39..18e11faaf9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -108,6 +109,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig()); + assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -115,6 +117,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo()); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) @@ -322,6 +325,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { calculatedField.setType(cfType); calculatedField.setName("Test Calculated Field for " + entityId); calculatedField.setConfigurationVersion(1); + calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode()); if (customConfiguration != null) { calculatedField.setConfiguration(customConfiguration); } else switch (cfType) { diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 60701cee1a..52a30f0e85 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -73,6 +73,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); + Assert.assertEquals(calculatedField.getAdditionalInfo(), calculatedFieldFromMsg.getAdditionalInfo()); edgeImitator.expectMessageAmount(1); savedCalculatedField.setName(UPDATED_CF_NAME); @@ -229,6 +230,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { config.setOutput(output); calculatedField.setConfiguration(config); + calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode()); return calculatedField; } @@ -260,6 +262,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class); Assert.assertNotNull(calculatedField); Assert.assertEquals(resourceTitle, calculatedField.getName()); + Assert.assertEquals(JacksonUtil.newObjectNode(), calculatedField.getAdditionalInfo()); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java index 6a7d631eb1..35e12691c9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java @@ -171,7 +171,14 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { if (this.leshanClient != null) { log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity)); - this.leshanClient.stop(false); + try { + this.leshanClient.stop(false); + } catch (Exception stopEx) { + // Leshan may throw NPE during CoAP observe-relation cleanup when the server + // reference is null (race condition in NotificationDataStore.toKey()). + // The client is still considered stopped at this point — proceed with restart. + log.warn("Exception during LwM2M client stop, proceeding with restart: {}", stopEx.getMessage()); + } log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity)); this.leshanClient.start(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java index 432fac6d1a..c61ca0b123 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import java.util.concurrent.atomic.AtomicReference; import static java.util.concurrent.TimeUnit.SECONDS; @@ -37,6 +38,12 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID @Slf4j public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest { + @Before + public void resetCollectedValueTimestamps() { + Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = 0; + Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = 0; + } + /** * Read {"id":"/3303/12/5700"} * Trigger a Send operation from the client with multiple values for the same resource as a payload diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index 021639bc8c..2d3928ee88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -17,15 +17,15 @@ package org.thingsboard.server.common.data.cf; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasAdditionalInfo; import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; @@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; -import java.io.Serial; import java.util.Collections; import java.util.EnumSet; import java.util.Map; @@ -48,10 +47,7 @@ import java.util.Set; @Schema @Data @EqualsAndHashCode(callSuper = true) -public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings { - - @Serial - private static final long serialVersionUID = 4491966747773381420L; +public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo { public static final Map> SUPPORTED_ENTITIES = Map.of( EntityType.DEVICE, CalculatedFieldType.all, @@ -90,28 +86,17 @@ public class CalculatedField extends BaseData implements HasN @Valid @NotNull private CalculatedFieldConfiguration configuration; - @Getter - @Setter private Long version; + @NoXss + @Schema(description = "Additional parameters of the calculated field") + private JsonNode additionalInfo; - public CalculatedField() { - super(); - } + public CalculatedField() {} public CalculatedField(CalculatedFieldId id) { super(id); } - public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) { - this.tenantId = tenantId; - this.entityId = entityId; - this.type = type; - this.name = name; - this.configurationVersion = configurationVersion; - this.configuration = configuration; - this.version = version; - } - public CalculatedField(CalculatedField calculatedField) { super(calculatedField); this.tenantId = calculatedField.tenantId; @@ -123,6 +108,7 @@ public class CalculatedField extends BaseData implements HasN this.configurationVersion = calculatedField.configurationVersion; this.configuration = calculatedField.configuration; this.version = calculatedField.version; + this.additionalInfo = calculatedField.additionalInfo; } @Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.") @@ -159,6 +145,7 @@ public class CalculatedField extends BaseData implements HasN .append(", name='").append(name) .append(", configurationVersion=").append(configurationVersion) .append(", configuration=").append(configuration) + .append(", additionalInfo=").append(additionalInfo) .append(", version=").append(version) .append(", createdTime=").append(createdTime) .append(", id=").append(id).append(']') 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..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 @@ -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..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,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 @@ -254,6 +258,17 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + 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) { @@ -1150,19 +1165,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof IOException) { if (log.isDebugEnabled()) { - log.debug("[{}][{}][{}] IOException: {}", sessionId, + 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(""), + clientAddr, + cause.getClass().getSimpleName(), cause.getMessage(), cause); } else if (log.isInfoEnabled()) { - log.info("[{}][{}][{}] IOException: {}", sessionId, + 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(""), + clientAddr, + cause.getClass().getSimpleName(), cause.getMessage()); } } else { - log.error("[{}] Unexpected Exception", sessionId, 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/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 5c7d552855..a8f8a8b863 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 statsName, AtomicInteger number, 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..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 @@ -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); + } + + 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) { + 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}") diff --git a/dao/pom.xml b/dao/pom.xml index 928a107079..d526eeed4f 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -280,7 +280,6 @@ org.apache.maven.plugins maven-jar-plugin - ${jar-plugin.version} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 6eb14092ae..6f971e61f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -731,6 +731,7 @@ public class ModelConstants { public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version"; public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_VERSION = "version"; + public static final String CALCULATED_FIELD_ADDITIONAL_INFO = ADDITIONAL_INFO_PROPERTY; /** * Tasks constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java index 5a507b7fd3..47c1520ac0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ADDITIONAL_INFO; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID; @@ -81,9 +82,11 @@ public class CalculatedFieldEntity extends BaseVersionedEntity @Column(name = DEBUG_SETTINGS) private String debugSettings; - public CalculatedFieldEntity() { - super(); - } + @Convert(converter = JsonConverter.class) + @Column(name = CALCULATED_FIELD_ADDITIONAL_INFO) + private JsonNode additionalInfo; + + public CalculatedFieldEntity() {} public CalculatedFieldEntity(CalculatedField calculatedField) { this.setUuid(calculatedField.getUuidId()); @@ -97,6 +100,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration()); this.version = calculatedField.getVersion(); this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings()); + this.additionalInfo = calculatedField.getAdditionalInfo(); } @Override @@ -111,6 +115,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); + calculatedField.setAdditionalInfo(additionalInfo); return calculatedField; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java index f6113155ca..0f9dcc20be 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java @@ -74,6 +74,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF JsonNode configuration = JacksonUtil.toJsonNode((String) row.get("configuration")); long version = row.get("version") != null ? (long) row.get("version") : 0; String debugSettings = (String) row.get("debug_settings"); + JsonNode additionalInfo = JacksonUtil.toJsonNode((String) row.get("additional_info")); CalculatedField calculatedField = new CalculatedField(); calculatedField.setId(new CalculatedFieldId(id)); @@ -91,6 +92,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF } calculatedField.setVersion(version); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); + calculatedField.setAdditionalInfo(additionalInfo); return calculatedField; }).collect(Collectors.toList()); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index afa7c6ed9e..6a66b203a4 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -936,6 +936,7 @@ CREATE TABLE IF NOT EXISTS calculated_field ( configuration varchar(1000000), version BIGINT DEFAULT 1, debug_settings varchar(1024), + additional_info varchar, CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index c9524ba7b5..3caf4bb5f4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.service; import org.apache.commons.lang3.RandomUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.TenantProfile; @@ -83,6 +84,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration()); + assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -90,6 +92,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo()); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); } @@ -495,6 +498,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId)); + calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode()); return calculatedField; } diff --git a/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json b/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json new file mode 100644 index 0000000000..f092315e94 --- /dev/null +++ b/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json @@ -0,0 +1,458 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-orange", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Total connections" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 15, + "w": 19, + "x": 0, + "y": 0 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (protocol) (thingsboard_transport_connections_active{})", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{protocol}}", + "refId": "A" + } + ], + "title": "Active Connections by Protocol", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(209, 138, 78, 1)", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 5, + "x": 19, + "y": 0 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum (thingsboard_transport_connections_active)", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Total Connections", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 19, + "x": 0, + "y": 15 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "thingsboard_transport_connections_active", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{protocol}} {{pod_name}}", + "range": true, + "refId": "A" + } + ], + "title": "Active Connections by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(209, 138, 78, 1)", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Connections" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + } + ] + }, + "gridPos": { + "h": 15, + "w": 5, + "x": 19, + "y": 15 + }, + "id": 23, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum by (pod_name) (thingsboard_transport_connections_active)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "__auto", + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Connection Count by Pod", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "app": true, + "controller_revision_hash": true, + "instance": true, + "job": true, + "kubernetes_name": true, + "kubernetes_namespace": true, + "node_name": true, + "pod_name": false, + "statefulset_kubernetes_io_pod_name": true + }, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value": "Connections", + "pod_name": "Pod" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "Pod" + } + ] + } + } + ], + "type": "table" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "thingsboard" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Transport Connections", + "uid": "lewbrlwjerwkj3", + "version": 1, + "weekStart": "" +} diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 6856da9233..c23aaabf80 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -125,7 +125,6 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.1 diff --git a/pom.xml b/pom.xml index bf125bf209..0021b3b1d0 100755 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,7 @@ 1.2.5 1.7.1 3.5.4 - 3.4.0 + 3.5.0 2.8.8TB 2.2.30 0.8 @@ -636,7 +636,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.1 + ${jar-plugin.version} org.apache.maven.plugins 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 c9247f5549..553349df8e 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 -