Skip to content

Commit d7f18ba

Browse files
authored
Merge pull request #136 from scality/feature/OSIS-113-getUsage-metrics
Feature/osis 113 get usage metrics
2 parents e176c8b + 99ffbce commit d7f18ba

File tree

20 files changed

+597
-44
lines changed

20 files changed

+597
-44
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
buildscript {
22
ext {
3-
osisVersion = '2.1.0'
3+
osisVersion = '2.2.0'
44
vaultclientVersion = '1.1.0'
55
springBootVersion = '2.7.6'
66
}
@@ -120,4 +120,4 @@ task app {
120120
}
121121
}
122122
}
123-
compileJava.dependsOn app
123+
compileJava.dependsOn app
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
*Copyright 2021 Scality, Inc.
3+
*SPDX-License-Identifier: Apache License 2.0
4+
*/
5+
6+
package com.scality.osis.healthcheck;
7+
8+
import com.scality.osis.ScalityAppEnv;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.boot.actuate.health.Health;
13+
import org.springframework.boot.actuate.health.HealthIndicator;
14+
import org.springframework.stereotype.Component;
15+
16+
import java.io.IOException;
17+
import java.net.HttpURLConnection;
18+
import java.net.SocketTimeoutException;
19+
20+
@Component("utapi")
21+
public class UtapiHealthIndicator implements HealthIndicator {
22+
private static final Logger logger = LoggerFactory.getLogger(UtapiHealthIndicator.class);
23+
24+
@Autowired
25+
private ScalityAppEnv appEnv;
26+
27+
@Override
28+
public Health health() {
29+
HttpURLConnection connection = null;
30+
try {
31+
// check if Utapi Endpoint is reachable
32+
connection = (HttpURLConnection) new java.net.URL(appEnv.getUtapiEndpoint()).openConnection();
33+
connection.setConnectTimeout(appEnv.getUtapiHealthCheckTimeout());
34+
connection.connect();
35+
} catch (SocketTimeoutException e) {
36+
logger.warn("Failed to connect to Utapi endpoint {} in timeout {}",appEnv.getUtapiEndpoint(), appEnv.getUtapiHealthCheckTimeout());
37+
return Health.down()
38+
.withDetail("error", e.getMessage())
39+
.build();
40+
} catch (IOException e) {
41+
logger.warn("an I/O error occurs while trying to connect {}.",appEnv.getUtapiEndpoint());
42+
return Health.down()
43+
.withDetail("error", e.getMessage())
44+
.build();
45+
} finally {
46+
if (connection != null) {
47+
connection.disconnect();
48+
}
49+
}
50+
return Health.up().build();
51+
}
52+
}

osis-app/src/main/java/com/scality/osis/resource/ScalityOsisController.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,17 +332,17 @@ public OsisTenant getTenant(
332332
* @return The usage of the tenant or user is returned (status code 200)
333333
* or The optional API is not implemented (status code 501)
334334
*/
335-
@Operation(summary = "Get the usage of the platform tenant or user", description = "Operation ID: getUsage<br> Get the platform usage of global (without query parameter), tenant (with tenant_id) or user (only with user_id)",
335+
@Operation(summary = "Get the usage of the platform tenant or user", description = "Operation ID: getUsage<br> Get the platform usage of global (without query parameter), tenant (only with tenant_id) or user (with tenant_id and user_id).",
336336
responses = {
337337
@ApiResponse(responseCode = "200", description = "The usage of the tenant or user is returned"),
338338
@ApiResponse(responseCode = "501", description = "The optional API is not implemented")
339339
},
340340
tags = {"usage", "optional"})
341341
@GetMapping(value = "/api/v1/usage", produces = "application/json")
342342
public OsisUsage getUsage(
343-
@Parameter(description = "The ID of the tenant to get its usage. 'tenant_id' takes precedence over 'user_id' to take effect if both are specified.") @Valid @RequestParam(value = "tenant_id", required = false) Optional<String> tenantId,
344-
@Parameter(description = "The ID of the user to get its usage. 'tenant_id' takes precedence over 'user_id' to take effect if both are specified.") @Valid @RequestParam(value = "user_id", required = false) Optional<String> userId) {
345-
if (!tenantId.isPresent() && userId.isPresent()) {
343+
@Parameter(description = "The ID of the tenant to get its usage.") @Valid @RequestParam(value = "tenant_id", required = false) Optional<String> tenantId,
344+
@Parameter(description = "The ID of the user to get its usage.") @Valid @RequestParam(value = "user_id", required = false) Optional<String> userId) {
345+
if (tenantId.isEmpty() && userId.isPresent()) {
346346
throw new BadRequestException("userId must be specified with associated tenantId!");
347347
}
348348
return osisService.getOsisUsage(tenantId, userId);

osis-core/src/main/java/com/scality/osis/App.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
*/
77
public interface App {
88

9-
String VERSION = "2.1.0";
9+
String VERSION = "2.2.0";
1010
Long DATE = 1678441656429L;
1111
}

osis-core/src/main/java/com/scality/osis/ScalityAppEnv.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,19 @@ public int getS3HealthCheckTimeout() {
190190
}
191191
return Integer.parseInt(s3HealthCheckTimeout);
192192
}
193+
194+
public boolean isUtapiEnabled() {
195+
return Boolean.parseBoolean(env.getProperty("osis.scality.utapi.enabled"));
196+
}
197+
public String getUtapiEndpoint() {
198+
return env.getProperty("osis.scality.utapi.endpoint");
199+
}
200+
201+
public int getUtapiHealthCheckTimeout() {
202+
String utapiHealthCheckTimeout = env.getProperty("osis.scality.utapi.healthcheck.timeout");
203+
if(StringUtils.isBlank(utapiHealthCheckTimeout)) {
204+
utapiHealthCheckTimeout = DEFAULT_UTAPI_HEALTHCHECK_TIMEOUT;
205+
}
206+
return Integer.parseInt(utapiHealthCheckTimeout);
207+
}
193208
}

osis-core/src/main/java/com/scality/osis/model/OsisUsage.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ public class OsisUsage {
2727
private Long usedBytes;
2828

2929
public OsisUsage() {
30-
totalBytes = 0L;
31-
availableBytes = 0L;
32-
usedBytes = 0L;
33-
bucketCount = 0L;
34-
objectCount = 0L;
30+
totalBytes = -1L;
31+
availableBytes = -1L;
32+
usedBytes = -1L;
33+
bucketCount = -1L;
34+
objectCount = -1L;
3535
}
3636

3737
public OsisUsage bucketCount(Long bucketCount) {
@@ -128,5 +128,11 @@ public Long getUsedBytes() {
128128
public void setUsedBytes(Long usedBytes) {
129129
this.usedBytes = usedBytes;
130130
}
131+
132+
public void consolidateUsage(OsisUsage usage) {
133+
this.bucketCount = Math.max(this.bucketCount, 0) + usage.getBucketCount();
134+
this.objectCount = Math.max(this.objectCount, 0) + usage.getObjectCount();
135+
this.usedBytes = Math.max(this.usedBytes, 0) + usage.getUsedBytes();
136+
}
131137
}
132138

osis-core/src/main/java/com/scality/osis/model/ScalityOsisConstants.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@ private ScalityOsisConstants() {
1414
}
1515

1616
public static final String DELETE_TENANT_API_CODE = "deleteTenant";
17-
public static final String GET_USAGE_API_CODE = "getUsage";
1817
public static final String GET_BUCKET_LIST_API_CODE = "getBucketList";
1918
public static final String GET_BUCKET_ID_LOGGING_API_CODE = "getBucketLoggingId";
2019

2120
public static final List<String> API_CODES = Arrays.asList(
2221
DELETE_TENANT_API_CODE,
23-
GET_USAGE_API_CODE,
2422
GET_BUCKET_LIST_API_CODE,
2523
GET_BUCKET_ID_LOGGING_API_CODE
2624
);

osis-core/src/main/java/com/scality/osis/service/impl/ScalityOsisServiceImpl.java

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package com.scality.osis.service.impl;
88

9+
import com.amazonaws.Response;
910
import com.amazonaws.services.identitymanagement.AmazonIdentityManagement;
1011
import com.amazonaws.services.identitymanagement.model.*;
1112
import com.amazonaws.services.s3.AmazonS3;
@@ -26,6 +27,11 @@
2627
import com.scality.osis.security.crypto.model.SecretKeyRepoData;
2728
import com.scality.osis.security.utils.CipherFactory;
2829
import com.scality.osis.service.ScalityOsisService;
30+
import com.scality.osis.utapi.Utapi;
31+
import com.scality.osis.utapiclient.dto.ListMetricsRequestDTO;
32+
import com.scality.osis.utapiclient.dto.MetricsData;
33+
import com.scality.osis.utapiclient.services.UtapiServiceClient;
34+
import com.scality.osis.utapiclient.utils.UtapiClientException;
2935
import com.scality.osis.utils.ScalityModelConverter;
3036
import com.scality.osis.utils.ScalityUtils;
3137
import com.scality.osis.vaultadmin.VaultAdmin;
@@ -44,6 +50,7 @@
4450
import java.util.concurrent.ConcurrentHashMap;
4551

4652
import static com.scality.osis.utils.ScalityConstants.*;
53+
import static com.scality.osis.utils.ScalityUtils.getHourTime;
4754

4855
/**
4956
* The type Scality osis service.
@@ -56,6 +63,7 @@ public class ScalityOsisServiceImpl implements ScalityOsisService {
5663
private ScalityAppEnv appEnv;
5764
private VaultAdmin vaultAdmin;
5865
private S3 s3;
66+
private Utapi utapi;
5967
private ScalityOsisCapsManager scalityOsisCapsManager;
6068

6169
@Autowired
@@ -75,13 +83,16 @@ public class ScalityOsisServiceImpl implements ScalityOsisService {
7583
* @param appEnv the app env
7684
* @param vaultAdmin the vault admin
7785
* @param s3 the s3 client
86+
* @param utapi the utapi client
7887
* @param scalityOsisCapsManager the osis caps manager
7988
*/
8089
public ScalityOsisServiceImpl(ScalityAppEnv appEnv, VaultAdmin vaultAdmin, S3 s3,
81-
ScalityOsisCapsManager scalityOsisCapsManager) {
90+
Utapi utapi,
91+
ScalityOsisCapsManager scalityOsisCapsManager) {
8292
this.appEnv = appEnv;
8393
this.vaultAdmin = vaultAdmin;
8494
this.s3 = s3;
95+
this.utapi = utapi;
8596
this.scalityOsisCapsManager = scalityOsisCapsManager;
8697
}
8798

@@ -324,7 +335,7 @@ public PageOfUsers queryUsers(long offset, long limit, String filter) {
324335
if (!Objects.equals(osisTenant.getName(), displayName)) {
325336
// 3. format user name ex. user1, represent username of a user
326337
logger.debug("Query Users filter display_name represents username");
327-
username = username !=null ? username : displayName;
338+
username = username != null ? username : displayName;
328339
} else {
329340
logger.debug("Query Users filter display_name represents tenant_name");
330341
}
@@ -551,7 +562,7 @@ public void deleteTenant(String tenantId, Boolean purgeData) {
551562
public OsisTenant updateTenant(String tenantId, OsisTenant osisTenant) {
552563
try {
553564
logger.info("Update Tenant request received, tenantId:{}, osisTenant:{}",
554-
tenantId, new Gson().toJson(osisTenant));
565+
tenantId, new Gson().toJson(osisTenant));
555566

556567
// check tenantID and OSIS tenant Consistency
557568
// special check for ensuring consistency between tenant name and ID
@@ -562,8 +573,8 @@ public OsisTenant updateTenant(String tenantId, OsisTenant osisTenant) {
562573
!Objects.equals(osisTenant.getTenantId(), osisTenantFromStoragePlatform.getTenantId())) {
563574
logger.error("Update Tenant failed. Tenant name and tenant ID doesn't match in the request and storage platform");
564575
throw new VaultServiceException(
565-
HttpStatus.BAD_REQUEST,
566-
"E_BAD_REQUEST", "Tenant name and tenant ID doesn't match in the request and storage platform"
576+
HttpStatus.BAD_REQUEST,
577+
"E_BAD_REQUEST", "Tenant name and tenant ID doesn't match in the request and storage platform"
567578
);
568579
}
569580

@@ -1190,7 +1201,7 @@ public PageOfOsisBucketMeta getBucketList(String tenantId, long offset, long lim
11901201
logger.debug("[S3] List all Buckets size:{}", buckets.size());
11911202

11921203
PageOfOsisBucketMeta pageOfOsisBucketMeta = ScalityModelConverter.toPageOfOsisBucketMeta(
1193-
buckets, accountData.getCanonicalId(), offset, limit);
1204+
buckets, accountData.getCanonicalId(), offset, limit);
11941205
logger.info("List Buckets response:{}", new Gson().toJson(pageOfOsisBucketMeta));
11951206

11961207
return pageOfOsisBucketMeta;
@@ -1218,7 +1229,116 @@ public PageOfOsisBucketMeta getBucketList(String tenantId, long offset, long lim
12181229

12191230
@Override
12201231
public OsisUsage getOsisUsage(Optional<String> tenantId, Optional<String> userId) {
1221-
return new OsisUsage();
1232+
1233+
if (!appEnv.isUtapiEnabled()) {
1234+
logger.info("Utapi is not enabled, returning empty usage");
1235+
return new OsisUsage();
1236+
}
1237+
1238+
logger.info("Get Osis Usage request received:: tenant ID:{}, user ID:{}", tenantId, userId);
1239+
OsisUsage osisUsage = new OsisUsage();
1240+
1241+
if (tenantId.isEmpty()) {
1242+
logger.info("tenant ID is empty, getting platform usage at global level");
1243+
1244+
int start = 0;
1245+
int limit = 100;
1246+
boolean hasMoreTenants = true;
1247+
while (hasMoreTenants) {
1248+
PageOfTenants pageOfTenants = listTenants(start, limit);
1249+
pageOfTenants.getItems().forEach(tenant -> {
1250+
logger.info("Getting usage for tenant:{}", tenant.getTenantId());
1251+
OsisUsage tenantOsisUsage = getOsisUsage(Optional.of(tenant.getTenantId()), Optional.empty());
1252+
logger.info("Usage for tenant:{} is:{}", tenant.getTenantId(), new Gson().toJson(tenantOsisUsage));
1253+
1254+
logger.debug("Consolidating usage for tenant:{}", tenant.getTenantId());
1255+
osisUsage.consolidateUsage(tenantOsisUsage);
1256+
logger.debug("Consolidated usage:{}", new Gson().toJson(osisUsage));
1257+
});
1258+
start += limit;
1259+
hasMoreTenants = pageOfTenants.getPageInfo().getTotal() >= limit;
1260+
}
1261+
1262+
logger.info("Usage for all tenants is:{}", new Gson().toJson(osisUsage));
1263+
1264+
} else {
1265+
1266+
try {
1267+
// getting utapi client for tenant
1268+
Credentials tempCredentials = getCredentials(tenantId.get());
1269+
final UtapiServiceClient utapiClient = this.utapi.getUtapiServiceClient(tempCredentials,
1270+
appEnv.getRegionInfo().get(0));
1271+
1272+
if (userId.isEmpty()) {
1273+
1274+
logger.info("tenant ID is specified, getting usage at tenant level, tenant ID:{}", tenantId.get());
1275+
1276+
// getting s3 client for tenant
1277+
final AmazonS3 s3Client = this.s3.getS3Client(tempCredentials,
1278+
appEnv.getRegionInfo().get(0));
1279+
1280+
logger.debug("Listing buckets for tenant:{}", tenantId.get());
1281+
List<Bucket> buckets = s3Client.listBuckets();
1282+
1283+
// set bucket count by the size of listing buckets
1284+
osisUsage.setBucketCount((long) buckets.size());
1285+
1286+
logger.debug("[UTAPI] Listing metrics for tenant:{}", tenantId.get());
1287+
ListMetricsRequestDTO listMetricsRequestDTO = ScalityModelConverter.toScalityListMetricsRequest(
1288+
"accounts",
1289+
List.of(tenantId.get()),
1290+
List.of(getHourTime())
1291+
);
1292+
Response<MetricsData[]> listMetricsResponseDTO = utapiClient.listAccountsMetrics(listMetricsRequestDTO);
1293+
MetricsData metricsData = listMetricsResponseDTO.getAwsResponse()[0];
1294+
logger.debug("[UTAPI] List Metrics response:{}", new Gson().toJson(metricsData));
1295+
1296+
// set object count and used bytes by the utapi metrics
1297+
osisUsage.setObjectCount(metricsData.getNumberOfObjects().get(1));
1298+
osisUsage.setUsedBytes(metricsData.getStorageUtilized().get(1));
1299+
1300+
AccountData account = vaultAdmin.getAccount(ScalityModelConverter.toGetAccountRequestWithID(tenantId.get()));
1301+
if (account.getQuota() > 0) {
1302+
osisUsage.setTotalBytes((long) account.getQuota()); // set total bytes by the vault quota
1303+
osisUsage.setAvailableBytes(osisUsage.getTotalBytes() - osisUsage.getUsedBytes()); // set available bytes by the difference between total bytes and used bytes
1304+
}
1305+
logger.info("Usage for tenant:{} is:{}", tenantId.get(), new Gson().toJson(osisUsage));
1306+
1307+
} else {
1308+
1309+
logger.info("tenant ID and user ID are specified, getting usage at user level, tenant ID:{}, user ID:{}", tenantId.get(), userId.get());
1310+
1311+
logger.debug("[UTAPI] Listing metrics for user:{}", userId.get());
1312+
ListMetricsRequestDTO listMetricsRequestDTO = ScalityModelConverter.toScalityListMetricsRequest(
1313+
"users",
1314+
List.of(userId.get()),
1315+
List.of(getHourTime())
1316+
);
1317+
Response<MetricsData[]> listMetricsResponseDTO = utapiClient.listUsersMetrics(listMetricsRequestDTO);
1318+
MetricsData metricsData = listMetricsResponseDTO.getAwsResponse()[0];
1319+
logger.debug("[UTAPI] List Metrics response:{}", new Gson().toJson(metricsData));
1320+
1321+
// set object count and used bytes by the utapi metrics
1322+
osisUsage.setObjectCount(metricsData.getNumberOfObjects().get(1));
1323+
osisUsage.setUsedBytes(metricsData.getStorageUtilized().get(1));
1324+
1325+
logger.info("Usage for user:{} is:{}", userId.get(), new Gson().toJson(osisUsage));
1326+
}
1327+
} catch (Exception err) {
1328+
if (isAdminPolicyError(err)) {
1329+
try {
1330+
generateAdminPolicy(tenantId.get());
1331+
return getOsisUsage(tenantId, userId);
1332+
} catch (Exception ex) {
1333+
err = ex;
1334+
}
1335+
}
1336+
logger.error("GetUsage error. Returning empty list. Error details: ", err);
1337+
// For errors, GetUsage should return empty OsisUsage
1338+
return osisUsage;
1339+
}
1340+
}
1341+
return osisUsage;
12221342
}
12231343

12241344
@Override
@@ -1355,7 +1475,11 @@ private boolean isAdminPolicyError(Exception e) {
13551475
return (e instanceof AmazonIdentityManagementException &&
13561476
(HttpStatus.FORBIDDEN.value() == ((AmazonIdentityManagementException) e).getStatusCode()))
13571477
||
1358-
(e instanceof AmazonS3Exception && (HttpStatus.FORBIDDEN.value() == ((AmazonS3Exception) e).getStatusCode()));
1478+
(e instanceof AmazonS3Exception &&
1479+
(HttpStatus.FORBIDDEN.value() == ((AmazonS3Exception) e).getStatusCode()))
1480+
||
1481+
(e instanceof UtapiClientException &&
1482+
(HttpStatus.FORBIDDEN.value() == ((UtapiClientException) e).getStatusCode()));
13591483
}
13601484

13611485
private void storeSecretKey(String repoKey, String secretAccessKey) throws Exception {

0 commit comments

Comments
 (0)