mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-11-04 04:29:13 +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
 | 
			
		||||
 | 
			
		||||
install:
 | 
			
		||||
	pip install ingestion/
 | 
			
		||||
	python3 -m pip install ingestion/
 | 
			
		||||
 | 
			
		||||
install_test:
 | 
			
		||||
	pip install -r ingestion/requirements-test.txt
 | 
			
		||||
	python3 -m pip install -r ingestion/requirements-test.txt
 | 
			
		||||
 | 
			
		||||
install_dev:
 | 
			
		||||
	pip install -r ingestion/requirements-dev.txt
 | 
			
		||||
	python3 -m pip install -r ingestion/requirements-dev.txt
 | 
			
		||||
 | 
			
		||||
precommit_install:
 | 
			
		||||
	@echo "Installing pre-commit hooks"
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,8 @@
 | 
			
		||||
        "Superset",
 | 
			
		||||
        "Looker",
 | 
			
		||||
        "Tableau",
 | 
			
		||||
        "Redash"
 | 
			
		||||
        "Redash",
 | 
			
		||||
        "Metabase"
 | 
			
		||||
      ],
 | 
			
		||||
      "javaEnums": [
 | 
			
		||||
        {
 | 
			
		||||
@ -26,6 +27,9 @@
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "Redash"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "Metabase"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
cd "$( dirname "${BASH_SOURCE[0]}" )"
 | 
			
		||||
echo "Maven Build - Skipping Tests"
 | 
			
		||||
cd ../ && mvn -DskipTests clean package
 | 
			
		||||
make install_dev generate
 | 
			
		||||
cd docker/local-metadata
 | 
			
		||||
echo "Starting Local Docker Containers"
 | 
			
		||||
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