Browse Source

Merge branch 'develop/3.5' of github.com:thingsboard/thingsboard into feature/home-page

pull/8359/head
Igor Kulikov 4 years ago
parent
commit
47909088f0
  1. 88
      application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
  2. 74
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  3. 43
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  4. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  5. 46
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  7. 1
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  8. 15
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  9. 9
      application/src/main/resources/thingsboard.yml
  10. 22
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  11. 14
      application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
  12. 4
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  13. 38
      application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
  14. 11
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  15. 40
      application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
  16. 2
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  17. 110
      application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java
  18. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
  19. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java
  20. 1
      common/cluster-api/src/main/proto/queue.proto
  21. 15
      common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java
  22. 32
      common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java
  23. 38
      common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java
  24. 32
      common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java
  25. 32
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java
  26. 13
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  27. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java
  28. 2
      common/edge-api/src/main/proto/edge.proto
  29. 37
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
  30. 124
      common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java
  31. 27
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  32. 1
      dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java
  33. 28
      dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java
  34. 160
      monitoring/pom.xml
  35. 46
      monitoring/src/main/conf/logback.xml
  36. 22
      monitoring/src/main/conf/tb-monitoring.conf
  37. 40
      monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java
  38. 49
      monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java
  39. 194
      monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java
  40. 59
      monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java
  41. 34
      monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java
  42. 26
      monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java
  43. 35
      monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java
  44. 33
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java
  45. 33
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java
  46. 39
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java
  47. 33
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java
  48. 30
      monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java
  49. 67
      monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java
  50. 22
      monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java
  51. 28
      monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java
  52. 32
      monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java
  53. 27
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java
  54. 28
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java
  55. 44
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java
  56. 28
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java
  57. 42
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java
  58. 22
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java
  59. 49
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java
  60. 31
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java
  61. 53
      monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java
  62. 24
      monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java
  63. 58
      monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java
  64. 144
      monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java
  65. 120
      monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java
  66. 143
      monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java
  67. 78
      monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java
  68. 67
      monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/HttpTransportHealthChecker.java
  69. 87
      monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/MqttTransportHealthChecker.java
  70. 41
      monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java
  71. 38
      monitoring/src/main/resources/logback.xml
  72. 107
      monitoring/src/main/resources/tb-monitoring.yml
  73. 30
      msa/monitoring/docker/Dockerfile
  74. 31
      msa/monitoring/docker/start-tb-monitoring.sh
  75. 192
      msa/monitoring/pom.xml
  76. 1
      msa/pom.xml
  77. 15
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  78. 9
      pom.xml
  79. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java
  80. 43
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java
  81. 15
      transport/coap/src/main/resources/tb-coap-transport.yml
  82. 15
      transport/http/src/main/resources/tb-http-transport.yml
  83. 15
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  84. 15
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  85. 15
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  86. 41
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  87. 40
      ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts
  88. 32
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  89. 8
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts
  90. 4
      ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html
  91. 3
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss
  92. 2
      ui-ngx/src/app/shared/components/notification/notification.component.scss
  93. 16
      ui-ngx/src/app/shared/models/alias.models.ts
  94. 16
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  95. 16
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  96. 14
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  97. 12
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  98. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  99. 16
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  100. 12
      ui-ngx/src/assets/locale/locale.constant-fa_IR.json

88
application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java

@ -15,8 +15,9 @@
*/
package org.thingsboard.server.service.apiusage;
import lombok.Data;
import lombok.Getter;
import org.springframework.data.util.Pair;
import lombok.Setter;
import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState;
@ -26,17 +27,17 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public abstract class BaseApiUsageState {
private final Map<ApiUsageRecordKey, Long> currentCycleValues = new ConcurrentHashMap<>();
private final Map<ApiUsageRecordKey, Long> currentHourValues = new ConcurrentHashMap<>();
private final Map<ApiUsageRecordKey, Map<String, Long>> lastGaugesByServiceId = new HashMap<>();
private final Map<ApiUsageRecordKey, Long> gaugesReportCycles = new HashMap<>();
@Getter
private final ApiUsageState apiUsageState;
@Getter
@ -46,6 +47,9 @@ public abstract class BaseApiUsageState {
@Getter
private volatile long currentHourTs;
@Setter
private long gaugeReportInterval;
public BaseApiUsageState(ApiUsageState apiUsageState) {
this.apiUsageState = apiUsageState;
this.currentCycleTs = SchedulerUtils.getStartOfCurrentMonth();
@ -53,43 +57,76 @@ public abstract class BaseApiUsageState {
this.currentHourTs = SchedulerUtils.getStartOfCurrentHour();
}
public void put(ApiUsageRecordKey key, Long value) {
currentCycleValues.put(key, value);
public StatsCalculationResult calculate(ApiUsageRecordKey key, long value, String serviceId) {
long currentValue = get(key);
long currentHourlyValue = getHourly(key);
long newValue;
long newHourlyValue;
if (key.isCounter()) {
newValue = currentValue + value;
newHourlyValue = currentHourlyValue + value;
} else {
Long newGaugeValue = calculateGauge(key, value, serviceId);
newValue = newGaugeValue != null ? newGaugeValue : currentValue;
newHourlyValue = newGaugeValue != null ? Math.max(newGaugeValue, currentHourlyValue) : currentHourlyValue;
}
set(key, newValue);
setHourly(key, newHourlyValue);
return StatsCalculationResult.of(newValue, newHourlyValue);
}
public void putHourly(ApiUsageRecordKey key, Long value) {
currentHourValues.put(key, value);
private Long calculateGauge(ApiUsageRecordKey key, long value, String serviceId) {
Map<String, Long> lastByServiceId = lastGaugesByServiceId.computeIfAbsent(key, k -> {
gaugesReportCycles.put(key, System.currentTimeMillis());
return new HashMap<>();
});
lastByServiceId.put(serviceId, value);
Long gaugeReportCycle = gaugesReportCycles.get(key);
if (gaugeReportCycle <= System.currentTimeMillis() - gaugeReportInterval) {
long newValue = lastByServiceId.values().stream().mapToLong(Long::longValue).sum();
lastGaugesByServiceId.remove(key);
gaugesReportCycles.remove(key);
return newValue;
} else {
return null;
}
}
public long add(ApiUsageRecordKey key, long value) {
long result = currentCycleValues.getOrDefault(key, 0L) + value;
currentCycleValues.put(key, result);
return result;
public void set(ApiUsageRecordKey key, Long value) {
currentCycleValues.put(key, value);
}
public long get(ApiUsageRecordKey key) {
return currentCycleValues.getOrDefault(key, 0L);
}
public long addToHourly(ApiUsageRecordKey key, long value) {
long result = currentHourValues.getOrDefault(key, 0L) + value;
currentHourValues.put(key, result);
return result;
public void setHourly(ApiUsageRecordKey key, Long value) {
currentHourValues.put(key, value);
}
public long getHourly(ApiUsageRecordKey key) {
return currentHourValues.getOrDefault(key, 0L);
}
public void setHour(long currentHourTs) {
this.currentHourTs = currentHourTs;
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
currentHourValues.put(key, 0L);
}
currentHourValues.clear();
lastGaugesByServiceId.clear();
gaugesReportCycles.clear();
}
public void setCycles(long currentCycleTs, long nextCycleTs) {
this.currentCycleTs = currentCycleTs;
this.nextCycleTs = nextCycleTs;
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
currentCycleValues.put(key, 0L);
}
currentCycleValues.clear();
}
public void onRepartitionEvent() {
lastGaugesByServiceId.clear();
gaugesReportCycles.clear();
}
public ApiUsageStateValue getFeatureValue(ApiFeature feature) {
@ -150,4 +187,11 @@ public abstract class BaseApiUsageState {
public EntityId getEntityId() {
return getApiUsageState().getEntityId();
}
@Data(staticConstructor = "of")
public static class StatsCalculationResult {
private final long newValue;
private final long newHourlyValue;
}
}

74
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -48,12 +48,12 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -62,6 +62,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.service.apiusage.BaseApiUsageState.StatsCalculationResult;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
@ -128,6 +129,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
@Value("${usage.stats.check.cycle:60000}")
private long nextCycleCheckInterval;
@Value("${usage.stats.gauge_report_interval:180000}")
private long gaugeReportInterval;
private final Lock updateLock = new ReentrantLock();
@PostConstruct
@ -155,19 +159,19 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
ToUsageStatsServiceMsg statsMsg = msg.getValue();
TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB()));
EntityId entityId;
EntityId ownerId;
if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) {
entityId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB()));
ownerId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB()));
} else {
entityId = tenantId;
ownerId = tenantId;
}
processEntityUsageStats(tenantId, entityId, statsMsg.getValuesList());
processEntityUsageStats(tenantId, ownerId, statsMsg.getValuesList(), statsMsg.getServiceId());
callback.onSuccess();
}
private void processEntityUsageStats(TenantId tenantId, EntityId entityId, List<UsageStatsKVProto> values) {
if (deletedEntities.contains(entityId)) return;
private void processEntityUsageStats(TenantId tenantId, EntityId ownerId, List<UsageStatsKVProto> values, String serviceId) {
if (deletedEntities.contains(ownerId)) return;
BaseApiUsageState usageState;
List<TsKvEntry> updatedEntries;
@ -175,7 +179,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
updateLock.lock();
try {
usageState = getOrFetchState(tenantId, entityId);
usageState = getOrFetchState(tenantId, ownerId);
long ts = usageState.getCurrentCycleTs();
long hourTs = usageState.getCurrentHourTs();
long newHourTs = SchedulerUtils.getStartOfCurrentHour();
@ -184,13 +188,18 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
}
updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length);
Set<ApiFeature> apiFeatures = new HashSet<>();
for (UsageStatsKVProto kvProto : values) {
ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(kvProto.getKey());
long newValue = usageState.add(recordKey, kvProto.getValue());
for (UsageStatsKVProto statsItem : values) {
ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(statsItem.getKey());
StatsCalculationResult calculationResult = usageState.calculate(recordKey, statsItem.getValue(), serviceId);
long newValue = calculationResult.getNewValue();
long newHourlyValue = calculationResult.getNewHourlyValue();
updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue)));
long newHourlyValue = usageState.addToHourly(recordKey, kvProto.getValue());
updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue)));
apiFeatures.add(recordKey.getApiFeature());
if (recordKey.getApiFeature() != null) {
apiFeatures.add(recordKey.getApiFeature());
}
}
if (usageState.getEntityType() == EntityType.TENANT && !usageState.getEntityId().equals(TenantId.SYS_TENANT_ID)) {
result = ((TenantApiUsageState) usageState).checkStateUpdatedDueToThreshold(apiFeatures);
@ -418,26 +427,26 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), counts, VOID_CALLBACK);
}
BaseApiUsageState getOrFetchState(TenantId tenantId, EntityId entityId) {
if (entityId == null || entityId.isNullUid()) {
entityId = tenantId;
BaseApiUsageState getOrFetchState(TenantId tenantId, EntityId ownerId) {
if (ownerId == null || ownerId.isNullUid()) {
ownerId = tenantId;
}
BaseApiUsageState state = myUsageStates.get(entityId);
BaseApiUsageState state = myUsageStates.get(ownerId);
if (state != null) {
return state;
}
ApiUsageState storedState = apiUsageStateService.findApiUsageStateByEntityId(entityId);
ApiUsageState storedState = apiUsageStateService.findApiUsageStateByEntityId(ownerId);
if (storedState == null) {
try {
storedState = apiUsageStateService.createDefaultApiUsageState(tenantId, entityId);
storedState = apiUsageStateService.createDefaultApiUsageState(tenantId, ownerId);
} catch (Exception e) {
storedState = apiUsageStateService.findApiUsageStateByEntityId(entityId);
storedState = apiUsageStateService.findApiUsageStateByEntityId(ownerId);
}
}
if (entityId.getEntityType() == EntityType.TENANT) {
if (!entityId.equals(TenantId.SYS_TENANT_ID)) {
state = new TenantApiUsageState(tenantProfileCache.get((TenantId) entityId), storedState);
if (ownerId.getEntityType() == EntityType.TENANT) {
if (!ownerId.equals(TenantId.SYS_TENANT_ID)) {
state = new TenantApiUsageState(tenantProfileCache.get((TenantId) ownerId), storedState);
} else {
state = new TenantApiUsageState(storedState);
}
@ -456,26 +465,27 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
cycleEntryFound = true;
boolean oldCount = tsKvEntry.getTs() == state.getCurrentCycleTs();
state.put(key, oldCount ? tsKvEntry.getLongValue().get() : 0L);
state.set(key, oldCount ? tsKvEntry.getLongValue().get() : 0L);
if (!oldCount) {
newCounts.add(key);
}
} else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) {
hourlyEntryFound = true;
state.putHourly(key, tsKvEntry.getTs() == state.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L);
state.setHourly(key, tsKvEntry.getTs() == state.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L);
}
if (cycleEntryFound && hourlyEntryFound) {
break;
}
}
}
log.debug("[{}] Initialized state: {}", entityId, storedState);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
state.setGaugeReportInterval(gaugeReportInterval);
log.debug("[{}] Initialized state: {}", ownerId, storedState);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, ownerId);
if (tpi.isMyPartition()) {
addEntityState(tpi, state);
} else {
otherUsageStates.put(entityId, state.getApiUsageState());
otherUsageStates.put(ownerId, state.getApiUsageState());
}
saveNewCounts(state, newCounts);
} catch (InterruptedException | ExecutionException e) {
@ -489,6 +499,12 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
protected void onRepartitionEvent() {
otherUsageStates.entrySet().removeIf(entry ->
partitionService.resolve(ServiceType.TB_CORE, entry.getValue().getTenantId(), entry.getKey()).isMyPartition());
updateLock.lock();
try {
myUsageStates.values().forEach(BaseApiUsageState::onRepartitionEvent);
} finally {
updateLock.unlock();
}
}
@Override

43
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.edge.rpc;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import io.grpc.Server;
@ -25,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
@ -35,6 +37,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
@ -66,6 +71,10 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT;
import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@Service
@Slf4j
@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true")
@ -174,7 +183,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Override
public StreamObserver<RequestMsg> handleMsgs(StreamObserver<ResponseMsg> outputStream) {
return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService).getInputStream();
return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService, this.maxInboundMessageSize).getInputStream();
}
@Override
@ -261,7 +270,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
newEventLock.unlock();
}
save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true);
save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, System.currentTimeMillis());
long lastConnectTs = System.currentTimeMillis();
save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs);
pushRuleEngineMessage(edgeGrpcSession.getEdge().getTenantId(), edgeId, lastConnectTs, CONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
scheduleEdgeEventsCheck(edgeGrpcSession);
}
@ -365,7 +376,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void onEdgeDisconnect(EdgeId edgeId) {
log.info("[{}] edge disconnected!", edgeId);
sessions.remove(edgeId);
EdgeGrpcSession removed = sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -374,7 +385,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
newEventLock.unlock();
}
save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false);
save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, System.currentTimeMillis());
long lastDisconnectTs = System.currentTimeMillis();
save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs);
pushRuleEngineMessage(removed.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
}
@ -423,4 +436,26 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.warn("[{}] Failed to update attribute [{}] with value [{}]", edgeId, key, value, t);
}
}
private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, String msgType) {
try {
ObjectNode edgeState = JacksonUtil.OBJECT_MAPPER.createObjectNode();
if (msgType.equals(CONNECT_EVENT)) {
edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true);
edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts);
} else {
edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false);
edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts);
}
String data = JacksonUtil.toString(edgeState);
TbMsgMetaData md = new TbMsgMetaData();
if (!persistToTelemetry) {
md.putValue(DataConstants.SCOPE, SERVER_SCOPE);
}
TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data);
clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push {}", tenantId, edgeId, msgType, e);
}
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -105,16 +105,20 @@ public final class EdgeGrpcSession implements Closeable {
private EdgeVersion edgeVersion;
private int maxInboundMessageSize;
private int clientMaxInboundMessageSize;
private ScheduledExecutorService sendDownlinkExecutorService;
EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver<ResponseMsg> outputStream, BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener,
Consumer<EdgeId> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService) {
Consumer<EdgeId> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
this.sessionId = UUID.randomUUID();
this.ctx = ctx;
this.outputStream = outputStream;
this.sessionOpenListener = sessionOpenListener;
this.sessionCloseListener = sessionCloseListener;
this.sendDownlinkExecutorService = sendDownlinkExecutorService;
this.maxInboundMessageSize = maxInboundMessageSize;
initInputStream();
}
@ -130,6 +134,10 @@ public final class EdgeGrpcSession implements Closeable {
if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) {
outputStream.onError(new RuntimeException(responseMsg.getErrorMsg()));
} else {
if (requestMsg.getConnectRequestMsg().hasMaxInboundMessageSize()) {
log.debug("[{}] Client max inbound message size: {}", sessionId, requestMsg.getConnectRequestMsg().getMaxInboundMessageSize());
clientMaxInboundMessageSize = requestMsg.getConnectRequestMsg().getMaxInboundMessageSize();
}
connected = true;
}
}
@ -408,9 +416,17 @@ public final class EdgeGrpcSession implements Closeable {
}
log.trace("[{}] [{}] downlink msg(s) are going to be send.", this.sessionId, copy.size());
for (DownlinkMsg downlinkMsg : copy) {
sendDownlinkMsg(ResponseMsg.newBuilder()
.setDownlinkMsg(downlinkMsg)
.build());
if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) {
log.error("[{}][{}][{}] Downlink msg size [{}] exceeds client max inbound message size [{}]. Skipping this message. " +
"Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it." +
"Message {}", edge.getTenantId(), edge.getId(), this.sessionId, downlinkMsg.getSerializedSize(),
this.clientMaxInboundMessageSize, downlinkMsg);
sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId());
} else {
sendDownlinkMsg(ResponseMsg.newBuilder()
.setDownlinkMsg(downlinkMsg)
.build());
}
}
if (attempt < MAX_DOWNLINK_ATTEMPTS) {
scheduleDownlinkMsgsPackSend(attempt + 1);
@ -638,7 +654,9 @@ public final class EdgeGrpcSession implements Closeable {
return ConnectResponseMsg.newBuilder()
.setResponseCode(ConnectResponseCode.ACCEPTED)
.setErrorMsg("")
.setConfiguration(ctx.getEdgeMsgConstructor().constructEdgeConfiguration(edge)).build();
.setConfiguration(ctx.getEdgeMsgConstructor().constructEdgeConfiguration(edge))
.setMaxInboundMessageSize(maxInboundMessageSize)
.build();
}
return ConnectResponseMsg.newBuilder()
.setResponseCode(ConnectResponseCode.BAD_CREDENTIALS)

46
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -26,6 +26,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
@ -33,6 +34,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
@ -61,6 +63,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.sql.query.EntityQueryRepository;
@ -85,6 +88,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
@ -151,6 +155,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private final TbServiceInfoProvider serviceInfoProvider;
private final EntityQueryRepository entityQueryRepository;
private final DbTypeInfoComponent dbTypeInfoComponent;
private final TbApiUsageReportClient apiUsageReportClient;
@Autowired @Lazy
private TelemetrySubscriptionService tsSubService;
@ -186,7 +191,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
TbClusterService clusterService, PartitionService partitionService,
TbServiceInfoProvider serviceInfoProvider,
EntityQueryRepository entityQueryRepository,
DbTypeInfoComponent dbTypeInfoComponent) {
DbTypeInfoComponent dbTypeInfoComponent,
TbApiUsageReportClient apiUsageReportClient) {
this.tenantService = tenantService;
this.deviceService = deviceService;
this.attributesService = attributesService;
@ -196,6 +202,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
this.serviceInfoProvider = serviceInfoProvider;
this.entityQueryRepository = entityQueryRepository;
this.dbTypeInfoComponent = dbTypeInfoComponent;
this.apiUsageReportClient = apiUsageReportClient;
}
@PostConstruct
@ -203,7 +210,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
super.init();
deviceStateExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(
Math.max(4, Runtime.getRuntime().availableProcessors()), "device-state"));
scheduledExecutor.scheduleAtFixedRate(this::updateInactivityStateIfExpired, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
scheduledExecutor.scheduleAtFixedRate(this::checkStates, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
}
@PreDestroy
@ -428,29 +435,48 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
}
void updateInactivityStateIfExpired() {
void checkStates() {
try {
final long ts = System.currentTimeMillis();
Map<TenantId, Pair<AtomicInteger, AtomicInteger>> devicesActivity = new HashMap<>();
partitionedEntities.forEach((tpi, deviceIds) -> {
log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size());
for (DeviceId deviceId : deviceIds) {
DeviceStateData stateData;
try {
updateInactivityStateIfExpired(ts, deviceId);
stateData = getOrFetchDeviceStateData(deviceId);
} catch (Exception e) {
log.error("[{}] Failed to get or fetch device state data", deviceId, e);
continue;
}
try {
updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e);
}
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger()));
if (stateData.getState().isActive()) {
tenantDevicesActivity.getLeft().incrementAndGet();
} else {
tenantDevicesActivity.getRight().incrementAndGet();
}
}
});
devicesActivity.forEach((tenantId, tenantDevicesActivity) -> {
int active = tenantDevicesActivity.getLeft().get();
int inactive = tenantDevicesActivity.getRight().get();
apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.ACTIVE_DEVICES, active);
apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.INACTIVE_DEVICES, inactive);
if (active > 0) {
log.info("[{}] Active devices: {}, inactive devices: {}", tenantId, active, inactive);
}
});
} catch (Throwable t) {
log.warn("Failed to update inactivity states", t);
log.warn("Failed to check devices states", t);
}
}
void updateInactivityStateIfExpired(long ts, DeviceId deviceId) {
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
updateInactivityStateIfExpired(ts, deviceId, stateData);
}
void updateInactivityStateIfExpired(long ts, DeviceId deviceId, DeviceStateData stateData) {
log.trace("Processing state {} for device {}", stateData, deviceId);
if (stateData != null) {

3
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java

@ -22,9 +22,6 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;

1
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -416,6 +416,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
private <S> void addVoidCallback(ListenableFuture<S> saveFuture, final FutureCallback<Void> callback) {
if (callback == null) return;
Futures.addCallback(saveFuture, new FutureCallback<S>() {
@Override
public void onSuccess(@Nullable S result) {

15
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -21,6 +21,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -65,8 +68,8 @@ public class DefaultUpdateService implements UpdateService {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-update-service"));
private ScheduledFuture checkUpdatesFuture = null;
private RestTemplate restClient = new RestTemplate();
private ScheduledFuture<?> checkUpdatesFuture = null;
private final RestTemplate restClient = new RestTemplate();
private UpdateMessage updateMessage;
@ -96,7 +99,7 @@ public class DefaultUpdateService implements UpdateService {
Path instanceIdPath = Paths.get(INSTANCE_ID_FILE);
if (instanceIdPath.toFile().exists()) {
byte[] data = Files.readAllBytes(instanceIdPath);
if (data != null && data.length > 0) {
if (data.length > 0) {
try {
result = UUID.fromString(new String(data));
} catch (IllegalArgumentException e) {
@ -126,13 +129,15 @@ public class DefaultUpdateService implements UpdateService {
Runnable checkUpdatesRunnable = () -> {
try {
log.trace("Executing check update method for instanceId [{}], platform [{}] and version [{}]", instanceId, platform, version);
var headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ObjectNode request = new ObjectMapper().createObjectNode();
request.put(PLATFORM_PARAM, platform);
request.put(VERSION_PARAM, version);
request.put(INSTANCE_ID_PARAM, instanceId.toString());
UpdateMessage prevUpdateMessage = updateMessage;
updateMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v2/thingsboard/updates", request, UpdateMessage.class);
if (updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) {
updateMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v2/thingsboard/updates", new HttpEntity<>(request.toString(), headers), UpdateMessage.class);
if (updateMessage != null && updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) {
notificationRuleProcessor.process(NewPlatformVersionTrigger.builder()
.updateInfo(updateMessage)
.build());

9
application/src/main/resources/thingsboard.yml

@ -140,6 +140,7 @@ usage:
interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
check:
cycle: "${USAGE_STATS_CHECK_CYCLE:60000}"
gauge_report_interval: "${USAGE_STATS_GAUGE_REPORT_INTERVAL:180000}"
# UI parameters
ui:
@ -961,7 +962,6 @@ edges:
scheduler_pool_size: "${EDGES_SCHEDULER_POOL_SIZE:1}"
send_scheduler_pool_size: "${EDGES_SEND_SCHEDULER_POOL_SIZE:1}"
grpc_callback_thread_pool_size: "${EDGES_GRPC_CALLBACK_POOL_SIZE:1}"
edge_events_ttl: "${EDGES_EDGE_EVENTS_TTL:0}"
state:
persistToTelemetry: "${EDGES_PERSIST_STATE_TO_TELEMETRY:false}"
@ -988,6 +988,13 @@ queue:
print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}"
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
ssl:
enabled: "${TB_KAFKA_SSL_ENABLED:false}"
truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}"
truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}"
keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}"
keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}"
key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}"
acks: "${TB_KAFKA_ACKS:all}"
retries: "${TB_KAFKA_RETRIES:1}"
compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip

22
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -30,10 +30,8 @@ import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.hibernate.exception.ConstraintViolationException;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
@ -78,6 +76,9 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
@ -533,6 +534,17 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return assetProfile;
}
protected Device createDevice(String name, String accessToken) throws Exception {
Device device = new Device();
device.setName(name);
device.setType("default");
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
deviceData.setConfiguration(new DefaultDeviceConfiguration());
device.setDeviceData(deviceData);
return doPost("/api/device?accessToken=" + accessToken, device, Device.class);
}
protected MqttDeviceProfileTransportConfiguration createMqttDeviceProfileTransportConfiguration(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration, boolean sendAckOnValidationException) {
MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration();
mqttDeviceProfileTransportConfiguration.setDeviceTelemetryTopic(MqttTopics.DEVICE_TELEMETRY_TOPIC);
@ -897,6 +909,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return (T) field.get(target);
}
protected void setStaticFieldValue(Class<?> targetCls, String fieldName, Object value) throws Exception {
Field field = targetCls.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(null, value);
}
protected int getDeviceActorSubscriptionCount(DeviceId deviceId, FeatureType featureType) {
DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId);
Map<UUID, SessionInfo> subscriptions = (Map<UUID, SessionInfo>) ReflectionTestUtils.getField(processor, getMapName(featureType));

14
application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java

@ -107,7 +107,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
Thread.sleep(1);
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityCountQuery countQuery = new EntityCountQuery(filter);
@ -115,11 +115,11 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
filter.setDeviceType("unknown");
filter.setDeviceTypes(List.of("unknown"));
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(0, count.longValue());
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("Device1");
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
@ -341,7 +341,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -442,7 +442,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
Thread.sleep(1000);
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -521,7 +521,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
KeyFilter highTemperatureFilter = new KeyFilter();
@ -576,7 +576,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
@Test
public void givenInvalidEntityDataPageLink_thenReturnError() throws Exception {
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
String invalidSortProperty = "created(Time)";

4
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java

@ -585,7 +585,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
@Test
public void testGetTelemetryWhenEntityViewTimeRangeInsideTimestampRange() throws Exception {
DeviceTypeFilter dtf = new DeviceTypeFilter(testDevice.getType(), testDevice.getName());
DeviceTypeFilter dtf = new DeviceTypeFilter(List.of(testDevice.getType()), testDevice.getName());
List<String> tsKeys = List.of("tsKey1", "tsKey2", "tsKey3");
DeviceCredentials deviceCredentials = doGet("/api/device/" + testDevice.getId().getId() + "/credentials", DeviceCredentials.class);
@ -684,7 +684,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
}
private Set<String> putAttributesAndWait(String stringKV, Set<String> expectedKeySet) throws Exception {
DeviceTypeFilter dtf = new DeviceTypeFilter(testDevice.getType(), testDevice.getName());
DeviceTypeFilter dtf = new DeviceTypeFilter(List.of(testDevice.getType()), testDevice.getName());
List<EntityKey> keysToSubscribe = expectedKeySet.stream()
.map(key -> new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, key))
.collect(Collectors.toList());

38
application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -87,7 +89,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
device = doPost("/api/device", device, Device.class);
dtf = new DeviceTypeFilter(device.getType(), device.getName());
dtf = new DeviceTypeFilter(List.of(device.getType()), device.getName());
}
@After
@ -195,7 +197,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
Assert.assertEquals(1, update1.getCmdId());
Assert.assertEquals(1, update1.getCount());
DeviceTypeFilter dtf2 = new DeviceTypeFilter("non-existing-device-type", "D");
DeviceTypeFilter dtf2 = new DeviceTypeFilter(List.of("non-existing-device-type"), "D");
EntityCountQuery edq2 = new EntityCountQuery(dtf2, Collections.emptyList());
EntityCountCmd cmd2 = new EntityCountCmd(2, edq2);
@ -214,7 +216,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
highTemperatureFilter.setPredicate(predicate);
highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
DeviceTypeFilter dtf3 = new DeviceTypeFilter("default", "D");
DeviceTypeFilter dtf3 = new DeviceTypeFilter(List.of("default"), "D");
EntityCountQuery edq3 = new EntityCountQuery(dtf3, Collections.singletonList(highTemperatureFilter));
EntityCountCmd cmd3 = new EntityCountCmd(3, edq3);
getWsClient().send(cmd3);
@ -231,7 +233,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
highTemperatureFilter2.setPredicate(predicate2);
highTemperatureFilter2.setValueType(EntityKeyValueType.NUMERIC);
DeviceTypeFilter dtf4 = new DeviceTypeFilter("default", "D");
DeviceTypeFilter dtf4 = new DeviceTypeFilter(List.of("default"), "D");
EntityCountQuery edq4 = new EntityCountQuery(dtf4, Collections.singletonList(highTemperatureFilter2));
EntityCountCmd cmd4 = new EntityCountCmd(4, edq4);
@ -637,6 +639,34 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest {
assertThat(update.get("data").get("attr").get(0).get(1).asText()).isEqualTo(expectedAttrValue);
}
@Test
public void testEntityCountCmd_filterTypeSingularCompatibilityTest() {
ObjectNode oldFormatDeviceTypeFilterSingular = JacksonUtil.OBJECT_MAPPER.createObjectNode();
oldFormatDeviceTypeFilterSingular.put("type", "deviceType");
oldFormatDeviceTypeFilterSingular.put("deviceType", "default");
oldFormatDeviceTypeFilterSingular.put("deviceNameFilter", "Device");
ObjectNode query = JacksonUtil.OBJECT_MAPPER.createObjectNode();
query.set("entityFilter", oldFormatDeviceTypeFilterSingular);
ObjectNode entityCountCmd = JacksonUtil.OBJECT_MAPPER.createObjectNode();
entityCountCmd.put("cmdId", 1);
entityCountCmd.set("query", query);
ArrayNode entityCountCmds = JacksonUtil.OBJECT_MAPPER.createArrayNode();
entityCountCmds.add(entityCountCmd);
ObjectNode wrapperNode = JacksonUtil.OBJECT_MAPPER.createObjectNode();
wrapperNode.set("entityCountCmds", entityCountCmds);
getWsClient().send(JacksonUtil.toString(wrapperNode));
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(1, update.getCount());
}
private void sendTelemetry(Device device, List<TsKvEntry> tsData) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
tsService.saveAndNotify(device.getTenantId(), null, device.getId(), tsData, 0, new FutureCallback<Void>() {

11
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -104,13 +104,14 @@ public class EdgeImitator {
ignoredTypes = new ArrayList<>();
this.routingKey = routingKey;
this.routingSecret = routingSecret;
setEdgeCredentials("rpcHost", host);
setEdgeCredentials("rpcPort", port);
setEdgeCredentials("timeoutSecs", 3);
setEdgeCredentials("keepAliveTimeSec", 300);
updateEdgeClientFields("rpcHost", host);
updateEdgeClientFields("rpcPort", port);
updateEdgeClientFields("timeoutSecs", 3);
updateEdgeClientFields("keepAliveTimeSec", 300);
updateEdgeClientFields("maxInboundMessageSize", 4194304);
}
private void setEdgeCredentials(String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
private void updateEdgeClientFields(String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field fieldToSet = edgeRpcClient.getClass().getDeclaredField(fieldName);
fieldToSet.setAccessible(true);
fieldToSet.set(edgeRpcClient, value);

40
application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
@Service
@Primary
public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService, NotificationTargetService notificationTargetService, NotificationTemplateService notificationTemplateService, NotificationRuleService notificationRuleService) {
super(adminSettingsService, notificationTargetService, notificationTemplateService, notificationRuleService);
}
@Override
public void createDefaultNotificationConfigs(TenantId tenantId) {
// do nothing
}
}

2
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -73,7 +73,7 @@ public class DefaultDeviceStateServiceTest {
@Before
public void setUp() {
service = spy(new DefaultDeviceStateService(tenantService, deviceService, attributesService, tsService, clusterService, partitionService, serviceInfoProvider, null, null));
service = spy(new DefaultDeviceStateService(tenantService, deviceService, attributesService, tsService, clusterService, partitionService, serviceInfoProvider, null, null, null));
}
@Test

110
application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java

@ -0,0 +1,110 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.stats;
import lombok.SneakyThrows;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"usage.stats.report.enabled=true",
"transport.http.enabled=true",
"usage.stats.report.interval=2",
"usage.stats.gauge_report_interval=1",
"state.defaultStateCheckIntervalInSec=3",
"state.defaultInactivityTimeoutInSec=10"
})
public class DevicesStatisticsTest extends AbstractControllerTest {
@Autowired
private TbApiUsageStateService apiUsageStateService;
@Autowired
private TimeseriesService timeseriesService;
private ApiUsageStateId apiUsageStateId;
@Before
public void beforeEach() throws Exception {
loginTenantAdmin();
apiUsageStateId = apiUsageStateService.getApiUsageState(tenantId).getId();
}
@Test
public void testDevicesActivityStats() throws Exception {
int activeDevicesCount = 5;
List<Device> activeDevices = new ArrayList<>();
for (int i = 1; i <= activeDevicesCount; i++) {
String name = "active_device_" + i;
Device device = createDevice(name, name);
activeDevices.add(device);
}
int inactiveDevicesCount = 10;
List<Device> inactiveDevices = new ArrayList<>();
for (int i = 1; i <= inactiveDevicesCount; i++) {
String name = "inactive_device_" + i;
Device device = createDevice(name, name);
inactiveDevices.add(device);
}
await().atMost(15, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertThat(getLatestStats(ApiUsageRecordKey.ACTIVE_DEVICES, false)).isZero();
assertThat(getLatestStats(ApiUsageRecordKey.INACTIVE_DEVICES, false)).isEqualTo(activeDevicesCount + inactiveDevicesCount);
});
for (Device device : activeDevices) {
postTelemetry(device.getName(), "{\"dp\":1}");
}
await().atMost(40, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertThat(getLatestStats(ApiUsageRecordKey.ACTIVE_DEVICES, false)).isEqualTo(activeDevicesCount);
assertThat(getLatestStats(ApiUsageRecordKey.INACTIVE_DEVICES, false)).isEqualTo(inactiveDevicesCount);
});
}
@SneakyThrows
private Long getLatestStats(ApiUsageRecordKey key, boolean hourly) {
return timeseriesService.findLatest(tenantId, apiUsageStateId, List.of(key.getApiCountKey() + (hourly ? "Hourly" : "")))
.get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null);
}
@SneakyThrows
private void postTelemetry(String accessToken, String json) {
doPost("/api/v1/" + accessToken + "/telemetry", json, new String[0]).andExpect(status().isOk());
}
}

2
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java

@ -370,7 +370,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
protected void processProtoTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception {
MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken);
DeviceTypeFilter dtf = new DeviceTypeFilter(savedDevice.getType(), savedDevice.getName());
DeviceTypeFilter dtf = new DeviceTypeFilter(List.of(savedDevice.getType()), savedDevice.getName());
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> clientKeysList = List.of(clientKeysStr.split(","));

2
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java

@ -190,7 +190,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest {
return device;
}
private Device createDevice(String deviceName, String accessToken) throws Exception {
protected Device createDevice(String deviceName, String accessToken) throws Exception {
Device device = new Device();
device.setName(deviceName);
device.setType("default");

1
common/cluster-api/src/main/proto/queue.proto

@ -1034,6 +1034,7 @@ message ToUsageStatsServiceMsg {
repeated UsageStatsKVProto values = 5;
int64 customerIdMSB = 6;
int64 customerIdLSB = 7;
string serviceId = 8;
}
message ToOtaPackageStateServiceMsg {

15
common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java

@ -26,7 +26,9 @@ public enum ApiUsageRecordKey {
JS_EXEC_COUNT(ApiFeature.JS, "jsExecutionCount", "jsExecutionLimit", "JavaScript execution"),
EMAIL_EXEC_COUNT(ApiFeature.EMAIL, "emailCount", "emailLimit", "email message"),
SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit", "SMS message"),
CREATED_ALARMS_COUNT(ApiFeature.ALARM, "createdAlarmsCount", "createdAlarmsLimit", "alarm");
CREATED_ALARMS_COUNT(ApiFeature.ALARM, "createdAlarmsCount", "createdAlarmsLimit", "alarm"),
ACTIVE_DEVICES("activeDevicesCount"),
INACTIVE_DEVICES("inactiveDevicesCount");
private static final ApiUsageRecordKey[] JS_RECORD_KEYS = {JS_EXEC_COUNT};
private static final ApiUsageRecordKey[] RE_RECORD_KEYS = {RE_EXEC_COUNT};
@ -44,12 +46,23 @@ public enum ApiUsageRecordKey {
private final String apiLimitKey;
@Getter
private final String unitLabel;
@Getter
private final boolean counter;
ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey, String unitLabel) {
this(apiFeature, apiCountKey, apiLimitKey, unitLabel, true);
}
ApiUsageRecordKey(String apiCountKey) {
this(null, apiCountKey, null, null, false);
}
ApiUsageRecordKey(ApiFeature apiFeature, String apiCountKey, String apiLimitKey, String unitLabel, boolean counter) {
this.apiFeature = apiFeature;
this.apiCountKey = apiCountKey;
this.apiLimitKey = apiLimitKey;
this.unitLabel = unitLabel;
this.counter = counter;
}
public static ApiUsageRecordKey[] getKeys(ApiFeature feature) {

32
common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java

@ -15,18 +15,38 @@
*/
package org.thingsboard.server.common.data.query;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.util.CollectionUtils;
@Data
import java.util.Collections;
import java.util.List;
@ToString
@EqualsAndHashCode
@Setter
public class AssetTypeFilter implements EntityFilter {
/**
* Replaced by {@link AssetTypeFilter#getAssetTypes()} instead.
*/
@Deprecated(since = "3.5", forRemoval = true)
private String assetType;
private List<String> assetTypes;
public List<String> getAssetTypes() {
return !CollectionUtils.isEmpty(assetTypes) ? assetTypes : Collections.singletonList(assetType);
}
@Getter
private String assetNameFilter;
@Override
public EntityFilterType getType() {
return EntityFilterType.ASSET_TYPE;
}
private String assetType;
private String assetNameFilter;
}

38
common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java

@ -16,21 +16,45 @@
package org.thingsboard.server.common.data.query;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
@EqualsAndHashCode
@Setter
public class DeviceTypeFilter implements EntityFilter {
@Override
public EntityFilterType getType() {
return EntityFilterType.DEVICE_TYPE;
}
/**
* Replaced by {@link DeviceTypeFilter#getDeviceTypes()} instead.
*/
@Deprecated(since = "3.5", forRemoval = true)
private String deviceType;
private List<String> deviceTypes;
public List<String> getDeviceTypes() {
return !CollectionUtils.isEmpty(deviceTypes) ? deviceTypes : Collections.singletonList(deviceType);
}
@Getter
private String deviceNameFilter;
public DeviceTypeFilter(List<String> deviceTypes, String deviceNameFilter) {
this.deviceTypes = deviceTypes;
this.deviceNameFilter = deviceNameFilter;
}
@Override
public EntityFilterType getType() {
return EntityFilterType.DEVICE_TYPE;
}
}

32
common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java

@ -15,18 +15,38 @@
*/
package org.thingsboard.server.common.data.query;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.util.CollectionUtils;
@Data
import java.util.Collections;
import java.util.List;
@ToString
@EqualsAndHashCode
@Setter
public class EdgeTypeFilter implements EntityFilter {
/**
* Replaced by {@link EdgeTypeFilter#getEdgeTypes()} instead.
*/
@Deprecated(since = "3.5", forRemoval = true)
private String edgeType;
private List<String> edgeTypes;
public List<String> getEdgeTypes() {
return !CollectionUtils.isEmpty(edgeTypes) ? edgeTypes : Collections.singletonList(edgeType);
}
@Getter
private String edgeNameFilter;
@Override
public EntityFilterType getType() {
return EntityFilterType.EDGE_TYPE;
}
private String edgeType;
private String edgeNameFilter;
}

32
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java

@ -15,18 +15,38 @@
*/
package org.thingsboard.server.common.data.query;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.util.CollectionUtils;
@Data
import java.util.Collections;
import java.util.List;
@ToString
@EqualsAndHashCode
@Setter
public class EntityViewTypeFilter implements EntityFilter {
/**
* Replaced by {@link EntityViewTypeFilter#getEntityViewTypes()} instead.
*/
@Deprecated(since = "3.5", forRemoval = true)
private String entityViewType;
private List<String> entityViewTypes;
public List<String> getEntityViewTypes() {
return !CollectionUtils.isEmpty(entityViewTypes) ? entityViewTypes : Collections.singletonList(entityViewType);
}
@Getter
private String entityViewNameFilter;
@Override
public EntityFilterType getType() {
return EntityFilterType.ENTITY_VIEW_TYPE;
}
private String entityViewType;
private String entityViewNameFilter;
}

13
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -20,6 +20,7 @@ import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
import io.grpc.stub.StreamObserver;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@ -62,6 +63,10 @@ public class EdgeGrpcClient implements EdgeRpcClient {
private boolean sslEnabled;
@Value("${cloud.rpc.ssl.cert:}")
private String certResource;
@Value("${cloud.rpc.max_inbound_message_size:4194304}")
private int maxInboundMessageSize;
@Getter
private int serverMaxInboundMessageSize;
private ManagedChannel channel;
@ -77,6 +82,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
Consumer<DownlinkMsg> onDownlink,
Consumer<Exception> onError) {
NettyChannelBuilder builder = NettyChannelBuilder.forAddress(rpcHost, rpcPort)
.maxInboundMessageSize(maxInboundMessageSize)
.keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS);
if (sslEnabled) {
try {
@ -101,7 +107,8 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_3_3_3)
.setEdgeVersion(EdgeVersion.V_3_4_0)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());
}
@ -117,6 +124,10 @@ public class EdgeGrpcClient implements EdgeRpcClient {
if (responseMsg.hasConnectResponseMsg()) {
ConnectResponseMsg connectResponseMsg = responseMsg.getConnectResponseMsg();
if (connectResponseMsg.getResponseCode().equals(ConnectResponseCode.ACCEPTED)) {
if (connectResponseMsg.hasMaxInboundMessageSize()) {
log.debug("[{}] Server max inbound message size: {}", edgeKey, connectResponseMsg.getMaxInboundMessageSize());
serverMaxInboundMessageSize = connectResponseMsg.getMaxInboundMessageSize();
}
log.info("[{}] Configuration received: {}", edgeKey, connectResponseMsg.getConfiguration());
onEdgeUpdate.accept(connectResponseMsg.getConfiguration());
} else {

2
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java

@ -41,4 +41,6 @@ public interface EdgeRpcClient {
void sendUplinkMsg(UplinkMsg uplinkMsg);
void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg);
int getServerMaxInboundMessageSize();
}

2
common/edge-api/src/main/proto/edge.proto

@ -68,6 +68,7 @@ message ConnectRequestMsg {
string edgeRoutingKey = 1;
string edgeSecret = 2;
EdgeVersion edgeVersion = 3;
optional int32 maxInboundMessageSize = 4;
}
enum ConnectResponseCode {
@ -80,6 +81,7 @@ message ConnectResponseMsg {
ConnectResponseCode responseCode = 1;
string errorMsg = 2;
EdgeConfiguration configuration = 3;
optional int32 maxInboundMessageSize = 4;
}
message SyncRequestMsg {

37
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java

@ -19,6 +19,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
@ -49,6 +50,24 @@ public class TbKafkaSettings {
@Value("${queue.kafka.bootstrap.servers}")
private String servers;
@Value("${queue.kafka.ssl.enabled:false}")
private boolean sslEnabled;
@Value("${queue.kafka.ssl.truststore.location}")
private String sslTruststoreLocation;
@Value("${queue.kafka.ssl.truststore.password}")
private String sslTruststorePassword;
@Value("${queue.kafka.ssl.keystore.location}")
private String sslKeystoreLocation;
@Value("${queue.kafka.ssl.keystore.password}")
private String sslKeystorePassword;
@Value("${queue.kafka.ssl.key.password}")
private String sslKeyPassword;
@Value("${queue.kafka.acks}")
private String acks;
@ -115,6 +134,8 @@ public class TbKafkaSettings {
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(AdminClientConfig.RETRIES_CONFIG, retries);
configureSSL(props);
return props;
}
@ -126,6 +147,8 @@ public class TbKafkaSettings {
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes);
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs);
configureSSL(props);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
@ -164,7 +187,21 @@ public class TbKafkaSettings {
if (other != null) {
other.forEach(kv -> props.put(kv.getKey(), kv.getValue()));
}
configureSSL(props);
return props;
}
private void configureSSL(Properties props) {
if (sslEnabled) {
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL");
props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocation);
props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePassword);
props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocation);
props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePassword);
props.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, sslKeyPassword);
}
}
}

124
common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java

@ -16,11 +16,11 @@
package org.thingsboard.server.queue.usagestats;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -32,12 +32,12 @@ import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import javax.annotation.PostConstruct;
import java.util.EnumMap;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -47,6 +47,7 @@ import java.util.concurrent.atomic.AtomicLong;
@Component
@Slf4j
@RequiredArgsConstructor
public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
@Value("${usage.stats.report.enabled:true}")
@ -56,19 +57,14 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
@Value("${usage.stats.report.interval:10}")
private int interval;
private final EnumMap<ApiUsageRecordKey, ConcurrentMap<OwnerId, AtomicLong>> stats = new EnumMap<>(ApiUsageRecordKey.class);
private final EnumMap<ApiUsageRecordKey, ConcurrentMap<ReportLevel, AtomicLong>> stats = new EnumMap<>(ApiUsageRecordKey.class);
private final PartitionService partitionService;
private final TbServiceInfoProvider serviceInfoProvider;
private final SchedulerComponent scheduler;
private final TbQueueProducerProvider producerProvider;
private TbQueueProducer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> msgProducer;
public DefaultTbApiUsageReportClient(PartitionService partitionService, SchedulerComponent scheduler, TbQueueProducerProvider producerProvider) {
this.partitionService = partitionService;
this.scheduler = scheduler;
this.producerProvider = producerProvider;
}
@PostConstruct
private void init() {
if (enabled) {
@ -87,42 +83,48 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
}
private void reportStats() {
ConcurrentMap<OwnerId, ToUsageStatsServiceMsg.Builder> report = new ConcurrentHashMap<>();
ConcurrentMap<ParentEntity, ToUsageStatsServiceMsg.Builder> report = new ConcurrentHashMap<>();
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
ConcurrentMap<OwnerId, AtomicLong> statsForKey = stats.get(key);
statsForKey.forEach((ownerId, statsValue) -> {
ConcurrentMap<ReportLevel, AtomicLong> statsForKey = stats.get(key);
statsForKey.forEach((reportLevel, statsValue) -> {
long value = statsValue.get();
if (value == 0) return;
if (value == 0 && key.isCounter()) return;
ToUsageStatsServiceMsg.Builder statsMsgBuilder = report.computeIfAbsent(ownerId, id -> {
ToUsageStatsServiceMsg.Builder newStatsMsgBuilder = ToUsageStatsServiceMsg.newBuilder();
ToUsageStatsServiceMsg.Builder statsMsg = report.computeIfAbsent(reportLevel.getParentEntity(), parent -> {
ToUsageStatsServiceMsg.Builder newStatsMsg = ToUsageStatsServiceMsg.newBuilder();
TenantId tenantId = ownerId.getTenantId();
newStatsMsgBuilder.setTenantIdMSB(tenantId.getId().getMostSignificantBits());
newStatsMsgBuilder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits());
TenantId tenantId = parent.getTenantId();
newStatsMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits());
newStatsMsg.setTenantIdLSB(tenantId.getId().getLeastSignificantBits());
EntityId entityId = ownerId.getEntityId();
if (entityId != null && entityId.getEntityType() == EntityType.CUSTOMER) {
newStatsMsgBuilder.setCustomerIdMSB(entityId.getId().getMostSignificantBits());
newStatsMsgBuilder.setCustomerIdLSB(entityId.getId().getLeastSignificantBits());
CustomerId customerId = parent.getCustomerId();
if (customerId != null) {
newStatsMsg.setCustomerIdMSB(customerId.getId().getMostSignificantBits());
newStatsMsg.setCustomerIdLSB(customerId.getId().getLeastSignificantBits());
}
return newStatsMsgBuilder;
newStatsMsg.setServiceId(serviceInfoProvider.getServiceId());
return newStatsMsg;
});
statsMsgBuilder.addValues(UsageStatsKVProto.newBuilder().setKey(key.name()).setValue(value).build());
UsageStatsKVProto.Builder statsItem = UsageStatsKVProto.newBuilder()
.setKey(key.name())
.setValue(value);
statsMsg.addValues(statsItem.build());
});
statsForKey.clear();
}
report.forEach(((ownerId, statsMsg) -> {
report.forEach(((parent, statsMsg) -> {
//TODO: figure out how to minimize messages into the queue. Maybe group by 100s of messages?
TenantId tenantId = ownerId.getTenantId();
EntityId entityId = Optional.ofNullable(ownerId.getEntityId()).orElse(tenantId);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId).newByTopic(msgProducer.getDefaultTopic());
msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null);
try {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId())
.newByTopic(msgProducer.getDefaultTopic());
msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null);
} catch (Exception e) {
log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e);
}
}));
if (!report.isEmpty()) {
@ -132,16 +134,17 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
@Override
public void report(TenantId tenantId, CustomerId customerId, ApiUsageRecordKey key, long value) {
if (enabled) {
ConcurrentMap<OwnerId, AtomicLong> statsForKey = stats.get(key);
statsForKey.computeIfAbsent(new OwnerId(tenantId), id -> new AtomicLong()).addAndGet(value);
statsForKey.computeIfAbsent(new OwnerId(TenantId.SYS_TENANT_ID), id -> new AtomicLong()).addAndGet(value);
if (!enabled) return;
if (enabledPerCustomer && customerId != null && !customerId.isNullUid()) {
statsForKey.computeIfAbsent(new OwnerId(tenantId, customerId), id -> new AtomicLong()).addAndGet(value);
}
ReportLevel[] reportLevels = new ReportLevel[3];
reportLevels[0] = ReportLevel.of(tenantId);
if (key.isCounter()) {
reportLevels[1] = ReportLevel.of(TenantId.SYS_TENANT_ID);
}
if (enabledPerCustomer && customerId != null && !customerId.isNullUid()) {
reportLevels[2] = ReportLevel.of(tenantId, customerId);
}
report(key, value, reportLevels);
}
@Override
@ -149,18 +152,47 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
report(tenantId, customerId, key, 1);
}
private void report(ApiUsageRecordKey key, long value, ReportLevel... levels) {
ConcurrentMap<ReportLevel, AtomicLong> statsForKey = stats.get(key);
for (ReportLevel level : levels) {
if (level == null) continue;
AtomicLong n = statsForKey.computeIfAbsent(level, k -> new AtomicLong());
if (key.isCounter()) {
n.addAndGet(value);
} else {
n.set(value);
}
}
}
@Data
private static class OwnerId {
private TenantId tenantId;
private EntityId entityId;
private static class ReportLevel {
private final TenantId tenantId;
private final CustomerId customerId;
public static ReportLevel of(TenantId tenantId) {
return new ReportLevel(tenantId, null);
}
public OwnerId(TenantId tenantId) {
this.tenantId = tenantId;
public static ReportLevel of(TenantId tenantId, CustomerId customerId) {
return new ReportLevel(tenantId, customerId);
}
public OwnerId(TenantId tenantId, EntityId entityId) {
this.tenantId = tenantId;
this.entityId = entityId;
public ParentEntity getParentEntity() {
return new ParentEntity(tenantId, customerId);
}
}
@Data
private static class ParentEntity {
private final TenantId tenantId;
private final CustomerId customerId;
public EntityId getId() {
return customerId != null ? customerId : tenantId;
}
}
}

27
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -816,36 +816,39 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
}
private String typeQuery(QueryContext ctx, EntityFilter filter) {
String type;
List<String> types;
String name;
switch (filter.getType()) {
case ASSET_TYPE:
type = ((AssetTypeFilter) filter).getAssetType();
types = ((AssetTypeFilter) filter).getAssetTypes();
name = ((AssetTypeFilter) filter).getAssetNameFilter();
break;
case DEVICE_TYPE:
type = ((DeviceTypeFilter) filter).getDeviceType();
types = ((DeviceTypeFilter) filter).getDeviceTypes();
name = ((DeviceTypeFilter) filter).getDeviceNameFilter();
break;
case ENTITY_VIEW_TYPE:
type = ((EntityViewTypeFilter) filter).getEntityViewType();
types = ((EntityViewTypeFilter) filter).getEntityViewTypes();
name = ((EntityViewTypeFilter) filter).getEntityViewNameFilter();
break;
case EDGE_TYPE:
type = ((EdgeTypeFilter) filter).getEdgeType();
types = ((EdgeTypeFilter) filter).getEdgeTypes();
name = ((EdgeTypeFilter) filter).getEdgeNameFilter();
break;
default:
throw new RuntimeException("Not supported!");
}
ctx.addStringParameter("entity_filter_type_query_type", type);
ctx.addStringParameter("entity_filter_type_query_name", name);
if (name.startsWith("%") || name.endsWith("%")) {
return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(:entity_filter_type_query_name)";
String typesFilter = "e.type in (:entity_filter_type_query_types)";
ctx.addStringListParameter("entity_filter_type_query_types", types);
if (!StringUtils.isEmpty(name)) {
ctx.addStringParameter("entity_filter_type_query_name", name);
if (name.startsWith("%") || name.endsWith("%")) {
return typesFilter + " and lower(e.search_text) like lower(:entity_filter_type_query_name)";
}
return typesFilter + " and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))";
} else {
return typesFilter;
}
return "e.type = :entity_filter_type_query_type and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))";
}
public static EntityType resolveEntityType(EntityFilter entityFilter) {

1
dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java

@ -126,6 +126,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
List<TsKvEntry> profileThresholds = new ArrayList<>();
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
if (key.getApiLimitKey() == null) continue;
profileThresholds.add(new BasicTsKvEntry(saved.getCreatedTime(), new LongDataEntry(key.getApiLimitKey(), configuration.getProfileThreshold(key))));
}
tsService.save(tenantId, saved.getId(), profileThresholds, 0L);

28
dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java

@ -131,7 +131,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityCountQuery countQuery = new EntityCountQuery(filter);
@ -139,11 +139,11 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(97, count);
filter.setDeviceType("unknown");
filter.setDeviceTypes(List.of("unknown"));
count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(0, count);
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("Device1");
count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(11, count);
@ -235,7 +235,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
EdgeTypeFilter filter = new EdgeTypeFilter();
filter.setEdgeType("default");
filter.setEdgeTypes(List.of("default"));
filter.setEdgeNameFilter("");
EntityCountQuery countQuery = new EntityCountQuery(filter);
@ -243,11 +243,11 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(97, count);
filter.setEdgeType("unknown");
filter.setEdgeTypes(List.of("unknown"));
count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(0, count);
filter.setEdgeType("default");
filter.setEdgeTypes(List.of("default"));
filter.setEdgeNameFilter("Edge1");
count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(11, count);
@ -813,7 +813,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -1337,7 +1337,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
private EntityDataQuery createDeviceSearchQuery(String deviceField, StringOperation operation, String searchQuery) {
DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
deviceTypeFilter.setDeviceType("default");
deviceTypeFilter.setDeviceTypes(List.of("default"));
deviceTypeFilter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -1402,7 +1402,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
Futures.allAsList(attributeFutures).get();
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -1502,7 +1502,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
Futures.allAsList(attributeFutures).get();
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -1648,7 +1648,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
Futures.allAsList(timeseriesFutures).get();
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -1780,7 +1780,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
Futures.allAsList(attributeFutures).get();
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -1912,7 +1912,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -2029,7 +2029,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), EntityDataSortOrder.Direction.DESC);

160
monitoring/pom.xml

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.5.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>monitoring</artifactId>
<name>ThingsBoard Monitoring Service</name>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/..</main.dir>
<pkg.type>java</pkg.type>
<pkg.disabled>false</pkg.disabled>
<pkg.process-resources.phase>process-resources</pkg.process-resources.phase>
<pkg.package.phase>package</pkg.package.phase>
<pkg.name>tb-monitoring</pkg.name>
<pkg.copyInstallScripts>false</pkg.copyInstallScripts>
<pkg.win.dist>${project.build.directory}/windows</pkg.win.dist>
<pkg.implementationTitle>ThingsBoard Monitoring Service</pkg.implementationTitle>
<pkg.mainClass>org.thingsboard.monitoring.ThingsboardMonitoringApplication</pkg.mainClass>
</properties>
<dependencies>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>util</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>rest-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>californium-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>scandium</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${pkg.name}-${project.version}</finalName>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.thingsboard</groupId>
<artifactId>gradle-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jenkins</id>
<name>Jenkins Repository</name>
<url>https://repo.jenkins-ci.org/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

46
monitoring/src/main/conf/logback.xml

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE configuration>
<configuration scan="true" scanPeriod="30 seconds">
<appender name="fileLogAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${pkg.logFolder}/${pkg.name}.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org" level="WARN"/>
<logger name="org.thingsboard.server" level="INFO"/>
<logger name="org.thingsboard.monitoring" level="DEBUG"/>
<logger name="org.thingsboard.monitoring.client" level="INFO"/>
<root level="INFO">
<appender-ref ref="fileLogAppender"/>
</root>
</configuration>

22
monitoring/src/main/conf/tb-monitoring.conf

@ -0,0 +1,22 @@
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export LOG_FILENAME=tb-monitoring.out
export LOADER_PATH=/usr/share/tb-monitoring/conf

40
monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@SpringBootApplication
@EnableScheduling
@Slf4j
public class ThingsboardMonitoringApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ThingsboardMonitoringApplication.class)
.properties(Map.of("spring.config.name", "tb-monitoring"))
.run(args);
}
}

49
monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.client;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.thingsboard.rest.client.RestClient;
import java.time.Duration;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TbClient extends RestClient {
@Value("${monitoring.rest.username}")
private String username;
@Value("${monitoring.rest.password}")
private String password;
public TbClient(@Value("${monitoring.rest.base_url}") String baseUrl,
@Value("${monitoring.rest.request_timeout_ms}") int requestTimeoutMs) {
super(new RestTemplateBuilder()
.setConnectTimeout(Duration.ofMillis(requestTimeoutMs))
.setReadTimeout(Duration.ofMillis(requestTimeoutMs))
.build(), baseUrl);
}
public String logIn() {
login(username, password);
return getToken();
}
}

194
monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java

@ -0,0 +1,194 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.client;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.monitoring.data.cmd.CmdsWrapper;
import org.thingsboard.monitoring.data.cmd.EntityDataCmd;
import org.thingsboard.monitoring.data.cmd.EntityDataUpdate;
import org.thingsboard.monitoring.data.cmd.LatestValueCmd;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import javax.net.ssl.SSLParameters;
import java.net.URI;
import java.nio.channels.NotYetConnectedException;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@Slf4j
public class WsClient extends WebSocketClient implements AutoCloseable {
public volatile JsonNode lastMsg;
private CountDownLatch reply;
private CountDownLatch update;
private final Lock updateLock = new ReentrantLock();
private long requestTimeoutMs;
public WsClient(URI serverUri, long requestTimeoutMs) {
super(serverUri);
this.requestTimeoutMs = requestTimeoutMs;
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
}
@Override
public void onMessage(String s) {
if (s == null) {
return;
}
updateLock.lock();
try {
lastMsg = JacksonUtil.toJsonNode(s);
log.trace("Received new msg: {}", lastMsg.toPrettyString());
if (update != null) {
update.countDown();
}
if (reply != null) {
reply.countDown();
}
} finally {
updateLock.unlock();
}
}
@Override
public void onClose(int i, String s, boolean b) {
log.debug("WebSocket client is closed");
}
@Override
public void onError(Exception e) {
log.error("WebSocket client error:", e);
}
public void registerWaitForUpdate() {
updateLock.lock();
try {
lastMsg = null;
update = new CountDownLatch(1);
} finally {
updateLock.unlock();
}
log.trace("Registered wait for update");
}
@Override
public void send(String text) throws NotYetConnectedException {
updateLock.lock();
try {
reply = new CountDownLatch(1);
} finally {
updateLock.unlock();
}
super.send(text);
}
public WsClient subscribeForTelemetry(List<UUID> devices, String key) {
EntityDataCmd cmd = new EntityDataCmd();
cmd.setCmdId(RandomUtils.nextInt(0, 1000));
EntityListFilter devicesFilter = new EntityListFilter();
devicesFilter.setEntityType(EntityType.DEVICE);
devicesFilter.setEntityList(devices.stream().map(UUID::toString).collect(Collectors.toList()));
EntityDataPageLink pageLink = new EntityDataPageLink(100,0, null, null);
EntityDataQuery devicesQuery = new EntityDataQuery(devicesFilter, pageLink, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
cmd.setQuery(devicesQuery);
LatestValueCmd latestCmd = new LatestValueCmd();
latestCmd.setKeys(List.of(new EntityKey(EntityKeyType.TIME_SERIES, key)));
cmd.setLatestCmd(latestCmd);
CmdsWrapper wrapper = new CmdsWrapper();
wrapper.setEntityDataCmds(List.of(cmd));
send(JacksonUtil.toString(wrapper));
return this;
}
public JsonNode waitForUpdate(long ms) {
log.trace("update latch count: {}", update.getCount());
try {
if (update.await(ms, TimeUnit.MILLISECONDS)) {
log.trace("Waited for update");
return getLastMsg();
}
} catch (InterruptedException e) {
log.debug("Failed to await reply", e);
}
log.trace("No update arrived within {} ms", ms);
return null;
}
public JsonNode waitForReply() {
try {
if (reply.await(requestTimeoutMs, TimeUnit.MILLISECONDS)) {
log.trace("Waited for reply");
return getLastMsg();
}
} catch (InterruptedException e) {
log.debug("Failed to await reply", e);
}
log.trace("No reply arrived within {} ms", requestTimeoutMs);
throw new IllegalStateException("No WS reply arrived within " + requestTimeoutMs + " ms");
}
private JsonNode getLastMsg() {
if (lastMsg != null) {
JsonNode errorMsg = lastMsg.get("errorMsg");
if (errorMsg != null && !errorMsg.isNull() && StringUtils.isNotEmpty(errorMsg.asText())) {
throw new RuntimeException("WS error from server: " + errorMsg.asText());
} else {
return lastMsg;
}
} else {
return null;
}
}
public Object getTelemetryUpdate(UUID deviceId, String key) {
JsonNode lastMsg = getLastMsg();
if (lastMsg == null || lastMsg.isNull()) return null;
EntityDataUpdate update = JacksonUtil.treeToValue(lastMsg, EntityDataUpdate.class);
return update.getLatest(deviceId, key);
}
@Override
protected void onSetSSLParameters(SSLParameters sslParameters) {
sslParameters.setEndpointIdentificationAlgorithm(null);
}
}

59
monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java

@ -0,0 +1,59 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.client;
import lombok.RequiredArgsConstructor;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.data.Latencies;
import org.thingsboard.monitoring.service.MonitoringReporter;
import org.thingsboard.monitoring.util.TbStopWatch;
import java.net.URI;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class WsClientFactory {
private final MonitoringReporter monitoringReporter;
private final TbStopWatch stopWatch;
@Value("${monitoring.ws.base_url}")
private String baseUrl;
@Value("${monitoring.ws.request_timeout_ms}")
private int requestTimeoutMs;
public WsClient createClient(String accessToken) throws Exception {
URI uri = new URI(baseUrl + "/api/ws/plugins/telemetry?token=" + accessToken);
stopWatch.start();
WsClient wsClient = new WsClient(uri, requestTimeoutMs);
if (baseUrl.startsWith("wss")) {
SSLContextBuilder builder = SSLContexts.custom();
builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
wsClient.setSocketFactory(builder.build().getSocketFactory());
}
boolean connected = wsClient.connectBlocking(requestTimeoutMs, TimeUnit.MILLISECONDS);
if (!connected) {
throw new IllegalStateException("Failed to establish WS session");
}
monitoringReporter.reportLatency(Latencies.WS_CONNECT, stopWatch.getTime());
return wsClient;
}
}

34
monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.config;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.util.UUID;
@Data
public class DeviceConfig {
private UUID id;
private DeviceCredentials credentials;
public void setId(String id) {
this.id = StringUtils.isNotEmpty(id) ? UUID.fromString(id) : null;
}
}

26
monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.config;
import lombok.Data;
@Data
public class MonitoringTargetConfig {
private String baseUrl;
private DeviceConfig device;
}

35
monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.config;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.thingsboard.monitoring.transport.TransportHealthChecker;
import org.thingsboard.monitoring.transport.impl.CoapTransportHealthChecker;
import org.thingsboard.monitoring.transport.impl.HttpTransportHealthChecker;
import org.thingsboard.monitoring.transport.impl.MqttTransportHealthChecker;
@AllArgsConstructor
@Getter
public enum TransportType {
MQTT(MqttTransportHealthChecker.class),
COAP(CoapTransportHealthChecker.class),
HTTP(HttpTransportHealthChecker.class);
private final Class<? extends TransportHealthChecker<?>> serviceClass;
}

33
monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.config.service;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.config.TransportType;
@Component
@ConditionalOnProperty(name = "monitoring.transports.coap.enabled", havingValue = "true")
@ConfigurationProperties(prefix = "monitoring.transports.coap")
public class CoapTransportMonitoringConfig extends TransportMonitoringConfig {
@Override
public TransportType getTransportType() {
return TransportType.COAP;
}
}

33
monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.config.service;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.config.TransportType;
@Component
@ConditionalOnProperty(name = "monitoring.transports.http.enabled", havingValue = "true")
@ConfigurationProperties(prefix = "monitoring.transports.http")
public class HttpTransportMonitoringConfig extends TransportMonitoringConfig {
@Override
public TransportType getTransportType() {
return TransportType.HTTP;
}
}

39
monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.config.service;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.config.TransportType;
@Component
@ConditionalOnProperty(name = "monitoring.transports.mqtt.enabled", havingValue = "true")
@ConfigurationProperties(prefix = "monitoring.transports.mqtt")
@Data
@EqualsAndHashCode(callSuper = true)
public class MqttTransportMonitoringConfig extends TransportMonitoringConfig {
private Integer qos;
@Override
public TransportType getTransportType() {
return TransportType.MQTT;
}
}

33
monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.config.service;
import lombok.Data;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import java.util.List;
@Data
public abstract class TransportMonitoringConfig {
private int requestTimeoutMs;
private List<MonitoringTargetConfig> targets;
public abstract TransportType getTransportType();
}

30
monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data;
import org.thingsboard.monitoring.config.TransportType;
public class Latencies {
public static final String WS_UPDATE = "wsUpdate";
public static final String WS_CONNECT = "wsConnect";
public static final String LOG_IN = "logIn";
public static String transportRequest(TransportType transportType) {
return String.format("%sTransportRequest", transportType.name().toLowerCase());
}
}

67
monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data;
import com.google.common.util.concurrent.AtomicDouble;
import lombok.RequiredArgsConstructor;
import java.util.concurrent.atomic.AtomicInteger;
@RequiredArgsConstructor
public class Latency {
private final String key;
private final AtomicDouble latencySum = new AtomicDouble();
private final AtomicInteger counter = new AtomicInteger();
public synchronized void report(double latencyInMs) {
latencySum.addAndGet(latencyInMs);
counter.incrementAndGet();
}
public synchronized double getAvg() {
return latencySum.get() / counter.get();
}
public boolean isNotEmpty() {
return counter.get() > 0;
}
public synchronized void reset() {
latencySum.set(0.0);
counter.set(0);
}
public String getKey() {
return key;
}
public synchronized Latency snapshot() {
Latency snapshot = new Latency(key);
snapshot.latencySum.set(latencySum.get());
snapshot.counter.set(counter.get());
return snapshot;
}
@Override
public String toString() {
return "Latency{" +
"key='" + key + '\'' +
", avgLatency=" + getAvg() +
'}';
}
}

22
monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data;
public class MonitoredServiceKey {
public static final String GENERAL = "Monitoring";
}

28
monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data;
public class TransportFailureException extends RuntimeException {
public TransportFailureException(Throwable cause) {
super(cause.getMessage(), cause);
}
public TransportFailureException(String message) {
super(message);
}
}

32
monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data;
import lombok.Data;
import org.thingsboard.monitoring.config.TransportType;
@Data
public class TransportInfo {
private final TransportType transportType;
private final String url;
@Override
public String toString() {
return String.format("%s (%s)", transportType, url);
}
}

27
monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.cmd;
import lombok.Data;
import java.util.List;
@Data
public class CmdsWrapper {
private List<EntityDataCmd> entityDataCmds;
}

28
monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.cmd;
import lombok.Data;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@Data
public class EntityDataCmd {
private int cmdId;
private EntityDataQuery query;
private LatestValueCmd latestCmd;
}

44
monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.cmd;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import java.util.List;
import java.util.UUID;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class EntityDataUpdate {
@JsonIgnoreProperties(ignoreUnknown = true)
private List<EntityData> update;
public String getLatest(UUID entityId, String key) {
if (update == null) return null;
return update.stream()
.filter(entityData -> entityData.getEntityId().getId().equals(entityId)).findFirst()
.map(EntityData::getLatest).map(latest -> latest.get(EntityKeyType.TIME_SERIES))
.map(latest -> latest.get(key)).map(TsValue::getValue)
.orElse(null);
}
}

28
monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.cmd;
import lombok.Data;
import org.thingsboard.server.common.data.query.EntityKey;
import java.util.List;
@Data
public class LatestValueCmd {
private List<EntityKey> keys;
}

42
monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.notification;
import org.thingsboard.monitoring.data.Latency;
import java.util.Collection;
public class HighLatencyNotification implements Notification {
private final Collection<Latency> latencies;
private final int thresholdMs;
public HighLatencyNotification(Collection<Latency> latencies, int thresholdMs) {
this.latencies = latencies;
this.thresholdMs = thresholdMs;
}
@Override
public String getText() {
StringBuilder text = new StringBuilder();
text.append("Some of the latencies are higher than ").append(thresholdMs).append(" ms:\n");
latencies.forEach(latency -> {
text.append(String.format("[%s] %,.2f ms\n", latency.getKey(), latency.getAvg()));
});
return text.toString();
}
}

22
monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.notification;
public interface Notification {
String getText();
}

49
monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.notification;
import lombok.Getter;
import org.apache.commons.lang3.exception.ExceptionUtils;
@Getter
public class ServiceFailureNotification implements Notification {
private final Object serviceKey;
private final Throwable error;
private final int failuresCount;
public ServiceFailureNotification(Object serviceKey, Throwable error, int failuresCount) {
this.serviceKey = serviceKey;
this.error = error;
this.failuresCount = failuresCount;
}
@Override
public String getText() {
String errorMsg = error.getMessage();
if (errorMsg == null || errorMsg.equals("null")) {
Throwable cause = ExceptionUtils.getRootCause(error);
if (cause != null) {
errorMsg = cause.getMessage();
}
}
if (errorMsg == null) {
errorMsg = error.getClass().getSimpleName();
}
return String.format("[%s] Failure: %s (number of subsequent failures: %s)", serviceKey, errorMsg, failuresCount);
}
}

31
monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.data.notification;
public class ServiceRecoveryNotification implements Notification {
private final Object serviceKey;
public ServiceRecoveryNotification(Object serviceKey) {
this.serviceKey = serviceKey;
}
@Override
public String getText() {
return String.format("[%s] is OK", serviceKey);
}
}

53
monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.notification;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.data.notification.Notification;
import org.thingsboard.monitoring.notification.channels.NotificationChannel;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationService {
private final List<NotificationChannel> notificationChannels;
private final ExecutorService notificationExecutor = Executors.newSingleThreadExecutor();
public void sendNotification(Notification notification) {
forEachNotificationChannel(notificationChannel -> notificationChannel.sendNotification(notification));
}
private void forEachNotificationChannel(Consumer<NotificationChannel> function) {
notificationChannels.forEach(notificationChannel -> {
notificationExecutor.submit(() -> {
try {
function.accept(notificationChannel);
} catch (Exception e) {
log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e);
}
});
});
}
}

24
monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.notification.channels;
import org.thingsboard.monitoring.data.notification.Notification;
public interface NotificationChannel {
void sendNotification(Notification notification);
}

58
monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java

@ -0,0 +1,58 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.notification.channels.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.monitoring.data.notification.Notification;
import org.thingsboard.monitoring.notification.channels.NotificationChannel;
import javax.annotation.PostConstruct;
import java.time.Duration;
import java.util.Map;
@Component
@ConditionalOnProperty(value = "monitoring.notification_channels.slack.enabled", havingValue = "true")
@Slf4j
public class SlackNotificationChannel implements NotificationChannel {
@Value("${monitoring.notification_channels.slack.webhook_url}")
private String webhookUrl;
private RestTemplate restTemplate;
@PostConstruct
private void init() {
restTemplate = new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(2))
.build();
}
@Override
public void sendNotification(Notification notification) {
sendNotification(notification.getText());
}
private void sendNotification(String message) {
restTemplate.postForObject(webhookUrl, Map.of("text", message), String.class);
}
}

144
monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java

@ -0,0 +1,144 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.service;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.monitoring.client.TbClient;
import org.thingsboard.monitoring.data.Latency;
import org.thingsboard.monitoring.data.MonitoredServiceKey;
import org.thingsboard.monitoring.data.notification.HighLatencyNotification;
import org.thingsboard.monitoring.data.notification.ServiceFailureNotification;
import org.thingsboard.monitoring.data.notification.ServiceRecoveryNotification;
import org.thingsboard.monitoring.notification.NotificationService;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
@Slf4j
public class MonitoringReporter {
private final NotificationService notificationService;
private final Map<String, Latency> latencies = new ConcurrentHashMap<>();
private final Map<Object, AtomicInteger> failuresCounters = new ConcurrentHashMap<>();
@Value("${monitoring.failures_threshold}")
private int failuresThreshold;
@Value("${monitoring.send_repeated_failure_notification}")
private boolean sendRepeatedFailureNotification;
@Value("${monitoring.latency.enabled}")
private boolean latencyReportingEnabled;
@Value("${monitoring.latency.threshold_ms}")
private int latencyThresholdMs;
@Value("${monitoring.latency.reporting_asset_id}")
private String reportingAssetId;
public void reportLatencies(TbClient tbClient) {
List<Latency> latencies = this.latencies.values().stream()
.filter(Latency::isNotEmpty)
.map(latency -> {
Latency snapshot = latency.snapshot();
latency.reset();
return snapshot;
})
.collect(Collectors.toList());
if (latencies.isEmpty()) {
return;
}
log.info("Latencies:\n{}", latencies.stream().map(latency -> latency.getKey() + ": " + latency.getAvg() + " ms")
.collect(Collectors.joining("\n")));
if (!latencyReportingEnabled) return;
if (latencies.stream().anyMatch(latency -> latency.getAvg() >= (double) latencyThresholdMs)) {
HighLatencyNotification highLatencyNotification = new HighLatencyNotification(latencies, latencyThresholdMs);
notificationService.sendNotification(highLatencyNotification);
}
try {
if (StringUtils.isBlank(reportingAssetId)) {
String assetName = "Monitoring";
Asset monitoringAsset = tbClient.findAsset(assetName).orElseGet(() -> {
Asset asset = new Asset();
asset.setType("Monitoring");
asset.setName(assetName);
asset = tbClient.saveAsset(asset);
log.info("Created monitoring asset {}", asset.getId());
return asset;
});
reportingAssetId = monitoringAsset.getId().toString();
}
ObjectNode msg = JacksonUtil.newObjectNode();
latencies.forEach(latency -> {
msg.set(latency.getKey(), new DoubleNode(latency.getAvg()));
});
tbClient.saveEntityTelemetry(new AssetId(UUID.fromString(reportingAssetId)), "time", msg);
} catch (Exception e) {
log.error("Failed to report latencies: {}", e.getMessage());
}
}
public void reportLatency(String key, long latencyInNanos) {
String latencyKey = key + "Latency";
double latencyInMs = (double) latencyInNanos / 1000_000;
log.trace("Reporting latency [{}]: {} ms", key, latencyInMs);
latencies.computeIfAbsent(latencyKey, k -> new Latency(latencyKey)).report(latencyInMs);
}
public void serviceFailure(Object serviceKey, Throwable error) {
if (log.isDebugEnabled()) {
log.error("Error occurred", error);
}
int failuresCount = failuresCounters.computeIfAbsent(serviceKey, k -> new AtomicInteger()).incrementAndGet();
ServiceFailureNotification notification = new ServiceFailureNotification(serviceKey, error, failuresCount);
log.error(notification.getText());
if (failuresCount == failuresThreshold || (sendRepeatedFailureNotification && failuresCount % failuresThreshold == 0)) {
notificationService.sendNotification(notification);
}
}
public void serviceIsOk(Object serviceKey) {
ServiceRecoveryNotification notification = new ServiceRecoveryNotification(serviceKey);
if (!serviceKey.equals(MonitoredServiceKey.GENERAL)) {
log.info(notification.getText());
}
AtomicInteger failuresCounter = failuresCounters.get(serviceKey);
if (failuresCounter != null) {
if (failuresCounter.get() >= failuresThreshold) {
notificationService.sendNotification(notification);
}
failuresCounter.set(0);
}
}
}

120
monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java

@ -0,0 +1,120 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.transport;
import com.fasterxml.jackson.databind.node.TextNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.monitoring.client.WsClient;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import org.thingsboard.monitoring.config.service.TransportMonitoringConfig;
import org.thingsboard.monitoring.data.Latencies;
import org.thingsboard.monitoring.data.MonitoredServiceKey;
import org.thingsboard.monitoring.data.TransportFailureException;
import org.thingsboard.monitoring.data.TransportInfo;
import org.thingsboard.monitoring.service.MonitoringReporter;
import org.thingsboard.monitoring.util.TbStopWatch;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
@Slf4j
public abstract class TransportHealthChecker<C extends TransportMonitoringConfig> {
protected final C config;
protected final MonitoringTargetConfig target;
private TransportInfo transportInfo;
@Autowired
private MonitoringReporter reporter;
@Autowired
private TbStopWatch stopWatch;
@Value("${monitoring.check_timeout_ms}")
private int resultCheckTimeoutMs;
public static final String TEST_TELEMETRY_KEY = "testData";
protected TransportHealthChecker(C config, MonitoringTargetConfig target) {
this.config = config;
this.target = target;
}
@PostConstruct
private void init() {
transportInfo = new TransportInfo(getTransportType(), target.getBaseUrl());
}
public final void check(WsClient wsClient) {
log.debug("[{}] Checking", transportInfo);
try {
wsClient.registerWaitForUpdate();
String testValue = UUID.randomUUID().toString();
String testPayload = JacksonUtil.newObjectNode().set(TEST_TELEMETRY_KEY, new TextNode(testValue)).toString();
try {
initClientAndSendPayload(testPayload);
log.trace("[{}] Sent test payload ({})", transportInfo, testPayload);
} catch (Throwable e) {
throw new TransportFailureException(e);
}
log.trace("[{}] Waiting for WS update", transportInfo);
checkWsUpdate(wsClient, testValue);
reporter.serviceIsOk(transportInfo);
reporter.serviceIsOk(MonitoredServiceKey.GENERAL);
} catch (TransportFailureException transportFailureException) {
reporter.serviceFailure(transportInfo, transportFailureException);
} catch (Exception e) {
reporter.serviceFailure(MonitoredServiceKey.GENERAL, e);
}
}
private void initClientAndSendPayload(String payload) throws Throwable {
initClient();
stopWatch.start();
sendTestPayload(payload);
reporter.reportLatency(Latencies.transportRequest(getTransportType()), stopWatch.getTime());
}
private void checkWsUpdate(WsClient wsClient, String testValue) {
stopWatch.start();
wsClient.waitForUpdate(resultCheckTimeoutMs);
log.trace("[{}] Waited for WS update. Last WS msg: {}", transportInfo, wsClient.lastMsg);
Object update = wsClient.getTelemetryUpdate(target.getDevice().getId(), TEST_TELEMETRY_KEY);
if (update == null) {
throw new TransportFailureException("No WS update arrived within " + resultCheckTimeoutMs + " ms");
} else if (!update.toString().equals(testValue)) {
throw new TransportFailureException("Was expecting value " + testValue + " but got " + update);
}
reporter.reportLatency(Latencies.WS_UPDATE, stopWatch.getTime());
}
protected abstract void initClient() throws Exception;
protected abstract void sendTestPayload(String payload) throws Exception;
@PreDestroy
protected abstract void destroyClient() throws Exception;
protected abstract TransportType getTransportType();
}

143
monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java

@ -0,0 +1,143 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.transport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.monitoring.client.TbClient;
import org.thingsboard.monitoring.client.WsClient;
import org.thingsboard.monitoring.client.WsClientFactory;
import org.thingsboard.monitoring.config.DeviceConfig;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.service.TransportMonitoringConfig;
import org.thingsboard.monitoring.data.Latencies;
import org.thingsboard.monitoring.data.MonitoredServiceKey;
import org.thingsboard.monitoring.service.MonitoringReporter;
import org.thingsboard.monitoring.util.TbStopWatch;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import javax.annotation.PostConstruct;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Slf4j
public final class TransportMonitoringService {
private final List<TransportMonitoringConfig> configs;
private final List<TransportHealthChecker<?>> transportHealthCheckers = new LinkedList<>();
private final List<UUID> devices = new LinkedList<>();
private final TbClient tbClient;
private final WsClientFactory wsClientFactory;
private final TbStopWatch stopWatch;
private final MonitoringReporter reporter;
private final ApplicationContext applicationContext;
private ScheduledExecutorService scheduler;
@Value("${monitoring.transports.monitoring_rate_ms}")
private int monitoringRateMs;
@PostConstruct
private void init() {
configs.forEach(config -> {
config.getTargets().stream()
.filter(target -> StringUtils.isNotBlank(target.getBaseUrl()))
.peek(target -> checkMonitoringTarget(config, target, tbClient))
.forEach(target -> {
TransportHealthChecker<?> transportHealthChecker = applicationContext.getBean(config.getTransportType().getServiceClass(), config, target);
transportHealthCheckers.add(transportHealthChecker);
devices.add(target.getDevice().getId());
});
});
scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor"));
}
@EventListener(ApplicationReadyEvent.class)
public void startMonitoring() {
scheduler.scheduleWithFixedDelay(() -> {
try {
log.debug("Starting transports check");
stopWatch.start();
String accessToken = tbClient.logIn();
reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime());
try (WsClient wsClient = wsClientFactory.createClient(accessToken)) {
wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply();
for (TransportHealthChecker<?> transportHealthChecker : transportHealthCheckers) {
transportHealthChecker.check(wsClient);
}
}
reporter.reportLatencies(tbClient);
log.debug("Finished transports check");
} catch (Throwable error) {
try {
reporter.serviceFailure(MonitoredServiceKey.GENERAL, error);
} catch (Throwable reportError) {
log.error("Error occurred during service failure reporting", reportError);
}
}
}, 0, monitoringRateMs, TimeUnit.MILLISECONDS);
}
private void checkMonitoringTarget(TransportMonitoringConfig config, MonitoringTargetConfig target, TbClient tbClient) {
DeviceConfig deviceConfig = target.getDevice();
tbClient.logIn();
DeviceId deviceId;
if (deviceConfig == null || deviceConfig.getId() == null) {
String deviceName = String.format("[%s] Monitoring device (%s)", config.getTransportType(), target.getBaseUrl());
Device device = tbClient.getTenantDevice(deviceName)
.orElseGet(() -> {
log.info("Creating new device '{}'", deviceName);
Device monitoringDevice = new Device();
monitoringDevice.setName(deviceName);
monitoringDevice.setType("default");
DeviceData deviceData = new DeviceData();
deviceData.setConfiguration(new DefaultDeviceConfiguration());
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
return tbClient.saveDevice(monitoringDevice);
});
deviceId = device.getId();
target.getDevice().setId(deviceId.toString());
} else {
deviceId = new DeviceId(deviceConfig.getId());
}
log.info("Loading credentials for device {}", deviceId);
DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(deviceId)
.orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + deviceId));
target.getDevice().setCredentials(credentials);
}
}

78
monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.transport.impl;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import org.thingsboard.monitoring.config.service.CoapTransportMonitoringConfig;
import org.thingsboard.monitoring.transport.TransportHealthChecker;
import java.io.IOException;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Slf4j
public class CoapTransportHealthChecker extends TransportHealthChecker<CoapTransportMonitoringConfig> {
private CoapClient coapClient;
protected CoapTransportHealthChecker(CoapTransportMonitoringConfig config, MonitoringTargetConfig target) {
super(config, target);
}
@Override
protected void initClient() throws Exception {
if (coapClient == null) {
String accessToken = target.getDevice().getCredentials().getCredentialsId();
String uri = target.getBaseUrl() + "/api/v1/" + accessToken + "/telemetry";
coapClient = new CoapClient(uri);
coapClient.setTimeout((long) config.getRequestTimeoutMs());
log.debug("Initialized CoAP client for URI {}", uri);
}
}
@Override
protected void sendTestPayload(String payload) throws Exception {
CoapResponse response = coapClient.post(payload, MediaTypeRegistry.APPLICATION_JSON);
CoAP.ResponseCode code = response.getCode();
if (code.codeClass != CoAP.CodeClass.SUCCESS_RESPONSE.value) {
throw new IOException("COAP client didn't receive success response from transport");
}
}
@Override
protected void destroyClient() throws Exception {
if (coapClient != null) {
coapClient.shutdown();
coapClient = null;
log.info("Disconnected CoAP client");
}
}
@Override
protected TransportType getTransportType() {
return TransportType.COAP;
}
}

67
monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/HttpTransportHealthChecker.java

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.transport.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import org.thingsboard.monitoring.config.service.HttpTransportMonitoringConfig;
import org.thingsboard.monitoring.transport.TransportHealthChecker;
import java.time.Duration;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Slf4j
public class HttpTransportHealthChecker extends TransportHealthChecker<HttpTransportMonitoringConfig> {
private RestTemplate restTemplate;
protected HttpTransportHealthChecker(HttpTransportMonitoringConfig config, MonitoringTargetConfig target) {
super(config, target);
}
@Override
protected void initClient() throws Exception {
if (restTemplate == null) {
restTemplate = new RestTemplateBuilder()
.setConnectTimeout(Duration.ofMillis(config.getRequestTimeoutMs()))
.setReadTimeout(Duration.ofMillis(config.getRequestTimeoutMs()))
.build();
log.debug("Initialized HTTP client");
}
}
@Override
protected void sendTestPayload(String payload) throws Exception {
String accessToken = target.getDevice().getCredentials().getCredentialsId();
restTemplate.postForObject(target.getBaseUrl() + "/api/v1/" + accessToken + "/telemetry", payload, String.class);
}
@Override
protected void destroyClient() throws Exception {}
@Override
protected TransportType getTransportType() {
return TransportType.HTTP;
}
}

87
monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/MqttTransportHealthChecker.java

@ -0,0 +1,87 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.transport.impl;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import org.thingsboard.monitoring.config.service.MqttTransportMonitoringConfig;
import org.thingsboard.monitoring.transport.TransportHealthChecker;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Slf4j
public class MqttTransportHealthChecker extends TransportHealthChecker<MqttTransportMonitoringConfig> {
private MqttClient mqttClient;
private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
protected MqttTransportHealthChecker(MqttTransportMonitoringConfig config, MonitoringTargetConfig target) {
super(config, target);
}
@Override
protected void initClient() throws Exception {
if (mqttClient == null || !mqttClient.isConnected()) {
String clientId = MqttAsyncClient.generateClientId();
String accessToken = target.getDevice().getCredentials().getCredentialsId();
mqttClient = new MqttClient(target.getBaseUrl(), clientId, new MemoryPersistence());
mqttClient.setTimeToWait(config.getRequestTimeoutMs());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
options.setConnectionTimeout(config.getRequestTimeoutMs() / 1000);
IMqttToken result = mqttClient.connectWithResult(options);
if (result.getException() != null) {
throw result.getException();
}
log.debug("Initialized MQTT client for URI {}", mqttClient.getServerURI());
}
}
@Override
protected void sendTestPayload(String payload) throws Exception {
MqttMessage message = new MqttMessage();
message.setPayload(payload.getBytes());
message.setQos(config.getQos());
mqttClient.publish(DEVICE_TELEMETRY_TOPIC, message);
}
@Override
protected void destroyClient() throws Exception {
if (mqttClient != null) {
mqttClient.disconnect();
mqttClient = null;
log.info("Disconnected MQTT client");
}
}
@Override
protected TransportType getTransportType() {
return TransportType.MQTT;
}
}

41
monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.monitoring.util;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TbStopWatch {
private final StopWatch internal = new StopWatch();
public void start() {
internal.reset();
internal.start();
}
public long getTime() {
internal.stop();
long nanoTime = internal.getNanoTime();
internal.reset();
return nanoTime;
}
}

38
monitoring/src/main/resources/logback.xml

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE configuration>
<configuration scan="true" scanPeriod="10 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org" level="WARN"/>
<logger name="org.thingsboard.server" level="INFO"/>
<logger name="org.thingsboard.monitoring" level="INFO"/>
<logger name="org.thingsboard.monitoring.client" level="WARN"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

107
monitoring/src/main/resources/tb-monitoring.yml

@ -0,0 +1,107 @@
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
monitoring:
# Monitored server domain
domain: "${DOMAIN:localhost}"
rest:
# Base REST API url, https://DOMAIN by default
base_url: '${REST_BASE_URL:https://${monitoring.domain}}'
# Authentication username
username: '${REST_AUTH_USERNAME:tenant@thingsboard.org}'
# Authentication password
password: '${REST_AUTH_PASSWORD:tenant}'
# REST request timeout in milliseconds
request_timeout_ms: '${REST_REQUEST_TIMEOUT_MS:5000}'
ws:
# WebSocket url, wss://DOMAIN by default
base_url: '${WS_BASE_URL:wss://${monitoring.domain}}'
# WebSocket request timeout
request_timeout_ms: '${WS_REQUEST_TIMEOUT_MS:3000}'
# Maximum time between request to transport and WebSocket update
check_timeout_ms: '${CHECK_TIMEOUT_MS:5000}'
# Failures threshold for notifying
failures_threshold: '${FAILURES_THRESHOLD:2}'
# Whether to notify about next failures after first notification (will notify after each FAILURES_THRESHOLD failures)
send_repeated_failure_notification: '${SEND_REPEATED_FAILURE_NOTIFICATION:true}'
transports:
# Transports check frequency in milliseconds
monitoring_rate_ms: '${TRANSPORTS_MONITORING_RATE_MS:10000}'
mqtt:
# Enable MQTT checks
enabled: '${MQTT_TRANSPORT_MONITORING_ENABLED:true}'
# MQTT request timeout in milliseconds
request_timeout_ms: '${MQTT_REQUEST_TIMEOUT_MS:4000}'
# MQTT QoS
qos: '${MQTT_QOS_LEVEL:1}'
targets:
# MQTT base url, tcp://DOMAIN:1883 by default
- base_url: '${MQTT_TRANSPORT_BASE_URL:tcp://${monitoring.domain}:1883}'
device:
# MQTT device to push telemetry for. If not set - device will be found or created automatically
id: '${MQTT_TRANSPORT_TARGET_DEVICE_ID:}'
# To add more targets, use following environment variables:
# monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[1].device.id,
# monitoring.transports.mqtt.targets[2].base_url, monitoring.transports.mqtt.targets[2].device.id, etc.
coap:
# Enable CoAP checks
enabled: '${COAP_TRANSPORT_MONITORING_ENABLED:true}'
# CoAP request timeout in milliseconds
request_timeout_ms: '${COAP_REQUEST_TIMEOUT_MS:4000}'
targets:
# CoAP base url, coap://DOMAIN by default
- base_url: '${COAP_TRANSPORT_BASE_URL:coap://${monitoring.domain}}'
# CoAP device to push telemetry for. If not set - device will be found or created automatically
device:
id: '${COAP_TRANSPORT_TARGET_DEVICE_ID:}'
# To add more targets, use following environment variables:
# monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[1].device.id,
# monitoring.transports.coap.targets[2].base_url, monitoring.transports.coap.targets[2].device.id, etc.
http:
# Enable HTTP checks
enabled: '${HTTP_TRANSPORT_MONITORING_ENABLED:true}'
# HTTP request timeout in milliseconds
request_timeout_ms: '${HTTP_REQUEST_TIMEOUT_MS:4000}'
targets:
# HTTP base url, https://DOMAIN by default
- base_url: '${HTTP_TRANSPORT_BASE_URL:https://${monitoring.domain}}'
device:
# HTTP device to push telemetry for. If not set - device will be found or created automatically
id: '${HTTP_TRANSPORT_TARGET_DEVICE_ID:}'
# To add more targets, use following environment variables:
# monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[1].device.id,
# monitoring.transports.http.targets[2].base_url, monitoring.transports.http.targets[2].device.id, etc.
notification_channels:
slack:
# Enable notifying via Slack
enabled: '${SLACK_NOTIFICATION_CHANNEL_ENABLED:false}'
# Slack webhook url
webhook_url: '${SLACK_WEBHOOK_URL:}'
latency:
# Enable latencies reporting
enabled: "${LATENCY_REPORTING_ENABLED:true}"
# Latency threshold for notifying
threshold_ms: '${LATENCY_THRESHOLD:2000}'
# ID of the asset to save latencies to. If not set and latencies reporting is enabled - asset will be found or created automatically
reporting_asset_id: '${LATENCY_REPORTING_ASSET_ID:}'

30
msa/monitoring/docker/Dockerfile

@ -0,0 +1,30 @@
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM thingsboard/openjdk11:bullseye-slim
COPY start-tb-monitoring.sh ${pkg.name}.deb /tmp/
RUN chmod a+x /tmp/*.sh \
&& mv /tmp/start-tb-monitoring.sh /usr/bin && \
(yes | dpkg -i /tmp/${pkg.name}.deb) && \
rm /tmp/${pkg.name}.deb && \
(systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :) && \
chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar
USER ${pkg.user}
CMD ["start-tb-monitoring.sh"]

31
msa/monitoring/docker/start-tb-monitoring.sh

@ -0,0 +1,31 @@
#!/bin/bash
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
CONF_FOLDER=${pkg.installFolder}/conf
jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
configfile=${pkg.name}.conf
source "${CONF_FOLDER}/${configfile}"
echo "Starting '${project.name}' ..."
cd ${pkg.installFolder}/bin
exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.monitoring.ThingsboardMonitoringApplication \
-Dspring.jpa.hibernate.ddl-auto=none \
-Dlogging.config=$CONF_FOLDER/logback.xml \
org.springframework.boot.loader.PropertiesLauncher

192
msa/monitoring/pom.xml

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.5.0-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>
<artifactId>monitoring</artifactId>
<packaging>pom</packaging>
<name>ThingsBoard Monitoring Microservice</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir>
<pkg.name>tb-monitoring</pkg.name>
<docker.name>tb-monitoring</docker.name>
<pkg.logFolder>/var/log/${pkg.name}</pkg.logFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<docker.push-arm-amd-image.phase>pre-integration-test</docker.push-arm-amd-image.phase>
</properties>
<dependencies>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>monitoring</artifactId>
<version>${project.version}</version>
<classifier>deb</classifier>
<type>deb</type>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-tb-monitoring-deb</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.thingsboard</groupId>
<artifactId>monitoring</artifactId>
<classifier>deb</classifier>
<type>deb</type>
<destFileName>${pkg.name}.deb</destFileName>
<outputDirectory>${project.build.directory}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-docker-config</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>docker</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<executions>
<execution>
<id>build-docker-image</id>
<phase>pre-integration-test</phase>
<goals>
<goal>build</goal>
</goals>
<configuration>
<skip>${dockerfile.skip}</skip>
<repository>${docker.repo}/${docker.name}</repository>
<verbose>true</verbose>
<googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
<contextDirectory>${project.build.directory}</contextDirectory>
</configuration>
</execution>
<execution>
<id>tag-docker-image</id>
<phase>pre-integration-test</phase>
<goals>
<goal>tag</goal>
</goals>
<configuration>
<skip>${dockerfile.skip}</skip>
<repository>${docker.repo}/${docker.name}</repository>
<tag>${project.version}</tag>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>push-docker-image</id>
<activation>
<property>
<name>push-docker-image</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<executions>
<execution>
<id>push-latest-docker-image</id>
<phase>pre-integration-test</phase>
<goals>
<goal>push</goal>
</goals>
<configuration>
<tag>latest</tag>
<repository>${docker.repo}/${docker.name}</repository>
</configuration>
</execution>
<execution>
<id>push-version-docker-image</id>
<phase>pre-integration-test</phase>
<goals>
<goal>push</goal>
</goals>
<configuration>
<tag>${project.version}</tag>
<repository>${docker.repo}/${docker.name}</repository>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>jenkins</id>
<name>Jenkins Repository</name>
<url>https://repo.jenkins-ci.org/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

1
msa/pom.xml

@ -47,6 +47,7 @@
<module>tb-node</module>
<module>transport</module>
<module>js-executor</module>
<module>monitoring</module>
</modules>
<profiles>

15
msa/vc-executor/src/main/resources/tb-vc-executor.yml

@ -50,6 +50,13 @@ queue:
print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}"
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
ssl:
enabled: "${TB_KAFKA_SSL_ENABLED:false}"
truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}"
truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}"
keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}"
keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}"
key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}"
acks: "${TB_KAFKA_ACKS:all}"
retries: "${TB_KAFKA_RETRIES:1}"
compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip
@ -167,6 +174,14 @@ vc:
io_pool_size: "${TB_VC_GIT_POOL_SIZE:3}"
repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}"
# Usage statistics parameters
usage:
stats:
report:
enabled: "${USAGE_STATS_REPORT_ENABLED:true}"
enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}"
interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"

9
pom.xml

@ -164,6 +164,7 @@
<module>application</module>
<module>msa</module>
<module>rest-client</module>
<module>monitoring</module>
</modules>
<profiles>
@ -194,6 +195,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-conf</id>
@ -623,7 +625,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -1084,6 +1086,11 @@
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb-runtime.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java

@ -77,9 +77,9 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
EdgeEventActionType actionType = getAlarmActionType(msg);
return buildEvent(ctx.getTenantId(), actionType, getUUIDFromMsgData(msg), getAlarmEventType(), null);
} else {
EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType);
Map<String, Object> entityBody = new HashMap<>();
Map<String, String> metadata = msg.getMetaData().getData();
EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType, metadata);
Map<String, Object> entityBody = new HashMap<>();
JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData());
switch (actionType) {
case ATTRIBUTES_UPDATED:
@ -148,7 +148,7 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
return scope;
}
protected EdgeEventActionType getEdgeEventActionTypeByMsgType(String msgType) {
protected EdgeEventActionType getEdgeEventActionTypeByMsgType(String msgType, Map<String, String> metadata) {
EdgeEventActionType actionType;
if (SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType)
|| DataConstants.TIMESERIES_UPDATED.equals(msgType)) {
@ -159,6 +159,16 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
actionType = EdgeEventActionType.POST_ATTRIBUTES;
} else if (DataConstants.ATTRIBUTES_DELETED.equals(msgType)) {
actionType = EdgeEventActionType.ATTRIBUTES_DELETED;
} else if (DataConstants.CONNECT_EVENT.equals(msgType)
|| DataConstants.DISCONNECT_EVENT.equals(msgType)
|| DataConstants.ACTIVITY_EVENT.equals(msgType)
|| DataConstants.INACTIVITY_EVENT.equals(msgType)) {
String scope = metadata.get(SCOPE);
if ( StringUtils.isEmpty(scope)) {
actionType = EdgeEventActionType.TIMESERIES_UPDATED;
} else {
actionType = EdgeEventActionType.ATTRIBUTES_UPDATED;
}
} else {
log.warn("Unsupported msg type [{}]", msgType);
throw new IllegalArgumentException("Unsupported msg type: " + msgType);
@ -172,7 +182,11 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
|| DataConstants.ATTRIBUTES_UPDATED.equals(msgType)
|| DataConstants.ATTRIBUTES_DELETED.equals(msgType)
|| DataConstants.TIMESERIES_UPDATED.equals(msgType)
|| DataConstants.ALARM.equals(msgType);
|| DataConstants.ALARM.equals(msgType)
|| DataConstants.CONNECT_EVENT.equals(msgType)
|| DataConstants.DISCONNECT_EVENT.equals(msgType)
|| DataConstants.ACTIVITY_EVENT.equals(msgType)
|| DataConstants.INACTIVITY_EVENT.equals(msgType);
}
protected boolean isSupportedOriginator(EntityType entityType) {

43
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.SettableFuture;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@ -28,6 +29,8 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
@ -106,4 +109,44 @@ public class TbMsgPushToEdgeNodeTest {
verify(edgeEventService).saveAsync(any());
}
@Test
public void testMiscEventsProcessedAsAttributesUpdated() {
List<String> miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT,
DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT);
for (String event : miscEvents) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE);
testEvent(event, metaData, EdgeEventActionType.ATTRIBUTES_UPDATED, "kv");
}
}
@Test
public void testMiscEventsProcessedAsTimeseriesUpdated() {
List<String> miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT,
DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT);
for (String event : miscEvents) {
testEvent(event, new TbMsgMetaData(), EdgeEventActionType.TIMESERIES_UPDATED, "data");
}
}
private void testEvent(String event, TbMsgMetaData metaData, EdgeEventActionType expectedType, String dataKey) {
Mockito.when(ctx.getTenantId()).thenReturn(tenantId);
Mockito.when(ctx.getEdgeService()).thenReturn(edgeService);
Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService);
Mockito.when(ctx.getDbCallbackExecutor()).thenReturn(dbCallbackExecutor);
Mockito.when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create());
TbMsg msg = TbMsg.newMsg(event, new EdgeId(UUID.randomUUID()), metaData,
TbMsgDataType.JSON, "{\"lastConnectTs\":1}", null, null);
node.onMsg(ctx, msg);
ArgumentMatcher<EdgeEvent> eventArgumentMatcher = edgeEvent ->
edgeEvent.getAction().equals(expectedType)
&& edgeEvent.getBody().get(dataKey).get("lastConnectTs").asInt() == 1;
verify(edgeEventService).saveAsync(Mockito.argThat(eventArgumentMatcher));
Mockito.reset(ctx, edgeEventService);
}
}

15
transport/coap/src/main/resources/tb-coap-transport.yml

@ -151,6 +151,13 @@ queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
ssl:
enabled: "${TB_KAFKA_SSL_ENABLED:false}"
truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}"
truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}"
keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}"
keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}"
key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}"
acks: "${TB_KAFKA_ACKS:all}"
retries: "${TB_KAFKA_RETRIES:1}"
compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip
@ -270,6 +277,14 @@ service:
# Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}"
# Usage statistics parameters
usage:
stats:
report:
enabled: "${USAGE_STATS_REPORT_ENABLED:true}"
enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}"
interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"

15
transport/http/src/main/resources/tb-http-transport.yml

@ -136,6 +136,13 @@ queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
ssl:
enabled: "${TB_KAFKA_SSL_ENABLED:false}"
truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}"
truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}"
keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}"
keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}"
key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}"
acks: "${TB_KAFKA_ACKS:all}"
retries: "${TB_KAFKA_RETRIES:1}"
compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip
@ -255,6 +262,14 @@ service:
# Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}"
# Usage statistics parameters
usage:
stats:
report:
enabled: "${USAGE_STATS_REPORT_ENABLED:true}"
enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}"
interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"

15
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -217,6 +217,13 @@ queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
ssl:
enabled: "${TB_KAFKA_SSL_ENABLED:false}"
truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}"
truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}"
keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}"
keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}"
key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}"
acks: "${TB_KAFKA_ACKS:all}"
retries: "${TB_KAFKA_RETRIES:1}"
compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip
@ -337,6 +344,14 @@ service:
# Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}"
# Usage statistics parameters
usage:
stats:
report:
enabled: "${USAGE_STATS_REPORT_ENABLED:true}"
enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}"
interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"

15
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -166,6 +166,13 @@ queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
ssl:
enabled: "${TB_KAFKA_SSL_ENABLED:false}"
truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}"
truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}"
keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}"
keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}"
key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}"
acks: "${TB_KAFKA_ACKS:all}"
retries: "${TB_KAFKA_RETRIES:1}"
compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip
@ -285,6 +292,14 @@ service:
# Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}"
# Usage statistics parameters
usage:
stats:
report:
enabled: "${USAGE_STATS_REPORT_ENABLED:true}"
enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}"
interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"

15
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -112,6 +112,13 @@ queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
ssl:
enabled: "${TB_KAFKA_SSL_ENABLED:false}"
truststore.location: "${TB_KAFKA_SSL_TRUSTSTORE_LOCATION:}"
truststore.password: "${TB_KAFKA_SSL_TRUSTSTORE_PASSWORD:}"
keystore.location: "${TB_KAFKA_SSL_KEYSTORE_LOCATION:}"
keystore.password: "${TB_KAFKA_SSL_KEYSTORE_PASSWORD:}"
key.password: "${TB_KAFKA_SSL_KEY_PASSWORD:}"
acks: "${TB_KAFKA_ACKS:all}"
retries: "${TB_KAFKA_RETRIES:1}"
compression.type: "${TB_KAFKA_COMPRESSION_TYPE:none}" # none or gzip
@ -235,6 +242,14 @@ service:
# Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}"
# Usage statistics parameters
usage:
stats:
report:
enabled: "${USAGE_STATS_REPORT_ENABLED:true}"
enabled_per_customer: "${USAGE_STATS_REPORT_PER_CUSTOMER_ENABLED:false}"
interval: "${USAGE_STATS_REPORT_INTERVAL:10}"
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"

41
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -604,6 +604,47 @@ export class DashboardUtilsService {
delete entityAlias.entityType;
delete entityAlias.entityFilter;
}
entityAlias = this.validateAndUpdateEntityAliasSingleTypeFilters(entityAlias);
return entityAlias;
}
private validateAndUpdateEntityAliasSingleTypeFilters(entityAlias: EntityAlias): EntityAlias {
if (entityAlias.filter.type === AliasFilterType.deviceType) {
if (entityAlias.filter.deviceType) {
if (!entityAlias.filter.deviceTypes) {
entityAlias.filter.deviceTypes = [];
}
entityAlias.filter.deviceTypes.push(entityAlias.filter.deviceType);
delete entityAlias.filter.deviceType;
}
}
if (entityAlias.filter.type === AliasFilterType.assetType) {
if (entityAlias.filter.assetType) {
if (!entityAlias.filter.assetTypes) {
entityAlias.filter.assetTypes = [];
}
entityAlias.filter.assetTypes.push(entityAlias.filter.assetType);
delete entityAlias.filter.assetType;
}
}
if (entityAlias.filter.type === AliasFilterType.entityViewType) {
if (entityAlias.filter.entityViewType) {
if (!entityAlias.filter.entityViewTypes) {
entityAlias.filter.entityViewTypes = [];
}
entityAlias.filter.entityViewTypes.push(entityAlias.filter.entityViewType);
delete entityAlias.filter.entityViewType;
}
}
if (entityAlias.filter.type === AliasFilterType.edgeType) {
if (entityAlias.filter.edgeType) {
if (!entityAlias.filter.edgeTypes) {
entityAlias.filter.edgeTypes = [];
}
entityAlias.filter.edgeTypes.push(entityAlias.filter.edgeType);
delete entityAlias.filter.edgeType;
}
}
return entityAlias;
}

40
ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts

@ -84,47 +84,63 @@ export class EntityFilterViewComponent implements ControlValueAccessor {
this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description');
break;
case AliasFilterType.assetType:
const assetType = this.filter.assetType;
const assetTypesQuoted = [];
this.filter.assetTypes.forEach((filterAssetType) => {
assetTypesQuoted.push(`'${filterAssetType}'`);
});
const assetTypes = assetTypesQuoted.join(', ');
prefix = this.filter.assetNameFilter;
if (prefix && prefix.length) {
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-and-name-description',
{assetType, prefix});
{assetTypes, prefix});
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-description',
{assetType});
{assetTypes});
}
break;
case AliasFilterType.deviceType:
const deviceType = this.filter.deviceType;
const deviceTypesQuoted = [];
this.filter.deviceTypes.forEach((filterDeviceType) => {
deviceTypesQuoted.push(`'${filterDeviceType}'`);
});
const deviceTypes = deviceTypesQuoted.join(', ');
prefix = this.filter.deviceNameFilter;
if (prefix && prefix.length) {
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-and-name-description',
{deviceType, prefix});
{deviceTypes, prefix});
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-description',
{deviceType});
{deviceTypes});
}
break;
case AliasFilterType.edgeType:
const edgeType = this.filter.edgeType;
const edgeTypesQuoted = [];
this.filter.edgeTypes.forEach((filterEdgeType) => {
edgeTypesQuoted.push(`'${filterEdgeType}'`);
});
const edgeTypes = edgeTypesQuoted.join(', ');
prefix = this.filter.edgeNameFilter;
if (prefix && prefix.length) {
this.filterDisplayValue = this.translate.instant('alias.filter-type-edge-type-and-name-description',
{edgeType, prefix});
{edgeTypes, prefix});
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-edge-type-description',
{edgeType});
{edgeTypes});
}
break;
case AliasFilterType.entityViewType:
const entityView = this.filter.entityViewType;
const entityViewTypesQuoted = [];
this.filter.entityViewTypes.forEach((entityViewType) => {
entityViewTypesQuoted.push(`'${entityViewType}'`);
});
const entityViewTypes = entityViewTypesQuoted.join(', ');
prefix = this.filter.entityViewNameFilter;
if (prefix && prefix.length) {
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-and-name-description',
{entityView, prefix});
{entityViewTypes, prefix});
} else {
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-description',
{entityView});
{entityViewTypes});
}
break;
case AliasFilterType.relationsQuery:

32
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -83,10 +83,10 @@
</div>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.assetType">
<tb-entity-subtype-autocomplete required
formControlName="assetType"
[entityType]="entityType.ASSET">
</tb-entity-subtype-autocomplete>
<tb-entity-subtype-list required
[entityType]="entityType.ASSET"
formControlName="assetTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block">
<mat-label translate>asset.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
@ -95,10 +95,10 @@
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.deviceType">
<tb-entity-subtype-autocomplete required
formControlName="deviceType"
[entityType]="entityType.DEVICE">
</tb-entity-subtype-autocomplete>
<tb-entity-subtype-list required
[entityType]="entityType.DEVICE"
formControlName="deviceTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block">
<mat-label translate>device.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
@ -107,10 +107,10 @@
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityViewType">
<tb-entity-subtype-autocomplete required
formControlName="entityViewType"
[entityType]="entityType.ENTITY_VIEW">
</tb-entity-subtype-autocomplete>
<tb-entity-subtype-list required
[entityType]="entityType.ENTITY_VIEW"
formControlName="entityViewTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block">
<mat-label translate>entity-view.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
@ -119,10 +119,10 @@
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.edgeType">
<tb-entity-subtype-autocomplete required
formControlName="edgeType"
[entityType]="entityType.EDGE">
</tb-entity-subtype-autocomplete>
<tb-entity-subtype-list required
[entityType]="entityType.EDGE"
formControlName="edgeTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block">
<mat-label translate>edge.name-starts-with</mat-label>
<input matInput formControlName="edgeNameFilter">

8
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts

@ -136,25 +136,25 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit {
break;
case AliasFilterType.assetType:
this.filterFormGroup = this.fb.group({
assetType: [filter ? filter.assetType : null, [Validators.required]],
assetTypes: [filter ? filter.assetTypes : null, [Validators.required]],
assetNameFilter: [filter ? filter.assetNameFilter : '', []],
});
break;
case AliasFilterType.deviceType:
this.filterFormGroup = this.fb.group({
deviceType: [filter ? filter.deviceType : null, [Validators.required]],
deviceTypes: [filter ? filter.deviceTypes : null, [Validators.required]],
deviceNameFilter: [filter ? filter.deviceNameFilter : '', []],
});
break;
case AliasFilterType.edgeType:
this.filterFormGroup = this.fb.group({
edgeType: [filter ? filter.edgeType : null, [Validators.required]],
edgeTypes: [filter ? filter.edgeTypes : null, [Validators.required]],
edgeNameFilter: [filter ? filter.edgeNameFilter : '', []],
});
break;
case AliasFilterType.entityViewType:
this.filterFormGroup = this.fb.group({
entityViewType: [filter ? filter.entityViewType : null, [Validators.required]],
entityViewTypes: [filter ? filter.entityViewTypes : null, [Validators.required]],
entityViewNameFilter: [filter ? filter.entityViewNameFilter : '', []],
});
break;

4
ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html

@ -25,13 +25,13 @@
</section>
<mat-divider></mat-divider>
<div *ngIf="(notifications$ | async).length; else emptyNotification" style="overflow: auto">
<section style="min-height: 100px; overflow: auto; padding: 10px 0;">
<section style="min-height: 100px; overflow: auto; padding: 6px 0;">
<div *ngFor="let notification of (notifications$ | async); let last = last; trackBy: trackById">
<tb-notification [notification]="notification"
[onClose]="onClose"
(markAsRead)="markAsRead($event)">
</tb-notification>
<mat-divider *ngIf="!last" style="margin-bottom: 4px"></mat-divider>
<mat-divider *ngIf="!last" style="margin: 4px"></mat-divider>
</div>
</section>
</div>

3
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss

@ -130,6 +130,8 @@
border-radius: 4px;
background: #fff;
padding: 12px 16px;
display: inline-grid;
width: 100%;
.subject {
padding-bottom: 12px;
@ -142,6 +144,7 @@
font-weight: 400;
font-size: 14px;
line-height: 20px;
overflow-x: auto;
}
}
}

2
ui-ngx/src/app/shared/components/notification/notification.component.scss

@ -20,6 +20,7 @@
display: block;
border: 1px solid;
border-radius: 4px;
margin: 4px;
}
.icon {
@ -61,6 +62,7 @@
.alarm-severity {
padding: 5px;
border-radius: 8px;
margin-right: 7px;
.severity {
font-weight: 500;

16
ui-ngx/src/app/shared/models/alias.models.ts

@ -86,22 +86,38 @@ export interface StateEntityFilter {
}
export interface AssetTypeFilter {
/**
* @deprecated
*/
assetType?: string;
assetTypes?: string[];
assetNameFilter?: string;
}
export interface DeviceTypeFilter {
/**
* @deprecated
*/
deviceType?: string;
deviceTypes?: string[];
deviceNameFilter?: string;
}
export interface EdgeTypeFilter {
/**
* @deprecated
*/
edgeType?: string;
edgeTypes?: string[];
edgeNameFilter?: string;
}
export interface EntityViewFilter {
/**
* @deprecated
*/
entityViewType?: string;
entityViewTypes?: string[];
entityViewNameFilter?: string;
}

16
ui-ngx/src/assets/locale/locale.constant-ca_ES.json

@ -497,17 +497,17 @@
"filter-type-state-entity-owner": "Propietari de l'entitat des de l'estat del tauler",
"filter-type-state-entity-owner-description": "Propietari de l'entitat presa dels paràmetres d'estat del tauler de control",
"filter-type-asset-type": "Tipus d'actiu",
"filter-type-asset-type-description": "Actius del tipus '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Actius del tipus '{{assetType}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-asset-type-description": "Actius del tipus '{{assetTypes}}'",
"filter-type-asset-type-and-name-description": "Actius del tipus '{{assetTypes}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-device-type": "Tipus de dispositiu",
"filter-type-device-type-description": "Dispositius de tipus '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Dispositius de tipus '{{deviceType}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-device-type-description": "Dispositius de tipus '{{deviceTypes}}'",
"filter-type-device-type-and-name-description": "Dispositius de tipus '{{deviceTypes}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-entity-view-type": "Tipus de vista d'entitat",
"filter-type-entity-view-type-description": "Vistes d'entitat del tipus '{{entityView}}'",
"filter-type-entity-view-type-and-name-description": "Vistes d'entitat del tipus '{{entityView}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-entity-view-type-description": "Vistes d'entitat del tipus '{{entityViewTypes}}'",
"filter-type-entity-view-type-and-name-description": "Vistes d'entitat del tipus '{{entityViewTypes}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-edge-type": "Tipus de vora",
"filter-type-edge-type-description": "Vores del tipus '{{edgeType}}'",
"filter-type-edge-type-and-name-description": "Vores del tipus '{{edgeType}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-edge-type-description": "Vores del tipus '{{edgeTypes}}'",
"filter-type-edge-type-and-name-description": "Vores del tipus '{{edgeTypes}}' i el nom del qual comenci per '{{prefix}}'",
"filter-type-relations-query": "Consulta de relacions",
"filter-type-relations-query-description": "{{entities}} que tenen {{relationType}} relació {{direction}} {{rootEntity}}",
"filter-type-edge-search-query": "Consultar cerca de vora",

16
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -324,17 +324,17 @@
"filter-type-state-entity": "Entita ze stavu dashboardu",
"filter-type-state-entity-description": "Entita převzata z parametrů stavu dashboardu",
"filter-type-asset-type": "Typ aktiva",
"filter-type-asset-type-description": "Aktiva typu '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Aktiva typu '{{assetType}}' s názvem začínajícím '{{prefix}}'",
"filter-type-asset-type-description": "Aktiva typu '{{assetTypes}}'",
"filter-type-asset-type-and-name-description": "Aktiva typu '{{assetTypes}}' s názvem začínajícím '{{prefix}}'",
"filter-type-device-type": "Typ zařízení",
"filter-type-device-type-description": "Zařízení typu '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Zařízení typu '{{deviceType}}' s názvem začínajícím '{{prefix}}'",
"filter-type-device-type-description": "Zařízení typu '{{deviceTypes}}'",
"filter-type-device-type-and-name-description": "Zařízení typu '{{deviceTypes}}' s názvem začínajícím '{{prefix}}'",
"filter-type-entity-view-type": "Typ entitního pohledu",
"filter-type-entity-view-type-description": "Entitní pohledy typu '{{entityView}}'",
"filter-type-entity-view-type-and-name-description": "Entitní pohledy typu '{{entityView}}' s názvem začínajícím '{{prefix}}'",
"filter-type-entity-view-type-description": "Entitní pohledy typu '{{entityViewTypes}}'",
"filter-type-entity-view-type-and-name-description": "Entitní pohledy typu '{{entityViewTypes}}' s názvem začínajícím '{{prefix}}'",
"filter-type-edge-type": "Typ edge",
"filter-type-edge-type-description": "Edge typu '{{edgeType}}'",
"filter-type-edge-type-and-name-description": "Edge typu '{{edgeType}}' s názvem začínajícím '{{prefix}}'",
"filter-type-edge-type-description": "Edge typu '{{edgeTypes}}'",
"filter-type-edge-type-and-name-description": "Edge typu '{{edgeTypes}}' s názvem začínajícím '{{prefix}}'",
"filter-type-relations-query": "Dotaz na vztahy",
"filter-type-relations-query-description": "{{entities}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Dotaz na vyhledání aktiva",

14
ui-ngx/src/assets/locale/locale.constant-de_DE.json

@ -212,16 +212,16 @@
"filter-type-state-entity": "Entität aus dem Dashboard Status",
"filter-type-state-entity-description": "Entität aus den Dashboard Status Parametern",
"filter-type-asset-type": "Objekttyp",
"filter-type-asset-type-description": "Objekte vom Typ '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Objekte vom Typ '{{assetType}}' und Name beginnend mit '{{prefix}}'",
"filter-type-asset-type-description": "Objekte vom Typ '{{assetTypes}}'",
"filter-type-asset-type-and-name-description": "Objekte vom Typ '{{assetTypes}}' und Name beginnend mit '{{prefix}}'",
"filter-type-device-type": "Gerätetyp",
"filter-type-device-type-description": "Geräte vom Typ '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Geräte vom Typ '{{deviceType}}' und Name beginnend mit '{{prefix}}'",
"filter-type-device-type-description": "Geräte vom Typ '{{deviceTypes}}'",
"filter-type-device-type-and-name-description": "Geräte vom Typ '{{deviceTypes}}' und Name beginnend mit '{{prefix}}'",
"filter-type-entity-view-type": "Entitätsansichtstyp",
"filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityView}}'",
"filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityView}}' und Name beginnend mit '{{prefix}}'",
"filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityViewTypes}}'",
"filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityViewTypes}}' und Name beginnend mit '{{prefix}}'",
"filter-type-edge-type": "Randtyp",
"filter-type-edge-type-description": "Rand vom Typ '{{edgeType}}'",
"filter-type-edge-type-description": "Rand vom Typ '{{edgeTypes}}'",
"filter-type-relations-query": "Beziehungsabfrage",
"filter-type-relations-query-description": "{{entities}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Objektabfrage",

12
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -179,14 +179,14 @@
"filter-type-state-entity": "Οντότητα από την κατάσταση του dashboard",
"filter-type-state-entity-description": "Οντότητα που λαμβάνεται από τις παραμέτρους κατάστασης του dashboard",
"filter-type-asset-type": "Τύπος Asset",
"filter-type-asset-type-description": "Assets του τύπου '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Assets του τύπου '{{assetType}}' με όνομα που αρχίζει από '{{prefix}}'",
"filter-type-asset-type-description": "Assets του τύπου '{{assetTypes}}'",
"filter-type-asset-type-and-name-description": "Assets του τύπου '{{assetTypes}}' με όνομα που αρχίζει από '{{prefix}}'",
"filter-type-device-type": "Τύπος Συσκευής",
"filter-type-device-type-description": "Συσκευές του τύπου '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Συσκευές του τύπου '{{deviceType}}' με όνομα που αρχίζει από '{{prefix}}'",
"filter-type-device-type-description": "Συσκευές του τύπου '{{deviceTypes}}'",
"filter-type-device-type-and-name-description": "Συσκευές του τύπου '{{deviceTypes}}' με όνομα που αρχίζει από '{{prefix}}'",
"filter-type-entity-view-type": "Τύπος προβολής Οντοτήτων",
"filter-type-entity-view-type-description": "Τύπος προβολής Οντοτήτων '{{entityView}}'",
"filter-type-entity-view-type-and-name-description": "Τύπος προβολής Οντοτήτων '{{entityView}}' με όνομα που αρχίζει από '{{prefix}}'",
"filter-type-entity-view-type-description": "Τύπος προβολής Οντοτήτων '{{entityViewTypes}}'",
"filter-type-entity-view-type-and-name-description": "Τύπος προβολής Οντοτήτων '{{entityViewTypes}}' με όνομα που αρχίζει από '{{prefix}}'",
"filter-type-relations-query": "Ερώτημα Σχέσεων",
"filter-type-relations-query-description": "{{entities}} που έχουν σχέση {{relationType}} {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Ερώτημα αναζήτησης Asset",

16
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -534,17 +534,17 @@
"filter-type-state-entity": "Entity from dashboard state",
"filter-type-state-entity-description": "Entity taken from dashboard state parameters",
"filter-type-asset-type": "Asset type",
"filter-type-asset-type-description": "Assets of type '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'",
"filter-type-asset-type-description": "Assets of type '{{assetTypes}}'",
"filter-type-asset-type-and-name-description": "Assets of type '{{assetTypes}}' and with name starting with '{{prefix}}'",
"filter-type-device-type": "Device type",
"filter-type-device-type-description": "Devices of type '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'",
"filter-type-device-type-description": "Devices of type '{{deviceTypes}}'",
"filter-type-device-type-and-name-description": "Devices of types '{{deviceTypes}}' and with name starting with '{{prefix}}'",
"filter-type-entity-view-type": "Entity View type",
"filter-type-entity-view-type-description": "Entity Views of type '{{entityViewType}}'",
"filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewType}}' and with name starting with '{{prefix}}'",
"filter-type-entity-view-type-description": "Entity Views of type '{{entityViewTypes}}'",
"filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewTypes}}' and with name starting with '{{prefix}}'",
"filter-type-edge-type": "Edge type",
"filter-type-edge-type-description": "Edges of type '{{edgeType}}'",
"filter-type-edge-type-and-name-description": "Edges of type '{{edgeType}}' and with name starting with '{{prefix}}'",
"filter-type-edge-type-description": "Edges of type '{{edgeTypes}}'",
"filter-type-edge-type-and-name-description": "Edges of type '{{edgeTypes}}' and with name starting with '{{prefix}}'",
"filter-type-relations-query": "Relations query",
"filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Asset search query",

16
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -465,17 +465,17 @@
"filter-type-state-entity": "Entidad desde estado de panel",
"filter-type-state-entity-description": "Entidad tomada de los parámetros de panel",
"filter-type-asset-type": "Tipo de activo",
"filter-type-asset-type-description": "Activos del tipo '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Activos del tipo '{{assetType}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-asset-type-description": "Activos del tipo '{{assetTypes}}'",
"filter-type-asset-type-and-name-description": "Activos del tipo '{{assetTypes}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-device-type": "Tipo de dispositivo",
"filter-type-device-type-description": "Dispositivos de tipo '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Dispositivos de tipo '{{deviceType}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-device-type-description": "Dispositivos de tipo '{{deviceTypes}}'",
"filter-type-device-type-and-name-description": "Dispositivos de tipo '{{deviceTypes}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-entity-view-type": "Tipo de vista de entidad",
"filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityView}}'",
"filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityView}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityViewTypes}}'",
"filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityViewTypes}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-edge-type": "Tipo de borde",
"filter-type-edge-type-description": "Bordes del tipo '{{edgeType}}'",
"filter-type-edge-type-and-name-description": "Vistas de entidad del tipo '{{entityViewType}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-edge-type-description": "Bordes del tipo '{{edgeTypes}}'",
"filter-type-edge-type-and-name-description": "Vistas de entidad del tipo '{{edgeTypes}}' y cuyo nombre comience por '{{prefix}}'",
"filter-type-relations-query": "Consulta de relaciones",
"filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Búsqueda de activos",

12
ui-ngx/src/assets/locale/locale.constant-fa_IR.json

@ -154,14 +154,14 @@
"filter-type-state-entity": "موجودي از وضعيت داشبورد",
"filter-type-state-entity-description": "پارامترهاي موجودي گرفته شده از وضعيت داشبورد",
"filter-type-asset-type": "نوع دارايي",
"filter-type-asset-type-description": "'{{assetType}}' دارايي هاي نوع",
"filter-type-asset-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{assetType}}' دارايي هاي نوع",
"filter-type-asset-type-description": "'{{assetTypes}}' دارايي هاي نوع",
"filter-type-asset-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{assetTypes}}' دارايي هاي نوع",
"filter-type-device-type": "نوع دستگاه",
"filter-type-device-type-description": "'{{deviceType}}' دستگاه هاي نوع",
"filter-type-device-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{deviceType}}' دستگاه هاي نوع",
"filter-type-device-type-description": "'{{deviceTypes}}' دستگاه هاي نوع",
"filter-type-device-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{deviceTypes}}' دستگاه هاي نوع",
"filter-type-entity-view-type": "نوع نمايش موجودي",
"filter-type-entity-view-type-description": "'{{entityView}}' نمايش هاي موجودي نوع ",
"filter-type-entity-view-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{entityView}}' نمايش هاي موجودي نوع",
"filter-type-entity-view-type-description": "'{{entityViewTypes}}' نمايش هاي موجودي نوع ",
"filter-type-entity-view-type-and-name-description": ".شروع مي شود '{{prefix}}' که نامشان با '{{entityViewTypes}}' نمايش هاي موجودي نوع",
"filter-type-relations-query": "پرس و جو درمورد ارتباطات",
"filter-type-relations-query-description": ". دارند {{direction}} {{rootEntity}} را {{relationType}} که ارتباط {{entities}}",
"filter-type-asset-search-query": "پرس و جو درمورد جستجوي دارايي",

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save