mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-08 06:53:11 +00:00
Added Metabase Connector (#1726)
* Added Metabase Connector * Metabase Added support for lineage * Modified Metabase as per comments * Metabase filter patterns added * Update databaseService.json
This commit is contained in:
parent
90618342cb
commit
07924f80c3
6
Makefile
6
Makefile
@ -8,13 +8,13 @@ clean_env37:
|
|||||||
rm -rf env38
|
rm -rf env38
|
||||||
|
|
||||||
install:
|
install:
|
||||||
pip install ingestion/
|
python3 -m pip install ingestion/
|
||||||
|
|
||||||
install_test:
|
install_test:
|
||||||
pip install -r ingestion/requirements-test.txt
|
python3 -m pip install -r ingestion/requirements-test.txt
|
||||||
|
|
||||||
install_dev:
|
install_dev:
|
||||||
pip install -r ingestion/requirements-dev.txt
|
python3 -m pip install -r ingestion/requirements-dev.txt
|
||||||
|
|
||||||
precommit_install:
|
precommit_install:
|
||||||
@echo "Installing pre-commit hooks"
|
@echo "Installing pre-commit hooks"
|
||||||
|
|||||||
@ -12,7 +12,8 @@
|
|||||||
"Superset",
|
"Superset",
|
||||||
"Looker",
|
"Looker",
|
||||||
"Tableau",
|
"Tableau",
|
||||||
"Redash"
|
"Redash",
|
||||||
|
"Metabase"
|
||||||
],
|
],
|
||||||
"javaEnums": [
|
"javaEnums": [
|
||||||
{
|
{
|
||||||
@ -26,6 +27,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Redash"
|
"name": "Redash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Metabase"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
cd "$( dirname "${BASH_SOURCE[0]}" )"
|
cd "$( dirname "${BASH_SOURCE[0]}" )"
|
||||||
echo "Maven Build - Skipping Tests"
|
echo "Maven Build - Skipping Tests"
|
||||||
cd ../ && mvn -DskipTests clean package
|
cd ../ && mvn -DskipTests clean package
|
||||||
|
make install_dev generate
|
||||||
cd docker/local-metadata
|
cd docker/local-metadata
|
||||||
echo "Starting Local Docker Containers"
|
echo "Starting Local Docker Containers"
|
||||||
docker-compose down && docker-compose up --build -d
|
docker-compose down && docker-compose up --build -d
|
||||||
|
|||||||
23
ingestion/examples/workflows/metabase.json
Normal file
23
ingestion/examples/workflows/metabase.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"type": "metabase",
|
||||||
|
"config": {
|
||||||
|
"username": "username",
|
||||||
|
"password": "password",
|
||||||
|
"host_port": "host:port",
|
||||||
|
"service_name": "local_metabase",
|
||||||
|
"database_service_name": "Optional - Create Lineage by adding relevant Database Service Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sink": {
|
||||||
|
"type": "metadata-rest",
|
||||||
|
"config": {}
|
||||||
|
},
|
||||||
|
"metadata_server": {
|
||||||
|
"type": "metadata-server",
|
||||||
|
"config": {
|
||||||
|
"api_endpoint": "http://localhost:8585/api",
|
||||||
|
"auth_provider_type": "no-auth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
210
ingestion/src/metadata/ingestion/source/metabase.py
Normal file
210
ingestion/src/metadata/ingestion/source/metabase.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# Copyright 2021 Collate
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
from typing import Iterable
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from pydantic import SecretStr
|
||||||
|
|
||||||
|
from metadata.generated.schema.api.lineage.addLineage import AddLineage
|
||||||
|
from metadata.generated.schema.entity.data.dashboard import Dashboard as Model_Dashboard
|
||||||
|
from metadata.generated.schema.entity.data.table import Table
|
||||||
|
from metadata.generated.schema.entity.services.dashboardService import (
|
||||||
|
DashboardServiceType,
|
||||||
|
)
|
||||||
|
from metadata.generated.schema.type.entityLineage import EntitiesEdge
|
||||||
|
from metadata.generated.schema.type.entityReference import EntityReference
|
||||||
|
from metadata.ingestion.api.common import (
|
||||||
|
ConfigModel,
|
||||||
|
Entity,
|
||||||
|
IncludeFilterPattern,
|
||||||
|
WorkflowContext,
|
||||||
|
)
|
||||||
|
from metadata.ingestion.api.source import Source, SourceStatus
|
||||||
|
from metadata.ingestion.models.table_metadata import Chart, Dashboard
|
||||||
|
from metadata.ingestion.ometa.ometa_api import OpenMetadata
|
||||||
|
from metadata.ingestion.ometa.openmetadata_rest import MetadataServerConfig
|
||||||
|
from metadata.ingestion.source.sql_alchemy_helper import SQLSourceStatus
|
||||||
|
from metadata.ingestion.source.sql_source import SQLSourceStatus
|
||||||
|
from metadata.utils.helpers import get_dashboard_service_or_create
|
||||||
|
|
||||||
|
HEADERS = {"Content-Type": "application/json", "Accept": "*/*"}
|
||||||
|
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MetabaseSourceConfig(ConfigModel):
|
||||||
|
username: str
|
||||||
|
password: SecretStr
|
||||||
|
host_port: str
|
||||||
|
dashboard_pattern: IncludeFilterPattern = IncludeFilterPattern.allow_all()
|
||||||
|
chart_pattern: IncludeFilterPattern = IncludeFilterPattern.allow_all()
|
||||||
|
service_name: str
|
||||||
|
service_type: str = "Metabase"
|
||||||
|
database_service_name: str = None
|
||||||
|
|
||||||
|
def get_connection_url(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MetabaseSource(Source[Entity]):
|
||||||
|
config: MetabaseSourceConfig
|
||||||
|
metadata_config: MetadataServerConfig
|
||||||
|
status: SQLSourceStatus
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: MetabaseSourceConfig,
|
||||||
|
metadata_config: MetadataServerConfig,
|
||||||
|
ctx: WorkflowContext,
|
||||||
|
):
|
||||||
|
super().__init__(ctx)
|
||||||
|
self.config = config
|
||||||
|
self.metadata_config = metadata_config
|
||||||
|
self.status = SQLSourceStatus()
|
||||||
|
params = {}
|
||||||
|
params["username"] = self.config.username
|
||||||
|
params["password"] = self.config.password.get_secret_value()
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
self.config.host_port + "/api/session/",
|
||||||
|
data=json.dumps(params),
|
||||||
|
headers=HEADERS,
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
raise ConnectionError(f"{err}")
|
||||||
|
session_id = resp.json()["id"]
|
||||||
|
self.metabase_session = {"X-Metabase-Session": session_id}
|
||||||
|
self.dashboard_service = get_dashboard_service_or_create(
|
||||||
|
config.service_name,
|
||||||
|
DashboardServiceType.Looker.name,
|
||||||
|
config.username,
|
||||||
|
config.password.get_secret_value(),
|
||||||
|
config.host_port,
|
||||||
|
metadata_config,
|
||||||
|
)
|
||||||
|
self.charts = []
|
||||||
|
self.metric_charts = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, config_dict, metadata_config_dict, ctx):
|
||||||
|
config = MetabaseSourceConfig.parse_obj(config_dict)
|
||||||
|
metadata_config = MetadataServerConfig.parse_obj(metadata_config_dict)
|
||||||
|
return cls(config, metadata_config, ctx)
|
||||||
|
|
||||||
|
def next_record(self) -> Iterable[Entity]:
|
||||||
|
yield from self.get_dashboards()
|
||||||
|
|
||||||
|
def get_charts(self, charts) -> Iterable[Chart]:
|
||||||
|
for chart in charts:
|
||||||
|
try:
|
||||||
|
chart_details = chart["card"]
|
||||||
|
if not self.config.chart_pattern.included(chart_details["name"]):
|
||||||
|
self.status.filter(chart_details["name"])
|
||||||
|
continue
|
||||||
|
yield Chart(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
name=chart_details["name"],
|
||||||
|
displayName=chart_details["name"],
|
||||||
|
description=chart_details["description"]
|
||||||
|
if chart_details["description"] is not None
|
||||||
|
else "",
|
||||||
|
chart_type=str(chart_details["display"]),
|
||||||
|
url=self.config.host_port,
|
||||||
|
service=EntityReference(
|
||||||
|
id=self.dashboard_service.id, type="dashboardService"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.charts.append(chart_details["name"])
|
||||||
|
self.status.scanned(chart_details["name"])
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(repr(err))
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
|
||||||
|
def get_dashboards(self):
|
||||||
|
resp_dashboards = self.req_get("/api/dashboard")
|
||||||
|
if resp_dashboards.status_code == 200:
|
||||||
|
for dashboard in resp_dashboards.json():
|
||||||
|
resp_dashboard = self.req_get(f"/api/dashboard/{dashboard['id']}")
|
||||||
|
dashboard_details = resp_dashboard.json()
|
||||||
|
self.charts = []
|
||||||
|
if not self.config.dashboard_pattern.included(
|
||||||
|
dashboard_details["name"]
|
||||||
|
):
|
||||||
|
self.status.filter(dashboard_details["name"])
|
||||||
|
continue
|
||||||
|
yield from self.get_charts(dashboard_details["ordered_cards"])
|
||||||
|
yield Dashboard(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
name=dashboard_details["name"],
|
||||||
|
url=self.config.host_port,
|
||||||
|
displayName=dashboard_details["name"],
|
||||||
|
description=dashboard_details["description"]
|
||||||
|
if dashboard_details["description"] is not None
|
||||||
|
else "",
|
||||||
|
charts=self.charts,
|
||||||
|
service=EntityReference(
|
||||||
|
id=self.dashboard_service.id, type="dashboardService"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield from self.get_lineage(
|
||||||
|
dashboard_details["ordered_cards"], dashboard_details["name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_lineage(self, chart_list, dashboard_name):
|
||||||
|
metadata = OpenMetadata(self.metadata_config)
|
||||||
|
for chart in chart_list:
|
||||||
|
try:
|
||||||
|
chart_details = chart["card"]
|
||||||
|
resp_tables = self.req_get(f"/api/table/{chart_details['table_id']}")
|
||||||
|
if resp_tables.status_code == 200:
|
||||||
|
table = resp_tables.json()
|
||||||
|
table_fqdn = f"{self.config.database_service_name}.{table['schema']}.{table['name']}"
|
||||||
|
dashboard_fqdn = (
|
||||||
|
f"{self.dashboard_service.name}.{quote(dashboard_name)}"
|
||||||
|
)
|
||||||
|
table_entity = metadata.get_by_name(entity=Table, fqdn=table_fqdn)
|
||||||
|
chart_entity = metadata.get_by_name(
|
||||||
|
entity=Model_Dashboard, fqdn=dashboard_fqdn
|
||||||
|
)
|
||||||
|
logger.debug("from entity {}".format(table_entity))
|
||||||
|
lineage = AddLineage(
|
||||||
|
edge=EntitiesEdge(
|
||||||
|
fromEntity=EntityReference(
|
||||||
|
id=table_entity.id.__root__, type="table"
|
||||||
|
),
|
||||||
|
toEntity=EntityReference(
|
||||||
|
id=chart_entity.id.__root__, type="dashboard"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield lineage
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(traceback.print_exc())
|
||||||
|
|
||||||
|
def req_get(self, path):
|
||||||
|
return requests.get(self.config.host_port + path, headers=self.metabase_session)
|
||||||
|
|
||||||
|
def get_status(self) -> SourceStatus:
|
||||||
|
return self.status
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
pass
|
||||||
Loading…
x
Reference in New Issue
Block a user