From 4a6177d85e94c3c221be5d9fdbeae73dde547479 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 27 Nov 2025 23:14:17 +0100 Subject: [PATCH 1/3] monitoring service improvements: provisioning dashboard, make monitoring asset and dashboard public, log dashboard url, notify on startup and shutdown, shutdown thread pools on PreDestroy, removed file based logback in favour of stdout, logback severity down to INFO, JAVA_OPTS adjusted --- monitoring/src/main/conf/logback.xml | 20 +- monitoring/src/main/conf/tb-monitoring.conf | 7 +- .../ThingsboardMonitoringApplication.java | 37 +- .../data/notification/InfoNotification.java | 27 + .../notification/NotificationService.java | 29 +- .../service/MonitoringEntityService.java | 120 +++- .../resources/dashboard_cloud_monitoring.json | 580 ++++++++++++++++++ monitoring/src/main/resources/logback.xml | 3 +- 8 files changed, 772 insertions(+), 51 deletions(-) create mode 100644 monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java create mode 100644 monitoring/src/main/resources/dashboard_cloud_monitoring.json diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml index 53caafe8bb..43001ffd43 100644 --- a/monitoring/src/main/conf/logback.xml +++ b/monitoring/src/main/conf/logback.xml @@ -19,20 +19,6 @@ - - ${pkg.logFolder}/${pkg.name}.log - - ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n @@ -40,11 +26,11 @@ - - + + + - diff --git a/monitoring/src/main/conf/tb-monitoring.conf b/monitoring/src/main/conf/tb-monitoring.conf index 493d498cd8..cd2947563f 100644 --- a/monitoring/src/main/conf/tb-monitoring.conf +++ b/monitoring/src/main/conf/tb-monitoring.conf @@ -14,9 +14,10 @@ # 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 -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=3,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 JAVA_OPTS="$JAVA_OPTS -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-monitoring.out export LOADER_PATH=/usr/share/tb-monitoring/conf diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index c8e8d7070b..f225d0d771 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -15,36 +15,43 @@ */ package org.thingsboard.monitoring; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.scheduling.annotation.EnableScheduling; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.monitoring.data.notification.InfoNotification; +import org.thingsboard.monitoring.notification.NotificationService; import org.thingsboard.monitoring.service.BaseMonitoringService; import org.thingsboard.monitoring.service.MonitoringEntityService; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.Map; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @SpringBootApplication @EnableScheduling @Slf4j +@RequiredArgsConstructor public class ThingsboardMonitoringApplication { - @Autowired - private List> monitoringServices; - @Autowired - private MonitoringEntityService entityService; + private final List> monitoringServices; + private final MonitoringEntityService entityService; + private final NotificationService notificationService; @Value("${monitoring.monitoring_rate_ms}") private int monitoringRateMs; + ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); + public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) .properties(Map.of("spring.config.name", "tb-monitoring")) @@ -56,12 +63,30 @@ public class ThingsboardMonitoringApplication { entityService.checkEntities(); monitoringServices.forEach(BaseMonitoringService::init); - ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); scheduler.scheduleWithFixedDelay(() -> { monitoringServices.forEach(monitoringService -> { monitoringService.runChecks(); }); }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + notificationService.sendNotification(new InfoNotification(":rocket: Monitoring started")); + } + + @EventListener(ContextClosedEvent.class) + public void onShutdown(ContextClosedEvent event) { + log.info("Shutting down monitoring service"); + try { + var futures = notificationService.sendNotification(new InfoNotification(":warning: Monitoring is shutting down")); + for (Future future : futures) { + future.get(5, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.warn("Failed to send shutdown notification", e); + } + } + + @PreDestroy + public void shutdownScheduler() { + scheduler.shutdown(); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java new file mode 100644 index 0000000000..6aa42cbf2b --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2025 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.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class InfoNotification implements Notification { + private final String message; + @Override + public String getText() { + return message; + } +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java index 5e4dcb2f34..3a496fc016 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java @@ -22,10 +22,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.monitoring.data.notification.Notification; import org.thingsboard.monitoring.notification.channels.NotificationChannel; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; @Component @RequiredArgsConstructor @@ -38,22 +41,38 @@ public class NotificationService { @Value("${monitoring.notifications.message_prefix}") private String messagePrefix; - public void sendNotification(Notification notification) { + public List> sendNotification(Notification notification) { String message; if (StringUtils.isEmpty(messagePrefix)) { message = notification.getText(); } else { - message = messagePrefix + System.lineSeparator() + notification.getText(); + message = messagePrefix + " " + notification.getText(); } - notificationChannels.forEach(notificationChannel -> { + return notificationChannels.stream().map(notificationChannel -> notificationExecutor.submit(() -> { try { notificationChannel.sendNotification(message); } catch (Exception e) { log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e); } - }); - }); + }) + ).toList(); } + @PreDestroy + public void shutdownExecutor() { + try { + notificationExecutor.shutdown(); + if (!notificationExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + java.util.List dropped = notificationExecutor.shutdownNow(); + log.warn("Notification executor did not terminate in time. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } + } catch (InterruptedException e) { + java.util.List dropped = notificationExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + log.warn("Interrupted during notification executor shutdown. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } catch (Exception e) { + log.warn("Unexpected error while shutting down notification executor", e); + } + } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index 062104ecd5..fad9bc6b7c 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -30,13 +30,19 @@ import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; import org.thingsboard.monitoring.config.transport.TransportType; import org.thingsboard.monitoring.util.ResourceUtils; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -57,7 +63,6 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -66,6 +71,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_CF_TELEMETRY_KEY; @@ -76,6 +83,9 @@ import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_TELEMETR @RequiredArgsConstructor public class MonitoringEntityService { + private static final String DASHBOARD_TITLE = "[Monitoring] Cloud monitoring"; + private static final String DASHBOARD_RESOURCE_PATH = "dashboard_cloud_monitoring.json"; + private final TbClient tbClient; @Value("${monitoring.calculated_fields.enabled:true}") @@ -95,27 +105,34 @@ public class MonitoringEntityService { int currentVersion = Integer.parseInt(attributes.getOrDefault("version", "0")); int newVersion = ruleChainDescriptor.get("version").asInt(); if (currentVersion == newVersion) { - log.info("Not updating rule chain, version is the same ({})", currentVersion); - return; + log.debug("Not updating rule chain, version is the same ({})", currentVersion); } else { log.info("Updating rule chain '{}' from version {} to {}", ruleChain.getName(), currentVersion, newVersion); + + String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), + "\\$\\{MONITORING:(.+?)}", matchResult -> { + String key = matchResult.group(1); + String value = attributes.get(key); + if (value == null) { + throw new IllegalArgumentException("No attribute found for key " + key); + } + log.info("Using {}: {}", key, value); + return value; + }); + RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); + metaData.setRuleChainId(ruleChainId); + tbClient.saveRuleChainMetaData(metaData); + tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() + .put("version", newVersion)); } - String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), - "\\$\\{MONITORING:(.+?)}", matchResult -> { - String key = matchResult.group(1); - String value = attributes.get(key); - if (value == null) { - throw new IllegalArgumentException("No attribute found for key " + key); - } - log.info("Using {}: {}", key, value); - return value; - }); - RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); - metaData.setRuleChainId(ruleChainId); - tbClient.saveRuleChainMetaData(metaData); - tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() - .put("version", newVersion)); + Asset asset = getOrCreateMonitoringAsset(); + Dashboard dashboard = getOrCreateMonitoringDashboard(); + + tbClient.assignAssetToPublicCustomer(asset.getId()); + tbClient.assignDashboardToPublicCustomer(dashboard.getId()); + + getDashboardPublicLink(dashboard); } public Asset getOrCreateMonitoringAsset() { @@ -249,4 +266,71 @@ public class MonitoringEntityService { tbClient.saveCalculatedField(calculatedField); } + private String getDashboardPublicLink(Dashboard dashboard) { + String link = ""; + try { + Optional infoOpt = tbClient.getDashboardInfoById(dashboard.getId()); + if (infoOpt.isPresent()) { + String publicCustomerId = null; + Set customers = infoOpt.get().getAssignedCustomers(); + if (customers != null) { + publicCustomerId = customers.stream() + .filter(ShortCustomerInfo::isPublic) + .map(c -> c.getCustomerId().getId().toString()) + .findFirst().orElse(null); + } + if (publicCustomerId != null) { + link = buildPublicDashboardLink(dashboard.getId(), publicCustomerId); + log.info("Public Monitoring dashboard link: {}", link); + } else { + log.warn("Dashboard is not assigned to public customer. Public link can't be generated."); + } + } + } catch (Exception e) { + log.error("Failed to get a public link to Monitoring dashboard ", e); + } + return link; + } + + private Dashboard getOrCreateMonitoringDashboard() { + Dashboard existing = findDashboardByTitle(DASHBOARD_TITLE).orElse(null); + if (existing != null) { + log.debug("Found Monitoring dashboard '{}' with id {}", existing.getTitle(), existing.getId()); + return existing; + } + + Dashboard dashboardFromResource = ResourceUtils.getResource(DASHBOARD_RESOURCE_PATH, Dashboard.class); + dashboardFromResource.setTitle(DASHBOARD_TITLE); + //Optional.ofNullable(existing).map(Dashboard::getId).ifPresent(dashboardFromResource::setId); + Dashboard saved = tbClient.saveDashboard(dashboardFromResource); + log.info("Created Monitoring dashboard '{}' with id {}", saved.getTitle(), saved.getId()); + return saved; + } + + private Optional findDashboardByTitle(String title) { + // Use text search first and then filter by exact title + PageData page = tbClient.getTenantDashboards(new PageLink(10, 0, title)); + return page.getData().stream() + .filter(info -> title.equals(info.getTitle())) + .findFirst() + .flatMap(info -> tbClient.getDashboardById(info.getId())); + } + + private String buildPublicDashboardLink(DashboardId dashboardId, String publicCustomerId) { + String base = getBaseUrl(); + return String.format("%s/dashboard/%s?publicId=%s", base, dashboardId.getId().toString(), publicCustomerId); + } + + private String getBaseUrl() { + // TbClient.baseURL contains the root url, without trailing slash + try { + var baseUrlField = tbClient.getClass().getSuperclass().getDeclaredField("baseURL"); + baseUrlField.setAccessible(true); + return (String) baseUrlField.get(tbClient); + } catch (Exception e) { + log.warn("Unable to access baseURL from RestClient. Falling back to http://localhost:8080"); + return "http://localhost:8080"; + } + } + } diff --git a/monitoring/src/main/resources/dashboard_cloud_monitoring.json b/monitoring/src/main/resources/dashboard_cloud_monitoring.json new file mode 100644 index 0000000000..8be9677c61 --- /dev/null +++ b/monitoring/src/main/resources/dashboard_cloud_monitoring.json @@ -0,0 +1,580 @@ +{ + "title": "Cloud - Monitoring", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "arrivalLatency", + "type": "timeseries", + "label": "Arrival latency", + "color": "#ffc107", + "settings": {}, + "_hash": 0.7424467450112592, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "processingTime", + "type": "timeseries", + "label": "Processing time", + "color": "#607d8b", + "settings": {}, + "_hash": 0.6065128737901203, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "mqttTransportWsUpdateLatency", + "type": "timeseries", + "label": "MQTT - overall", + "color": "#f44336", + "settings": {}, + "_hash": 0.19638031054010696, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportWsUpdateLatency", + "type": "timeseries", + "label": "CoAP - overall", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.8167481665043521, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportWsUpdateLatency", + "type": "timeseries", + "label": "HTTP - overall", + "color": "#8bc34a", + "settings": {}, + "_hash": 0.45369210935861803, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportWsUpdateLatency", + "type": "timeseries", + "label": "LwM2M - overall", + "color": "#3f51b5", + "settings": {}, + "_hash": 0.3477627917073993, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationWsUpdateLatency", + "type": "timeseries", + "label": "CoAP integration - overall", + "color": "#e91e63", + "settings": {}, + "_hash": 0.8332881819050183, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsSubscribeLatency", + "type": "timeseries", + "label": "WS subscribe latency", + "color": "#03a9f4", + "settings": {}, + "_hash": 0.7843180730206556, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsConnectLatency", + "type": "timeseries", + "label": "WS connect latency", + "color": "#4caf50", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.23140687573220564, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "logInLatency", + "type": "timeseries", + "label": "Log in latency", + "color": "#2196f3", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.6130919996800452, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": null, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 4, + "smoothLines": false, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [], + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "General latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "7db7f580-2aac-d7ee-20f6-3d9315e7003f", + "typeFullFqn": "system.charts.basic_timeseries" + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "mqttTransportRequestLatency", + "type": "timeseries", + "label": "MQTT transport request latency", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8576659620523571, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportRequestLatency", + "type": "timeseries", + "label": "HTTP transport request latency", + "color": "#4caf50", + "settings": {}, + "_hash": 0.9749033105491403, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportRequestLatency", + "type": "timeseries", + "label": "CoAP transport request latency", + "color": "#f44336", + "settings": {}, + "_hash": 0.6977575200148037, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationRequestLatency", + "type": "timeseries", + "label": "CoAP integration request latency", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.18839685494200875, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportRequestLatency", + "type": "timeseries", + "label": "LwM2M transport request latency", + "color": "#ffc107", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.85549409179514, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "shadowSize": 4, + "fontColor": "#545454", + "fontSize": 10, + "xaxis": { + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "showLabels": true, + "color": "#545454" + }, + "grid": { + "color": "#545454", + "tickColor": "#DDDDDD", + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1 + }, + "legend": { + "show": true, + "position": "nw", + "backgroundColor": "#f0f0f0", + "backgroundOpacity": 0.85, + "labelBoxBorderColor": "rgba(1, 1, 1, 0.45)" + }, + "decimals": 1, + "stack": false, + "tooltipIndividual": false, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "Transport latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1", + "typeFullFqn": "system.charts.basic_timeseries" + } + }, + "states": { + "default": { + "name": "Cloud - Monitoring", + "root": true, + "layouts": { + "main": { + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 0 + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 12 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "outerMargin": true, + "layoutType": "default" + } + } + } + } + }, + "entityAliases": { + "6451461f-d748-9528-e3cd-64078c7fae05": { + "id": "6451461f-d748-9528-e3cd-64078c7fae05", + "alias": "Monitoring stats asset", + "filter": { + "type": "entityName", + "resolveMultiple": true, + "entityType": "ASSET", + "entityNameFilter": "[Monitoring] Latencies" + } + } + }, + "filters": {}, + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 120000, + "timewindowMs": 18000000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 600000, + "timewindowMs": 43200000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 2500 + }, + "timezone": null + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true + } + }, + "name": "Cloud - Monitoring", + "resources": null +} \ No newline at end of file diff --git a/monitoring/src/main/resources/logback.xml b/monitoring/src/main/resources/logback.xml index df3a7224c6..43001ffd43 100644 --- a/monitoring/src/main/resources/logback.xml +++ b/monitoring/src/main/resources/logback.xml @@ -17,7 +17,7 @@ --> - + @@ -34,5 +34,4 @@ - From 9850e7a4664525f34f569a8efa12995d3ced3e4e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 28 Nov 2025 00:06:42 +0100 Subject: [PATCH 2/3] monitoring: show dashboard link on startup notification. Initial delay set for services to avoid spikes --- .../ThingsboardMonitoringApplication.java | 17 ++++++++++------- .../service/MonitoringEntityService.java | 10 ++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index f225d0d771..88e9ac1fa3 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -50,7 +50,7 @@ public class ThingsboardMonitoringApplication { @Value("${monitoring.monitoring_rate_ms}") private int monitoringRateMs; - ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); + ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring"); public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) @@ -63,12 +63,15 @@ public class ThingsboardMonitoringApplication { entityService.checkEntities(); monitoringServices.forEach(BaseMonitoringService::init); - scheduler.scheduleWithFixedDelay(() -> { - monitoringServices.forEach(monitoringService -> { - monitoringService.runChecks(); - }); - }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); - notificationService.sendNotification(new InfoNotification(":rocket: Monitoring started")); + for (int i = 0; i < monitoringServices.size(); i++) { + int initialDelay = (monitoringRateMs / monitoringServices.size()) * i; + BaseMonitoringService service = monitoringServices.get(i); + log.info("Scheduling initialDelay {}, fixedDelay {} for monitoring '{}' ", initialDelay, monitoringRateMs, service.getClass().getSimpleName()); + scheduler.scheduleWithFixedDelay(service::runChecks, initialDelay, monitoringRateMs, TimeUnit.MILLISECONDS); + } + + String publicDashboardUrl = entityService.getDashboardPublicLink(); + notificationService.sendNotification(new InfoNotification(":rocket: <"+publicDashboardUrl+"|Monitoring> started")); } @EventListener(ContextClosedEvent.class) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index fad9bc6b7c..a0991bce72 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -91,6 +91,8 @@ public class MonitoringEntityService { @Value("${monitoring.calculated_fields.enabled:true}") private boolean calculatedFieldsMonitoringEnabled; + DashboardId dashboardId = null; + public void checkEntities() { RuleChain ruleChain = tbClient.getRuleChains(RuleChainType.CORE, new PageLink(10)).getData().stream() .filter(RuleChain::isRoot) @@ -132,7 +134,7 @@ public class MonitoringEntityService { tbClient.assignAssetToPublicCustomer(asset.getId()); tbClient.assignDashboardToPublicCustomer(dashboard.getId()); - getDashboardPublicLink(dashboard); + this.dashboardId = Optional.ofNullable(dashboard).map(Dashboard::getId).orElse(null); } public Asset getOrCreateMonitoringAsset() { @@ -266,10 +268,10 @@ public class MonitoringEntityService { tbClient.saveCalculatedField(calculatedField); } - private String getDashboardPublicLink(Dashboard dashboard) { + public String getDashboardPublicLink() { String link = ""; try { - Optional infoOpt = tbClient.getDashboardInfoById(dashboard.getId()); + Optional infoOpt = tbClient.getDashboardInfoById(dashboardId); if (infoOpt.isPresent()) { String publicCustomerId = null; Set customers = infoOpt.get().getAssignedCustomers(); @@ -280,7 +282,7 @@ public class MonitoringEntityService { .findFirst().orElse(null); } if (publicCustomerId != null) { - link = buildPublicDashboardLink(dashboard.getId(), publicCustomerId); + link = buildPublicDashboardLink(dashboardId, publicCustomerId); log.info("Public Monitoring dashboard link: {}", link); } else { log.warn("Dashboard is not assigned to public customer. Public link can't be generated."); From f2538117ce35c06362efdf7903532d6b56e53ebf Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 15 Dec 2025 19:19:09 +0100 Subject: [PATCH 3/3] refactoring --- .../monitoring/notification/NotificationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java index 3a496fc016..36529822b1 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java @@ -64,11 +64,11 @@ public class NotificationService { try { notificationExecutor.shutdown(); if (!notificationExecutor.awaitTermination(10, TimeUnit.SECONDS)) { - java.util.List dropped = notificationExecutor.shutdownNow(); + var dropped = notificationExecutor.shutdownNow(); log.warn("Notification executor did not terminate in time. Forced shutdown; {} task(s) will not be executed.", dropped.size()); } } catch (InterruptedException e) { - java.util.List dropped = notificationExecutor.shutdownNow(); + var dropped = notificationExecutor.shutdownNow(); Thread.currentThread().interrupt(); log.warn("Interrupted during notification executor shutdown. Forced shutdown; {} task(s) will not be executed.", dropped.size()); } catch (Exception e) {