From 8481b71b7ebbed08a2caee73831245cda96d35fc Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 3 Apr 2023 10:07:16 +0200 Subject: [PATCH] added entity usage --- .../controller/UsageInfoController.java | 45 ++++++++ .../controller/BaseHomePageApiTest.java | 100 ++++++++++++++++ .../server/dao/usage/UsageInfoService.java | 25 ++++ .../server/common/data/UsageInfo.java | 43 +++++++ .../dao/usage/BasicUsageInfoService.java | 109 ++++++++++++++++++ .../thingsboard/rest/client/RestClient.java | 9 ++ 6 files changed, 331 insertions(+) create mode 100644 application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/usage/UsageInfoService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/UsageInfo.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/usage/BasicUsageInfoService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java b/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java new file mode 100644 index 0000000000..88296993d6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java @@ -0,0 +1,45 @@ +/** + * 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.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.UsageInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.dao.usage.UsageInfoService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class UsageInfoController extends BaseController { + + @Autowired + private UsageInfoService usageInfoService; + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/usageInfo", method = RequestMethod.GET) + @ResponseBody + public UsageInfo getTenantUsageInfo() throws ThingsboardException { + return checkNotNull(usageInfoService.getUsageInfo(getCurrentUser().getTenantId())); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java index 14fdfc4fac..50f165bf70 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java @@ -26,11 +26,13 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.TenantId; @@ -49,7 +51,9 @@ import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.TbApiUsageStateClient; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; @@ -69,6 +73,9 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { @Autowired private TbApiUsageStateClient apiUsageStateClient; + @Autowired + private TbTenantProfileCache tenantProfileCache; + //For system administrator @Test public void testTenantsCountWsCmd() throws Exception { @@ -329,6 +336,99 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { Assert.assertTrue(featuresInfo.isOauthEnabled()); } + @Test + public void testUsageInfo() throws Exception { + loginTenantAdmin(); + + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + + Assert.assertNotNull(tenantProfile); + + DefaultTenantProfileConfiguration configuration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); + + UsageInfo usageInfo = doGet("/api/tenant/usageInfo", UsageInfo.class); + Assert.assertNotNull(usageInfo); + Assert.assertEquals(0, usageInfo.getDevices()); + Assert.assertEquals(configuration.getMaxDevices(), usageInfo.getMaxDevices()); + + Assert.assertEquals(0, usageInfo.getAssets()); + Assert.assertEquals(configuration.getMaxAssets(), usageInfo.getMaxAssets()); + + Assert.assertEquals(1, usageInfo.getCustomers()); + Assert.assertEquals(configuration.getMaxCustomers(), usageInfo.getMaxCustomers()); + + Assert.assertEquals(2, usageInfo.getUsers()); + Assert.assertEquals(configuration.getMaxUsers(), usageInfo.getMaxUsers()); + + Assert.assertEquals(0, usageInfo.getDashboards()); + Assert.assertEquals(configuration.getMaxDashboards(), usageInfo.getMaxDashboards()); + + Assert.assertEquals(0, usageInfo.getTransportMessages()); + Assert.assertEquals(configuration.getMaxTransportMessages(), usageInfo.getMaxTransportMessages()); + + Assert.assertEquals(0, usageInfo.getJsExecutions()); + Assert.assertEquals(configuration.getMaxJSExecutions(), usageInfo.getMaxJsExecutions()); + + Assert.assertEquals(0, usageInfo.getEmails()); + Assert.assertEquals(configuration.getMaxEmails(), usageInfo.getMaxEmails()); + + Assert.assertEquals(0, usageInfo.getSms()); + Assert.assertEquals(configuration.getMaxSms(), usageInfo.getMaxSms()); + + Assert.assertEquals(0, usageInfo.getAlarms()); + Assert.assertEquals(configuration.getMaxCreatedAlarms(), usageInfo.getMaxAlarms()); + + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setName("device" + i); + devices.add(doPost("/api/device", device, Device.class)); + } + + usageInfo = doGet("/api/tenant/usageInfo", UsageInfo.class); + Assert.assertEquals(devices.size(), usageInfo.getDevices()); + + List assets = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Asset asset = new Asset(); + asset.setName("asset" + i); + assets.add(doPost("/api/asset", asset, Asset.class)); + } + + usageInfo = doGet("/api/tenant/usageInfo", UsageInfo.class); + Assert.assertEquals(assets.size(), usageInfo.getAssets()); + + List customers = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Customer customer = new Customer(); + customer.setTitle("customer" + i); + customers.add(doPost("/api/customer", customer, Customer.class)); + } + + usageInfo = doGet("/api/tenant/usageInfo", UsageInfo.class); + Assert.assertEquals(customers.size() + 1, usageInfo.getCustomers()); + + List users = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + User user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setEmail(i + "user@thingsboard.org"); + users.add(doPost("/api/user", user, User.class)); + } + + usageInfo = doGet("/api/tenant/usageInfo", UsageInfo.class); + Assert.assertEquals(users.size() + 2, usageInfo.getUsers()); + + List dashboards = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("dashboard" + i); + dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class)); + } + + usageInfo = doGet("/api/tenant/usageInfo", UsageInfo.class); + Assert.assertEquals(dashboards.size(), usageInfo.getDashboards()); + } private OAuth2Info createDefaultOAuth2Info() { return new OAuth2Info(true, Lists.newArrayList( diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/usage/UsageInfoService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/usage/UsageInfoService.java new file mode 100644 index 0000000000..01e90b86e5 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/usage/UsageInfoService.java @@ -0,0 +1,25 @@ +/** + * 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.dao.usage; + +import org.thingsboard.server.common.data.UsageInfo; +import org.thingsboard.server.common.data.id.TenantId; + +public interface UsageInfoService { + + UsageInfo getUsageInfo(TenantId tenantId); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UsageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/UsageInfo.java new file mode 100644 index 0000000000..297e050011 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UsageInfo.java @@ -0,0 +1,43 @@ +/** + * 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.common.data; + +import lombok.Data; + +@Data +public class UsageInfo { + private long devices; + private long maxDevices; + private long assets; + private long maxAssets; + private long customers; + private long maxCustomers; + private long users; + private long maxUsers; + private long dashboards; + private long maxDashboards; + + private long transportMessages; + private long maxTransportMessages; + private long jsExecutions; + private long maxJsExecutions; + private long emails; + private long maxEmails; + private long sms; + private long maxSms; + private long alarms; + private long maxAlarms; +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/usage/BasicUsageInfoService.java b/dao/src/main/java/org/thingsboard/server/dao/usage/BasicUsageInfoService.java new file mode 100644 index 0000000000..dd1ff6cd50 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/usage/BasicUsageInfoService.java @@ -0,0 +1,109 @@ +/** + * 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.dao.usage; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.UsageInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.asset.AssetDao; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.dashboard.DashboardDao; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; +import org.thingsboard.server.dao.user.UserDao; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BasicUsageInfoService implements UsageInfoService { + + private final DeviceDao deviceDao; + private final AssetDao assetDao; + private final CustomerDao customerDao; + private final UserDao userDao; + private final DashboardDao dashboardDao; + private final ApiUsageStateService apiUsageStateService; + private final TimeseriesService tsService; + @Lazy + private final TbTenantProfileCache tenantProfileCache; + + @Override + public UsageInfo getUsageInfo(TenantId tenantId) { + DefaultTenantProfileConfiguration profileConfiguration = + (DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration(); + UsageInfo usageInfo = new UsageInfo(); + usageInfo.setDevices(deviceDao.countByTenantId(tenantId)); + usageInfo.setMaxDevices(profileConfiguration.getMaxDevices()); + usageInfo.setAssets(assetDao.countByTenantId(tenantId)); + usageInfo.setMaxAssets(profileConfiguration.getMaxAssets()); + usageInfo.setCustomers(customerDao.countByTenantId(tenantId)); + usageInfo.setMaxCustomers(profileConfiguration.getMaxCustomers()); + usageInfo.setUsers(userDao.countByTenantId(tenantId)); + usageInfo.setMaxUsers(profileConfiguration.getMaxUsers()); + usageInfo.setDashboards(dashboardDao.countByTenantId(tenantId)); + usageInfo.setMaxDashboards(profileConfiguration.getMaxDashboards()); + + usageInfo.setMaxAlarms(profileConfiguration.getMaxCreatedAlarms()); + usageInfo.setMaxTransportMessages(profileConfiguration.getMaxTransportMessages()); + usageInfo.setMaxJsExecutions(profileConfiguration.getMaxJSExecutions()); + usageInfo.setMaxEmails(profileConfiguration.getMaxEmails()); + usageInfo.setMaxSms(profileConfiguration.getMaxSms()); + ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); + if (apiUsageState != null) { + Collection keys = Arrays.asList( + ApiUsageRecordKey.TRANSPORT_MSG_COUNT.getApiCountKey(), + ApiUsageRecordKey.JS_EXEC_COUNT.getApiCountKey(), + ApiUsageRecordKey.EMAIL_EXEC_COUNT.getApiCountKey(), + ApiUsageRecordKey.SMS_EXEC_COUNT.getApiCountKey(), + ApiUsageRecordKey.CREATED_ALARMS_COUNT.getApiCountKey()); + try { + List entries = tsService.findLatest(tenantId, apiUsageState.getId(), keys).get(); + usageInfo.setTransportMessages(getLongValueFromTsEntries(entries, ApiUsageRecordKey.TRANSPORT_MSG_COUNT.getApiCountKey())); + usageInfo.setJsExecutions(getLongValueFromTsEntries(entries, ApiUsageRecordKey.JS_EXEC_COUNT.getApiCountKey())); + usageInfo.setEmails(getLongValueFromTsEntries(entries, ApiUsageRecordKey.EMAIL_EXEC_COUNT.getApiCountKey())); + usageInfo.setSms(getLongValueFromTsEntries(entries, ApiUsageRecordKey.SMS_EXEC_COUNT.getApiCountKey())); + usageInfo.setAlarms(getLongValueFromTsEntries(entries, ApiUsageRecordKey.CREATED_ALARMS_COUNT.getApiCountKey())); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Failed to fetch api usage values from timeseries!"); + } + } + return usageInfo; + } + + private long getLongValueFromTsEntries(List entries, String key) { + Optional entryOpt = entries.stream().filter(e -> e.getKey().equals(key)).findFirst(); + if (entryOpt.isPresent() && entryOpt.get().getLongValue().isPresent()) { + return entryOpt.get().getLongValue().get(); + } else { + return 0; + } + } +} diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index e84113524a..b52c64cd52 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.UpdateMessage; +import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; @@ -2467,6 +2468,14 @@ public class RestClient implements Closeable { }, params).getBody(); } + public UsageInfo getUsageInfo() { + return restTemplate.exchange( + baseURL + "/api/tenant/usageInfo", + HttpMethod.GET, + HttpEntity.EMPTY, + UsageInfo.class).getBody(); + } + public Optional getTenantProfileById(TenantProfileId tenantProfileId) { try { ResponseEntity tenantProfile = restTemplate.getForEntity(baseURL + "/api/tenantProfile/{tenantProfileId}", TenantProfile.class, tenantProfileId);