* Added Kpi Resource

* Fix typo

* Fixed failing test

* Review Suggestions
This commit is contained in:
Mohit Yadav 2022-11-09 13:06:13 +05:30 committed by GitHub
parent 1d93d0e650
commit 5bd4e56b3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1210 additions and 0 deletions

View File

@ -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)
);

View File

@ -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)
);

View File

@ -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";

View File

@ -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<Dashboard> {
@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<Kpi> {
@Override
default String getTableName() {
return "kpi_entity";
}
@Override
default Class<Kpi> getEntityClass() {
return Kpi.class;
}
@Override
default String getNameColumn() {
return "name";
}
}
}

View File

@ -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<Kpi> {
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<DataInsightChart> 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<KpiTarget> kpiTargetDef, List<ChartParameterValues> dataInsightChartMetric) {
if (kpiTargetDef.isEmpty() && !dataInsightChartMetric.isEmpty()) {
throw new IllegalArgumentException("Parameter Values doesn't match Kpi Definition Parameters");
}
Map<String, Object> 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<KpiResult> getKpiResults(String fqn, Long startTs, Long endTs) throws IOException {
List<KpiResult> 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());
}
}
}

View File

@ -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<Kpi, KpiRepository> {
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<Kpi> {
@SuppressWarnings("unused")
public KpiList() {
// Empty constructor needed for deserialization
}
}
public static class KpiResultList extends ResultList<KpiResult> {
@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<Kpi> 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<KpiResult> 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());
}
}

View File

@ -459,5 +459,14 @@
"ViewAll",
"EditDescription"
]
},
{
"name" : "kpi",
"operations" : [
"Create",
"Delete",
"ViewAll",
"EditDescription"
]
}
]

View File

@ -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<T extends EntityInterface, K extends Cr
public static TestDefinition TEST_DEFINITION3;
public static EntityReference TEST_DEFINITION3_REFERENCE;
public static DataInsightChart DI_CHART1;
public static EntityReference DI_CHART1_REFERENCE;
public static KpiTarget KPI_TARGET;
public static List<Column> COLUMNS;
public static Type INT_TYPE;
@ -332,6 +338,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
new TestDefinitionResourceTest().setupTestDefinitions(test);
new TestCaseResourceTest().setupTestCase(test);
new TypeResourceTest().setupTypes();
new KpiResourceTest().setupKpi(test);
runWebhookTests = new Random().nextBoolean();
if (runWebhookTests) {

View File

@ -0,0 +1,274 @@
package org.openmetadata.service.resources.kpi;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.CREATED;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static javax.ws.rs.core.Response.Status.OK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.openmetadata.service.security.SecurityUtil.getPrincipalName;
import static org.openmetadata.service.util.EntityUtil.fieldUpdated;
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
import static org.openmetadata.service.util.TestUtils.assertResponse;
import static org.openmetadata.service.util.TestUtils.assertResponseContains;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.client.WebTarget;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpResponseException;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import org.openmetadata.schema.api.dataInsight.CreateDataInsightChart;
import org.openmetadata.schema.api.dataInsight.kpi.CreateKpiRequest;
import org.openmetadata.schema.dataInsight.ChartParameterValues;
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.dataInsight.type.KpiTargetType;
import org.openmetadata.schema.type.ChangeDescription;
import org.openmetadata.schema.type.DataInsightChartDataType;
import org.openmetadata.schema.type.DataReportIndex;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationTest;
import org.openmetadata.service.resources.EntityResourceTest;
import org.openmetadata.service.resources.dataInsight.DataInsightResourceTest;
import org.openmetadata.service.util.ResultList;
import org.openmetadata.service.util.TestUtils;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j
public class KpiResourceTest extends EntityResourceTest<Kpi, CreateKpiRequest> {
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<KpiResult> 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<KpiResult> 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<String, String> authHeaders)
throws HttpResponseException {
WebTarget target = OpenMetadataApplicationTest.getResource("kpi/" + fqn + "/kpiResult");
TestUtils.put(target, data, CREATED, authHeaders);
}
public static ResultList<KpiResult> getKpiResults(String fqn, Long start, Long end, Map<String, String> 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<KpiResult> actualKpiResults, List<KpiResult> expectedKpiResults, int expectedCount) {
assertEquals(expectedCount, actualKpiResults.getPaging().getTotal());
assertEquals(expectedKpiResults.size(), actualKpiResults.getData().size());
Map<Long, KpiResult> 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<String, String> 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<String, String> 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
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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
}