diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolver.java index 9a267feb270fd..729446eab1fa9 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolver.java @@ -10,6 +10,7 @@ import com.linkedin.datahub.graphql.generated.Dataset; import com.linkedin.datahub.graphql.generated.Health; import com.linkedin.datahub.graphql.generated.HealthStatus; +import com.linkedin.datahub.graphql.generated.HealthStatusType; import com.linkedin.metadata.Constants; import com.linkedin.metadata.graph.GraphClient; import com.linkedin.metadata.query.filter.Condition; @@ -35,6 +36,8 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; /** @@ -44,34 +47,45 @@ * health status will be undefined for the Dataset. * */ -public class DatasetHealthResolver implements DataFetcher> { +@Slf4j +public class DatasetHealthResolver implements DataFetcher>> { private static final String ASSERTS_RELATIONSHIP_NAME = "Asserts"; private static final String ASSERTION_RUN_EVENT_SUCCESS_TYPE = "SUCCESS"; - private static final CachedHealth NO_HEALTH = new CachedHealth(false, null); private final GraphClient _graphClient; private final TimeseriesAspectService _timeseriesAspectService; + private final Config _config; private final Cache _statusCache; - public DatasetHealthResolver(final GraphClient graphClient, final TimeseriesAspectService timeseriesAspectService) { + public DatasetHealthResolver( + final GraphClient graphClient, + final TimeseriesAspectService timeseriesAspectService) { + this(graphClient, timeseriesAspectService, new Config(true)); + + } + public DatasetHealthResolver( + final GraphClient graphClient, + final TimeseriesAspectService timeseriesAspectService, + final Config config) { _graphClient = graphClient; _timeseriesAspectService = timeseriesAspectService; _statusCache = CacheBuilder.newBuilder() .maximumSize(10000) .expireAfterWrite(1, TimeUnit.MINUTES) .build(); + _config = config; } @Override - public CompletableFuture get(final DataFetchingEnvironment environment) throws Exception { + public CompletableFuture> get(final DataFetchingEnvironment environment) throws Exception { final Dataset parent = environment.getSource(); return CompletableFuture.supplyAsync(() -> { try { final CachedHealth cachedStatus = _statusCache.get(parent.getUrn(), () -> ( computeHealthStatusForDataset(parent.getUrn(), environment.getContext()))); - return cachedStatus.hasStatus ? cachedStatus.health : null; + return cachedStatus.healths; } catch (Exception e) { throw new RuntimeException("Failed to resolve dataset's health status.", e); } @@ -87,11 +101,15 @@ public CompletableFuture get(final DataFetchingEnvironment environment) * */ private CachedHealth computeHealthStatusForDataset(final String datasetUrn, final QueryContext context) { - final Health result = computeAssertionHealthForDataset(datasetUrn, context); - if (result == null) { - return NO_HEALTH; + final List healthStatuses = new ArrayList<>(); + + if (_config.getAssertionsEnabled()) { + final Health assertionsHealth = computeAssertionHealthForDataset(datasetUrn, context); + if (assertionsHealth != null) { + healthStatuses.add(assertionsHealth); + } } - return new CachedHealth(true, result); + return new CachedHealth(healthStatuses); } /** @@ -132,6 +150,7 @@ private Health computeAssertionHealthForDataset(final String datasetUrn, final Q // Finally compute & return the health. final Health health = new Health(); + health.setType(HealthStatusType.ASSERTIONS); if (failingAssertionUrns.size() > 0) { health.setStatus(HealthStatus.FAIL); health.setMessage(String.format("Dataset is failing %s/%s assertions.", failingAssertionUrns.size(), @@ -217,9 +236,14 @@ private List resultToFailedAssertionUrns(final StringArrayArray rows, fi return failedAssertionUrns; } - @AllArgsConstructor + @Data + @AllArgsConstructor + public static class Config { + private Boolean assertionsEnabled; + } + + @AllArgsConstructor private static class CachedHealth { - private final boolean hasStatus; - private final Health health; + private final List healths; } } \ No newline at end of file diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 47311339ccee4..ddfdc8e17222d 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -928,9 +928,9 @@ type Dataset implements EntityWithRelationships & Entity { lineage(input: LineageInput!): EntityLineageResult """ - Experimental! The resolved health status of the Dataset + Experimental! The resolved health statuses of the Dataset """ - health: Health + health: [Health!] """ Schema metadata of the dataset @@ -1119,7 +1119,7 @@ type VersionedDataset implements Entity { """ Experimental! The resolved health status of the Dataset """ - health: Health + health: [Health!] """ Schema metadata of the dataset @@ -8202,10 +8202,25 @@ enum HealthStatus { FAIL } +""" +The type of the health status +""" +enum HealthStatusType { + """ + Assertions status + """ + ASSERTIONS +} + """ The resolved Health of an Asset """ type Health { + """ + An enum representing the type of health indicator + """ + type: HealthStatusType! + """ An enum representing the resolved Health status of an Asset """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolverTest.java index 6dbe4e12bb6a4..ea9ab2a1b768b 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/dataset/DatasetHealthResolverTest.java @@ -19,6 +19,7 @@ import com.linkedin.timeseries.GenericTable; import graphql.schema.DataFetchingEnvironment; import java.util.Collections; +import java.util.List; import org.mockito.Mockito; import org.testng.annotations.Test; @@ -90,9 +91,10 @@ public void testGetSuccessHealthy() throws Exception { parentDataset.setUrn(TEST_DATASET_URN); Mockito.when(mockEnv.getSource()).thenReturn(parentDataset); - Health result = resolver.get(mockEnv).get(); + List result = resolver.get(mockEnv).get(); assertNotNull(result); - assertEquals(result.getStatus(), HealthStatus.PASS); + assertEquals(result.size(), 1); + assertEquals(result.get(0).getStatus(), HealthStatus.PASS); } @Test @@ -129,8 +131,8 @@ public void testGetSuccessNullHealth() throws Exception { parentDataset.setUrn(TEST_DATASET_URN); Mockito.when(mockEnv.getSource()).thenReturn(parentDataset); - Health result = resolver.get(mockEnv).get(); - assertNull(result); + List result = resolver.get(mockEnv).get(); + assertEquals(result.size(), 0); Mockito.verify(mockAspectService, Mockito.times(0)).getAggregatedStats( Mockito.any(), @@ -206,8 +208,8 @@ public void testGetSuccessUnhealthy() throws Exception { parentDataset.setUrn(TEST_DATASET_URN); Mockito.when(mockEnv.getSource()).thenReturn(parentDataset); - Health result = resolver.get(mockEnv).get(); - assertNotNull(result); - assertEquals(result.getStatus(), HealthStatus.FAIL); + List result = resolver.get(mockEnv).get(); + assertEquals(result.size(), 1); + assertEquals(result.get(0).getStatus(), HealthStatus.FAIL); } } diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 4e1f161625928..84dbb303b63fd 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -207,7 +207,7 @@ export const dataset1 = { container: null, upstream: null, downstream: null, - health: null, + health: [], assertions: null, deprecation: null, testResults: null, @@ -288,7 +288,7 @@ export const dataset2 = { container: null, upstream: null, downstream: null, - health: null, + health: [], assertions: null, status: null, deprecation: null, @@ -498,7 +498,7 @@ export const dataset3 = { container: null, lineage: null, relationships: null, - health: null, + health: [], assertions: null, status: null, readRuns: null, diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx index ff21db388c08f..b5a34ee9a1edd 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx @@ -192,12 +192,13 @@ export const EntityHeader = ({ refreshBrowser, headerDropdownItems, isNameEditab )} - {entityData?.health && ( + {entityData?.health?.map((health) => ( - )} + ))} diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHealthStatus.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHealthStatus.tsx index eef4d6c5227e6..4d16fc9ac0bff 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHealthStatus.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHealthStatus.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Tooltip } from 'antd'; import styled from 'styled-components'; import { getHealthIcon } from '../../../../../shared/health/healthUtils'; -import { HealthStatus } from '../../../../../../types.generated'; +import { HealthStatus, HealthStatusType } from '../../../../../../types.generated'; const StatusContainer = styled.div` display: flex; @@ -12,12 +12,13 @@ const StatusContainer = styled.div` `; type Props = { + type: HealthStatusType; status: HealthStatus; message?: string | undefined; }; -export const EntityHealthStatus = ({ status, message }: Props) => { - const icon = getHealthIcon(status, 18); +export const EntityHealthStatus = ({ type, status, message }: Props) => { + const icon = getHealthIcon(type, status, 18); return ( {icon} diff --git a/datahub-web-react/src/app/entity/shared/types.ts b/datahub-web-react/src/app/entity/shared/types.ts index 7a802d8384a80..c03efc5dfb1a7 100644 --- a/datahub-web-react/src/app/entity/shared/types.ts +++ b/datahub-web-react/src/app/entity/shared/types.ts @@ -79,7 +79,7 @@ export type GenericEntityProperties = { subTypes?: Maybe; entityCount?: number; container?: Maybe; - health?: Maybe; + health?: Maybe>; status?: Maybe; deprecation?: Maybe; parentContainers?: Maybe; diff --git a/datahub-web-react/src/app/shared/health/healthUtils.tsx b/datahub-web-react/src/app/shared/health/healthUtils.tsx index 4278395a96b08..e43abee54fd72 100644 --- a/datahub-web-react/src/app/shared/health/healthUtils.tsx +++ b/datahub-web-react/src/app/shared/health/healthUtils.tsx @@ -1,6 +1,6 @@ import { CheckOutlined, CloseOutlined, WarningOutlined } from '@ant-design/icons'; import React from 'react'; -import { HealthStatus } from '../../../types.generated'; +import { HealthStatus, HealthStatusType } from '../../../types.generated'; export const getHealthColor = (status: HealthStatus) => { switch (status) { @@ -18,7 +18,7 @@ export const getHealthColor = (status: HealthStatus) => { } }; -export const getHealthIcon = (status: HealthStatus, fontSize: number) => { +export const getAssertionsHealthIcon = (status: HealthStatus, fontSize: number) => { switch (status) { case HealthStatus.Pass: { return ; @@ -33,3 +33,13 @@ export const getHealthIcon = (status: HealthStatus, fontSize: number) => { throw new Error(`Unrecognized Health Status ${status} provided`); } }; + +export const getHealthIcon = (type: HealthStatusType, status: HealthStatus, fontSize: number) => { + switch (type) { + case HealthStatusType.Assertions: { + return getAssertionsHealthIcon(status, fontSize); + } + default: + throw new Error(`Unrecognized Health Status Type ${type} provided`); + } +}; diff --git a/datahub-web-react/src/graphql-mock/fixtures/entity/datasetEntity.ts b/datahub-web-react/src/graphql-mock/fixtures/entity/datasetEntity.ts index b7d90561f393f..cc67eeb7a527c 100644 --- a/datahub-web-react/src/graphql-mock/fixtures/entity/datasetEntity.ts +++ b/datahub-web-react/src/graphql-mock/fixtures/entity/datasetEntity.ts @@ -83,5 +83,6 @@ export const datasetEntity = ({ previousSchemaMetadata: null, __typename: 'Dataset', subTypes: null, + health: [], }; }; diff --git a/datahub-web-react/src/graphql/dataset.graphql b/datahub-web-react/src/graphql/dataset.graphql index 3341089122b88..057072a573019 100644 --- a/datahub-web-react/src/graphql/dataset.graphql +++ b/datahub-web-react/src/graphql/dataset.graphql @@ -127,6 +127,7 @@ fragment nonSiblingDatasetFields on Dataset { } } health { + type status message causes diff --git a/docs/how/updating-datahub.md b/docs/how/updating-datahub.md index 8067659d9d77f..2fc59644af10a 100644 --- a/docs/how/updating-datahub.md +++ b/docs/how/updating-datahub.md @@ -6,6 +6,8 @@ This file documents any backwards-incompatible changes in DataHub and assists pe ### Breaking Changes +- Refactored the `health` field of the `Dataset` GraphQL Type to be of type **list of HealthStatus** (was type **HealthStatus**). See [this PR](https://github.com/datahub-project/datahub/pull/5222/files) for more details. + ### Potential Downtime ### Deprecations