mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-25 06:28:22 +00:00
Feat kpis (#8584)
* Added Kpi Resource * Fix typo * Fixed failing test * Review Suggestions
This commit is contained in:
parent
1d93d0e650
commit
5bd4e56b3f
@ -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)
|
||||
);
|
||||
|
||||
@ -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)
|
||||
);
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -459,5 +459,14 @@
|
||||
"ViewAll",
|
||||
"EditDescription"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "kpi",
|
||||
"operations" : [
|
||||
"Create",
|
||||
"Delete",
|
||||
"ViewAll",
|
||||
"EditDescription"
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user