Пример #1
0
def _get_docker_image_version() -> str:
    """Returns a version for the feature server Docker image.

    If the feast.constants.DOCKER_IMAGE_TAG_ENV_NAME environment variable is set,
    we return that (mostly used for integration tests, but can be used for local testing too).

    For public Feast releases this equals to the Feast SDK version modified by replacing "." with "_".
    For example, Feast SDK version "0.14.1" would correspond to Docker image version "0_14_1".

    During development (when Feast is installed in editable mode) this equals to the Feast SDK version
    modified by removing the "dev..." suffix and replacing "." with "_". For example, Feast SDK version
    "0.14.1.dev41+g1cbfa225.d20211103" would correspond to Docker image version "0_14_1". This way,
    Feast SDK will use an already existing Docker image built during the previous public release.

    """
    tag = os.environ.get(DOCKER_IMAGE_TAG_ENV_NAME)
    if tag is not None:
        return tag
    else:
        version = get_version()
        if "dev" in version:
            version = version[: version.find("dev") - 1].replace(".", "_")
            _logger.warning(
                "You are trying to use AWS Lambda feature server while Feast is in a development mode. "
                f"Feast will use a docker image version {version} derived from Feast SDK "
                f"version {get_version()}. If you want to update the Feast SDK version, make "
                "sure to first fetch all new release tags from Github and then reinstall the library:\n"
                "> git fetch --all --tags\n"
                "> pip install -e sdk/python"
            )
        else:
            version = version.replace(".", "_")
        return version
Пример #2
0
    def log(self, function_name: str):
        self.check_env_and_configure()

        if self._telemetry_enabled and self.telemetry_id:
            if function_name == "get_online_features":
                if self._telemetry_counter["get_online_features"] % 10000 != 0:
                    self._telemetry_counter["get_online_features"] += 1
                    return

            json = {
                "function_name": function_name,
                "telemetry_id": self.telemetry_id,
                "timestamp": datetime.utcnow().isoformat(),
                "version": get_version(),
                "os": sys.platform,
                "is_test": self._is_test,
            }
            try:
                requests.post(TELEMETRY_ENDPOINT, json=json)
            except Exception as e:
                if self._is_test:
                    raise e
                else:
                    pass
            return
Пример #3
0
 def log(self, function_name: str):
     self.check_env_and_configure()
     if self._usage_enabled and self.usage_id:
         if function_name == "get_online_features":
             self._usage_counter["get_online_features"] += 1
             if self._usage_counter["get_online_features"] % 10000 != 2:
                 return
             self._usage_counter[
                 "get_online_features"] = 2  # avoid overflow
         json = {
             "function_name": function_name,
             "usage_id": self.usage_id,
             "timestamp": datetime.utcnow().isoformat(),
             "version": get_version(),
             "os": sys.platform,
             "is_test": self._is_test,
         }
         try:
             requests.post(USAGE_ENDPOINT, json=json)
         except Exception as e:
             if self._is_test:
                 raise e
             else:
                 pass
         return
Пример #4
0
 def log_function(self, function_name: str):
     self.check_env_and_configure()
     if self._usage_enabled and self.usage_id:
         if (function_name == "get_online_features"
                 and not self.should_log_for_get_online_features_event(
                     function_name)):
             return
         json = {
             "function_name": function_name,
             "usage_id": self.usage_id,
             "timestamp": datetime.utcnow().isoformat(),
             "version": get_version(),
             "os": sys.platform,
             "is_test": self._is_test,
         }
         self._send_usage_request(json)
Пример #5
0
 def log_event(self, event: UsageEvent):
     self.check_env_and_configure()
     if self._usage_enabled and self.usage_id:
         event_name = str(event)
         if (event == UsageEvent.GET_ONLINE_FEATURES_WITH_ODFV
                 and not self.should_log_for_get_online_features_event(
                     event_name)):
             return
         json = {
             "event_name": event_name,
             "usage_id": self.usage_id,
             "timestamp": datetime.utcnow().isoformat(),
             "version": get_version(),
             "os": sys.platform,
             "is_test": self._is_test,
         }
         self._send_usage_request(json)
Пример #6
0
 def log_exception(self, error_type: str, traceback: List[Tuple[str, int, str]]):
     self.check_env_and_configure()
     if self._telemetry_enabled and self.telemetry_id:
         json = {
             "error_type": error_type,
             "traceback": traceback,
             "telemetry_id": self.telemetry_id,
             "version": get_version(),
             "os": sys.platform,
             "is_test": self._is_test,
         }
         try:
             requests.post(TELEMETRY_ENDPOINT, json=json)
         except Exception as e:
             if self._is_test:
                 raise e
             else:
                 pass
         return
Пример #7
0
    def version(self) -> str:
        """Returns the version of the current Feast SDK/CLI"""

        return get_version()
Пример #8
0
USAGE_ENDPOINT = "https://usage.feast.dev"

_logger = logging.getLogger(__name__)
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)

_is_enabled = os.getenv(FEAST_USAGE,
                        default=DEFAULT_FEAST_USAGE_VALUE) == "True"

_constant_attributes = {
    "session_id":
    str(uuid.uuid4()),
    "installation_id":
    None,
    "version":
    get_version(),
    "python_version":
    platform.python_version(),
    "platform":
    platform.platform(),
    "env_signature":
    hashlib.md5(",".join(
        sorted([k for k in os.environ.keys()
                if not k.startswith("FEAST")])).encode()).hexdigest(),
}


@dataclasses.dataclass
class FnCall:
    fn_name: str
    id: str
Пример #9
0
def _get_version_for_aws():
    """Returns Feast version with certain characters replaced.

    This allows the version to be included in names for AWS resources.
    """
    return get_version().replace(".", "_").replace("+", "_")
Пример #10
0
    def _deploy_feature_server(self, project: str, image_uri: str):
        _logger.info("Deploying feature server...")

        if not self.repo_config.repo_path:
            raise RepoConfigPathDoesNotExist()
        with open(self.repo_config.repo_path / "feature_store.yaml",
                  "rb") as f:
            config_bytes = f.read()
            config_base64 = base64.b64encode(config_bytes).decode()

        resource_name = _get_lambda_name(project)
        lambda_client = boto3.client("lambda")
        api_gateway_client = boto3.client("apigatewayv2")
        function = aws_utils.get_lambda_function(lambda_client, resource_name)

        if function is None:
            # If the Lambda function does not exist, create it.
            _logger.info("  Creating AWS Lambda...")
            assert isinstance(self.repo_config.feature_server,
                              AwsLambdaFeatureServerConfig)
            lambda_client.create_function(
                FunctionName=resource_name,
                Role=self.repo_config.feature_server.execution_role_name,
                Code={"ImageUri": image_uri},
                PackageType="Image",
                MemorySize=1769,
                Environment={
                    "Variables": {
                        FEATURE_STORE_YAML_ENV_NAME: config_base64,
                        FEAST_USAGE: "False",
                    }
                },
                Tags={
                    "feast-owned": "True",
                    "project": project,
                    "feast-sdk-version": get_version(),
                },
            )
            function = aws_utils.get_lambda_function(lambda_client,
                                                     resource_name)
            if not function:
                raise AwsLambdaDoesNotExist(resource_name)
        else:
            # If the feature_store.yaml has changed, need to update the environment variable.
            env = function.get("Environment", {}).get("Variables", {})
            if env.get(FEATURE_STORE_YAML_ENV_NAME) != config_base64:
                # Note, that this does not update Lambda gracefully (e.g. no rolling deployment).
                # It's expected that feature_store.yaml is not regularly updated while the lambda
                # is serving production traffic. However, the update in registry (e.g. modifying
                # feature views, feature services, and other definitions does not update lambda).
                _logger.info("  Updating AWS Lambda...")

                lambda_client.update_function_configuration(
                    FunctionName=resource_name,
                    Environment={
                        "Variables": {
                            FEATURE_STORE_YAML_ENV_NAME: config_base64
                        }
                    },
                )

        api = aws_utils.get_first_api_gateway(api_gateway_client,
                                              resource_name)
        if not api:
            # If the API Gateway doesn't exist, create it
            _logger.info("  Creating AWS API Gateway...")
            api = api_gateway_client.create_api(
                Name=resource_name,
                ProtocolType="HTTP",
                Target=function["FunctionArn"],
                RouteKey="POST /get-online-features",
                Tags={
                    "feast-owned": "True",
                    "project": project,
                    "feast-sdk-version": get_version(),
                },
            )
            if not api:
                raise AwsAPIGatewayDoesNotExist(resource_name)
            # Make sure to give AWS Lambda a permission to be invoked by the newly created API Gateway
            api_id = api["ApiId"]
            region = lambda_client.meta.region_name
            account_id = aws_utils.get_account_id()
            lambda_client.add_permission(
                FunctionName=function["FunctionArn"],
                StatementId=str(uuid.uuid4()),
                Action="lambda:InvokeFunction",
                Principal="apigateway.amazonaws.com",
                SourceArn=
                f"arn:aws:execute-api:{region}:{account_id}:{api_id}/*/*/get-online-features",
            )
Пример #11
0
import requests

from feast.constants import FEAST_USAGE
from feast.version import get_version

USAGE_ENDPOINT = "https://usage.feast.dev"

_logger = logging.getLogger(__name__)
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)

_is_enabled = os.getenv(FEAST_USAGE, default="True") == "True"

_constant_attributes = {
    "session_id": str(uuid.uuid4()),
    "installation_id": None,
    "version": get_version(),
    "python_version": platform.python_version(),
    "platform": platform.platform(),
    "env_signature": hashlib.md5(
        ",".join(
            sorted([k for k in os.environ.keys() if not k.startswith("FEAST")])
        ).encode()
    ).hexdigest(),
}


@dataclasses.dataclass
class FnCall:
    fn_name: str
    id: str
Пример #12
0
    def update_infra(
        self,
        project: str,
        tables_to_delete: Sequence[Union[FeatureTable, FeatureView]],
        tables_to_keep: Sequence[Union[FeatureTable, FeatureView]],
        entities_to_delete: Sequence[Entity],
        entities_to_keep: Sequence[Entity],
        partial: bool,
    ):
        self.online_store.update(
            config=self.repo_config,
            tables_to_delete=tables_to_delete,
            tables_to_keep=tables_to_keep,
            entities_to_keep=entities_to_keep,
            entities_to_delete=entities_to_delete,
            partial=partial,
        )

        if self.repo_config.feature_server and self.repo_config.feature_server.enabled:
            if not enable_aws_lambda_feature_server(self.repo_config):
                raise ExperimentalFeatureNotEnabled(FLAG_AWS_LAMBDA_FEATURE_SERVER_NAME)

            # Since the AWS Lambda feature server will attempt to load the registry, we
            # only allow the registry to be in S3.
            registry_path = (
                self.repo_config.registry
                if isinstance(self.repo_config.registry, str)
                else self.repo_config.registry.path
            )
            registry_store_class = get_registry_store_class_from_scheme(registry_path)
            if registry_store_class != S3RegistryStore:
                raise IncompatibleRegistryStoreClass(
                    registry_store_class.__name__, S3RegistryStore.__name__
                )

            image_uri = self._upload_docker_image(project)
            _logger.info("Deploying feature server...")

            if not self.repo_config.repo_path:
                raise RepoConfigPathDoesNotExist()
            with open(self.repo_config.repo_path / "feature_store.yaml", "rb") as f:
                config_bytes = f.read()
                config_base64 = base64.b64encode(config_bytes).decode()

            resource_name = self._get_lambda_name(project)
            lambda_client = boto3.client("lambda")
            api_gateway_client = boto3.client("apigatewayv2")
            function = aws_utils.get_lambda_function(lambda_client, resource_name)

            if function is None:
                # If the Lambda function does not exist, create it.
                _logger.info("  Creating AWS Lambda...")
                lambda_client.create_function(
                    FunctionName=resource_name,
                    Role=self.repo_config.feature_server.execution_role_name,
                    Code={"ImageUri": image_uri},
                    PackageType="Image",
                    MemorySize=1769,
                    Environment={
                        "Variables": {
                            FEATURE_STORE_YAML_ENV_NAME: config_base64,
                            FEAST_USAGE: "False",
                        }
                    },
                    Tags={
                        "feast-owned": "True",
                        "project": project,
                        "feast-sdk-version": get_version(),
                    },
                )
                function = aws_utils.get_lambda_function(lambda_client, resource_name)
                if not function:
                    raise AwsLambdaDoesNotExist(resource_name)
            else:
                # If the feature_store.yaml has changed, need to update the environment variable.
                env = function.get("Environment", {}).get("Variables", {})
                if env.get(FEATURE_STORE_YAML_ENV_NAME) != config_base64:
                    # Note, that this does not update Lambda gracefully (e.g. no rolling deployment).
                    # It's expected that feature_store.yaml is not regularly updated while the lambda
                    # is serving production traffic. However, the update in registry (e.g. modifying
                    # feature views, feature services, and other definitions does not update lambda).
                    _logger.info("  Updating AWS Lambda...")

                    lambda_client.update_function_configuration(
                        FunctionName=resource_name,
                        Environment={
                            "Variables": {FEATURE_STORE_YAML_ENV_NAME: config_base64}
                        },
                    )

            api = aws_utils.get_first_api_gateway(api_gateway_client, resource_name)
            if not api:
                # If the API Gateway doesn't exist, create it
                _logger.info("  Creating AWS API Gateway...")
                api = api_gateway_client.create_api(
                    Name=resource_name,
                    ProtocolType="HTTP",
                    Target=function["FunctionArn"],
                    RouteKey="POST /get-online-features",
                    Tags={
                        "feast-owned": "True",
                        "project": project,
                        "feast-sdk-version": get_version(),
                    },
                )
                if not api:
                    raise AwsAPIGatewayDoesNotExist(resource_name)
                # Make sure to give AWS Lambda a permission to be invoked by the newly created API Gateway
                api_id = api["ApiId"]
                region = lambda_client.meta.region_name
                account_id = aws_utils.get_account_id()
                lambda_client.add_permission(
                    FunctionName=function["FunctionArn"],
                    StatementId=str(uuid.uuid4()),
                    Action="lambda:InvokeFunction",
                    Principal="apigateway.amazonaws.com",
                    SourceArn=f"arn:aws:execute-api:{region}:{account_id}:{api_id}/*/*/get-online-features",
                )