Fix #6754: Added User Details for Usage (#8449)

This commit is contained in:
Mayur Singal 2022-11-02 14:07:41 +05:30 committed by GitHub
parent ef5108c47b
commit fa68a1f18f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 74 additions and 21 deletions

View File

@ -74,6 +74,7 @@ def parse_sql_statement(record: TableQuery) -> Optional[ParsedData]:
databaseName=record.databaseName, databaseName=record.databaseName,
databaseSchema=record.databaseSchema, databaseSchema=record.databaseSchema,
sql=record.query, sql=record.query,
userName=record.userName,
date=start_date.__root__.strftime("%Y-%m-%d"), date=start_date.__root__.strftime("%Y-%m-%d"),
serviceName=record.serviceName, serviceName=record.serviceName,
) )

View File

@ -23,9 +23,12 @@ from metadata.generated.schema.entity.data.table import SqlQuery
from metadata.generated.schema.entity.services.connections.metadata.openMetadataConnection import ( from metadata.generated.schema.entity.services.connections.metadata.openMetadataConnection import (
OpenMetadataConnection, OpenMetadataConnection,
) )
from metadata.generated.schema.entity.teams.user import User
from metadata.generated.schema.type.entityReference import EntityReference
from metadata.generated.schema.type.queryParserData import QueryParserData from metadata.generated.schema.type.queryParserData import QueryParserData
from metadata.generated.schema.type.tableUsageCount import TableUsageCount from metadata.generated.schema.type.tableUsageCount import TableUsageCount
from metadata.ingestion.api.stage import Stage, StageStatus from metadata.ingestion.api.stage import Stage, StageStatus
from metadata.ingestion.ometa.ometa_api import OpenMetadata
from metadata.utils.constants import UTF_8 from metadata.utils.constants import UTF_8
from metadata.utils.logger import ingestion_logger from metadata.utils.logger import ingestion_logger
@ -54,6 +57,7 @@ class TableUsageStage(Stage[QueryParserData]):
): ):
self.config = config self.config = config
self.metadata_config = metadata_config self.metadata_config = metadata_config
self.metadata = OpenMetadata(self.metadata_config)
self.status = StageStatus() self.status = StageStatus()
self.table_usage = {} self.table_usage = {}
self.table_queries = {} self.table_queries = {}
@ -70,11 +74,41 @@ class TableUsageStage(Stage[QueryParserData]):
config = TableStageConfig.parse_obj(config_dict) config = TableStageConfig.parse_obj(config_dict)
return cls(config, metadata_config) return cls(config, metadata_config)
def _get_user_entity(self, username: str):
if username:
user = self.metadata.get_by_name(entity=User, fqn=username)
if user:
return [
EntityReference(
id=user.id,
type="user",
name=user.name.__root__,
fullyQualifiedName=user.fullyQualifiedName.__root__,
description=user.description,
displayName=user.displayName,
deleted=user.deleted,
href=user.href,
)
]
return []
def _add_sql_query(self, record, table): def _add_sql_query(self, record, table):
if self.table_queries.get((table, record.date)): if self.table_queries.get((table, record.date)):
self.table_queries[(table, record.date)].append(SqlQuery(query=record.sql)) self.table_queries[(table, record.date)].append(
SqlQuery(
query=record.sql,
users=self._get_user_entity(record.userName),
queryDate=record.date,
)
)
else: else:
self.table_queries[(table, record.date)] = [SqlQuery(query=record.sql)] self.table_queries[(table, record.date)] = [
SqlQuery(
query=record.sql,
users=self._get_user_entity(record.userName),
queryDate=record.date,
)
]
def stage_record(self, record: QueryParserData) -> None: def stage_record(self, record: QueryParserData) -> None:
if not record or not record.parsedData: if not record or not record.parsedData:

View File

@ -39,6 +39,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -380,6 +381,14 @@ public class TableRepository extends EntityRepository<Table> {
storedMapQueries.put(q.getChecksum(), q); storedMapQueries.put(q.getChecksum(), q);
} }
} }
SQLQuery oldQuery = storedMapQueries.get(query.getChecksum());
if (oldQuery != null && query.getUsers() != null) {
// Merge old and new users
List<EntityReference> userList = query.getUsers();
userList.addAll(oldQuery.getUsers());
HashSet<EntityReference> userSet = new HashSet<>(userList);
query.setUsers(new ArrayList<>(userSet));
}
storedMapQueries.put(query.getChecksum(), query); storedMapQueries.put(query.getChecksum(), query);
List<SQLQuery> updatedQueries = new ArrayList<>(storedMapQueries.values()); List<SQLQuery> updatedQueries = new ArrayList<>(storedMapQueries.values());
daoCollection daoCollection

View File

@ -578,9 +578,12 @@
"description": "How long did the query took to run in seconds.", "description": "How long did the query took to run in seconds.",
"type": "number" "type": "number"
}, },
"user": { "users": {
"description": "User who ran this query.", "description": "List of users who ran this query.",
"$ref": "../../type/entityReference.json", "type":"array",
"items": {
"$ref": "../../type/entityReference.json"
},
"default": null "default": null
}, },
"vote": { "vote": {

View File

@ -38,6 +38,10 @@
"description": "Name that identifies this database service.", "description": "Name that identifies this database service.",
"type": "string" "type": "string"
}, },
"userName": {
"description": "Name of the user that executed the SQL query",
"type": "string"
},
"date": { "date": {
"description": "Date of execution of SQL query", "description": "Date of execution of SQL query",
"type": "string" "type": "string"

View File

@ -14,7 +14,7 @@
import { import {
findByTestId, findByTestId,
findByText, findByText,
getByTestId, // getByTestId,
queryByTestId, queryByTestId,
render, render,
} from '@testing-library/react'; } from '@testing-library/react';
@ -25,13 +25,15 @@ import QueryCard from './QueryCard';
const mockQueryData = { const mockQueryData = {
query: 'select products from raw_product_catalog', query: 'select products from raw_product_catalog',
duration: 0.309, duration: 0.309,
user: { users: [
id: 'd4785e53-bbdb-4dbd-b368-009fdb50c2c6', {
type: 'user', id: 'd4785e53-bbdb-4dbd-b368-009fdb50c2c6',
name: 'aaron_johnson0', type: 'user',
displayName: 'Aaron Johnson', name: 'aaron_johnson0',
href: 'http://localhost:8585/api/v1/users/d4785e53-bbdb-4dbd-b368-009fdb50c2c6', displayName: 'Aaron Johnson',
}, href: 'http://localhost:8585/api/v1/users/d4785e53-bbdb-4dbd-b368-009fdb50c2c6',
},
],
vote: 1, vote: 1,
checksum: '0232b0368458aadb29230ccc531462c9', checksum: '0232b0368458aadb29230ccc531462c9',
}; };
@ -49,7 +51,7 @@ describe('Test QueryCard Component', () => {
const { container } = render(<QueryCard query={mockQueryData} />, { const { container } = render(<QueryCard query={mockQueryData} />, {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
const queryHeader = getByTestId(container, 'query-header'); // const queryHeader = getByTestId(container, 'query-header');
const query = await findByText(container, /SchemaEditor/i); const query = await findByText(container, /SchemaEditor/i);
const copyQueryButton = await findByText( const copyQueryButton = await findByText(
container, container,
@ -61,7 +63,7 @@ describe('Test QueryCard Component', () => {
'expand-collapse-button' 'expand-collapse-button'
); );
expect(queryHeader).toBeInTheDocument(); // expect(queryHeader).toBeInTheDocument();
expect(query).toBeInTheDocument(); expect(query).toBeInTheDocument();
expect(copyQueryButton).toBeInTheDocument(); expect(copyQueryButton).toBeInTheDocument();
expect(expandButton).toBeInTheDocument(); expect(expandButton).toBeInTheDocument();
@ -69,7 +71,7 @@ describe('Test QueryCard Component', () => {
it('Should not render header if user is undefined', async () => { it('Should not render header if user is undefined', async () => {
const { container } = render( const { container } = render(
<QueryCard query={{ ...mockQueryData, user: undefined }} />, <QueryCard query={{ ...mockQueryData, users: undefined }} />,
{ {
wrapper: MemoryRouter, wrapper: MemoryRouter,
} }

View File

@ -12,10 +12,10 @@
*/ */
import classNames from 'classnames'; import classNames from 'classnames';
import { isUndefined } from 'lodash'; // import { isUndefined } from 'lodash';
import React, { FC, HTMLAttributes, useState } from 'react'; import React, { FC, HTMLAttributes, useState } from 'react';
import { Link } from 'react-router-dom'; // import { Link } from 'react-router-dom';
import { getUserPath } from '../../constants/constants'; // import { getUserPath } from '../../constants/constants';
import { CSMode } from '../../enums/codemirror.enum'; import { CSMode } from '../../enums/codemirror.enum';
import { SQLQuery } from '../../generated/entity/data/table'; import { SQLQuery } from '../../generated/entity/data/table';
import SVGIcons, { Icons } from '../../utils/SvgUtils'; import SVGIcons, { Icons } from '../../utils/SvgUtils';
@ -34,7 +34,7 @@ const QueryCard: FC<QueryCardProp> = ({ className, query }) => {
className className
)} )}
onClick={() => setExpanded((pre) => !pre)}> onClick={() => setExpanded((pre) => !pre)}>
{!isUndefined(query.user) && !isUndefined(query.duration) ? ( {/* {!isUndefined(query.user) && !isUndefined(query.duration) ? (
<div data-testid="query-header"> <div data-testid="query-header">
<p> <p>
Last run by{' '} Last run by{' '}
@ -49,7 +49,7 @@ const QueryCard: FC<QueryCardProp> = ({ className, query }) => {
<span className="tw-font-medium">{query.duration} seconds</span> <span className="tw-font-medium">{query.duration} seconds</span>
</p> </p>
</div> </div>
) : null} ) : null} */}
<div className="tw-border tw-border-main tw-rounded-md tw-p-px"> <div className="tw-border tw-border-main tw-rounded-md tw-p-px">
<div <div
className={classNames('tw-overflow-hidden tw-relative', { className={classNames('tw-overflow-hidden tw-relative', {