From 5bd4e56b3f8a8aac613f71964dabd7a4d55e3cf4 Mon Sep 17 00:00:00 2001 From: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> Date: Wed, 9 Nov 2022 13:06:13 +0530 Subject: [PATCH] Feat kpis (#8584) * Added Kpi Resource * Fix typo * Fixed failing test * Review Suggestions --- .../v006__create_db_connection_info.sql | 11 + .../v006__create_db_connection_info.sql | 11 + .../java/org/openmetadata/service/Entity.java | 1 + .../service/jdbi3/CollectionDAO.java | 21 + .../service/jdbi3/KpiRepository.java | 227 +++++++++ .../service/resources/kpi/KpiResource.java | 455 ++++++++++++++++++ .../json/data/ResourceDescriptors.json | 9 + .../service/resources/EntityResourceTest.java | 7 + .../resources/kpi/KpiResourceTest.java | 274 +++++++++++ .../api/dataInsight/kpi/createKpiRequest.json | 51 ++ .../json/schema/dataInsight/kpi/basic.json | 54 +++ .../json/schema/dataInsight/kpi/kpi.json | 89 ++++ 12 files changed, 1210 insertions(+) create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/KpiRepository.java create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java create mode 100644 openmetadata-service/src/test/java/org/openmetadata/service/resources/kpi/KpiResourceTest.java create mode 100644 openmetadata-spec/src/main/resources/json/schema/api/dataInsight/kpi/createKpiRequest.json create mode 100644 openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/basic.json create mode 100644 openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/kpi.json diff --git a/bootstrap/sql/com.mysql.cj.jdbc.Driver/v006__create_db_connection_info.sql b/bootstrap/sql/com.mysql.cj.jdbc.Driver/v006__create_db_connection_info.sql index 14e83da7cf6..c261eb5853f 100644 --- a/bootstrap/sql/com.mysql.cj.jdbc.Driver/v006__create_db_connection_info.sql +++ b/bootstrap/sql/com.mysql.cj.jdbc.Driver/v006__create_db_connection_info.sql @@ -52,3 +52,14 @@ WHERE serviceType = 'Dagster'; UPDATE pipeline_service_entity SET json = JSON_REMOVE(json ,'$.connection.config.hostPort', '$.connection.config.numberOfStatus') WHERE serviceType = 'Dagster'; + +CREATE TABLE IF NOT EXISTS kpi_entity ( + id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL, + name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') NOT NULL, + json JSON NOT NULL, + updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL, + updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL, + deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted'), + PRIMARY KEY (id), + UNIQUE (name) +); diff --git a/bootstrap/sql/org.postgresql.Driver/v006__create_db_connection_info.sql b/bootstrap/sql/org.postgresql.Driver/v006__create_db_connection_info.sql index dca802573f1..ab82d5b77a7 100644 --- a/bootstrap/sql/org.postgresql.Driver/v006__create_db_connection_info.sql +++ b/bootstrap/sql/org.postgresql.Driver/v006__create_db_connection_info.sql @@ -57,3 +57,14 @@ where servicetype = 'Dagster'; UPDATE pipeline_service_entity SET json = json::jsonb #- '{connection,config,hostPort}' #- '{connection,config,numberOfStatus}' where servicetype = 'Dagster'; + +CREATE TABLE IF NOT EXISTS kpi_entity ( + id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL, + name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL, + json JSONB NOT NULL, + updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL, + updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL, + deleted BOOLEAN GENERATED ALWAYS AS ((json ->> 'deleted')::boolean) STORED, + PRIMARY KEY (id), + UNIQUE (name) +); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java index 3a8cb6dfaa1..1aa75c9d3da 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java @@ -97,6 +97,7 @@ public final class Entity { public static final String TYPE = "type"; public static final String TEST_DEFINITION = "testDefinition"; public static final String TEST_SUITE = "testSuite"; + public static final String KPI = "kpi"; public static final String TEST_CASE = "testCase"; public static final String WEB_ANALYTIC_EVENT = "webAnalyticEvent"; public static final String DATA_INSIGHT_CHART = "dataInsightChart"; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java index 937ba7a3a8e..5233e78d882 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java @@ -50,6 +50,7 @@ import org.openmetadata.schema.auth.PasswordResetToken; import org.openmetadata.schema.auth.RefreshToken; import org.openmetadata.schema.auth.TokenType; import org.openmetadata.schema.dataInsight.DataInsightChart; +import org.openmetadata.schema.dataInsight.kpi.Kpi; import org.openmetadata.schema.entity.Bot; import org.openmetadata.schema.entity.Type; import org.openmetadata.schema.entity.data.Chart; @@ -239,6 +240,9 @@ public interface CollectionDAO { @CreateSqlObject TokenDAO getTokenDAO(); + @CreateSqlObject + KpiDAO kpiDAO(); + interface DashboardDAO extends EntityDAO { @Override default String getTableName() { @@ -3258,4 +3262,21 @@ public interface CollectionDAO { @SqlUpdate(value = "DELETE from user_tokens WHERE userid = :userid AND tokenType = :tokenType") void deleteTokenByUserAndType(@Bind("userid") String userid, @Bind("tokenType") String tokenType); } + + interface KpiDAO extends EntityDAO { + @Override + default String getTableName() { + return "kpi_entity"; + } + + @Override + default Class getEntityClass() { + return Kpi.class; + } + + @Override + default String getNameColumn() { + return "name"; + } + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/KpiRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/KpiRepository.java new file mode 100644 index 00000000000..85d582c58b4 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/KpiRepository.java @@ -0,0 +1,227 @@ +package org.openmetadata.service.jdbi3; + +import static org.openmetadata.service.Entity.DATA_INSIGHT_CHART; +import static org.openmetadata.service.Entity.KPI; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.jdbi.v3.sqlobject.transaction.Transaction; +import org.openmetadata.schema.EntityInterface; +import org.openmetadata.schema.dataInsight.ChartParameterValues; +import org.openmetadata.schema.dataInsight.DataInsightChart; +import org.openmetadata.schema.dataInsight.kpi.Kpi; +import org.openmetadata.schema.dataInsight.type.KpiResult; +import org.openmetadata.schema.dataInsight.type.KpiTarget; +import org.openmetadata.schema.type.ChangeDescription; +import org.openmetadata.schema.type.ChangeEvent; +import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.schema.type.EventType; +import org.openmetadata.schema.type.FieldChange; +import org.openmetadata.schema.type.Include; +import org.openmetadata.schema.type.Relationship; +import org.openmetadata.service.Entity; +import org.openmetadata.service.exception.EntityNotFoundException; +import org.openmetadata.service.resources.kpi.KpiResource; +import org.openmetadata.service.util.EntityUtil; +import org.openmetadata.service.util.JsonUtils; +import org.openmetadata.service.util.RestUtil; +import org.openmetadata.service.util.ResultList; + +public class KpiRepository extends EntityRepository { + public static final String COLLECTION_PATH = "/v1/kpi"; + private static final String UPDATE_FIELDS = "owner,targetDefinition,dataInsightChart"; + private static final String PATCH_FIELDS = "description,owner,startDate,endDate,metricType"; + public static final String KPI_RESULT_EXTENSION = "kpi.kpiResult"; + + public KpiRepository(CollectionDAO dao) { + super(KpiResource.COLLECTION_PATH, KPI, Kpi.class, dao.kpiDAO(), dao, PATCH_FIELDS, UPDATE_FIELDS); + } + + @Override + public Kpi setFields(Kpi kpi, EntityUtil.Fields fields) throws IOException { + kpi.setDataInsightChart(fields.contains("dataInsightChart") ? getDataInsightChart(kpi) : null); + kpi.setKpiResult(fields.contains("kpiResult") ? getKpiResult(kpi.getFullyQualifiedName()) : null); + kpi.setOwner(fields.contains("owner") ? getOwner(kpi) : null); + return kpi; + } + + @Override + public void prepare(Kpi kpi) throws IOException { + // validate targetDefinition + Entity.getEntityReferenceById(Entity.DATA_INSIGHT_CHART, kpi.getDataInsightChart().getId(), Include.NON_DELETED); + EntityRepository dataInsightChartRepository = Entity.getEntityRepository(DATA_INSIGHT_CHART); + DataInsightChart chart = + dataInsightChartRepository.get( + null, kpi.getDataInsightChart().getId(), dataInsightChartRepository.getFields("metrics")); + // Valiadte here if this chart already has some kpi in progress + validateKpiTargetDefinition(kpi.getTargetDefinition(), chart.getMetrics()); + kpi.setFullyQualifiedName(kpi.getName()); + } + + private void validateKpiTargetDefinition( + List kpiTargetDef, List dataInsightChartMetric) { + if (kpiTargetDef.isEmpty() && !dataInsightChartMetric.isEmpty()) { + throw new IllegalArgumentException("Parameter Values doesn't match Kpi Definition Parameters"); + } + Map values = new HashMap<>(); + for (ChartParameterValues parameterValue : dataInsightChartMetric) { + values.put(parameterValue.getName(), parameterValue.getDataType()); + } + for (KpiTarget kpiTarget : kpiTargetDef) { + if (!values.containsKey(kpiTarget.getName())) { + throw new IllegalArgumentException( + "Kpi Target Definition " + + kpiTarget.getName() + + " is not valid, metric not defined in corresponding chart"); + } + } + } + + @Override + public void storeEntity(Kpi kpi, boolean update) throws IOException { + EntityReference owner = kpi.getOwner(); + EntityReference dataInsightChart = kpi.getDataInsightChart(); + + // Don't store owner, database, href and tags as JSON. Build it on the fly based on relationships + kpi.withOwner(null).withHref(null).withDataInsightChart(null); + store(kpi.getId(), kpi, update); + + // Restore the relationships + kpi.withOwner(owner).withDataInsightChart(dataInsightChart); + } + + @Override + public void storeRelationships(Kpi kpi) { + // Add relationship from Kpi to dataInsightChart + addRelationship(kpi.getId(), kpi.getDataInsightChart().getId(), KPI, DATA_INSIGHT_CHART, Relationship.USES); + // Add kpi owner relationship + storeOwner(kpi, kpi.getOwner()); + } + + @Transaction + public RestUtil.PutResponse addKpiResult(UriInfo uriInfo, String fqn, KpiResult kpiResult) throws IOException { + // Validate the request content + Kpi kpi = dao.findEntityByName(fqn); + + KpiResult storedKpiResult = + JsonUtils.readValue( + daoCollection + .entityExtensionTimeSeriesDao() + .getExtensionAtTimestamp(kpi.getFullyQualifiedName(), KPI_RESULT_EXTENSION, kpiResult.getTimestamp()), + KpiResult.class); + if (storedKpiResult != null) { + daoCollection + .entityExtensionTimeSeriesDao() + .update( + kpi.getFullyQualifiedName(), + KPI_RESULT_EXTENSION, + JsonUtils.pojoToJson(kpiResult), + kpiResult.getTimestamp()); + } else { + daoCollection + .entityExtensionTimeSeriesDao() + .insert(kpi.getFullyQualifiedName(), KPI_RESULT_EXTENSION, "kpiResult", JsonUtils.pojoToJson(kpiResult)); + } + ChangeDescription change = addKpiResultChangeDescription(kpi.getVersion(), kpiResult, storedKpiResult); + ChangeEvent changeEvent = getChangeEvent(withHref(uriInfo, kpi), change, entityType, kpi.getVersion()); + + return new RestUtil.PutResponse<>(Response.Status.CREATED, changeEvent, RestUtil.ENTITY_FIELDS_CHANGED); + } + + @Transaction + public RestUtil.PutResponse deleteKpiResult(String fqn, Long timestamp) throws IOException { + // Validate the request content + Kpi kpi = dao.findEntityByName(fqn); + KpiResult storedKpiResult = + JsonUtils.readValue( + daoCollection.entityExtensionTimeSeriesDao().getExtensionAtTimestamp(fqn, KPI_RESULT_EXTENSION, timestamp), + KpiResult.class); + if (storedKpiResult != null) { + daoCollection.entityExtensionTimeSeriesDao().deleteAtTimestamp(fqn, KPI_RESULT_EXTENSION, timestamp); + kpi.setKpiResult(storedKpiResult); + ChangeDescription change = deleteKpiChangeDescription(kpi.getVersion(), storedKpiResult); + ChangeEvent changeEvent = getChangeEvent(kpi, change, entityType, kpi.getVersion()); + return new RestUtil.PutResponse<>(Response.Status.OK, changeEvent, RestUtil.ENTITY_FIELDS_CHANGED); + } + throw new EntityNotFoundException( + String.format("Failed to find kpi result for %s at %s", kpi.getName(), timestamp)); + } + + private ChangeDescription addKpiResultChangeDescription(Double version, Object newValue, Object oldValue) { + FieldChange fieldChange = new FieldChange().withName("kpiResult").withNewValue(newValue).withOldValue(oldValue); + ChangeDescription change = new ChangeDescription().withPreviousVersion(version); + change.getFieldsUpdated().add(fieldChange); + return change; + } + + private ChangeDescription deleteKpiChangeDescription(Double version, Object oldValue) { + FieldChange fieldChange = new FieldChange().withName("kpiResult").withOldValue(oldValue); + ChangeDescription change = new ChangeDescription().withPreviousVersion(version); + change.getFieldsDeleted().add(fieldChange); + return change; + } + + private EntityReference getDataInsightChart(Kpi kpi) throws IOException { + return getToEntityRef(kpi.getId(), Relationship.USES, DATA_INSIGHT_CHART, true); + } + + public KpiResult getKpiResult(String fqn) throws IOException { + return JsonUtils.readValue( + daoCollection.entityExtensionTimeSeriesDao().getLatestExtension(fqn, KPI_RESULT_EXTENSION), KpiResult.class); + } + + public ResultList getKpiResults(String fqn, Long startTs, Long endTs) throws IOException { + List kpiResults; + kpiResults = + JsonUtils.readObjects( + daoCollection + .entityExtensionTimeSeriesDao() + .listBetweenTimestamps(fqn, KPI_RESULT_EXTENSION, startTs, endTs), + KpiResult.class); + return new ResultList<>(kpiResults, String.valueOf(startTs), String.valueOf(endTs), kpiResults.size()); + } + + private ChangeEvent getChangeEvent( + EntityInterface updated, ChangeDescription change, String entityType, Double prevVersion) { + return new ChangeEvent() + .withEntity(updated) + .withChangeDescription(change) + .withEventType(EventType.ENTITY_UPDATED) + .withEntityType(entityType) + .withEntityId(updated.getId()) + .withEntityFullyQualifiedName(updated.getFullyQualifiedName()) + .withUserName(updated.getUpdatedBy()) + .withTimestamp(System.currentTimeMillis()) + .withCurrentVersion(updated.getVersion()) + .withPreviousVersion(prevVersion); + } + + @Override + public EntityUpdater getUpdater(Kpi original, Kpi updated, Operation operation) { + return new KpiUpdater(original, updated, operation); + } + + public class KpiUpdater extends EntityUpdater { + public KpiUpdater(Kpi original, Kpi updated, Operation operation) { + super(original, updated, operation); + } + + @Override + public void entitySpecificUpdate() throws IOException { + updateFromRelationships( + "dataInsightChart", + KPI, + new ArrayList<>(List.of(original.getDataInsightChart())), + new ArrayList<>(List.of(updated.getDataInsightChart())), + Relationship.USES, + DATA_INSIGHT_CHART, + updated.getId()); + recordChange("targetDefinition", original.getTargetDefinition(), updated.getTargetDefinition()); + } + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java new file mode 100644 index 00000000000..4119526b267 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java @@ -0,0 +1,455 @@ +package org.openmetadata.service.resources.kpi; + +import com.google.inject.Inject; +import io.swagger.annotations.Api; +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.io.IOException; +import java.util.UUID; +import javax.json.JsonPatch; +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.Encoded; +import javax.ws.rs.GET; +import javax.ws.rs.PATCH; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.openmetadata.schema.api.dataInsight.kpi.CreateKpiRequest; +import org.openmetadata.schema.dataInsight.kpi.Kpi; +import org.openmetadata.schema.dataInsight.type.KpiResult; +import org.openmetadata.schema.type.EntityHistory; +import org.openmetadata.schema.type.Include; +import org.openmetadata.service.Entity; +import org.openmetadata.service.jdbi3.CollectionDAO; +import org.openmetadata.service.jdbi3.KpiRepository; +import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.resources.Collection; +import org.openmetadata.service.resources.EntityResource; +import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.util.RestUtil; +import org.openmetadata.service.util.ResultList; + +@Slf4j +@Path("/v1/kpi") +@Api(value = "Kpi collection", tags = "Kpi collection") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Collection(name = "kpi") +public class KpiResource extends EntityResource { + public static final String COLLECTION_PATH = "/v1/kpi"; + + static final String FIELDS = "owner,startDate,endDate,targetDefinition,dataInsightChart,metricType"; + + @Override + public Kpi addHref(UriInfo uriInfo, Kpi kpi) { + kpi.withHref(RestUtil.getHref(uriInfo, COLLECTION_PATH, kpi.getId())); + Entity.withHref(uriInfo, kpi.getOwner()); + Entity.withHref(uriInfo, kpi.getDataInsightChart()); + return kpi; + } + + @Inject + public KpiResource(CollectionDAO dao, Authorizer authorizer) { + super(Kpi.class, new KpiRepository(dao), authorizer); + } + + public static class KpiList extends ResultList { + @SuppressWarnings("unused") + public KpiList() { + // Empty constructor needed for deserialization + } + } + + public static class KpiResultList extends ResultList { + @SuppressWarnings("unused") + public KpiResultList() { + /* Required for serde */ + } + } + + @GET + @Operation( + operationId = "listKpis", + summary = "List Kpi", + tags = "kpi", + description = + "Get a list of kpi. Use `fields` " + + "parameter to get only necessary fields. Use cursor-based pagination to limit the number " + + "entries in the list using `limit` and `before` or `after` query params.", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of kpi", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = KpiList.class))) + }) + public ResultList list( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter( + description = "Fields requested in the returned resource", + schema = @Schema(type = "string", example = FIELDS)) + @QueryParam("fields") + String fieldsParam, + @Parameter(description = "Limit the number kpi returned. (1 to 1000000, default = " + "10)") + @DefaultValue("10") + @QueryParam("limit") + @Min(0) + @Max(1000000) + int limitParam, + @Parameter(description = "Returns list of tests before this cursor", schema = @Schema(type = "string")) + @QueryParam("before") + String before, + @Parameter(description = "Returns list of tests after this cursor", schema = @Schema(type = "string")) + @QueryParam("after") + String after, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) + throws IOException { + ListFilter filter = new ListFilter(include); + return super.listInternal(uriInfo, securityContext, fieldsParam, filter, limitParam, before, after); + } + + @GET + @Path("/{id}/versions") + @Operation( + operationId = "listAllKpiVersion", + summary = "List kpi versions", + tags = "kpi", + description = "Get a list of all the versions of a Kpi identified by `id`", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of Kpi versions", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = EntityHistory.class))) + }) + public EntityHistory listVersions( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Kpi Id", schema = @Schema(type = "string")) @PathParam("id") UUID id) + throws IOException { + return super.listVersionsInternal(securityContext, id); + } + + @GET + @Path("/{id}") + @Operation( + summary = "Get a Kpi", + tags = "kpi", + description = "Get a Kpi by `id`.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The Kpi", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Kpi.class))), + @ApiResponse(responseCode = "404", description = "Kpi for instance {id} is not found") + }) + public Kpi get( + @Context UriInfo uriInfo, + @PathParam("id") UUID id, + @Context SecurityContext securityContext, + @Parameter( + description = "Fields requested in the returned resource", + schema = @Schema(type = "string", example = FIELDS)) + @QueryParam("fields") + String fieldsParam, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) + throws IOException { + return getInternal(uriInfo, securityContext, id, fieldsParam, include); + } + + @GET + @Path("/name/{name}") + @Operation( + operationId = "getKpiByName", + summary = "Get a Kpi by name", + tags = "kpi", + description = "Get a Kpi by name.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The Kpi", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Kpi.class))), + @ApiResponse(responseCode = "404", description = "Kpi for instance {id} is not found") + }) + public Kpi getByName( + @Context UriInfo uriInfo, + @PathParam("name") String name, + @Context SecurityContext securityContext, + @Parameter( + description = "Fields requested in the returned resource", + schema = @Schema(type = "string", example = FIELDS)) + @QueryParam("fields") + String fieldsParam, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) + throws IOException { + return getByNameInternal(uriInfo, securityContext, name, fieldsParam, include); + } + + @GET + @Path("/{id}/versions/{version}") + @Operation( + operationId = "getSpecificKpiVersion", + summary = "Get a version of the Kpi", + tags = "kpi", + description = "Get a version of the Kpi by given `id`", + responses = { + @ApiResponse( + responseCode = "200", + description = "Kpi", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Kpi.class))), + @ApiResponse( + responseCode = "404", + description = "Kpi for instance {id} and version {version} is " + "not found") + }) + public Kpi getVersion( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Kpi Id", schema = @Schema(type = "string")) @PathParam("id") UUID id, + @Parameter( + description = "Kpi version number in the form `major`.`minor`", + schema = @Schema(type = "string", example = "0.1 or 1.1")) + @PathParam("version") + String version) + throws IOException { + return super.getVersionInternal(securityContext, id, version); + } + + @POST + @Operation( + operationId = "createKpi", + summary = "Create a Kpi", + tags = "kpi", + description = "Create a Kpi.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The Kpi", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Kpi.class))), + @ApiResponse(responseCode = "400", description = "Bad request") + }) + public Response create( + @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateKpiRequest create) + throws IOException { + Kpi Kpi = getKpi(create, securityContext.getUserPrincipal().getName()); + return create(uriInfo, securityContext, Kpi); + } + + @PATCH + @Path("/{id}") + @Operation( + operationId = "patchKpi", + summary = "Update a Kpi", + tags = "kpi", + description = "Update an existing Kpi using JsonPatch.", + externalDocs = @ExternalDocumentation(description = "JsonPatch RFC", url = "https://tools.ietf.org/html/rfc6902")) + @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON) + public Response patchKpi( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @PathParam("id") UUID id, + @RequestBody( + description = "JsonPatch with array of operations", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON_PATCH_JSON, + examples = { + @ExampleObject("[" + "{op:remove, path:/a}," + "{op:add, path: /b, value: val}" + "]") + })) + JsonPatch patch) + throws IOException { + return patchInternal(uriInfo, securityContext, id, patch); + } + + @PUT + @Operation( + operationId = "createOrUpdateKpi", + summary = "Update Kpi", + tags = "kpi", + description = "Create a Kpi, it it does not exist or update an existing Kpi.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The updated kpi Objective ", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Kpi.class))) + }) + public Response createOrUpdate( + @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateKpiRequest create) + throws IOException { + Kpi Kpi = getKpi(create, securityContext.getUserPrincipal().getName()); + return createOrUpdate(uriInfo, securityContext, Kpi); + } + + @DELETE + @Path("/{id}") + @Operation( + operationId = "deleteKpi", + summary = "Delete a Kpi", + tags = "kpi", + description = "Delete a Kpi by `id`.", + responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "Kpi for instance {id} is not found") + }) + public Response delete( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Recursively delete this entity and it's children. (Default `false`)") + @DefaultValue("false") + @QueryParam("recursive") + boolean recursive, + @Parameter(description = "Hard delete the entity. (Default = `false`)") + @QueryParam("hardDelete") + @DefaultValue("false") + boolean hardDelete, + @Parameter(description = "Kpi Id", schema = @Schema(type = "UUID")) @PathParam("id") UUID id) + throws IOException { + return delete(uriInfo, securityContext, id, recursive, hardDelete); + } + + @PUT + @Path("/{fqn}/kpiResult") + @Operation( + operationId = "addKpiResult", + summary = "Add kpi result data", + tags = "kpi", + description = "Add Kpi Result data to the kpi.", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully updated the Kpi. ", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Kpi.class))) + }) + public Response addKpiResult( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Encoded @Parameter(description = "fqn of the kpi", schema = @Schema(type = "string")) @PathParam("fqn") + String fqn, + @Valid KpiResult kpiResult) + throws IOException { + return dao.addKpiResult(uriInfo, fqn, kpiResult).toResponse(); + } + + @GET + @Path("/{fqn}/kpiResult") + @Operation( + operationId = "listKpiResults", + summary = "List of kpi results", + tags = "kpi", + description = + "Get a list of all the kpi results for the given kpi id, optionally filtered by `startTs` and `endTs` of the profile. " + + "Use cursor-based pagination to limit the number of " + + "entries in the list using `limit` and `before` or `after` query params.", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of kpi results", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = KpiResource.KpiResultList.class))) + }) + public ResultList listKpiResults( + @Context SecurityContext securityContext, + @Parameter(description = "fqn of the kpi", schema = @Schema(type = "string")) @PathParam("fqn") String fqn, + @Parameter(description = "Filter kpi results after the given start timestamp", schema = @Schema(type = "number")) + @NonNull + @QueryParam("startTs") + Long startTs, + @Parameter(description = "Filter kpi results before the given end timestamp", schema = @Schema(type = "number")) + @NonNull + @QueryParam("endTs") + Long endTs) + throws IOException { + return dao.getKpiResults(fqn, startTs, endTs); + } + + @GET + @Path("/{fqn}/latestKpiResult") + @Operation( + operationId = "getLatestKpiResults", + summary = "Get a latest Kpi Result", + tags = "kpi", + description = "Get Latest Kpi Result for the given kpi", + responses = { + @ApiResponse( + responseCode = "200", + description = "Latest Kpi Result", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = KpiResource.KpiResultList.class))) + }) + public KpiResult listKpiResults( + @Context SecurityContext securityContext, + @Parameter(description = "fqn of the kpi", schema = @Schema(type = "string")) @PathParam("fqn") String fqn) + throws IOException { + return dao.getKpiResult(fqn); + } + + @DELETE + @Path("/{fqn}/kpiResult/{timestamp}") + @Operation( + operationId = "deleteKpiResult", + summary = "Delete kpi result.", + tags = "kpi", + description = "Delete kpi result for a kpi.", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully deleted the KpiResult", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Kpi.class))) + }) + public Response deleteKpiResult( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "fqn of the kpi", schema = @Schema(type = "string")) @PathParam("fqn") String fqn, + @Parameter(description = "Timestamp of the kpi result", schema = @Schema(type = "long")) @PathParam("timestamp") + Long timestamp) + throws IOException { + return dao.deleteKpiResult(fqn, timestamp).toResponse(); + } + + private Kpi getKpi(CreateKpiRequest create, String user) throws IOException { + return copy(new Kpi(), create, user) + .withStartDate(create.getStartDate()) + .withEndDate(create.getEndDate()) + .withTargetDefinition(create.getTargetDefinition()) + .withDataInsightChart(create.getDataInsightChart()) + .withMetricType(create.getMetricType()); + } +} diff --git a/openmetadata-service/src/main/resources/json/data/ResourceDescriptors.json b/openmetadata-service/src/main/resources/json/data/ResourceDescriptors.json index 80abe4a233a..c8e7bbfcf6c 100644 --- a/openmetadata-service/src/main/resources/json/data/ResourceDescriptors.json +++ b/openmetadata-service/src/main/resources/json/data/ResourceDescriptors.json @@ -459,5 +459,14 @@ "ViewAll", "EditDescription" ] + }, + { + "name" : "kpi", + "operations" : [ + "Create", + "Delete", + "ViewAll", + "EditDescription" + ] } ] \ No newline at end of file diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index 2a13f36252d..8edd7920eb7 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -101,6 +101,8 @@ import org.openmetadata.schema.EntityInterface; import org.openmetadata.schema.api.data.TermReference; import org.openmetadata.schema.api.teams.CreateTeam; import org.openmetadata.schema.api.teams.CreateTeam.TeamType; +import org.openmetadata.schema.dataInsight.DataInsightChart; +import org.openmetadata.schema.dataInsight.type.KpiTarget; import org.openmetadata.schema.entity.Type; import org.openmetadata.schema.entity.data.Database; import org.openmetadata.schema.entity.data.DatabaseSchema; @@ -136,6 +138,7 @@ import org.openmetadata.service.resources.dqtests.TestSuiteResourceTest; import org.openmetadata.service.resources.events.EventResource.ChangeEventList; import org.openmetadata.service.resources.events.WebhookResourceTest; import org.openmetadata.service.resources.glossary.GlossaryResourceTest; +import org.openmetadata.service.resources.kpi.KpiResourceTest; import org.openmetadata.service.resources.metadata.TypeResourceTest; import org.openmetadata.service.resources.policies.PolicyResourceTest; import org.openmetadata.service.resources.services.DashboardServiceResourceTest; @@ -269,7 +272,10 @@ public abstract class EntityResourceTest COLUMNS; public static Type INT_TYPE; @@ -332,6 +338,7 @@ public abstract class EntityResourceTest { + public KpiResourceTest() { + super(Entity.KPI, Kpi.class, KpiResource.KpiList.class, "kpi", KpiResource.FIELDS); + supportsEmptyDescription = false; + supportsFollowers = false; + supportsAuthorizedMetadataOperations = false; + supportsOwner = false; + supportsPatch = false; + } + + public void setupKpi(TestInfo test) throws IOException { + DataInsightResourceTest dataInsightResourceTest = new DataInsightResourceTest(); + CreateDataInsightChart chartRequest = + dataInsightResourceTest + .createRequest(test) + .withName("TestChart") + .withOwner(USER1_REF) + .withDataIndexType(DataReportIndex.ENTITY_REPORT_DATA_INDEX) + .withMetrics( + List.of( + new ChartParameterValues() + .withName("Percentage") + .withDataType(DataInsightChartDataType.PERCENTAGE))); + DI_CHART1 = dataInsightResourceTest.createAndCheckEntity(chartRequest, ADMIN_AUTH_HEADERS); + DI_CHART1_REFERENCE = DI_CHART1.getEntityReference(); + KPI_TARGET = new KpiTarget().withName("Percentage").withValue("80"); + } + + @Test + void post_testWithoutRequiredFields_4xx(TestInfo test) { + // name is required field + assertResponse( + () -> createEntity(createRequest(test).withName(null), ADMIN_AUTH_HEADERS), + BAD_REQUEST, + "[name must not be null]"); + } + + @Test + void post_testWithInvalidValues_4xx(TestInfo test) { + CreateKpiRequest create1 = createRequest("Test2" + UUID.randomUUID()); + create1.withDataInsightChart(USER1_REF); + + assertResponseContains( + () -> createAndCheckEntity(create1, ADMIN_AUTH_HEADERS), + NOT_FOUND, + "dataInsightChart instance for " + USER1_REF.getId() + " not found"); + + CreateKpiRequest create2 = createRequest(String.format("Test%s", UUID.randomUUID())); + KpiTarget target = new KpiTarget().withName("Test").withValue("Test"); + create2.withTargetDefinition(List.of(target)); + + assertResponseContains( + () -> createAndCheckEntity(create2, ADMIN_AUTH_HEADERS), + BAD_REQUEST, + "Kpi Target Definition " + target.getName() + " is not valid, metric not defined in corresponding chart"); + } + + @Test + void createUpdate_tests_200(TestInfo test) throws IOException { + CreateKpiRequest create = createRequest("Test" + UUID.randomUUID()); + Kpi createdKpi = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); + createdKpi = getEntity(createdKpi.getId(), KpiResource.FIELDS, ADMIN_AUTH_HEADERS); + validateCreatedEntity(createdKpi, create, ADMIN_AUTH_HEADERS); + + KpiTarget newTarget = new KpiTarget().withName(KPI_TARGET.getName()).withValue("newValue"); + create.withTargetDefinition(List.of(newTarget)); + ChangeDescription change = getChangeDescription(createdKpi.getVersion()); + fieldUpdated(change, "targetDefinition", KPI_TARGET, newTarget); + + createdKpi = updateAndCheckEntity(create, OK, ADMIN_AUTH_HEADERS, TestUtils.UpdateType.MINOR_UPDATE, change); + createdKpi = getEntity(createdKpi.getId(), KpiResource.FIELDS, ADMIN_AUTH_HEADERS); + validateCreatedEntity(createdKpi, create, ADMIN_AUTH_HEADERS); + } + + @Test + void put_kpiResults_200(TestInfo test) throws IOException, ParseException { + CreateKpiRequest create = createRequest(test); + Kpi createdKpi = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); + + KpiResult kpiResult = + new KpiResult() + .withTimestamp(TestUtils.dateToTimestamp("2021-09-09")) + .withTargetResult(List.of(new KpiTarget().withName(KPI_TARGET.getName()).withValue("10"))); + putKpiResult(createdKpi.getFullyQualifiedName(), kpiResult, ADMIN_AUTH_HEADERS); + + ResultList kpiResults = + getKpiResults( + createdKpi.getFullyQualifiedName(), + TestUtils.dateToTimestamp("2021-09-09"), + TestUtils.dateToTimestamp("2021-09-10"), + ADMIN_AUTH_HEADERS); + verifyKpiResults(kpiResults, List.of(kpiResult), 1); + + // Add new date for KpiResult + KpiResult newKpiResult = + new KpiResult() + .withTimestamp(TestUtils.dateToTimestamp("2021-09-10")) + .withTargetResult(List.of(new KpiTarget().withName(KPI_TARGET.getName()).withValue("20"))); + putKpiResult(createdKpi.getFullyQualifiedName(), newKpiResult, ADMIN_AUTH_HEADERS); + + kpiResults = + getKpiResults( + createdKpi.getFullyQualifiedName(), + TestUtils.dateToTimestamp("2021-09-09"), + TestUtils.dateToTimestamp("2021-09-10"), + ADMIN_AUTH_HEADERS); + verifyKpiResults(kpiResults, List.of(kpiResult, newKpiResult), 2); + + // Replace kpi result for a date + KpiResult newKpiResult1 = + new KpiResult() + .withTimestamp(TestUtils.dateToTimestamp("2021-09-10")) + .withTargetResult(List.of(new KpiTarget().withName(KPI_TARGET.getName()).withValue("25"))); + putKpiResult(createdKpi.getFullyQualifiedName(), newKpiResult1, ADMIN_AUTH_HEADERS); + + createdKpi = getEntity(createdKpi.getId(), "targetDefinition", ADMIN_AUTH_HEADERS); + // first result should be the latest date + kpiResults = + getKpiResults( + createdKpi.getFullyQualifiedName(), + TestUtils.dateToTimestamp("2021-09-09"), + TestUtils.dateToTimestamp("2021-09-10"), + ADMIN_AUTH_HEADERS); + verifyKpiResults(kpiResults, List.of(newKpiResult1, kpiResult), 2); + + String dateStr = "2021-09-"; + List kpiResultList = new ArrayList<>(); + kpiResultList.add(kpiResult); + kpiResultList.add(newKpiResult1); + for (int i = 11; i <= 20; i++) { + kpiResult = + new KpiResult() + .withTimestamp(TestUtils.dateToTimestamp(dateStr + i)) + .withTargetResult( + List.of(new KpiTarget().withName(KPI_TARGET.getName()).withValue(String.valueOf(50 + i)))); + putKpiResult(createdKpi.getFullyQualifiedName(), kpiResult, ADMIN_AUTH_HEADERS); + kpiResultList.add(kpiResult); + } + kpiResults = + getKpiResults( + createdKpi.getFullyQualifiedName(), + TestUtils.dateToTimestamp("2021-09-09"), + TestUtils.dateToTimestamp("2021-09-20"), + ADMIN_AUTH_HEADERS); + verifyKpiResults(kpiResults, kpiResultList, 12); + } + + public static void putKpiResult(String fqn, KpiResult data, Map authHeaders) + throws HttpResponseException { + WebTarget target = OpenMetadataApplicationTest.getResource("kpi/" + fqn + "/kpiResult"); + TestUtils.put(target, data, CREATED, authHeaders); + } + + public static ResultList getKpiResults(String fqn, Long start, Long end, Map authHeaders) + throws HttpResponseException { + WebTarget target = OpenMetadataApplicationTest.getResource("kpi/" + fqn + "/kpiResult"); + target = target.queryParam("startTs", start); + target = target.queryParam("endTs", end); + return TestUtils.get(target, KpiResource.KpiResultList.class, authHeaders); + } + + private void verifyKpiResults( + ResultList actualKpiResults, List expectedKpiResults, int expectedCount) { + assertEquals(expectedCount, actualKpiResults.getPaging().getTotal()); + assertEquals(expectedKpiResults.size(), actualKpiResults.getData().size()); + Map kpiResultMap = new HashMap<>(); + for (KpiResult result : actualKpiResults.getData()) { + kpiResultMap.put(result.getTimestamp(), result); + } + for (KpiResult result : expectedKpiResults) { + KpiResult storedKpiResult = kpiResultMap.get(result.getTimestamp()); + verifyKpiResult(storedKpiResult, result); + } + } + + private void verifyKpiResult(KpiResult expected, KpiResult actual) { + assertEquals(expected, actual); + } + + @Override + public CreateKpiRequest createRequest(String name) { + return new CreateKpiRequest() + .withName(name) + .withDescription(name) + .withDisplayName(name) + .withStartDate(0L) + .withEndDate(30L) + .withDataInsightChart(DI_CHART1_REFERENCE) + .withOwner(USER1_REF) + .withMetricType(KpiTargetType.PERCENTAGE) + .withTargetDefinition(List.of(KPI_TARGET)); + } + + @Override + public void validateCreatedEntity(Kpi createdEntity, CreateKpiRequest request, Map authHeaders) + throws HttpResponseException { + validateCommonEntityFields(createdEntity, request, getPrincipalName(authHeaders)); + assertEquals(request.getStartDate(), createdEntity.getStartDate()); + assertEquals(request.getEndDate(), createdEntity.getEndDate()); + assertEquals(request.getDataInsightChart(), createdEntity.getDataInsightChart()); + assertEquals(request.getMetricType(), createdEntity.getMetricType()); + assertEquals(request.getTargetDefinition(), createdEntity.getTargetDefinition()); + } + + @Override + public void compareEntities(Kpi expected, Kpi updated, Map authHeaders) throws HttpResponseException { + validateCommonEntityFields(expected, updated, getPrincipalName(authHeaders)); + assertEquals(expected.getStartDate(), updated.getStartDate()); + assertEquals(expected.getEndDate(), updated.getEndDate()); + assertEquals(expected.getDataInsightChart(), updated.getDataInsightChart()); + assertEquals(expected.getMetricType(), updated.getMetricType()); + assertEquals(expected.getTargetDefinition(), updated.getTargetDefinition()); + } + + @Override + public Kpi validateGetWithDifferentFields(Kpi entity, boolean byName) throws HttpResponseException { + // TODO: + return null; + } + + @Override + public void assertFieldChange(String fieldName, Object expected, Object actual) { + if (expected == actual) { + return; + } + // TODO fix this + } +} diff --git a/openmetadata-spec/src/main/resources/json/schema/api/dataInsight/kpi/createKpiRequest.json b/openmetadata-spec/src/main/resources/json/schema/api/dataInsight/kpi/createKpiRequest.json new file mode 100644 index 00000000000..a53bfd2416c --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/api/dataInsight/kpi/createKpiRequest.json @@ -0,0 +1,51 @@ +{ + "$id": "https://open-metadata.org/schema/api/dataInsight/kpi/createKpiRequest.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CreateKpiRequest", + "description": "Schema corresponding to a Kpi.", + "type": "object", + "javaType": "org.openmetadata.schema.api.dataInsight.kpi.CreateKpiRequest", + "javaInterfaces": ["org.openmetadata.schema.CreateEntity"], + "properties": { + "name": { + "description": "Name that identifies this Kpi.", + "$ref": "../../../type/basic.json#/definitions/entityName" + }, + "displayName": { + "description": "Display Name that identifies this Kpi.", + "type": "string" + }, + "description": { + "description": "Description of the Kpi.", + "$ref": "../../../type/basic.json#/definitions/markdown" + }, + "owner": { + "description": "Owner of this Kpi", + "$ref": "../../../type/entityReference.json" + }, + "dataInsightChart": { + "description": "Chart this kpi refers to", + "$ref": "../../../type/entityReference.json" + }, + "startDate": { + "description": "Start Date for the KPIs", + "$ref": "../../../type/basic.json#/definitions/timestamp" + }, + "endDate": { + "description": "End Date for the KPIs", + "$ref": "../../../type/basic.json#/definitions/timestamp" + }, + "targetDefinition": { + "description": "Metrics from the chart and the target to achieve the result.", + "type": "array", + "items": { + "$ref": "../../../dataInsight/kpi/basic.json#/definitions/kpiTarget" + } + }, + "metricType": { + "$ref": "../../../dataInsight/kpi/basic.json#/definitions/kpiTargetType" + } + }, + "required": ["name", "dataInsightChart", "startDate", "endDate", "targetDefinition", "metricType"], + "additionalProperties": false +} diff --git a/openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/basic.json b/openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/basic.json new file mode 100644 index 00000000000..f3a0d6bd438 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/basic.json @@ -0,0 +1,54 @@ +{ + "$id": "https://open-metadata.org/schema/dataInsight/kpi/basic.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Basic", + "description": "This schema defines basic types that are used by other Kpi Definitions", + "definitions": { + "kpiTargetType": { + "javaType": "org.openmetadata.schema.dataInsight.type.KpiTargetType", + "description": "This enum defines the type of key Result", + "type": "string", + "enum": [ + "NUMBER", + "PERCENTAGE" + ] + }, + "kpiTarget": { + "type": "object", + "javaType": "org.openmetadata.schema.dataInsight.type.KpiTarget", + "description": "This schema defines the parameter values that can be passed for a Kpi Parameter.", + "properties": { + "name": { + "description": "name of the parameter. Must match the parameter names in metrics of the chart this objective refers", + "type": "string" + }, + "value": { + "description": "value to be passed for the Parameters. These are input from Users. We capture this in in string and convert during the runtime.", + "type": "string" + } + }, + "required": ["name", "value"], + "additionalProperties": false + }, + "kpiResult": { + "description": "Schema to capture kpi result.", + "javaType": "org.openmetadata.schema.dataInsight.type.KpiResult", + "type": "object", + "properties": { + "timestamp": { + "description": "Data one which result is updated", + "$ref": "../../type/basic.json#/definitions/timestamp" + }, + "targetResult": { + "description": "Metric and their corresponding current results", + "type": "array", + "items": { + "$ref": "#/definitions/kpiTarget" + } + } + }, + "required": ["timestamp", "targetResult"], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/kpi.json b/openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/kpi.json new file mode 100644 index 00000000000..544a0ee6685 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/dataInsight/kpi/kpi.json @@ -0,0 +1,89 @@ +{ + "$id": "https://open-metadata.org/schema/dataInsight/kpi/kpi.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Kpi", + "description": "Defines a kpi with a metric and target.", + "type": "object", + "javaType": "org.openmetadata.schema.dataInsight.kpi.Kpi", + "javaInterfaces": ["org.openmetadata.schema.EntityInterface"], + "properties": { + "id": { + "description": "Unique identifier of this KPI Definition instance.", + "$ref": "../../type/basic.json#/definitions/uuid" + }, + "name": { + "description": "Name that identifies this KPI Definition.", + "$ref": "../../type/basic.json#/definitions/entityName" + }, + "displayName": { + "description": "Display Name that identifies this KPI Definition.", + "type": "string" + }, + "fullyQualifiedName": { + "description": "FullyQualifiedName same as `name`.", + "$ref": "../../type/basic.json#/definitions/fullyQualifiedEntityName" + }, + "description": { + "description": "Description of the KpiObjective.", + "$ref": "../../type/basic.json#/definitions/markdown" + }, + "metricType": { + "$ref": "./basic.json#/definitions/kpiTargetType" + }, + "dataInsightChart": { + "description": "Data Insight Chart Referred by this Kpi Objective.", + "$ref": "../../type/entityReference.json" + }, + "targetDefinition": { + "description": "Metrics from the chart and the target to achieve the result.", + "type": "array", + "items": { + "$ref": "./basic.json#/definitions/kpiTarget" + } + }, + "kpiResult": { + "description": "Result of the Kpi", + "$ref": "./basic.json#/definitions/kpiResult" + }, + "startDate": { + "description": "Start Date for the KPIs", + "$ref": "../../type/basic.json#/definitions/timestamp" + }, + "endDate": { + "description": "End Date for the KPIs", + "$ref": "../../type/basic.json#/definitions/timestamp" + }, + "owner": { + "description": "Owner of this KPI definition.", + "$ref": "../../type/entityReference.json", + "default": null + }, + "version": { + "description": "Metadata version of the entity.", + "$ref": "../../type/entityHistory.json#/definitions/entityVersion" + }, + "updatedAt": { + "description": "Last update time corresponding to the new version of the entity in Unix epoch time milliseconds.", + "$ref": "../../type/basic.json#/definitions/timestamp" + }, + "updatedBy": { + "description": "User who made the update.", + "type": "string" + }, + "href": { + "description": "Link to the resource corresponding to this entity.", + "$ref": "../../type/basic.json#/definitions/href" + }, + "changeDescription": { + "description": "Change that lead to this version of the entity.", + "$ref": "../../type/entityHistory.json#/definitions/changeDescription" + }, + "deleted": { + "description": "When `true` indicates the entity has been soft deleted.", + "type": "boolean", + "default": false + } + }, + "required": ["name", "description", "dateInsightChart", "startDate", "endDate", "targetDefinition", "metricType"], + "additionalProperties": false +}