Browse Source

Merge branch 'develop/3.5' of https://github.com/thingsboard/thingsboard into assignee

pull/8370/head
Seraphym-Tuhai 4 years ago
parent
commit
31fdce656b
  1. 50
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 8
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  3. 88
      application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
  4. 74
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  5. 41
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  6. 1
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  7. 9
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java
  8. 46
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  9. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  10. 11
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  11. 1
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  12. 19
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  13. 9
      application/src/main/resources/thingsboard.yml
  14. 22
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  15. 14
      application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
  16. 4
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  17. 38
      application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java
  18. 40
      application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
  19. 2
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  20. 110
      application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java
  21. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
  22. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java
  23. 1
      common/cluster-api/src/main/proto/queue.proto
  24. 4
      common/data/pom.xml
  25. 15
      common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java
  26. 1
      common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java
  27. 32
      common/data/src/main/java/org/thingsboard/server/common/data/query/AssetTypeFilter.java
  28. 38
      common/data/src/main/java/org/thingsboard/server/common/data/query/DeviceTypeFilter.java
  29. 32
      common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java
  30. 32
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java
  32. 37
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
  33. 124
      common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java
  34. 2
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  35. 16
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java
  36. 1
      dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java
  37. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  38. 27
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  39. 1
      dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java
  40. 28
      dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java
  41. 160
      monitoring/pom.xml
  42. 46
      monitoring/src/main/conf/logback.xml
  43. 22
      monitoring/src/main/conf/tb-monitoring.conf
  44. 40
      monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java
  45. 49
      monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java
  46. 194
      monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java
  47. 59
      monitoring/src/main/java/org/thingsboard/monitoring/client/WsClientFactory.java
  48. 34
      monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java
  49. 26
      monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java
  50. 35
      monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java
  51. 33
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java
  52. 33
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java
  53. 39
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java
  54. 33
      monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java
  55. 30
      monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java
  56. 67
      monitoring/src/main/java/org/thingsboard/monitoring/data/Latency.java
  57. 22
      monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java
  58. 28
      monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java
  59. 32
      monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java
  60. 27
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/CmdsWrapper.java
  61. 28
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataCmd.java
  62. 44
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java
  63. 28
      monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/LatestValueCmd.java
  64. 42
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java
  65. 22
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/Notification.java
  66. 49
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java
  67. 31
      monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java
  68. 53
      monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java
  69. 24
      monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java
  70. 58
      monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java
  71. 144
      monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java
  72. 120
      monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java
  73. 143
      monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java
  74. 78
      monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java
  75. 67
      monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/HttpTransportHealthChecker.java
  76. 87
      monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/MqttTransportHealthChecker.java
  77. 41
      monitoring/src/main/java/org/thingsboard/monitoring/util/TbStopWatch.java
  78. 38
      monitoring/src/main/resources/logback.xml
  79. 107
      monitoring/src/main/resources/tb-monitoring.yml
  80. 30
      msa/monitoring/docker/Dockerfile
  81. 31
      msa/monitoring/docker/start-tb-monitoring.sh
  82. 192
      msa/monitoring/pom.xml
  83. 1
      msa/pom.xml
  84. 15
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  85. 33
      pom.xml
  86. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java
  87. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java
  88. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
  89. 43
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java
  90. 15
      transport/coap/src/main/resources/tb-coap-transport.yml
  91. 15
      transport/http/src/main/resources/tb-http-transport.yml
  92. 15
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  93. 15
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  94. 15
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  95. 10
      ui-ngx/src/app/core/auth/auth.service.ts
  96. 5
      ui-ngx/src/app/core/http/admin.service.ts
  97. 30
      ui-ngx/src/app/core/http/user-settings.service.ts
  98. 43
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  99. 4
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html
  100. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html

50
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

@ -0,0 +1,50 @@
{
"widgetsBundle": {
"alias": "home_page_widgets",
"title": "Home page widgets",
"image": null,
"description": null,
"externalId": null,
"name": "Home page widgets"
},
"widgetTypes": [
{
"alias": "documentation_links",
"name": "Documentation links",
"image": null,
"description": null,
"descriptor": {
"type": "static",
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-doc-links-widget\n [ctx]=\"ctx\">\n</tb-doc-links-widget>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-doc-links-widget-settings",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true}"
}
},
{
"alias": "getting_started",
"name": "Getting started",
"image": null,
"description": null,
"descriptor": {
"type": "static",
"sizeX": 7.5,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-getting-started-widget\n [ctx]=\"ctx\">\n</tb-getting-started-widget>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}"
}
}
]
}

8
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -84,6 +84,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private long sendTimeout;
@Value("${server.ws.ping_timeout:30000}")
private long pingTimeout;
@Value("${server.ws.max_queue_messages_per_session:1000}")
private int wsMaxQueueMessagesPerSession;
private final ConcurrentMap<String, WebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TbRateLimits> perSessionUpdateLimits = new ConcurrentHashMap<>();
@ -143,9 +145,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
return;
}
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
int wsTenantProfileQueueLimit = tenantProfileConfiguration != null ?
tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : wsMaxQueueMessagesPerSession;
internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef,
tenantProfileConfiguration != null && tenantProfileConfiguration.getWsMsgQueueLimitPerSession() > 0 ?
tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500));
(wsTenantProfileQueueLimit > 0 && wsTenantProfileQueueLimit < wsMaxQueueMessagesPerSession) ?
wsTenantProfileQueueLimit : wsMaxQueueMessagesPerSession));
externalSessionMap.put(externalSessionId, internalSessionId);
processInWebSocketService(sessionRef, SessionEvent.onEstablished());

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

41
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")
@ -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);
}
}
}

1
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -508,6 +508,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
this.deleteSystemWidgetBundle("entity_admin_widgets");
this.deleteSystemWidgetBundle("navigation_widgets");
this.deleteSystemWidgetBundle("edge_widgets");
this.deleteSystemWidgetBundle("home_page_widgets");
installScripts.loadSystemWidgets();
}

9
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationDetails.java

@ -20,7 +20,6 @@ import ua_parser.Client;
import ua_parser.Parser;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
@Data
@ -43,11 +42,7 @@ public class RestAuthenticationDetails implements Serializable {
}
private static Client getUserAgent(HttpServletRequest request) {
try {
Parser uaParser = new Parser();
return uaParser.parse(request.getHeader("User-Agent"));
} catch (IOException e) {
return new Client(null, null, null);
}
Parser uaParser = new Parser();
return uaParser.parse(request.getHeader("User-Agent"));
}
}

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;

11
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -26,10 +26,12 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.SystemInfoData;
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.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -50,6 +52,7 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.annotation.Nullable;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
@ -143,9 +146,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (mailSettings != null) {
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom");
if (mailFrom != null) {
return DataValidator.doValidateEmail(mailFrom.asText());
}
return StringUtils.isNotEmpty(mailFrom.asText()) && !"ThingsBoard <sysadmin@localhost.localdomain>".equalsIgnoreCase(mailFrom.asText());
}
return false;
}
@ -154,13 +155,13 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
long ts = System.currentTimeMillis();
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
doSave(Collections.singletonList(clusterDataKv));
doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv));
}
private void saveCurrentMonolithSystemInfo() {
long ts = System.currentTimeMillis();
List<TsKvEntry> tsList = new ArrayList<>();
tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false)));
getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", (long) v))));
getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", (long) v))));
getDiscSpaceUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", (long) v))));

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) {

19
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;
@ -77,7 +80,9 @@ public class DefaultUpdateService implements UpdateService {
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void init() {
version = buildProperties != null ? buildProperties.getVersion() : "unknown";
updateMessage = new UpdateMessage(false, version, "", "", "", "");
updateMessage = new UpdateMessage(false, version, "", "",
"https://thingsboard.io/docs/reference/releases",
"https://thingsboard.io/docs/reference/releases");
if (updatesEnabled) {
try {
platform = System.getProperty("platform", "unknown");
@ -94,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) {
@ -124,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

@ -65,6 +65,7 @@ server:
max_per_user: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_MAX_PER_USER:10}"
max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}"
max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}"
max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}"
rest:
server_side_rpc:
# Minimum value of the server side RPC timeout. May override value provided in the REST API call.
@ -140,6 +141,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:
@ -987,6 +989,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>() {

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 {

4
common/data/pom.xml

@ -100,6 +100,10 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>

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) {

1
common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java

@ -38,5 +38,4 @@ public class UpdateMessage implements Serializable {
@ApiModelProperty(position = 6, value = "Latest ThingsBoard version release notes URL.")
private final String latestVersionReleaseNotesUrl;
}

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;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java

@ -19,7 +19,7 @@ import lombok.Getter;
public enum UserSettingsType {
GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS;
GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED;
@Getter
private final boolean reserved;

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;
}
}
}

2
common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -20,7 +20,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
@ -191,7 +190,6 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
}, callbackExecutor);
}
@NotNull
private JsInvokeProtos.RemoteJsRequest buildJsInvokeRequest(JsScriptInfo jsInfo, Object[] args, boolean includeScriptBody, String scriptBody) {
JsInvokeProtos.JsInvokeRequest.Builder jsRequestBuilder = JsInvokeProtos.JsInvokeRequest.newBuilder()
.setScriptHash(jsInfo.getHash())

16
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.server.store;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
@ -48,7 +49,12 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
if (data == null) {
return null;
} else {
return deserialize(data);
try {
return deserialize(data);
} catch (Exception e) {
log.warn("[{}] Failed to deserialize client from data: {}", endpoint, Hex.encodeHexString(data), e);
return null;
}
}
}
}
@ -70,7 +76,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
scans.forEach(scan -> {
scan.forEachRemaining(key -> {
byte[] element = connection.get(key);
clients.add(deserialize(element));
if (element != null) {
try {
clients.add(deserialize(element));
} catch (Exception e) {
log.warn("[{}] Failed to deserialize client from data: {}", Hex.encodeHexString(key), Hex.encodeHexString(element), e);
}
}
});
});
return clients;

1
dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;

2
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.sql.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
@ -303,7 +302,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query);
}
@NotNull
private static String getPropagationTypes(AlarmPropagationInfo ap) {
String propagateRelationTypes;
if (!CollectionUtils.isEmpty(ap.getPropagateRelationTypes())) {

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}"

33
pom.xml

@ -39,13 +39,13 @@
<javax-annotation.version>1.3.2</javax-annotation.version>
<jakarta.xml.bind-api.version>2.3.2</jakarta.xml.bind-api.version>
<jaxb-runtime.version>2.3.2</jaxb-runtime.version>
<spring-boot.version>2.7.0</spring-boot.version>
<spring-data.version>2.7.0</spring-data.version>
<spring.version>5.3.20</spring.version>
<spring-redis.version>5.5.12</spring-redis.version>
<spring-security.version>5.7.1</spring-security.version>
<spring-data-redis.version>2.7.0</spring-data-redis.version>
<jedis.version>3.7.1</jedis.version>
<spring-boot.version>2.7.10</spring-boot.version>
<spring-data.version>2.7.10</spring-data.version>
<spring.version>5.3.26</spring.version>
<spring-redis.version>5.5.17</spring-redis.version>
<spring-security.version>5.7.7</spring-security.version>
<spring-data-redis.version>2.7.10</spring-data-redis.version>
<jedis.version>3.8.0</jedis.version>
<jjwt.version>0.7.0</jjwt.version>
<slf4j.version>1.7.32</slf4j.version>
<log4j.version>2.17.1</log4j.version>
@ -75,14 +75,14 @@
<freemarker.version>2.3.30</freemarker.version>
<mail.version>1.6.2</mail.version>
<curator.version>4.2.0</curator.version>
<zookeeper.version>3.5.5</zookeeper.version>
<zookeeper.version>3.8.1</zookeeper.version>
<protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version>
<tbel.version>1.0.6</tbel.version>
<lombok.version>1.18.18</lombok.version>
<paho.client.version>1.2.4</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>
<netty.version>4.1.75.Final</netty.version>
<netty.version>4.1.91.Final</netty.version>
<netty-tcnative.version>2.0.51.Final</netty-tcnative.version>
<os-maven-plugin.version>1.7.0</os-maven-plugin.version>
<rabbitmq.version>4.8.0</rabbitmq.version>
@ -107,13 +107,13 @@
<bucket4j.version>4.1.1</bucket4j.version>
<fst.version>2.57</fst.version>
<antlr.version>2.7.7</antlr.version>
<snakeyaml.version>1.27</snakeyaml.version>
<snakeyaml.version>2.0</snakeyaml.version>
<aws.sdk.version>1.11.747</aws.sdk.version>
<pubsub.client.version>1.105.0</pubsub.client.version>
<google.common.protos.version>2.1.0</google.common.protos.version> <!-- required by io.grpc:grpc-protobuf:1.38.0-->
<azure-servicebus.version>3.2.0</azure-servicebus.version>
<passay.version>1.5.0</passay.version>
<ua-parser.version>1.4.3</ua-parser.version>
<ua-parser.version>1.5.4</ua-parser.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
<commons-collections.version>3.2.2</commons-collections.version>
<micrometer.version>1.9.0</micrometer.version>
@ -123,7 +123,7 @@
<hibernate-validator.version>6.0.20.Final</hibernate-validator.version>
<javax.el.version>3.0.0</javax.el.version>
<javax.validation-api.version>2.0.1.Final</javax.validation-api.version>
<antisamy.version>1.6.8</antisamy.version>
<antisamy.version>1.7.2</antisamy.version>
<snmp4j.version>2.8.5</snmp4j.version>
<!-- TEST SCOPE -->
<awaitility.version>4.1.0</awaitility.version>
@ -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>

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java

@ -21,7 +21,6 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
@ -127,7 +126,6 @@ public class TbCopyAttributesToEntityViewNode implements TbNode {
}
}
@NotNull
private FutureCallback<Void> getFutureCallback(TbContext ctx, TbMsg msg, EntityView entityView) {
return new FutureCallback<Void>() {
@Override

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) {

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
type = ComponentType.FILTER,
name = "entity type switch",
configClazz = EmptyNodeConfiguration.class,
relationTypes = {"Device", "Asset", "Alarm", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"},
relationTypes = {"Device", "Asset", "Alarm", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node", "Edge"},
nodeDescription = "Route incoming messages by Message Originator Type",
nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).",
uiResources = {"static/rulenode/rulenode-core-config.js"},

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}"

10
ui-ngx/src/app/core/auth/auth.service.ts

@ -297,16 +297,6 @@ export class AuthService {
} else if (authState.authUser.isPublic) {
result = this.router.parseUrl(`dashboard/${authState.lastPublicDashboardId}`);
}
} else if (authState.authUser.authority === Authority.SYS_ADMIN) {
this.adminService.checkUpdates().subscribe((updateMessage) => {
if (updateMessage && updateMessage.updateAvailable) {
this.store.dispatch(new ActionNotificationShow(
{message: updateMessage.message,
type: 'info',
verticalPosition: 'bottom',
horizontalPosition: 'right'}));
}
});
}
}
} else {

5
ui-ngx/src/app/core/http/admin.service.ts

@ -21,6 +21,7 @@ import { HttpClient } from '@angular/common/http';
import {
AdminSettings,
AutoCommitSettings,
FeaturesInfo,
JwtSettings,
MailServerSettings,
RepositorySettings,
@ -131,4 +132,8 @@ export class AdminService {
public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> {
return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config));
}
public getFeaturesInfo(config?: RequestConfig): Observable<FeaturesInfo> {
return this.http.get<FeaturesInfo>('/api/admin/featuresInfo', defaultHttpOptionsFromConfig(config));
}
}

30
ui-ngx/src/app/core/http/user-settings.service.ts

@ -16,10 +16,16 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models';
import {
DocumentationLink, DocumentationLinks,
GettingStarted,
initialUserSettings,
UserSettings,
UserSettingsType
} from '@shared/models/user-settings.models';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig } from '@core/http/http-utils';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
@Injectable({
providedIn: 'root'
@ -51,4 +57,24 @@ export class UserSettingsService {
defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}));
}
public getDocumentationLinks(config?: RequestConfig): Observable<DocumentationLinks> {
return this.http.get<DocumentationLinks>(`/api/user/settings/${UserSettingsType.DOC_LINKS}`,
defaultHttpOptionsFromConfig(config));
}
public updateDocumentationLinks(documentationLinks: DocumentationLinks, config?: RequestConfig): Observable<void> {
return this.http.put<void>(`/api/user/settings/${UserSettingsType.DOC_LINKS}`, documentationLinks,
defaultHttpOptionsFromConfig(config));
}
public getGettingStarted(config?: RequestConfig): Observable<GettingStarted> {
return this.http.get<GettingStarted>(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`,
defaultHttpOptionsFromConfig(config));
}
public updateGettingStarted(gettingStarted: GettingStarted, config?: RequestConfig): Observable<void> {
return this.http.put<void>(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`, gettingStarted,
defaultHttpOptionsFromConfig(config));
}
}

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

@ -238,6 +238,7 @@ export class DashboardUtilsService {
backgroundColor: '#eeeeee',
columns: 24,
margin: 10,
outerMargin: true,
backgroundSizeMode: '100%'
};
}
@ -282,6 +283,7 @@ export class DashboardUtilsService {
layout.gridSettings.margin = layout.gridSettings.margins[0];
delete layout.gridSettings.margins;
}
layout.gridSettings.outerMargin = isDefined(layout.gridSettings.outerMargin) ? layout.gridSettings.outerMargin : true;
layout.gridSettings.margin = isDefined(layout.gridSettings.margin) ? layout.gridSettings.margin : 10;
}
@ -604,6 +606,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;
}

4
ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div style="min-width: 600px;">
<form style="width: 600px;">
<mat-toolbar color="primary">
<span fxFlex></span>
<button mat-icon-button (click)="close()" type="button">
@ -30,4 +30,4 @@
[alarmActivityOnly]="true">
</tb-alarm-comment>
</div>
</div>
</form>

2
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="alarmFormGroup" style="min-width: 600px;">
<form [formGroup]="alarmFormGroup" style="width: 600px;">
<mat-toolbar color="primary">
<h2>{{ 'alarm.alarm-details' | translate }}</h2>
<span fxFlex></span>

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

Loading…
Cancel
Save