/* * Copyright 2021 Collate * 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.openmetadata.service.resources.usage; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.openmetadata.common.utils.CommonUtil.getDateStringByOffset; import static org.openmetadata.common.utils.CommonUtil.listOf; import static org.openmetadata.service.Entity.INGESTION_BOT_NAME; import static org.openmetadata.service.Entity.TABLE; import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound; import static org.openmetadata.service.exception.CatalogExceptionMessage.entityRepositoryNotFound; import static org.openmetadata.service.security.SecurityUtil.authHeaders; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.NON_EXISTENT_ENTITY; import static org.openmetadata.service.util.TestUtils.TEST_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.TEST_USER_NAME; import static org.openmetadata.service.util.TestUtils.assertResponse; import java.io.IOException; import java.net.URISyntaxException; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.HttpResponseException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import org.openmetadata.schema.api.data.CreateTable; import org.openmetadata.schema.entity.data.Database; import org.openmetadata.schema.entity.data.Table; import org.openmetadata.schema.type.DailyCount; import org.openmetadata.schema.type.EntityUsage; import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.schema.type.UsageDetails; import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationTest; import org.openmetadata.service.exception.CatalogExceptionMessage; import org.openmetadata.service.resources.databases.DatabaseResourceTest; import org.openmetadata.service.resources.databases.TableResourceTest; import org.openmetadata.service.util.RestUtil; import org.openmetadata.service.util.TestUtils; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class UsageResourceTest extends OpenMetadataApplicationTest { private static final String PUT = "PUT"; private static final String POST = "POST"; public static final List TABLES = new ArrayList<>(); public static final int TABLE_COUNT = 10; public static final int DAYS_OF_USAGE = 32; @BeforeAll public static void setup(TestInfo test) throws IOException, URISyntaxException { // Create TABLE_COUNT number of tables TableResourceTest tableResourceTest = new TableResourceTest(); tableResourceTest.setup(test); // Initialize TableResourceTest for using helper methods for (int i = 0; i < TABLE_COUNT; i++) { CreateTable createTable = tableResourceTest.createRequest(test, i); TABLES.add(tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS)); } } @Test void post_usageWithNonExistentEntityId_4xx() { assertResponse( () -> reportUsage(TABLE, NON_EXISTENT_ENTITY, usageReport(), ADMIN_AUTH_HEADERS), NOT_FOUND, entityNotFound(TABLE, NON_EXISTENT_ENTITY)); } @Test void put_usageWithNonExistentEntityId_4xx() { assertResponse( () -> reportUsagePut(TABLE, NON_EXISTENT_ENTITY, usageReport(), ADMIN_AUTH_HEADERS), NOT_FOUND, entityNotFound(TABLE, NON_EXISTENT_ENTITY)); } @Test void post_usageInvalidEntityName_4xx() { String invalidEntityType = "invalid"; assertResponse( () -> reportUsage(invalidEntityType, UUID.randomUUID(), usageReport(), ADMIN_AUTH_HEADERS), NOT_FOUND, entityRepositoryNotFound(invalidEntityType)); } @Test void put_usageInvalidEntityName_4xx() { String invalidEntityType = "invalid"; assertResponse( () -> reportUsagePut(invalidEntityType, UUID.randomUUID(), usageReport(), ADMIN_AUTH_HEADERS), NOT_FOUND, entityRepositoryNotFound(invalidEntityType)); } @Test void post_usageWithNegativeCountName_4xx() { DailyCount dailyCount = usageReport().withCount(-1); // Negative usage count assertResponse( () -> reportUsage(TABLE, UUID.randomUUID(), dailyCount, ADMIN_AUTH_HEADERS), BAD_REQUEST, "[count must be greater than or equal to 0]"); } @Test void put_usageWithNegativeCountName_4xx() { DailyCount dailyCount = usageReport().withCount(-1); // Negative usage count assertResponse( () -> reportUsagePut(TABLE, UUID.randomUUID(), dailyCount, ADMIN_AUTH_HEADERS), BAD_REQUEST, "[count must be greater than or equal to 0]"); } @Test void post_usageWithoutDate_4xx() { DailyCount usageReport = usageReport().withDate(null); // Negative usage count assertResponse( () -> reportUsage(TABLE, UUID.randomUUID(), usageReport, ADMIN_AUTH_HEADERS), BAD_REQUEST, "[date must not be null]"); } @Test void put_usageWithoutDate_4xx() { DailyCount usageReport = usageReport().withDate(null); // Negative usage count assertResponse( () -> reportUsagePut(TABLE, UUID.randomUUID(), usageReport, ADMIN_AUTH_HEADERS), BAD_REQUEST, "[date must not be null]"); } @Test void post_validUsageByNameAsAdmin_200(TestInfo test) { testValidUsageByName(test, POST, ADMIN_AUTH_HEADERS); } @Test void post_validUsageByNameAsIngestionBot_200(TestInfo test) { testValidUsageByName(test, POST, authHeaders(INGESTION_BOT_NAME)); } @Test void post_validUsageByNameAsNonPrivilegedUser_401(TestInfo test) { assertResponse( () -> testValidUsageByName(test, POST, TEST_AUTH_HEADERS), Status.FORBIDDEN, CatalogExceptionMessage.permissionNotAllowed( TEST_USER_NAME, listOf(MetadataOperation.EDIT_USAGE))); } @Test void put_validUsageByNameAsAdmin_200(TestInfo test) { testValidUsageByName(test, PUT, ADMIN_AUTH_HEADERS); } @Test void put_validUsageByNameAsIngestionBot_200(TestInfo test) { testValidUsageByName(test, PUT, authHeaders(INGESTION_BOT_NAME)); } @Test void put_validUsageByNameAsNonPrivilegedUser_401(TestInfo test) { assertResponse( () -> testValidUsageByName(test, PUT, TEST_AUTH_HEADERS), Status.FORBIDDEN, CatalogExceptionMessage.permissionNotAllowed( TEST_USER_NAME, listOf(MetadataOperation.EDIT_USAGE))); } @SneakyThrows void testValidUsageByName(TestInfo test, String methodType, Map authHeaders) { TableResourceTest tableResourceTest = new TableResourceTest(); Table table = tableResourceTest.createEntity(tableResourceTest.createRequest(test), ADMIN_AUTH_HEADERS); DailyCount usageReport = usageReport().withCount(100).withDate(RestUtil.DATE_FORMAT.format(LocalDate.now())); reportUsageByNameAndCheckPut( TABLE, table.getFullyQualifiedName(), usageReport, 100, 100, authHeaders); // a put request updates the data again if (methodType.equals(PUT)) { reportUsageByNamePut(TABLE, table.getFullyQualifiedName(), usageReport, authHeaders); checkUsageByName( usageReport.getDate(), TABLE, table.getFullyQualifiedName(), 200, 200, 200, authHeaders); } } @Order(1) // Run this method first before other usage records are created @Test void put_validUsageForTables_200_OK() throws HttpResponseException { // This test creates TABLE_COUNT of tables. // For these tables, publish usage data for DAYS_OF_USAGE number of days starting from today. // For 100 tables send usage report for last 30 days. This test checks if the daily, rolling // weekly and monthly usage count is correct. This test also checks if the daily, rolling weekly // and monthly usage percentile rank is correct. // Publish usage for DAYS_OF_USAGE number of days starting from today String today = RestUtil.DATE_FORMAT.format(LocalDate.now()); // today // Add table usages of each table - 0, 1 to TABLE_COUNT - 1 to get database usage final int dailyDatabaseUsageCount = TABLE_COUNT * (TABLE_COUNT - 1) / 2; UUID databaseId = TABLES.get(0).getDatabase().getId(); UUID schemaId = TABLES.get(0).getDatabaseSchema().getId(); for (int day = 0; day < DAYS_OF_USAGE; day++) { String date = getDateStringByOffset(RestUtil.DATE_FORMAT, today, day); LOG.info("Posting usage information for date {}", date); // For each day report usage for all the tables in TABLES list int databaseDailyCount = 0; int databaseWeeklyCount; int databaseMonthlyCount; for (int tableIndex = 0; tableIndex < TABLES.size(); tableIndex++) { // Usage count is set same as tableIndex. // First table as usage count = 0, Second table has count = 1 and so on int usageCount = tableIndex; UUID id = TABLES.get(tableIndex).getId(); DailyCount usageReport = usageReport().withCount(usageCount).withDate(date); // Report usage int weeklyCount = Math.min(day + 1, 7) * usageCount; // Expected cumulative weekly count int monthlyCount = Math.min(day + 1, 30) * usageCount; // Expected cumulative monthly count reportUsageAndCheckPut( TABLE, id, usageReport, weeklyCount, monthlyCount, ADMIN_AUTH_HEADERS); // Database has cumulative count of all the table usage databaseDailyCount += usageCount; // Cumulative weekly count for database databaseWeeklyCount = Math.min(day, 6) * dailyDatabaseUsageCount + databaseDailyCount; // Cumulative monthly count for database databaseMonthlyCount = Math.min(day, 29) * dailyDatabaseUsageCount + databaseDailyCount; LOG.info( "dailyDatabaseUsageCount {}, databaseDailyCount {} weekly {} monthly {}", dailyDatabaseUsageCount, databaseDailyCount, databaseWeeklyCount, databaseMonthlyCount); checkUsage( date, Entity.DATABASE, databaseId, databaseDailyCount, databaseWeeklyCount, databaseMonthlyCount, ADMIN_AUTH_HEADERS); // Schema also has cumulative count checkUsage( date, Entity.DATABASE_SCHEMA, schemaId, databaseDailyCount, databaseWeeklyCount, databaseMonthlyCount, ADMIN_AUTH_HEADERS); } // Compute daily percentiles now that all table usage have been published for a given date computePercentile(TABLE, date, ADMIN_AUTH_HEADERS); computePercentile(Entity.DATABASE, date, ADMIN_AUTH_HEADERS); // TODO check database percentile // For each day check percentile for (int tableIndex = 0; tableIndex < TABLES.size(); tableIndex++) { int expectedPercentile = 100 * (tableIndex) / TABLES.size(); EntityUsage usage = getUsage(TABLE, TABLES.get(tableIndex).getId(), date, 1, ADMIN_AUTH_HEADERS); assertEquals( expectedPercentile, usage.getUsage().get(0).getDailyStats().getPercentileRank()); assertEquals( expectedPercentile, usage.getUsage().get(0).getWeeklyStats().getPercentileRank()); assertEquals( expectedPercentile, usage.getUsage().get(0).getMonthlyStats().getPercentileRank()); } } // Test API returns right number of days of usage requests String date = getDateStringByOffset(RestUtil.DATE_FORMAT, today, DAYS_OF_USAGE - 1); // Number of days defaults to 1 when unspecified UUID tableId = TABLES.get(0).getId(); getAndCheckUsage(TABLE, tableId, date, null /*, days unspecified */, 1, ADMIN_AUTH_HEADERS); // Usage for specified number of days is returned getAndCheckUsage(TABLE, tableId, date, 1, 1, ADMIN_AUTH_HEADERS); getAndCheckUsage(TABLE, tableId, date, 5, 5, ADMIN_AUTH_HEADERS); getAndCheckUsage(TABLE, tableId, date, 30, 30, ADMIN_AUTH_HEADERS); // Usage for days out of range returned default number of days // 0 days is defaulted to 1 getAndCheckUsage(TABLE, tableId, date, 0, 1, ADMIN_AUTH_HEADERS); // -1 days is defaulted to 1 getAndCheckUsage(TABLE, tableId, date, -1, 1, ADMIN_AUTH_HEADERS); // More than 30 days is defaulted to 30 getAndCheckUsage(TABLE, tableId, date, 100, 30, ADMIN_AUTH_HEADERS); // Nothing is returned when usage for a date is not available // One day beyond the last day of usage published date = getDateStringByOffset(RestUtil.DATE_FORMAT, today, DAYS_OF_USAGE); // 0 days of usage resulted getAndCheckUsage(TABLE, tableId, date, 1, 0, ADMIN_AUTH_HEADERS); // Only 4 past usage records returned. For the given date there is no usage report. getAndCheckUsage(TABLE, tableId, date, 5, 4, ADMIN_AUTH_HEADERS); // Ensure GET .../tables/{id}?fields=usageSummary returns the latest usage date = getDateStringByOffset( RestUtil.DATE_FORMAT, today, DAYS_OF_USAGE - 1); // Latest usage report date EntityUsage usage = getUsage(TABLE, tableId, date, null /* days not specified */, ADMIN_AUTH_HEADERS); Table table = new TableResourceTest() .getEntity(TABLES.get(0).getId(), "usageSummary", ADMIN_AUTH_HEADERS); Assertions.assertEquals(usage.getUsage().get(0), table.getUsageSummary()); // Ensure GET .../databases/{id}?fields=usageSummary returns the latest usage usage = getUsage( Entity.DATABASE, databaseId, date, null /* days not specified */, ADMIN_AUTH_HEADERS); Database database = new DatabaseResourceTest().getEntity(databaseId, "usageSummary", ADMIN_AUTH_HEADERS); Assertions.assertEquals(usage.getUsage().get(0), database.getUsageSummary()); } public DailyCount usageReport() { Random random = new Random(); String today = RestUtil.DATE_FORMAT.format(LocalDate.now()); return new DailyCount().withCount(random.nextInt(100)).withDate(today); } public void reportUsageByNameAndCheckPut( String entity, String fqn, DailyCount usage, int weeklyCount, int monthlyCount, Map authHeaders) throws HttpResponseException { reportUsageByNamePut(entity, fqn, usage, authHeaders); checkUsageByName( usage.getDate(), entity, fqn, usage.getCount(), weeklyCount, monthlyCount, authHeaders); } public void reportUsageAndCheckPut( String entity, UUID id, DailyCount usage, int weeklyCount, int monthlyCount, Map authHeaders) throws HttpResponseException { reportUsagePut(entity, id, usage, authHeaders); checkUsage( usage.getDate(), entity, id, usage.getCount(), weeklyCount, monthlyCount, authHeaders); } public void reportUsageByNamePut( String entity, String name, DailyCount usage, Map authHeaders) throws HttpResponseException { WebTarget target = getResource("usage/").path(entity).path("/name/").path(name); TestUtils.put(target, usage, Response.Status.CREATED, authHeaders); } public void reportUsage(String entity, UUID id, DailyCount usage, Map authHeaders) throws HttpResponseException { WebTarget target = getResource("usage/").path(entity).path("/").path(id.toString()); TestUtils.post(target, usage, authHeaders); } public void reportUsagePut( String entity, UUID id, DailyCount usage, Map authHeaders) throws HttpResponseException { WebTarget target = getResource("usage/").path(entity).path("/").path(id.toString()); TestUtils.put(target, usage, Response.Status.CREATED, authHeaders); } public void computePercentile(String entity, String date, Map authHeaders) throws HttpResponseException { WebTarget target = getResource("usage/compute.percentile/" + entity + "/" + date); TestUtils.post(target, authHeaders); } public void getAndCheckUsage( String entity, UUID id, String date, Integer days, int expectedRecords, Map authHeaders) throws HttpResponseException { EntityUsage usage = getUsage(entity, id, date, days, authHeaders); assertEquals(expectedRecords, usage.getUsage().size()); } public EntityUsage getUsageByName( String entity, String fqn, String date, Integer days, Map authHeaders) throws HttpResponseException { return getUsage(getResource("usage/" + entity + "/name/").path(fqn), date, days, authHeaders); } public EntityUsage getUsage( String entity, UUID id, String date, Integer days, Map authHeaders) throws HttpResponseException { return getUsage(getResource("usage/" + entity + "/" + id), date, days, authHeaders); } public EntityUsage getUsage( WebTarget target, String date, Integer days, Map authHeaders) throws HttpResponseException { target = date != null ? target.queryParam("date", date) : target; target = days != null ? target.queryParam("days", days) : target; return TestUtils.get(target, EntityUsage.class, authHeaders); } public void checkUsage( String date, String entity, UUID id, int dailyCount, int weeklyCount, int monthlyCount, Map authHeaders) throws HttpResponseException { EntityUsage usage = getUsage(entity, id, date, 1, authHeaders); assertEquals(id, usage.getEntity().getId()); checkUsage(usage, date, entity, dailyCount, weeklyCount, monthlyCount); } public void checkUsageByName( String date, String entity, String name, int dailyCount, int weeklyCount, int monthlyCount, Map authHeaders) throws HttpResponseException { EntityUsage usage = getUsageByName(entity, name, date, 1, authHeaders); checkUsage(usage, date, entity, dailyCount, weeklyCount, monthlyCount); } public static void checkUsage( EntityUsage usage, String date, String entity, int dailyCount, int weeklyCount, int monthlyCount) { assertEquals(entity, usage.getEntity().getType()); UsageDetails usageDetails = usage.getUsage().get(0); assertEquals(date, usageDetails.getDate()); assertEquals(dailyCount, usageDetails.getDailyStats().getCount()); assertEquals(weeklyCount, usageDetails.getWeeklyStats().getCount()); assertEquals(monthlyCount, usageDetails.getMonthlyStats().getCount()); } }