From b94f8538afe9af9cf7e783a09c5aba933faa04ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20de=20la=20R=C3=BAa=20Mart=C3=ADnez?= Date: Tue, 6 Feb 2024 09:28:55 +0100 Subject: [PATCH] [FSTORE_612] Support for feature monitoring (#1692) Co-authored-by: Robin Andersson @robzor92 Co-authored-by: Victor Jouffrey @vatj Co-authored-by: Kenneth Mak @kennethmhc Co-authored-by: Dhananjay Mukhedkar @dhananjay-mk Co-authored-by: Ehsan Heydari @ehsan-github --- .../test/ruby/spec/feature_monitoring_spec.rb | 1026 +++++++++++++++++ .../ruby/spec/feature_store_activity_spec.rb | 755 ++++++------ ...rt_spec.rb => feature_store_alert_spec.rb} | 98 +- .../ruby/spec/featurestore_statistics_spec.rb | 38 + .../test/ruby/spec/helpers/alert_helper.rb | 31 +- .../helpers/feature_group_alert_helper.rb | 51 +- .../spec/helpers/feature_monitoring_helper.rb | 381 ++++++ .../helpers/featurestore_statistics_helper.rb | 34 +- .../src/test/ruby/spec/helpers/job_helper.rb | 4 +- .../ruby/spec/helpers/project_alert_helper.rb | 21 +- .../src/test/ruby/spec/project_alert_spec.rb | 14 +- .../src/test/ruby/spec/spec_helper.rb | 1 + .../hopsworks/alert/FixReceiversTimer.java | 12 + .../hops/hopsworks/alert/util/ConfigUtil.java | 26 + .../hops/hopsworks/alert/util/Constants.java | 24 +- .../alert/util/PostableAlertBuilder.java | 48 +- .../java/TestAlertManagerConfigTimer.java | 6 +- .../alert/FeatureStoreAlertController.java | 219 ++++ .../alert/FeatureStoreAlertValidation.java | 140 +++ .../alert/FeatureGroupAlertDTO.java | 13 +- .../alert/FeatureGroupAlertResource.java | 367 ------ .../alert/FeatureGroupAlertValues.java | 15 +- .../FeatureGroupValidationAlertResource.java | 90 ++ .../alert/FeatureStoreAlertResource.java | 379 ++++++ ...s.java => PostableFeatureStoreAlerts.java} | 29 +- ...eatureMonitoringConfigurationResource.java | 66 ++ ...eGroupFeatureMonitoringResultResource.java | 45 + .../featuregroup/FeaturegroupService.java | 53 +- ...FeatureMonitoringConfigurationBuilder.java | 217 ++++ ...eatureMonitoringConfigurationResource.java | 345 ++++++ .../FeatureMonitoringResultBeanParam.java | 88 ++ .../FeatureMonitoringResultBuilder.java | 114 ++ ...ureMonitoringResultExpansionBeanParam.java | 47 + .../FeatureMonitoringResultExpansions.java | 39 + .../FeatureMonitoringResultFilterBy.java | 60 + .../FeatureMonitoringResultResource.java | 217 ++++ .../result/FeatureMonitoringResultSortBy.java | 62 + .../featureview/FeatureViewAlertBuilder.java | 134 +++ .../featureview/FeatureViewAlertDTO.java | 62 + .../featureview/FeatureViewBuilder.java | 4 +- ...eatureViewFeatureMonitorAlertResource.java | 92 ++ ...eatureMonitoringConfigurationResource.java | 66 ++ ...reViewFeatureMonitoringResultResource.java | 45 + ...r.java => FeatureViewInputValidation.java} | 2 +- .../featureview/FeatureViewService.java | 93 ++ .../statistics/StatisticsBuilder.java | 88 ++ .../statistics/StatisticsResource.java | 32 +- .../jobs/scheduler/JobScheduleV2Builder.java | 4 +- .../jobs/scheduler/JobScheduleV2Resource.java | 16 +- .../project/alert/ProjectAlertsResource.java | 23 +- .../project/alert/ProjectAllAlertsDTO.java | 13 +- .../common/alert/AlertController.java | 163 ++- .../hopsworks/common/api/ResourceRequest.java | 3 +- .../featurestore/FeaturestoreConstants.java | 12 + .../activity/FeaturestoreActivityFacade.java | 12 + .../app/FsJobManagerController.java | 62 +- .../FeatureGroupAlertFacade.java | 10 +- .../reports/ValidationReportController.java | 6 +- .../FeatureMonitoringAlertController.java | 178 +++ ...eStatisticsComparisonConfigurationDTO.java | 30 + ...tureMonitoringConfigurationController.java | 233 ++++ .../FeatureMonitoringConfigurationDTO.java | 50 + .../FeatureMonitoringConfigurationFacade.java | 135 +++ ...onitoringConfigurationInputValidation.java | 218 ++++ .../MonitoringWindowConfigurationDTO.java | 33 + ...ingWindowConfigurationInputValidation.java | 168 +++ .../FeatureMonitoringResultController.java | 97 ++ .../result/FeatureMonitoringResultDTO.java | 48 + .../result/FeatureMonitoringResultFacade.java | 156 +++ ...eatureMonitoringResultInputValidation.java | 132 +++ .../featureview/FeatureViewAlertFacade.java | 174 +++ .../featureview/FeatureViewController.java | 13 + .../featureview/FeatureViewFacade.java | 14 + .../FeatureDescriptiveStatisticsFacade.java | 6 +- ...eatureViewDescriptiveStatisticsFacade.java | 52 + .../FeatureViewStatisticsFacade.java | 162 +++ .../statistics/StatisticsController.java | 158 ++- .../statistics/StatisticsInputValidation.java | 21 + .../hopsworks/common/jobs/JobController.java | 13 + .../scheduler/JobScheduleV2Controller.java | 12 +- .../hops/hopsworks/common/util/Settings.java | 11 + ...onitoringConfigurationInputValidation.java | 286 +++++ ...eatureMonitoringResultInputValidation.java | 278 +++++ .../TestMonitoringAlertValidation.java | 58 + ...ingWindowConfigurationInputValidation.java | 442 +++++++ .../statistics/TestStatisticsController.java | 2 - .../TestStatisticsInputValidation.java | 68 +- .../jobs/TestJobSchedulerV2Controller.java | 44 +- .../entity/alertmanager/AlertReceiver.java | 17 +- .../activity/FeaturestoreActivity.java | 16 +- .../featurestore/alert/FeatureStoreAlert.java | 174 +++ .../FeatureStoreAlertStatus.java} | 44 +- .../featuregroup/Featuregroup.java | 14 +- .../alert/FeatureGroupAlert.java | 209 +--- .../FeatureMonitoringConfiguration.java | 293 +++++ .../config/FeatureMonitoringType.java} | 38 +- .../config/MonitoringWindowConfiguration.java | 152 +++ .../config/WindowConfigurationType.java | 25 + ...DescriptiveStatisticsComparisonConfig.java | 126 ++ .../MetricDescriptiveStatistics.java | 34 + .../result/FeatureMonitoringResult.java | 246 ++++ .../featureview/alert/FeatureViewAlert.java | 93 ++ .../statistics/FeatureGroupStatistics.java | 23 +- .../FeatureViewDescriptiveStatistics.java | 83 ++ .../FeatureViewDescriptiveStatisticsPK.java | 83 ++ .../statistics/FeatureViewStatistics.java | 163 +++ .../alert/ProjectServiceAlertStatus.java | 37 +- .../main/resources/META-INF/persistence.xml | 7 + .../hops/hopsworks/restutils/RESTCodes.java | 10 +- 109 files changed, 10112 insertions(+), 1184 deletions(-) create mode 100644 hopsworks-IT/src/test/ruby/spec/feature_monitoring_spec.rb rename hopsworks-IT/src/test/ruby/spec/{feature_group_alert_spec.rb => feature_store_alert_spec.rb} (58%) create mode 100644 hopsworks-IT/src/test/ruby/spec/helpers/feature_monitoring_helper.rb create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertController.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertValidation.java delete mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupValidationAlertResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureStoreAlertResource.java rename hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/{PostableFeatureGroupAlerts.java => PostableFeatureStoreAlerts.java} (70%) create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringConfigurationResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringResultResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationBuilder.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBeanParam.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBuilder.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansionBeanParam.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansions.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultFilterBy.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultSortBy.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertBuilder.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertDTO.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitorAlertResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringConfigurationResource.java create mode 100644 hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringResultResource.java rename hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/{FeatureViewInputValidator.java => FeatureViewInputValidation.java} (98%) create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/alert/FeatureMonitoringAlertController.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/DescriptiveStatisticsComparisonConfigurationDTO.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationController.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationDTO.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationFacade.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationInputValidation.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationDTO.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationInputValidation.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultController.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultDTO.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultFacade.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultInputValidation.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewAlertFacade.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewDescriptiveStatisticsFacade.java create mode 100644 hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewStatisticsFacade.java create mode 100644 hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringConfigurationInputValidation.java create mode 100644 hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringResultInputValidation.java create mode 100644 hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringAlertValidation.java create mode 100644 hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringWindowConfigurationInputValidation.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/alert/FeatureStoreAlert.java rename hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/{featuregroup/datavalidation/alert/ValidationRuleAlertStatus.java => alert/FeatureStoreAlertStatus.java} (54%) create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/FeatureMonitoringConfiguration.java rename hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/{featuregroup/datavalidation/FeatureGroupValidationStatus.java => featuremonitoring/config/FeatureMonitoringType.java} (52%) create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/MonitoringWindowConfiguration.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/WindowConfigurationType.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/DescriptiveStatisticsComparisonConfig.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/MetricDescriptiveStatistics.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/result/FeatureMonitoringResult.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featureview/alert/FeatureViewAlert.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatistics.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatisticsPK.java create mode 100644 hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewStatistics.java diff --git a/hopsworks-IT/src/test/ruby/spec/feature_monitoring_spec.rb b/hopsworks-IT/src/test/ruby/spec/feature_monitoring_spec.rb new file mode 100644 index 0000000000..526f4e0f4a --- /dev/null +++ b/hopsworks-IT/src/test/ruby/spec/feature_monitoring_spec.rb @@ -0,0 +1,1026 @@ +# This file is part of Hopsworks +# Copyright (C) 2024, Hopsworks AB. All rights reserved +# +# Hopsworks is free software: you can redistribute it and/or modify it under the terms of +# the GNU Affero General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. +# If not, see . +# + +describe "On #{ENV['OS']}" do + + before :all do + # ensure feature monitoring is enabled + @enable_feature_monitoring = getVar('enable_feature_monitoring') + setVar('enable_feature_monitoring', "true") + end + + after :all do + # revert feature monitoring flag + setVar('enable_feature_monitoring', @enable_feature_monitoring[:value]) + clean_all_test_projects(spec: "feature_monitoring_persistence") + end + + describe 'feature monitoring persistence' do + + describe "Create feature monitoring configuration for Feature Groups" do + context 'with valid project, featurestore, feature_group and feature_view' do + before :all do + with_valid_project + @featurestore_id = get_featurestore_id(@project[:id]) + fg_dto, featuregroup_name = create_cached_featuregroup(@project[:id], @featurestore_id) + expect_status_details(201) + @fg_json = JSON.parse(fg_dto) + expect_status_details(201) + fv_dto = create_feature_view_from_feature_group(@project[:id], @featurestore_id, @fg_json) + @fv_json = JSON.parse(fv_dto) + expect_status_details(201) + @template_config_name_fg = "test_config" + @job_name_fg = "#{@fg_json['name']}_1_#{@template_config_name_fg}_run_feature_monitoring" + @template_stats_config_name_fg = "test_stats_config" + @job_stats_name_fg = "#{@fg_json['name']}_1_#{@template_stats_config_name_fg}_run_feature_monitoring" + @template_config_name_fv = "test_config" + @job_name_fv = "#{@fv_json['name']}_1_#{@template_config_name_fv}_run_feature_monitoring" + @template_stats_config_name_fv = "test_stats_config" + @job_stats_name_fv = "#{@fv_json['name']}_1_#{@template_stats_config_name_fv}_run_feature_monitoring" + end + + it "should create a feature monitoring config attached to a featuregroup" do + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + config_res = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config) + expect_status_details(201) + config_json = JSON.parse(config_res) + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fg, @template_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should create a feature monitoring config attached to a feature view" do + config = generate_template_feature_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + config_res = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config) + expect_status_details(201) + config_json = JSON.parse(config_res) + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fv, @template_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should create a stats only feature monitoring config attached to a featuregroup" do + config = generate_template_stats_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + config_res = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config) + expect_status_details(201) + config_json = JSON.parse(config_res) + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fg, @template_stats_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should create a stats only feature monitoring config attached to a feature view" do + config = generate_template_stats_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + config_res = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config) + expect_status_details(201) + config_json = JSON.parse(config_res) + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fv, @template_stats_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + end + end + + describe "Retrieve, Update and Delete feature monitoring configuration for Feature Group and Feature View" do + context 'with valid project, featurestore, feature_group, feature_view and corresponding feature monitoring configs' do + before :all do + with_valid_project + @featurestore_id = get_featurestore_id(@project[:id]) + fg_dto, featuregroup_name = create_cached_featuregroup(@project[:id], @featurestore_id, online:true) + expect_status_details(201) + @fg_json = JSON.parse(fg_dto) + expect_status_details(201) + fv_dto = create_feature_view_from_feature_group(@project[:id], @featurestore_id, @fg_json) + @fv_json = JSON.parse(fv_dto) + expect_status_details(201) + @feature_name = "testfeature" + @template_config_name_fg = "test_config" + @job_name_fg = "#{@fg_json['name']}_1_#{@template_config_name_fg}_run_feature_monitoring" + @template_config_name_fv = "test_config" + @job_name_fv = "#{@fv_json['name']}_1_#{@template_config_name_fv}_run_feature_monitoring" + # Create a feature monitoring config for both featuregroup and feature view + config = generate_template_feature_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + config_res = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config) + expect_status_details(201) + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + config_res = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config) + expect_status_details(201) + end + + + it "should fetch a list of feature monitoring config attached to a featuregroup via feature name" do + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_configs = get_feature_monitoring_configuration_by_feature_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @feature_name) + expect_status_details(200) + config_json = JSON.parse(fetched_configs) + expect(config_json.key?("count")).to be true + expect(config_json["count"] == 1).to be true + expect(config_json.key?("items")).to be true + expect(config_json["items"].length == 1).to be true + expect_feature_monitoring_equal(\ + config_json["items"][0], config, @job_name_fg, @template_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should fetch a list of feature monitoring config attached to a featuregroup" do + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_configs = get_feature_monitoring_configuration_by_entity_fg(\ + @project[:id], @featurestore_id, @fg_json["id"]) + expect_status_details(200) + config_json = JSON.parse(fetched_configs) + expect(config_json.key?("count")).to be true + expect(config_json["count"] == 1).to be true + expect(config_json.key?("items")).to be true + expect(config_json["items"].length == 1).to be true + expect_feature_monitoring_equal(\ + config_json["items"][0], config, @job_name_fg, @template_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should fetch a feature monitoring config attached to a featuregroup via its name" do + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_config_name_fg) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_name) + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fg, @template_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should trigger the execution of the job associated to a feature monitoring config" do + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_config_name_fg) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_name) + execution_res = start_execution(@project[:id], config_json["jobName"]) + execution_json = JSON.parse(execution_res) + expect(execution_json.has_key?("id")).to eq(true) + args = execution_json["args"] + expect(args[-11,args.length]).to eql("config.json") + expect(args[0,26]).to eql("-op run_feature_monitoring") + expect(args[args.length - 12 - config_json['jobName'].length..args.length-13]).to eql(config_json['jobName']) + end + + it "should fetch a feature monitoring config attached to a featuregroup via config id" do + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_config_name_fg) + expect_status_details(200) + json_config_by_name = JSON.parse(fetched_config_by_name) + config_id = json_config_by_name["id"] + fetched_config_by_id = get_feature_monitoring_configuration_by_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config_id) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_id) + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fg, @template_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should update the threshold of the feature monitoring config" do + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_config_name_fg) + expect_status_details(200) + json_config = JSON.parse(fetched_config_by_name) + threshold = config[:statisticsComparisonConfig][:threshold] + 1 + config[:statisticsComparisonConfig][:threshold] = threshold + config[:statisticsComparisonConfig][:strict] = false + config[:jobSchedule][:id] = json_config["jobSchedule"]["id"] + config[:jobSchedule][:cronExpression] = "0 10 * ? * * *" + config[:jobSchedule][:enabled] = false + config[:description] = "updated description" + # job_name is not editable but should be set by backend + config[:jobName] = @job_name_fg + config[:id] = json_config["id"] + updated_config_res = update_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config) + expect_status_details(200) + updated_config_json = JSON.parse(updated_config_res) + expect(updated_config_json["statisticsComparisonConfig"]["threshold"]).to be > (threshold - 0.01) + expect_partial_feature_monitoring_equal(\ + updated_config_json, config, @job_name_fg, @template_config_name_fg, fg_id: @fg_json["id"]) + expect_window_config_equal(updated_config_json["detectionWindowConfig"], config[:detectionWindowConfig]) + expect_window_config_equal(updated_config_json["referenceWindowConfig"], config[:referenceWindowConfig]) + expect_monitoring_scheduler_equal(updated_config_json["jobSchedule"], config[:jobSchedule]) + expect_stats_comparison_equal(updated_config_json["statisticsComparisonConfig"], config[:statisticsComparisonConfig], threshold: threshold) + end + + it "should fail to delete a job attached to a feature monitoring config" do + delete_job(@project[:id], @job_name_fg, expected_status: 400, error_code: 130012) + end + + it "should delete a feature monitoring config attached to a Feature Group" do + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_config_name_fg) + expect_status_details(200) + json_config_by_name = JSON.parse(fetched_config_by_name) + id = json_config_by_name["id"] + delete_feature_monitoring_configuration_by_id_fg(@project[:id], @featurestore_id, @fg_json["id"], id) + expect_status_details(204) + end + + it "should fetch a feature monitoring config attached to a feature view via feature name" do + config = generate_template_feature_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_configs = get_feature_monitoring_configuration_by_feature_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @feature_name) + expect_status_details(200) + config_json = JSON.parse(fetched_configs)["items"][0] + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fv, @template_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should fetch a feature monitoring config attached to a feature view" do + config = generate_template_feature_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_configs = get_feature_monitoring_configuration_by_entity_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"]) + expect_status_details(200) + config_json = JSON.parse(fetched_configs)["items"][0] + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fv, @template_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should fetch a feature monitoring config attached to a feature view via its name" do + config = generate_template_feature_monitoring_config(\ + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_config = get_feature_monitoring_configuration_by_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_config_name_fv) + expect_status_details(200) + config_json = JSON.parse(fetched_config) + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fv, @template_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should trigger the job associated to a feature monitoring config" do + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_config_name_fv) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_name) + execution_res = start_execution(@project[:id], config_json["jobName"]) + execution_json = JSON.parse(execution_res) + expect(execution_json.has_key?("id")).to eq(true) + args = execution_json["args"] + expect(args[-11,args.length]).to eql("config.json") + expect(args[0,26]).to eql("-op run_feature_monitoring") + expect(args[args.length - 12 - config_json['jobName'].length..args.length-13]).to eql(config_json['jobName']) + end + + it "should fetch a feature monitoring config attached to a feature view via config id" do + config = generate_template_feature_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_config_by_feature_name = get_feature_monitoring_configuration_by_feature_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @feature_name) + expect_status_details(200) + json_config_by_feature_name = JSON.parse(fetched_config_by_feature_name) + config_id = json_config_by_feature_name["items"][0]["id"] + fetched_config_by_id = get_feature_monitoring_configuration_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config_id) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_id) + expect_feature_monitoring_equal(\ + config_json, config, @job_name_fv, @template_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should update the threshold of the feature monitoring config attached to a feature view" do + config = generate_template_feature_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_config_name_fv) + expect_status_details(200) + json_config = JSON.parse(fetched_config_by_name) + threshold = config[:statisticsComparisonConfig][:threshold] + 1 + config[:statisticsComparisonConfig][:threshold] = threshold + config[:statisticsComparisonConfig][:strict] = false + config[:jobSchedule][:id] = json_config["jobSchedule"]["id"] + config[:jobSchedule][:cronExpression] = "0 10 * ? * * *" + config[:jobSchedule][:enabled] = false + config[:description] = "updated description" + # job_name is not editable but should be set by backend + config[:jobName] = @job_name_fv + config[:id] = json_config["id"] + updated_config_res = update_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config) + expect_status_details(200) + updated_config_json = JSON.parse(updated_config_res) + expect(updated_config_json["statisticsComparisonConfig"]["threshold"]).to be > (threshold - 0.01) + expect_partial_feature_monitoring_equal(\ + updated_config_json, config, @job_name_fv, @template_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + expect_monitoring_scheduler_equal(updated_config_json["jobSchedule"], config[:jobSchedule]) + expect_window_config_equal(updated_config_json["detectionWindowConfig"], config[:detectionWindowConfig]) + expect_window_config_equal(updated_config_json["referenceWindowConfig"], config[:referenceWindowConfig]) + expect_stats_comparison_equal(updated_config_json["statisticsComparisonConfig"], config[:statisticsComparisonConfig], threshold: threshold) + end + + it "should delete a feature monitoring config attached to a Feature View" do + feature_name = "testfeature" + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fv( + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_config_name_fv) + expect_status_details(200) + json_config_by_name = JSON.parse(fetched_config_by_name) + id = json_config_by_name["id"] + delete_feature_monitoring_configuration_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], id) + expect_status_details(204) + end + end + end + + describe "Retrieve, Update and Delete statistics monitoring configuration for Feature Group and Feature View" do + context 'with valid project, featurestore, feature_group, feature_view and corresponding statistics monitoring configs' do + before :all do + with_valid_project + @featurestore_id = get_featurestore_id(@project[:id]) + fg_dto, featuregroup_name = create_cached_featuregroup(@project[:id], @featurestore_id, online:true) + expect_status_details(201) + @fg_json = JSON.parse(fg_dto) + expect_status_details(201) + fv_dto = create_feature_view_from_feature_group(@project[:id], @featurestore_id, @fg_json) + @fv_json = JSON.parse(fv_dto) + expect_status_details(201) + @feature_name = "testfeature" + @template_stats_config_name_fg = "test_stats_config" + @job_stats_name_fg = "#{@fg_json['name']}_1_#{@template_stats_config_name_fg}_run_feature_monitoring" + @template_stats_config_name_fv = "test_stats_config" + @job_stats_name_fv = "#{@fv_json['name']}_1_#{@template_stats_config_name_fv}_run_feature_monitoring" + # Create a stats monitoring config for both featuregroup and feature view + config = generate_template_stats_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + config_res = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config) + expect_status_details(201) + config = generate_template_stats_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + config_res = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config) + expect_status_details(201) + end + + it "should fetch a list of stats only feature monitoring config attached to a featuregroup via feature name" do + config = generate_template_stats_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_configs = get_feature_monitoring_configuration_by_feature_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @feature_name) + expect_status_details(200) + config_json = JSON.parse(fetched_configs) + expect(config_json.key?("count")).to be true + expect(config_json["count"] == 1).to be true + expect(config_json.key?("items")).to be true + expect(config_json["items"].length == 1).to be true + expect_feature_monitoring_equal(\ + config_json["items"][0], config, @job_stats_name_fg, @template_stats_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should fetch a list of stats only feature monitoring config attached to a featuregroup" do + config = generate_template_stats_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_configs = get_feature_monitoring_configuration_by_entity_fg(\ + @project[:id], @featurestore_id, @fg_json["id"]) + expect_status_details(200) + config_json = JSON.parse(fetched_configs) + expect(config_json.key?("count")).to be true + expect(config_json["count"] == 1).to be true + expect(config_json.key?("items")).to be true + expect(config_json["items"].length == 1).to be true + expect_feature_monitoring_equal(\ + config_json["items"][0], config, @job_stats_name_fg, @template_stats_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should fetch a stats only feature monitoring config attached to a featuregroup via its name" do + config = generate_template_stats_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_stats_config_name_fg) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_name) + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fg, @template_stats_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should trigger the execution of the job associated to a stats only feature monitoring config" do + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_stats_config_name_fg) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_name) + execution_res = start_execution(@project[:id], config_json["jobName"]) + execution_json = JSON.parse(execution_res) + expect(execution_json.has_key?("id")).to eq(true) + args = execution_json["args"] + expect(args[-11,args.length]).to eql("config.json") + expect(args[0,26]).to eql("-op run_feature_monitoring") + expect(args[args.length - 12 - config_json['jobName'].length..args.length-13]).to eql(config_json['jobName']) + end + + it "should fetch a stats only feature monitoring config attached to a featuregroup via config id" do + config = generate_template_stats_monitoring_config(\ + @featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_stats_config_name_fg) + expect_status_details(200) + json_config_by_name = JSON.parse(fetched_config_by_name) + config_id = json_config_by_name["id"] + fetched_config_by_id = get_feature_monitoring_configuration_by_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config_id) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_id) + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fg, @template_stats_config_name_fg, fg_id: @fg_json["id"]) + end + + it "should delete a stats only feature monitoring config" do + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @template_stats_config_name_fg) + expect_status_details(200) + json_config_by_name = JSON.parse(fetched_config_by_name) + id = json_config_by_name["id"] + delete_feature_monitoring_configuration_by_id_fg(@project[:id], @featurestore_id, @fg_json["id"], id) + expect_status_details(204) + end + + it "should fetch a stats only feature monitoring config attached to a feature view via feature name" do + config = generate_template_stats_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_configs = get_feature_monitoring_configuration_by_feature_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @feature_name) + expect_status_details(200) + config_json = JSON.parse(fetched_configs)["items"][0] + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fv, @template_stats_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should fetch a stats only feature monitoring config attached to a feature view" do + config = generate_template_stats_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_configs = get_feature_monitoring_configuration_by_entity_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"]) + expect_status_details(200) + config_json = JSON.parse(fetched_configs)["items"][0] + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fv, @template_stats_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should fetch a stats only feature monitoring config attached to a feature view via its name" do + config = generate_template_stats_monitoring_config(\ + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_config = get_feature_monitoring_configuration_by_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_stats_config_name_fv) + expect_status_details(200) + config_json = JSON.parse(fetched_config) + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fv, @template_stats_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should trigger the job associated to a stats only feature monitoring config" do + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_stats_config_name_fv) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_name) + execution_res = start_execution(@project[:id], config_json["jobName"]) + execution_json = JSON.parse(execution_res) + expect(execution_json.has_key?("id")).to eq(true) + args = execution_json["args"] + expect(args[-11,args.length]).to eql("config.json") + expect(args[0,26]).to eql("-op run_feature_monitoring") + expect(args[args.length - 12 - config_json['jobName'].length..args.length-13]).to eql(config_json['jobName']) + end + + it "should fetch a stats only feature monitoring config attached to a feature view via config id" do + config = generate_template_stats_monitoring_config( + @featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_stats_config_name_fv) + expect_status_details(200) + json_config_by_name = JSON.parse(fetched_config_by_name) + config_id = json_config_by_name["id"] + fetched_config_by_id = get_feature_monitoring_configuration_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config_id) + expect_status_details(200) + config_json = JSON.parse(fetched_config_by_id) + expect_feature_monitoring_equal(\ + config_json, config, @job_stats_name_fv, @template_stats_config_name_fv, fg_id: nil, fv_name: @fv_json["name"], fv_version: @fv_json["version"]) + end + + it "should delete a stats only feature monitoring config attached to a Feature View" do + fetched_config_by_name = get_feature_monitoring_configuration_by_name_fv( + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @template_stats_config_name_fv) + expect_status_details(200) + json_config_by_name = JSON.parse(fetched_config_by_name) + id = json_config_by_name["id"] + delete_feature_monitoring_configuration_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], id) + expect_status_details(204) + end + + # Testing here so we can test fg and fv endpoints for both config and result with same test... + it "should raise feature flag exception when feature monitoring is not enabled for all config endpoints" do + setVar('enable_feature_monitoring', 'false') + config = generate_template_feature_monitoring_config(@featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true) + err_res = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config) + err_json = JSON.parse(err_res) + expect_status_details(400) + expect(err_json["errorCode"]).to eq(270234) + expect(err_json["errorMsg"]).to eql("Feature monitoring is not enabled.") + config = generate_template_feature_monitoring_config(@featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false) + err_res = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config) + err_json = JSON.parse(err_res) + expect_status_details(400) + expect(err_json["errorCode"]).to eq(270234) + expect(err_json["errorMsg"]).to eql("Feature monitoring is not enabled.") + monitoring_time = 1676457000 + result = generate_template_feature_monitoring_result(@featurestore_id, 0, monitoring_time, nil) + err_res = create_feature_monitoring_result_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], result) + err_json = JSON.parse(err_res) + expect_status_details(400) + expect(err_json["errorCode"]).to eq(270234) + expect(err_json["errorMsg"]).to eql("Feature monitoring is not enabled.") + err_res = create_feature_monitoring_result_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result) + err_json = JSON.parse(err_res) + expect_status_details(400) + expect(err_json["errorCode"]).to eq(270234) + expect(err_json["errorMsg"]).to eql("Feature monitoring is not enabled.") + setVar('enable_feature_monitoring', 'true') + end + end + end + + describe "Create feature monitoring result" do + context 'with valid project, featurestore, featuregroup and feature view as well as respective feature monitoring configurations' do + before :all do + with_valid_project + @featurestore_id = get_featurestore_id(@project[:id]) + # create feature monitoring configs from feature group + fg_dto, featuregroup_name = create_cached_featuregroup(@project[:id], @featurestore_id, online:true) + expect_status_details(201) + @fg_json = JSON.parse(fg_dto) + expect_status_details(201) + @config_td = "test_config_td" + config_fg = generate_template_feature_monitoring_config(@featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true,\ + det_window_type: "ROLLING_TIME", ref_window_type: "TRAINING_DATASET", name: @config_td) + config_res_fg = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config_fg) + expect_status_details(201) + @config_td_json_fg = JSON.parse(config_res_fg) # det window: ROLLING_TIME, ref window: TRAINING_DATASET + @config_specific_value = "test_config_specific_value" + config_fg = generate_template_feature_monitoring_config(@featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true,\ + det_window_type: "ROLLING_TIME", ref_window_type: "SPECIFIC_VALUE", name: @config_specific_value) + config_res_fg = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config_fg) + expect_status_details(201) + @config_specific_value_json_fg = JSON.parse(config_res_fg) # det window: ROLLING_TIME, ref window: SPECIFIC_VALUE + # create feature monitoring configs from feature view + fv_dto = create_feature_view_from_feature_group(@project[:id], @featurestore_id, @fg_json) + @fv_json = JSON.parse(fv_dto) + expect_status_details(201) + config_fv = generate_template_feature_monitoring_config(@featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false,\ + det_window_type: "ROLLING_TIME", ref_window_type: "TRAINING_DATASET", name: @config_td) + config_res_fv = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config_fv) + expect_status_details(201) + @config_td_json_fv = JSON.parse(config_res_fv) # det window: ROLLING_TIME, ref window: TRAINING_DATASET + config_fv = generate_template_feature_monitoring_config(@featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false,\ + det_window_type: "ROLLING_TIME", ref_window_type: "SPECIFIC_VALUE", name: @config_specific_value) + config_res_fv = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config_fv) + expect_status_details(201) + @config_specific_value_json_fv = JSON.parse(config_res_fv) # det window: ROLLING_TIME, ref window: SPECIFIC_VALUE + @monitoring_time = 1676457346 + end + + it "should create a feature monitoring result attached to a config via Feature Group Api" do + # create statistics + create_statistics_commit_fg(@project[:id], @featurestore_id, @fg_json["id"], computation_time: 1677670460000) + expect_status_details(200) + parsed_json = JSON.parse(json_body.to_json) + expect(parsed_json.key?("featureDescriptiveStatistics")).to be true + descriptive_statistics_id = parsed_json["featureDescriptiveStatistics"][0]["id"] + # create result + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_specific_value_json_fg["id"], @monitoring_time, \ + descriptive_statistics_id, specific_value: @config_specific_value_json_fg["referenceWindowConfig"]["specificValue"]) + result_res = create_feature_monitoring_result_fg(@project[:id], @featurestore_id, @fg_json["id"], result) + expect_status_details(201) + result_json = JSON.parse(result_res) + result_res = get_feature_monitoring_result_by_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], result_json["id"]) + result_json = JSON.parse(result_res) + + expect(result_json["featureStoreId"]).to eq(@featurestore_id) + expect(result_json.has_key?("id")).to eq(true) + expect(result_json["executionId"]).to eq(result[:executionId]) + expect(result_json["monitoringTime"]).to eq(result[:monitoringTime]) + expect(result_json["shiftDetected"]).to eq(result[:shiftDetected]) + expect(result_json["featureName"]).to eq(result[:featureName]) + expect(result_json["difference"]).to be > (result[:difference] - 0.1) + expect(result_json["difference"]).to be < (result[:difference] + 0.1) + expect(result_json["detectionStatisticsId"]).to be nil + expect(result_json["referenceStatisticsId"]).to be nil + expect(result_json["detectionStatistics"]).not_to be_nil + expect(result_json["referenceStatistics"]).to be_nil + expect(result_json["specificValue"]).to eq(result[:specificValue]) + expect(result_json["emptyDetectionWindow"]).to eq(result[:emptyDetectionWindow]) + expect(result_json["emptyReferenceWindow"]).to eq(result[:emptyReferenceWindow]) + end + + it "should fail to create a feature monitoring result without statistics via Feature Group Api" do + descriptive_statistics_id = 1072 + # create result + result = generate_template_feature_monitoring_result(@featurestore_id, @config_td_json_fg["id"], @monitoring_time, nil) + + # should fail to create without detection statistics + result[:detectionStatisticsId] = nil + result[:referenceStatisticsId] = descriptive_statistics_id + result_res = create_feature_monitoring_result_fg(@project[:id], @featurestore_id, @fg_json["id"], result) + expect_status_details(422) + expect_json(usrMsg: "Descriptive statistics id not provided for the detection window.") + + # should fail to create result without reference statistics if window config is not specific value + result[:detectionStatisticsId] = descriptive_statistics_id + result[:referenceStatisticsId] = nil + result_res = create_feature_monitoring_result_fg(@project[:id], @featurestore_id, @fg_json["id"], result) + expect_status_details(422) + expect_json(usrMsg: "Feature monitoring configuration " + @config_td_json_fg["name"] + " result cannot have null reference statistics id" + + " field when the reference window is configured to use training dataset.") + + # should fail to create result with reference statistics if window config is specific value + result = generate_template_feature_monitoring_result(@featurestore_id, @config_specific_value_json_fg["id"], @monitoring_time, nil) + result[:detectionStatisticsId] = descriptive_statistics_id + result[:referenceStatisticsId] = descriptive_statistics_id + result_res = create_feature_monitoring_result_fg(@project[:id], @featurestore_id, @fg_json["id"], result) + expect_status_details(422) + expect_json(usrMsg: "Feature monitoring configuration " + @config_specific_value_json_fv["name"] + " result cannot have null specific value field" + + " when the reference window is configured to use specific value.") + end + + it "should create a feature monitoring result attached to a config via Feature View Api" do + # create statistics + create_statistics_commit_fv(@project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], computation_time: 1677670460000) + expect_status_details(200) + parsed_json = JSON.parse(json_body.to_json) + expect(parsed_json.key?("featureDescriptiveStatistics")).to be true + descriptive_statistics_id = parsed_json["featureDescriptiveStatistics"][0]["id"] + # create result + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_specific_value_json_fv["id"], @monitoring_time, \ + descriptive_statistics_id, specific_value: @config_specific_value_json_fg["referenceWindowConfig"]["specificValue"]) + result_res = create_feature_monitoring_result_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result) + expect_status_details(201) + result_json = JSON.parse(result_res) + result_res = get_feature_monitoring_result_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result_json["id"]) + result_json = JSON.parse(result_res) + + result_json = JSON.parse(result_res) + expect(result_json["featureStoreId"]).to eq(@featurestore_id) + expect(result_json.has_key?("id")).to eq(true) + expect(result_json["executionId"]).to eq(result[:executionId]) + expect(result_json["monitoringTime"]).to eq(result[:monitoringTime]) + expect(result_json["shiftDetected"]).to eq(result[:shiftDetected]) + expect(result_json["featureName"]).to eq(result[:featureName]) + expect(result_json["difference"]).to be > (result[:difference] - 0.1) + expect(result_json["difference"]).to be < (result[:difference] + 0.1) + expect(result_json["detectionStatisticsId"]).to be nil + expect(result_json["referenceStatisticsId"]).to be nil + expect(result_json["detectionStatistics"]).not_to be_nil + expect(result_json["referenceStatistics"]).to be nil + expect(result_json["specificValue"]).to eq(result[:specificValue]) + expect(result_json["emptyDetectionWindow"]).to eq(result[:emptyDetectionWindow]) + expect(result_json["emptyReferenceWindow"]).to eq(result[:emptyReferenceWindow]) + end + + it "should fail to create a feature monitoring result without statistics via Feature View Api" do + descriptive_statistics_id = 1072 + # create result + result = generate_template_feature_monitoring_result(@featurestore_id, @config_td_json_fv["id"], @monitoring_time, nil) + + # should fail to create without detection statistics + result[:detectionStatisticsId] = nil + result[:referenceStatisticsId] = descriptive_statistics_id + result_res = create_feature_monitoring_result_fv(@project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result) + expect_status_details(422) + expect_json(usrMsg: "Descriptive statistics id not provided for the detection window.") + + # should fail to create result without reference statistics if window config is not specific value + result[:detectionStatisticsId] = descriptive_statistics_id + result[:referenceStatisticsId] = nil + result_res = create_feature_monitoring_result_fv(@project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result) + expect_status_details(422) + expect_json(usrMsg: "Feature monitoring configuration " + @config_td_json_fg["name"] + " result cannot have null reference statistics id" + + " field when the reference window is configured to use training dataset.") + + # should fail to create result with reference statistics if window config is specific value + result = generate_template_feature_monitoring_result(@featurestore_id, @config_specific_value_json_fv["id"], @monitoring_time, nil) + result[:detectionStatisticsId] = descriptive_statistics_id + result[:referenceStatisticsId] = descriptive_statistics_id + result_res = create_feature_monitoring_result_fv(@project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result) + expect_status_details(422) + expect_json(usrMsg: "Feature monitoring configuration " + @config_specific_value_json_fv["name"] + " result cannot have null specific value field" + + " when the reference window is configured to use specific value.") + end + end + end + + describe "Retrieve feature monitoring results" do + context 'with valid project, featurestore, feature group, feature view, feature monitoring configurations and results' do + before :all do + with_valid_project + @featurestore_id = get_featurestore_id(@project[:id]) + # create feature monitoring configs from feature group + fg_dto, featuregroup_name = create_cached_featuregroup(@project[:id], @featurestore_id, online:true) + expect_status_details(201) + @fg_json = JSON.parse(fg_dto) + expect_status_details(201) + @config_td = "test_config_td" + config_fg = generate_template_feature_monitoring_config(@featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true,\ + det_window_type: "ROLLING_TIME", ref_window_type: "TRAINING_DATASET", name: @config_td) + config_res_fg = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config_fg) + expect_status_details(201) + @config_rt_td_json_fg = JSON.parse(config_res_fg) # det window: ROLLING_TIME, ref window: TRAINING_DATASET + @config_specific_value = "test_config_specific_value" + config_fg = generate_template_feature_monitoring_config(@featurestore_id, @fg_json["id"], @fg_json["name"], @fg_json["version"], true,\ + det_window_type: "ROLLING_TIME", ref_window_type: "SPECIFIC_VALUE", name: @config_specific_value) + config_res_fg = create_feature_monitoring_configuration_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], config_fg) + expect_status_details(201) + @config_rt_sv_json_fg = JSON.parse(config_res_fg) # det window: ROLLING_TIME, ref window: SPECIFIC_VALUE + # create feature monitoring configs from feature view + fv_dto = create_feature_view_from_feature_group(@project[:id], @featurestore_id, @fg_json) + @fv_json = JSON.parse(fv_dto) + expect_status_details(201) + config_fv = generate_template_feature_monitoring_config(@featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false,\ + det_window_type: "ROLLING_TIME", ref_window_type: "TRAINING_DATASET", name: @config_td) + config_res_fv = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config_fv) + expect_status_details(201) + @config_rt_td_json_fv = JSON.parse(config_res_fv) # det window: ROLLING_TIME, ref window: TRAINING_DATASET + config_fv = generate_template_feature_monitoring_config(@featurestore_id, @fv_json["id"], @fv_json["name"], @fv_json["version"], false,\ + det_window_type: "ROLLING_TIME", ref_window_type: "SPECIFIC_VALUE", name: @config_specific_value) + config_res_fv = create_feature_monitoring_configuration_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], config_fv) + expect_status_details(201) + @config_rt_sv_json_fv = JSON.parse(config_res_fv) # det window: ROLLING_TIME, ref window: SPECIFIC_VALUE + # create feature monitoring results + @monitoring_time = 1676457000 + @desc_stats = generate_template_feature_descriptive_statistics(exact_uniqueness: true, shift_delta: 0.4) + create_statistics_commit_fg(@project[:id], @featurestore_id, @fg_json["id"], feature_descriptive_statistics: @desc_stats, computation_time: 1677670460000) + expect_status_details(200) + parsed_json = JSON.parse(json_body.to_json) + expect(parsed_json.key?("featureDescriptiveStatistics")).to be true + @descriptive_statistics_id_fg = parsed_json["featureDescriptiveStatistics"][0]["id"] + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fg["id"], @monitoring_time, @descriptive_statistics_id_fg, reference_statistics_id: @descriptive_statistics_id_fg) + result_res = create_feature_monitoring_result_fg(@project[:id], @featurestore_id, @fg_json["id"], result) + expect_status_details(201) + create_statistics_commit_fv(@project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], feature_descriptive_statistics: @desc_stats, computation_time: 1677670460000) + expect_status_details(200) + parsed_json = JSON.parse(json_body.to_json) + expect(parsed_json.key?("featureDescriptiveStatistics")).to be true + @descriptive_statistics_id_fv = parsed_json["featureDescriptiveStatistics"][0]["id"] + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fv["id"], @monitoring_time, @descriptive_statistics_id_fv, reference_statistics_id: @descriptive_statistics_id_fv) + result_res = create_feature_monitoring_result_fv(@project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result) + expect_status_details(201) + end + + it "should fetch results attached to a monitoring config via Feature Group Api" do + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fg["id"], @monitoring_time, @descriptive_statistics_id_fg, reference_statistics_id: @descriptive_statistics_id_fg) + result_res = get_all_feature_monitoring_results_by_config_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @config_rt_td_json_fg["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + + # validate first FM result against the template + expect(result_json["count"]).to eq(1) + expect(result_json["items"][0]["featureStoreId"]).to eql(@featurestore_id) + expect(result_json["items"][0].has_key?("id")).to eq(true) + expect(result_json["items"][0]["executionId"]).to eq(result[:executionId]) + expect(result_json["items"][0]["monitoringTime"]).to eq(result[:monitoringTime]) + expect(result_json["items"][0]["shiftDetected"]).to eq(result[:shiftDetected]) + expect(result_json["items"][0]["featureName"]).to eq(result[:featureName]) + expect(result_json["items"][0]["difference"]).to be > (result[:difference] - 0.1) + expect(result_json["items"][0]["difference"]).to be < (result[:difference] + 0.1) + expect(result_json["items"][0]["detectionStatisticsId"]).not_to be_nil + expect(result_json["items"][0]["referenceStatisticsId"]).not_to be_nil + expect(result_json["items"][0]["detectionStatistics"]).to be nil + expect(result_json["items"][0]["referenceStatistics"]).to be nil + + # validate first FM result against the FM result in the database + result_single_res = get_feature_monitoring_result_by_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], result_json["items"][0]["id"]) + expect_status_details(200) + result_single_json = JSON.parse(result_single_res) + expect(result_single_json["detectionStatisticsId"]).to be nil + expect(result_single_json["referenceStatisticsId"]).to be nil + expect(result_single_json["detectionStatistics"]).not_to be_nil + expect(result_single_json["referenceStatistics"]).not_to be_nil + result_single_json["detectionStatisticsId"] = result_json["items"][0]["detectionStatisticsId"] + result_single_json["referenceStatisticsId"] = result_json["items"][0]["referenceStatisticsId"] + result_single_json.delete("detectionStatistics") # stats not included without expansion \ + result_single_json.delete("referenceStatistics") # when fetching multiple results + expect(result_single_json).to eql(result_json["items"][0]) + end + + it "should fetch results attached to a monitoring config, including statistics when the statistics expansion is set via Feature Group Api" do + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fg["id"], @monitoring_time, @descriptive_statistics_id_fg, reference_statistics_id: @descriptive_statistics_id_fg) + desc_stats = generate_template_feature_descriptive_statistics(exact_uniqueness: true, shift_delta: 0.4)[0] + result[:detectionStatistics] = desc_stats + result[:referenceStatistics] = desc_stats + # get results + result_res = get_all_feature_monitoring_results_by_config_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @config_rt_td_json_fg["id"], with_stats: true) + expect_status_details(200) + result_json = JSON.parse(result_res) + + # validate first FM result against the templates + expect(result_json["count"]).to eq(1) + expect(result_json["items"][0]["featureStoreId"]).to eql(@featurestore_id) + expect(result_json["items"][0].has_key?("id")).to eq(true) + expect(result_json["items"][0]["executionId"]).to eq(result[:executionId]) + expect(result_json["items"][0]["monitoringTime"]).to eq(result[:monitoringTime]) + expect(result_json["items"][0]["shiftDetected"]).to eq(result[:shiftDetected]) + expect(result_json["items"][0]["featureName"]).to eq(result[:featureName]) + expect(result_json["items"][0]["difference"]).to be > (result[:difference] - 0.1) + expect(result_json["items"][0]["difference"]).to be < (result[:difference] + 0.1) + expect(result_json["items"][0]["detectionStatisticsId"]).to be_nil + expect(result_json["items"][0]["referenceStatisticsId"]).to be_nil + expect_desc_statistics_to_be_eq(result_json["items"][0]["detectionStatistics"], result[:detectionStatistics]) + expect_desc_statistics_to_be_eq(result_json["items"][0]["referenceStatistics"], result[:referenceStatistics]) + # validate first FM result against the FM result in the database + result_single_res = get_feature_monitoring_result_by_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], result_json["items"][0]["id"]) + expect_status_details(200) + result_single_json = JSON.parse(result_single_res) + expect(result_single_json).to eql(result_json["items"][0]) + end + + it "should fetch results attached to a monitoring config via Feature View Api" do + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fg["id"], @monitoring_time, @descriptive_statistics_id_fv, reference_statistics_id: @descriptive_statistics_id_fv) + result_res = get_all_feature_monitoring_results_by_config_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @config_rt_td_json_fv["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + + # validate first FM result against the templates + expect(result_json["count"]).to eq(1) + expect(result_json["items"][0]["featureStoreId"]).to eql(@featurestore_id) + expect(result_json["items"][0].has_key?("id")).to eq(true) + expect(result_json["items"][0]["executionId"]).to eq(result[:executionId]) + expect(result_json["items"][0]["monitoringTime"]).to eq(result[:monitoringTime]) + expect(result_json["items"][0]["shiftDetected"]).to eq(result[:shiftDetected]) + expect(result_json["items"][0]["featureName"]).to eq(result[:featureName]) + expect(result_json["items"][0]["difference"]).to be > (result[:difference] - 0.1) + expect(result_json["items"][0]["difference"]).to be < (result[:difference] + 0.1) + expect(result_json["items"][0]["detectionStatisticsId"]).not_to be_nil + expect(result_json["items"][0]["referenceStatisticsId"]).not_to be_nil + expect(result_json["items"][0]["detectionStatistics"]).to be nil + expect(result_json["items"][0]["referenceStatistics"]).to be nil + # validate first FM result against the FM result in the database + result_single_res = get_feature_monitoring_result_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result_json["items"][0]["id"]) + expect_status_details(200) + result_single_json = JSON.parse(result_single_res) + expect(result_single_json["detectionStatisticsId"]).to be nil + expect(result_single_json["referenceStatisticsId"]).to be nil + expect(result_single_json["detectionStatistics"]).not_to be_nil + expect(result_single_json["referenceStatistics"]).not_to be_nil + result_single_json["detectionStatisticsId"] = result_json["items"][0]["detectionStatisticsId"] + result_single_json["referenceStatisticsId"] = result_json["items"][0]["referenceStatisticsId"] + result_single_json.delete("detectionStatistics") # stats not included without expansion \ + result_single_json.delete("referenceStatistics") # when fetching multiple results + expect(result_single_json).to eql(result_json["items"][0]) + end + + it "should fetch results attached to a monitoring config, including statistics when the statistics expansion is set via Feature View Api" do + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fg["id"], @monitoring_time, @descriptive_statistics_id_fv, reference_statistics_id: @descriptive_statistics_id_fv) + desc_stats = generate_template_feature_descriptive_statistics(exact_uniqueness: true, shift_delta: 0.4)[0] + result[:detectionStatistics] = desc_stats + result[:referenceStatistics] = desc_stats + # get results + result_res = get_all_feature_monitoring_results_by_config_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @config_rt_td_json_fv["id"], with_stats: true) + expect_status_details(200) + result_json = JSON.parse(result_res) + + # validate first FM result against the templates + expect(result_json["count"]).to eq(1) + expect(result_json["items"][0]["featureStoreId"]).to eql(@featurestore_id) + expect(result_json["items"][0].has_key?("id")).to eq(true) + expect(result_json["items"][0]["executionId"]).to eq(result[:executionId]) + expect(result_json["items"][0]["monitoringTime"]).to eq(result[:monitoringTime]) + expect(result_json["items"][0]["shiftDetected"]).to eq(result[:shiftDetected]) + expect(result_json["items"][0]["featureName"]).to eq(result[:featureName]) + expect(result_json["items"][0]["difference"]).to be > (result[:difference] - 0.1) + expect(result_json["items"][0]["difference"]).to be < (result[:difference] + 0.1) + expect(result_json["items"][0]["detectionStatisticsId"]).to be_nil + expect(result_json["items"][0]["referenceStatisticsId"]).to be_nil + expect_desc_statistics_to_be_eq(result_json["items"][0]["detectionStatistics"], result[:detectionStatistics]) + expect_desc_statistics_to_be_eq(result_json["items"][0]["referenceStatistics"], result[:referenceStatistics]) + # validate first FM result against the FM result in the database + result_single_res = get_feature_monitoring_result_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result_json["items"][0]["id"]) + expect_status_details(200) + result_single_json = JSON.parse(result_single_res) + expect(result_single_json).to eql(result_json["items"][0]) + end + + it "should fetch multiple feature monitoring result attached to a config via Feature Group Api" do + monitoring_time_1 = 1676457000 + monitoring_time_2 = 1676557000 + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fg["id"], monitoring_time_2, @descriptive_statistics_id_fg, reference_statistics_id: @descriptive_statistics_id_fg) + result_res = create_feature_monitoring_result_fg(@project[:id], @featurestore_id, @fg_json["id"], result) + expect_status_details(201) + result_res = get_all_feature_monitoring_results_by_config_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @config_rt_td_json_fg["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + expect(result_json["count"]).to eq(2) + expect(result_json["items"][0]["monitoringTime"]).to eq(monitoring_time_2) + expect(result_json["items"][1]["monitoringTime"]).to eq(monitoring_time_1) + end + + it "should fetch multiple feature monitoring result attached to a config via Feature View Api" do + monitoring_time_1 = 1676457000 + monitoring_time_2 = 1676557000 + result = generate_template_feature_monitoring_result(\ + @featurestore_id, @config_rt_td_json_fv["id"], monitoring_time_2, @descriptive_statistics_id_fv, reference_statistics_id: @descriptive_statistics_id_fv) + result_res = create_feature_monitoring_result_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result) + expect_status_details(201) + result_res = get_all_feature_monitoring_results_by_config_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @config_rt_td_json_fv["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + expect(result_json["count"]).to eq(2) + expect(result_json["items"][0]["monitoringTime"]).to eq(monitoring_time_2) + expect(result_json["items"][1]["monitoringTime"]).to eq(monitoring_time_1) + end + + it "should delete a single feature monitoring result attached to a config via Feature Group Api" do + result_res = get_all_feature_monitoring_results_by_config_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @config_rt_td_json_fg["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + expect(result_json["count"]).to eq(2) + delete_feature_monitoring_result_by_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], result_json["items"][0]["id"]) + expect_status_details(204) + result_res = get_all_feature_monitoring_results_by_config_id_fg(\ + @project[:id], @featurestore_id, @fg_json["id"], @config_rt_td_json_fg["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + expect(result_json["count"]).to eq(1) + end + + it "should delete a single feature monitoring result attached to a config via Feature View Api" do + result_res = get_all_feature_monitoring_results_by_config_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @config_rt_td_json_fv["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + expect(result_json["count"]).to eq(2) + delete_feature_monitoring_result_by_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], result_json["items"][0]["id"]) + expect_status_details(204) + result_res = get_all_feature_monitoring_results_by_config_id_fv(\ + @project[:id], @featurestore_id, @fv_json["name"], @fv_json["version"], @config_rt_td_json_fv["id"]) + expect_status_details(200) + result_json = JSON.parse(result_res) + expect(result_json["count"]).to eq(1) + end + end + end + end +end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/feature_store_activity_spec.rb b/hopsworks-IT/src/test/ruby/spec/feature_store_activity_spec.rb index 6dca0ba9c6..e626fe3cdd 100644 --- a/hopsworks-IT/src/test/ruby/spec/feature_store_activity_spec.rb +++ b/hopsworks-IT/src/test/ruby/spec/feature_store_activity_spec.rb @@ -1,388 +1,415 @@ -# This file is part of Hopsworks -# Copyright (C) 2021, Logical Clocks AB. All rights reserved -# -# Hopsworks is free software: you can redistribute it and/or modify it under the terms of -# the GNU Affero General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# -# Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -# PURPOSE. See the GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License along with this program. -# If not, see . - -describe "On #{ENV['OS']}" do - after :all do - clean_all_test_projects(spec: "featuregroup") - end - - describe "fs activity" do - before :all do - with_valid_project - end + # This file is part of Hopsworks + # Copyright (C) 2021, Logical Clocks AB. All rights reserved + # + # Hopsworks is free software: you can redistribute it and/or modify it under the terms of + # the GNU Affero General Public License as published by the Free Software Foundation, + # either version 3 of the License, or (at your option) any later version. + # + # Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + # PURPOSE. See the GNU Affero General Public License for more details. + # + # You should have received a copy of the GNU Affero General Public License along with this program. + # If not, see . - it "should be able to retrieve feature group creation event" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - expect_status_details(201) - feature_group_id = JSON.parse(json_result)["id"] - - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{feature_group_id}/activity" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("METADATA") - expect(activity["items"][0]["metadata"]).to eql("Feature group was created") + describe "On #{ENV['OS']}" do + after :all do + clean_all_test_projects(spec: "featuregroup") end - it "should be able to retrieve alter features event" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, feature_group_name = create_cached_featuregroup(@project[:id], featurestore_id) - parsed_json = JSON.parse(json_result) - expect_status_details(201) - new_schema = [ - {type: "INT", name: "testfeature", description: "testfeaturedescription", primary: true, partition: false}, - {type: "DOUBLE", name: "testfeature2", description: "testfeaturedescription", primary: false, - onlineType: "DOUBLE", partition: false, defaultValue: "10.0"}, - ] - _ = update_cached_featuregroup_metadata(@project[:id], featurestore_id, - parsed_json["id"], - parsed_json["version"], - featuregroup_name: feature_group_name, - features: new_schema) - - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity?sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("METADATA") - expect(activity["items"][0]["metadata"]).to eql("Feature group was altered New features: testfeature2") - end + describe "fs activity" do + before :all do + with_valid_project + end - it "should be able to retrieve online enable event" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - parsed_json = JSON.parse(json_result) - expect_status_details(201) - sleep 1 - enable_cached_featuregroup_online(@project[:id], featurestore_id, parsed_json["id"]) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity?sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("METADATA") - expect(activity["items"][0]["metadata"]).to eql("Feature group available online") - end + it "should be able to retrieve feature group creation event" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + expect_status_details(201) + feature_group_id = JSON.parse(json_result)["id"] - it "should be able to retrieve commit events" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, feature_group_name = create_cached_featuregroup_with_partition(@project[:id], - featurestore_id, - time_travel_format: "HUDI") - parsed_json = JSON.parse(json_result) - feature_group_id = parsed_json["id"] - commit_metadata = {commitDateString:20201024221125,commitTime:1603577485000,rowsInserted:4,rowsUpdated:2,rowsDeleted:0} - commit_cached_featuregroup(@project[:id], featurestore_id, feature_group_id, commit_metadata: commit_metadata) - - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{feature_group_id}/activity?filter_by=type:commit&expand=commits" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("COMMIT") - expect(activity["items"][0]["timestamp"]).to eql(activity["items"][0]["commit"]["commitTime"]) - expect(activity["items"][0]["commit"]['rowsInserted']).to eql(4) - expect(activity["items"][0]["commit"]['rowsUpdated']).to eql(2) - expect(activity["items"][0]["commit"]['rowsDeleted']).to eql(0) - end + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{feature_group_id}/activity" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("METADATA") + expect(activity["items"][0]["metadata"]).to eql("Feature group was created") + end - it "should be able to retrieve statistics events" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - parsed_json = JSON.parse(json_result) - create_statistics_commit_fg(@project[:id], featurestore_id, parsed_json["id"]) - expect_status_details(200) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity?filter_by=type:statistics&expand=statistics" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("STATISTICS") - expect(activity["items"][0]["statistics"]["computationTime"]).to eql(1597903688000) - end + it "should be able to retrieve alter features event" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, feature_group_name = create_cached_featuregroup(@project[:id], featurestore_id) + parsed_json = JSON.parse(json_result) + expect_status_details(201) + new_schema = [ + {type: "INT", name: "testfeature", description: "testfeaturedescription", primary: true, partition: false}, + {type: "DOUBLE", name: "testfeature2", description: "testfeaturedescription", primary: false, + onlineType: "DOUBLE", partition: false, defaultValue: "10.0"}, + ] + _ = update_cached_featuregroup_metadata(@project[:id], featurestore_id, + parsed_json["id"], + parsed_json["version"], + featuregroup_name: feature_group_name, + features: new_schema) - it "should be able to retrieve activity related to creation of an expectation suite" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - expect_status_details(201) - suite_json = JSON.parse(suite_dto) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") - expect(activity["items"][0]["metadata"]).to eql("An Expectation Suite was attached to the Feature Group. ") - expect(activity["items"][0]["expectationSuite"]["expectationSuiteName"]).to eql(suite_json["expectationSuiteName"]) - expect(activity["items"][0]["expectationSuite"]["expectations"][0]["expectationType"]).to eql(suite_json["expectations"][0]["expectationType"]) - expect(activity["items"][0]["expectationSuite"]["expectations"][0]["meta"]["expectationId"]).to eql(suite_json["expectations"][0]["meta"]["expectationId"]) - end + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity?sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("METADATA") + expect(activity["items"][0]["metadata"]).to eql("Feature group was altered New features: testfeature2") + end - it "should be able to retrieve activity related to the update of an expectation suite" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - expect_status_details(201) - suite_json = JSON.parse(suite_dto) - template_expectation = generate_template_expectation() - template_expectation["expectationType"] = "expect_column_std_to_be_between" - suite_json["expectations"].append(template_expectation) - suite_json["expectationSuiteName"] = "updated_suite" - sleep 1 - update_expectation_suite(@project[:id], featurestore_id, fg_json["id"], suite_json) - expect_status_details(200) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") - expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. ") - expect(activity["items"][0]["expectationSuite"]["expectationSuiteName"]).to eql("updated_suite") - expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(2) - end + it "should be able to retrieve online enable event" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + parsed_json = JSON.parse(json_result) + expect_status_details(201) + sleep 1 + enable_cached_featuregroup_online(@project[:id], featurestore_id, parsed_json["id"]) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity?sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("METADATA") + expect(activity["items"][0]["metadata"]).to eql("Feature group available online") + end - it "should be able to retrieve activity related to the metadata update of an expectation suite" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - expect_status_details(201) - suite_json = JSON.parse(suite_dto) - suite_json["expectationSuiteName"] = "updated_suite" - sleep 1 - update_metadata_expectation_suite(@project[:id], featurestore_id, fg_json["id"], suite_json) - expect_status_details(200) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") - expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite metadata was updated. ") - expect(activity["items"][0]["expectationSuite"]["expectationSuiteName"]).to eql("updated_suite") - end + it "should be able to retrieve commit events" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, feature_group_name = create_cached_featuregroup_with_partition(@project[:id], + featurestore_id, + time_travel_format: "HUDI") + parsed_json = JSON.parse(json_result) + feature_group_id = parsed_json["id"] + commit_metadata = {commitDateString:20201024221125,commitTime:1603577485000,rowsInserted:4,rowsUpdated:2,rowsDeleted:0} + commit_cached_featuregroup(@project[:id], featurestore_id, feature_group_id, commit_metadata: commit_metadata) - it "should be able to retrieve activity related to deletion of an expectation suite" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - expect_status_details(201) - suite_json = JSON.parse(suite_dto) - sleep 1 - delete_expectation_suite(@project[:id], featurestore_id, fg_json["id"], suite_json["id"]) - expect_status_details(204) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") - expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was deleted. ") - end + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{feature_group_id}/activity?filter_by=type:commit&expand=commits" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("COMMIT") + expect(activity["items"][0]["timestamp"]).to eql(activity["items"][0]["commit"]["commitTime"]) + expect(activity["items"][0]["commit"]['rowsInserted']).to eql(4) + expect(activity["items"][0]["commit"]['rowsUpdated']).to eql(2) + expect(activity["items"][0]["commit"]['rowsDeleted']).to eql(0) + end - it "should be able to retrieve activity related to append of an expectation" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - expect_status_details(201) - suite_json = JSON.parse(suite_dto) - template_expectation = generate_template_expectation() - template_expectation["expectationType"] = "expect_column_std_to_be_between" - sleep 1 - dto_expectation = create_expectation(@project[:id], featurestore_id, fg_json["id"], suite_json["id"], template_expectation) - expect_status_details(201) - json_expectation = JSON.parse(dto_expectation) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") - expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. Created expectation with id: #{json_expectation["id"]}.") - expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(2) - end + it "should be able to retrieve feature group statistics events" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + parsed_json = JSON.parse(json_result) + create_statistics_commit_fg(@project[:id], featurestore_id, parsed_json["id"]) + expect_status_details(200) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity?filter_by=type:statistics&expand=statistics" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("STATISTICS") + expect(activity["items"][0]["statistics"]["computationTime"]).to eql(1597903688000) + end - it "should be able to retrieve activity related to the update of a single expectation" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - expect_status_details(201) - suite_json = JSON.parse(suite_dto) - template_expectation = generate_template_expectation() - template_expectation["meta"] = "{\"whoAmI\": \"updated_expectation\"}" - template_expectation["id"] = suite_json["expectations"][0]["id"] - sleep 1 - update_expectation(@project[:id], featurestore_id, fg_json["id"], suite_json["id"], template_expectation["id"], template_expectation) - expect_status_details(200) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") - expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. Updated expectation with id: #{template_expectation["id"]}.") - expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(1) - updated_meta = JSON.parse(activity["items"][0]["expectationSuite"]["expectations"][0]["meta"]) - expect(updated_meta["whoAmI"]).to eql("updated_expectation") - end + it "should be able to retrieve feature view statistics events" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + parsed_json = JSON.parse(json_result) + json_result = create_feature_view_from_feature_group(@project[:id], featurestore_id, parsed_json) + parsed_json = JSON.parse(json_result) + expect_status_details(201) + create_statistics_commit_fv(@project[:id], featurestore_id, parsed_json["name"], parsed_json["version"]) + expect_status_details(200) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featureview/#{parsed_json["name"]}/version/#{parsed_json["version"]}/activity?filter_by=type:statistics&expand=statistics" + expect_status_details(400, error_code: 260002) + end - it "should be able to retrieve activity related to deletion of an expectation" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - suite_json = JSON.parse(suite_dto) - sleep 1 - delete_expectation(@project[:id], featurestore_id, fg_json["id"], suite_json["id"], suite_json["expectations"][0]["id"]) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") - expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. Deleted expectation with id: #{suite_json["expectations"][0]["id"]}.") - expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(0) - end + it "should be able to retrieve training dataset statistics events" do + featurestore_id = get_featurestore_id(@project[:id]) + all_metadata = create_featureview_training_dataset_from_project(@project) + trainingdataset = all_metadata["response"] + featureview = all_metadata["featureView"] + create_statistics_commit_td(@project[:id], featurestore_id, featureview["name"], featureview["version"], trainingdataset["version"]) + expect_status_details(200) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/trainingdatasets/#{trainingdataset["id"]}/activity?filter_by=type:statistics&expand=statistics" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("STATISTICS") + expect(activity["items"][0]["statistics"]["computationTime"]).to eql(1597903688000) + end - it "should be able to retrieve activity related to upload of a validation report" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - fg_json = JSON.parse(json_result) - expectation_suite = generate_template_expectation_suite() - suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) - expect_status_details(201) - suite_json = JSON.parse(suite_dto) - report = generate_template_validation_report() - report_dto = create_validation_report(@project[:id], featurestore_id, fg_json["id"], report) - expect_status_details(201) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:validations&expand=validationreport" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("VALIDATIONS") - expect(activity["items"][0]["metadata"]).to eql("A Validation Report was uploaded.") - expect(activity["items"][0]["validationReport"]["success"]).to eql(report[:success]) - expect(activity["items"][0]["validationReport"]["exceptionInfo"]).to eql(report[:exceptionInfo]) - expect(activity["items"][0]["validationReport"]["statistics"]).to eql(report[:statistics]) - expect(activity["items"][0]["validationReport"]["meta"]).to eql(report[:meta]) - end + it "should be able to retrieve activity related to creation of an expectation suite" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + expect_status_details(201) + suite_json = JSON.parse(suite_dto) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") + expect(activity["items"][0]["metadata"]).to eql("An Expectation Suite was attached to the Feature Group. ") + expect(activity["items"][0]["expectationSuite"]["expectationSuiteName"]).to eql(suite_json["expectationSuiteName"]) + expect(activity["items"][0]["expectationSuite"]["expectations"][0]["expectationType"]).to eql(suite_json["expectations"][0]["expectationType"]) + expect(activity["items"][0]["expectationSuite"]["expectations"][0]["meta"]["expectationId"]).to eql(suite_json["expectations"][0]["meta"]["expectationId"]) + end - it "should be able to retrieve a limited set of events" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - parsed_json = JSON.parse(json_result) - base_timestamp = 1597903688000 - increment = 100000 - (1..15).each do |i| - create_statistics_commit_fg(@project[:id], featurestore_id, parsed_json["id"], computation_time: base_timestamp + increment * i) + it "should be able to retrieve activity related to the update of an expectation suite" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + expect_status_details(201) + suite_json = JSON.parse(suite_dto) + template_expectation = generate_template_expectation() + template_expectation["expectationType"] = "expect_column_std_to_be_between" + suite_json["expectations"].append(template_expectation) + suite_json["expectationSuiteName"] = "updated_suite" + sleep 1 + update_expectation_suite(@project[:id], featurestore_id, fg_json["id"], suite_json) expect_status_details(200) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") + expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. ") + expect(activity["items"][0]["expectationSuite"]["expectationSuiteName"]).to eql("updated_suite") + expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(2) end - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + - "?filter_by=type:statistics&expand=statistics&sort_by=timestamp:desc&limit=5&offset=0" - expect_status_details(200) - activity = JSON.parse(response.body) - - expect(activity["items"].count).to eql(5) - # The first element should be the most recent, day 15 - expect(activity["items"][0]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 15) - # The last element should be the least recent of the batch, day 11 - expect(activity["items"][4]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 11) - - # Test second batch - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + - "?filter_by=type:statistics&expand=statistics&sort_by=timestamp:desc&limit=5&offset=5" - expect_status_details(200) - activity = JSON.parse(response.body) - - expect(activity["items"].count).to eql(5) - # The first element should be the most recent, day 10 - expect(activity["items"][0]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 10) - # The last element should be the least recent of the batch, day 6 - expect(activity["items"][4]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 6) - end - it "should be able to filter events by timestamp" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - parsed_json = JSON.parse(json_result) - base_timestamp = 1597903688000 - increment = 100000 - (1..15).each do |i| - create_statistics_commit_fg(@project[:id], featurestore_id, parsed_json["id"], computation_time: base_timestamp + increment * i) + it "should be able to retrieve activity related to the metadata update of an expectation suite" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + expect_status_details(201) + suite_json = JSON.parse(suite_dto) + suite_json["expectationSuiteName"] = "updated_suite" + sleep 1 + update_metadata_expectation_suite(@project[:id], featurestore_id, fg_json["id"], suite_json) expect_status_details(200) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") + expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite metadata was updated. ") + expect(activity["items"][0]["expectationSuite"]["expectationSuiteName"]).to eql("updated_suite") end - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + - "?filter_by=type:statistics&expand=statistics&sort_by=timestamp:desc&limit=5&offset=2" - expect_status_details(200) - activity = JSON.parse(response.body) - upper_timestamp = activity['items'][0]["timestamp"].to_i + 1 - lower_timestamp = activity['items'][4]["timestamp"].to_i - 1 - - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + - "?filter_by=type:statistics&filter_by=timestamp_lt:#{upper_timestamp}&filter_by=timestamp_gt:#{lower_timestamp}&expand=statistics&sort_by=timestamp" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["timestamp"].to_i).to eql(upper_timestamp - 1) - expect(activity["items"][4]["timestamp"].to_i).to eql(lower_timestamp + 1) - end - it "should be able to expand users resource" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - expect_status_details(201) - feature_group_id = JSON.parse(json_result)["id"] + it "should be able to retrieve activity related to deletion of an expectation suite" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + expect_status_details(201) + suite_json = JSON.parse(suite_dto) + sleep 1 + delete_expectation_suite(@project[:id], featurestore_id, fg_json["id"], suite_json["id"]) + expect_status_details(204) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") + expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was deleted. ") + end - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{feature_group_id}/activity?expand=users" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["user"]["username"]).to eql(@user[:username]) - end + it "should be able to retrieve activity related to append of an expectation" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + expect_status_details(201) + suite_json = JSON.parse(suite_dto) + template_expectation = generate_template_expectation() + template_expectation["expectationType"] = "expect_column_std_to_be_between" + sleep 1 + dto_expectation = create_expectation(@project[:id], featurestore_id, fg_json["id"], suite_json["id"], template_expectation) + expect_status_details(201) + json_expectation = JSON.parse(dto_expectation) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") + expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. Created expectation with id: #{json_expectation["id"]}.") + expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(2) + end - it "should be able to return metadata activities for training datasets" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_hopsfs_training_dataset(@project[:id], featurestore_id, nil) - expect_status_details(201) - parsed_json = JSON.parse(json_result) - get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/trainingdatasets/#{parsed_json["id"]}/activity" - expect_status_details(200) - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("METADATA") - expect(activity["items"][0]["metadata"]).to eql("The training dataset was created") - end - - it "should be able to retrieve feature view creation event" do - featurestore_id = get_featurestore_id(@project[:id]) - json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) - expect_status_details(201) - - parsed_json = JSON.parse(json_result) - # create queryDTO object - query = { - leftFeatureGroup: { - id: parsed_json["id"], - type: parsed_json["type"], - }, - leftFeatures: ['testfeature'].map do |feat_name| - {name: feat_name} - end, - joins: [] - } - - json_result = create_feature_view(@project.id, featurestore_id, query) - parsed_json = JSON.parse(json_result) - expect_status_details(201) - - feature_view_name = parsed_json["name"] - feature_view_version = parsed_json["version"] - json_result = get "#{ENV['HOPSWORKS_API']}/project/#{@project.id}/featurestores/#{featurestore_id}/featureview/#{feature_view_name}/version/#{feature_view_version}/activity" - expect_status_details(200) - - activity = JSON.parse(response.body) - expect(activity["items"][0]["type"]).to eql("METADATA") - expect(activity["items"][0]["metadata"]).to eql("The feature view was created") + it "should be able to retrieve activity related to the update of a single expectation" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + expect_status_details(201) + suite_json = JSON.parse(suite_dto) + template_expectation = generate_template_expectation() + template_expectation["meta"] = "{\"whoAmI\": \"updated_expectation\"}" + template_expectation["id"] = suite_json["expectations"][0]["id"] + sleep 1 + update_expectation(@project[:id], featurestore_id, fg_json["id"], suite_json["id"], template_expectation["id"], template_expectation) + expect_status_details(200) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") + expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. Updated expectation with id: #{template_expectation["id"]}.") + expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(1) + updated_meta = JSON.parse(activity["items"][0]["expectationSuite"]["expectations"][0]["meta"]) + expect(updated_meta["whoAmI"]).to eql("updated_expectation") + end + + it "should be able to retrieve activity related to deletion of an expectation" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + suite_json = JSON.parse(suite_dto) + sleep 1 + delete_expectation(@project[:id], featurestore_id, fg_json["id"], suite_json["id"], suite_json["expectations"][0]["id"]) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:expectations&expand=expectationsuite&sort_by=timestamp:desc" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("EXPECTATIONS") + expect(activity["items"][0]["metadata"]).to eql("The Expectation Suite was updated. Deleted expectation with id: #{suite_json["expectations"][0]["id"]}.") + expect(activity["items"][0]["expectationSuite"]["expectations"].length).to eql(0) + end + + it "should be able to retrieve activity related to upload of a validation report" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + fg_json = JSON.parse(json_result) + expectation_suite = generate_template_expectation_suite() + suite_dto = create_expectation_suite(@project[:id], featurestore_id, fg_json["id"], expectation_suite) + expect_status_details(201) + suite_json = JSON.parse(suite_dto) + report = generate_template_validation_report() + report_dto = create_validation_report(@project[:id], featurestore_id, fg_json["id"], report) + expect_status_details(201) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{fg_json["id"]}/activity?filter_by=type:validations&expand=validationreport" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("VALIDATIONS") + expect(activity["items"][0]["metadata"]).to eql("A Validation Report was uploaded.") + expect(activity["items"][0]["validationReport"]["success"]).to eql(report[:success]) + expect(activity["items"][0]["validationReport"]["exceptionInfo"]).to eql(report[:exceptionInfo]) + expect(activity["items"][0]["validationReport"]["statistics"]).to eql(report[:statistics]) + expect(activity["items"][0]["validationReport"]["meta"]).to eql(report[:meta]) + end + + it "should be able to retrieve a limited set of events" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + parsed_json = JSON.parse(json_result) + base_timestamp = 1597903688000 + increment = 100000 + (1..15).each do |i| + create_statistics_commit_fg(@project[:id], featurestore_id, parsed_json["id"], computation_time: base_timestamp + increment * i) + expect_status_details(200) + end + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + + "?filter_by=type:statistics&expand=statistics&sort_by=timestamp:desc&limit=5&offset=0" + expect_status_details(200) + activity = JSON.parse(response.body) + + expect(activity["items"].count).to eql(5) + # The first element should be the most recent, day 15 + expect(activity["items"][0]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 15) + # The last element should be the least recent of the batch, day 11 + expect(activity["items"][4]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 11) + + # Test second batch + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + + "?filter_by=type:statistics&expand=statistics&sort_by=timestamp:desc&limit=5&offset=5" + expect_status_details(200) + activity = JSON.parse(response.body) + + expect(activity["items"].count).to eql(5) + # The first element should be the most recent, day 10 + expect(activity["items"][0]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 10) + # The last element should be the least recent of the batch, day 6 + expect(activity["items"][4]["statistics"]["computationTime"]).to eql(base_timestamp + increment * 6) + end + + it "should be able to filter events by timestamp" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + parsed_json = JSON.parse(json_result) + base_timestamp = 1597903688000 + increment = 100000 + (1..15).each do |i| + create_statistics_commit_fg(@project[:id], featurestore_id, parsed_json["id"], computation_time: base_timestamp + increment * i) + expect_status_details(200) + end + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + + "?filter_by=type:statistics&expand=statistics&sort_by=timestamp:desc&limit=5&offset=2" + expect_status_details(200) + activity = JSON.parse(response.body) + upper_timestamp = activity['items'][0]["timestamp"].to_i + 1 + lower_timestamp = activity['items'][4]["timestamp"].to_i - 1 + + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{parsed_json["id"]}/activity" + + "?filter_by=type:statistics&filter_by=timestamp_lt:#{upper_timestamp}&filter_by=timestamp_gt:#{lower_timestamp}&expand=statistics&sort_by=timestamp" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["timestamp"].to_i).to eql(upper_timestamp - 1) + expect(activity["items"][4]["timestamp"].to_i).to eql(lower_timestamp + 1) + end + + it "should be able to expand users resource" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + expect_status_details(201) + feature_group_id = JSON.parse(json_result)["id"] + + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/featuregroups/#{feature_group_id}/activity?expand=users" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["user"]["username"]).to eql(@user[:username]) + end + + it "should be able to return metadata activities for training datasets" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_hopsfs_training_dataset(@project[:id], featurestore_id, nil) + expect_status_details(201) + parsed_json = JSON.parse(json_result) + get "#{ENV['HOPSWORKS_API']}/project/#{@project[:id]}/featurestores/#{featurestore_id}/trainingdatasets/#{parsed_json["id"]}/activity" + expect_status_details(200) + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("METADATA") + expect(activity["items"][0]["metadata"]).to eql("The training dataset was created") + end + + it "should be able to retrieve feature view creation event" do + featurestore_id = get_featurestore_id(@project[:id]) + json_result, _ = create_cached_featuregroup(@project[:id], featurestore_id) + expect_status_details(201) + + parsed_json = JSON.parse(json_result) + # create queryDTO object + query = { + leftFeatureGroup: { + id: parsed_json["id"], + type: parsed_json["type"], + }, + leftFeatures: ['testfeature'].map do |feat_name| + {name: feat_name} + end, + joins: [] + } + + json_result = create_feature_view(@project.id, featurestore_id, query) + parsed_json = JSON.parse(json_result) + expect_status_details(201) + + feature_view_name = parsed_json["name"] + feature_view_version = parsed_json["version"] + json_result = get "#{ENV['HOPSWORKS_API']}/project/#{@project.id}/featurestores/#{featurestore_id}/featureview/#{feature_view_name}/version/#{feature_view_version}/activity" + expect_status_details(200) + + activity = JSON.parse(response.body) + expect(activity["items"][0]["type"]).to eql("METADATA") + expect(activity["items"][0]["metadata"]).to eql("The feature view was created") + end end - end -end \ No newline at end of file + end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/feature_group_alert_spec.rb b/hopsworks-IT/src/test/ruby/spec/feature_store_alert_spec.rb similarity index 58% rename from hopsworks-IT/src/test/ruby/spec/feature_group_alert_spec.rb rename to hopsworks-IT/src/test/ruby/spec/feature_store_alert_spec.rb index 9d8da283ff..1a9c24674e 100644 --- a/hopsworks-IT/src/test/ruby/spec/feature_group_alert_spec.rb +++ b/hopsworks-IT/src/test/ruby/spec/feature_store_alert_spec.rb @@ -1,21 +1,32 @@ -=begin - This file is part of Hopsworks - Copyright (C) 2021, Logical Clocks AB. All rights reserved +# This file is part of Hopsworks +# Copyright (C) 2024, Hopsworks AB. All rights reserved +# +# Hopsworks is free software: you can redistribute it and/or modify it under the terms of +# the GNU Affero General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. +# If not, see . +# - Hopsworks is free software: you can redistribute it and/or modify it under the terms of - the GNU Affero General Public License as published by the Free Software Foundation, - either version 3 of the License, or (at your option) any later version. +describe "On #{ENV['OS']}" do - Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - PURPOSE. See the GNU Affero General Public License for more details. + before :all do + # ensure feature monitoring is enabled + @enable_feature_monitoring = getVar('enable_feature_monitoring') + setVar('enable_feature_monitoring', "true") + end - You should have received a copy of the GNU Affero General Public License along with this program. - If not, see . -=end + after :all do + # revert feature monitoring flag + setVar('enable_feature_monitoring', @enable_feature_monitoring[:value]) + clean_all_test_projects(spec: "fg_alert") + end -describe "On #{ENV['OS']}" do - after(:all) {clean_all_test_projects(spec: "fg_alert")} describe 'Alert' do context 'without authentication' do before :all do @@ -31,17 +42,25 @@ create_fg_alert(@project, @featuregroup, get_fg_alert_success(@project)) expect_status_details(401) end + it "should fail to create feature monitoring status" do + create_fg_alert(@project, @featuregroup, get_fm_alert_success(@project)) + expect_status_details(401) + end end context 'with authentication' do before :all do with_valid_project @featuregroup = with_valid_fg(@project) + json_result = create_feature_view_from_feature_group(@project.id, get_featurestore_id(@project.id), @featuregroup) + @feature_view = JSON.parse(json_result) create_fg_alerts(@project, @featuregroup) + create_fm_alerts(@project, @featuregroup, @feature_view) + end it "should get" do get_fg_alerts(@project, @featuregroup) expect_status_details(200) - expect(json_body[:count]).to eq(2) + expect(json_body[:count]).to eq(3) end it "should update" do get_fg_alerts(@project, @featuregroup) @@ -68,7 +87,7 @@ expect_status_details(201) check_route_created(@project, alert[:receiver], alert[:status], fg: @featuregroup) get_fg_alerts(@project, @featuregroup) - expect(json_body[:count]).to eq(3) + expect(json_body[:count]).to eq(4) end it "should fail to create duplicate" do create_fg_alert(@project, @featuregroup, get_fg_alert_warning(@project)) @@ -81,7 +100,48 @@ expect_status_details(204) check_route_deleted(@project, alert[:receiver], alert[:status], fg: @featuregroup) get_fg_alerts(@project, @featuregroup) - expect(json_body[:count]).to eq(2) + expect(json_body[:count]).to eq(3) + end + it "should create alert with feature monitoring status" do + alert_data = get_fm_alert_success(@project) + json_result = create_fg_alert(@project, @featuregroup, alert_data) + parsed_alert_json = JSON.parse(json_result) + expect_status_details(201) + check_route_created_fm(@project, alert_data[:receiver], alert_data[:status], fg: @featuregroup) + expect(parsed_alert_json['status']).to eql(alert_data[:status]) + expect(parsed_alert_json['receiver']).to eql(alert_data[:receiver]) + expect(parsed_alert_json['severity']).to eql(alert_data[:severity]) + expect(parsed_alert_json['featureGroupId']).to eql(@featuregroup["id"]) + end + it "should create feature view monitoring alert" do + alert_data = get_fm_alert_success(@project) + json_result = create_feature_view_alert(@project, @feature_view, alert_data) + parsed_alert_json = JSON.parse(json_result) + expect_status_details(201) + expect(parsed_alert_json['status']).to eql(alert_data[:status]) + expect(parsed_alert_json['receiver']).to eql(alert_data[:receiver]) + expect(parsed_alert_json['severity']).to eql(alert_data[:severity]) + expect(parsed_alert_json['featureViewName']).to eql(@feature_view["name"]) + expect(parsed_alert_json['featureViewVersion']).to eql(@feature_view["version"]) + check_route_created_fm(@project, alert_data[:receiver], alert_data[:status], fv: @feature_view) + end + it "should get and update feature view monitoring alert" do + get_featureview_alerts(@project, @feature_view) + expect_status_details(200) + alert = json_body[:items].detect { |a| a[:status] == "FEATURE_MONITOR_SHIFT_UNDETECTED" && a[:featureViewId] + .present? } + receiver_original = alert[:receiver] + alert[:receiver] = "#{@project[:projectname]}__slack1" + json_result = update_featureview_alert(@project, @feature_view, alert[:id], alert) + expect_status_details(200) + parsed_updated_alert = JSON.parse(json_result) + expect(parsed_updated_alert['status']).to eql(alert[:status]) + expect(parsed_updated_alert['receiver']).to eql(alert[:receiver]) + expect(parsed_updated_alert['severity']).to eql(alert[:severity]) + expect(parsed_updated_alert['featureViewName']).to eql(@feature_view["name"]) + expect(parsed_updated_alert['featureViewVersion']).to eql(@feature_view["version"]) + check_route_created_fm(@project, alert[:receiver], alert[:status], fv: @feature_view) + check_route_deleted_fm(@project, receiver_original, alert[:status], fv: @feature_view) end it "should cleanup receivers and routes when deleting project" do delete_project(@project) @@ -102,7 +162,7 @@ featuregroup = with_valid_fg(project) create_fg_alerts_global(project, featuregroup) get_fg_alerts(project, featuregroup) - expect(json_body[:count]).to eq(3) + expect(json_body[:count]).to eq(5) alert_receiver = AlertReceiver.where("name LIKE '#{project[:projectname]}__%'") expect(alert_receiver.length()).to eq(0) get_routes_admin() @@ -161,4 +221,4 @@ end end end -end +end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/featurestore_statistics_spec.rb b/hopsworks-IT/src/test/ruby/spec/featurestore_statistics_spec.rb index d115de55f9..ad858b5466 100644 --- a/hopsworks-IT/src/test/ruby/spec/featurestore_statistics_spec.rb +++ b/hopsworks-IT/src/test/ruby/spec/featurestore_statistics_spec.rb @@ -46,6 +46,14 @@ commit_metadata = {commitDateString:20201025231125,commitTime:1603667485000,rowsInserted:4,rowsUpdated:0,rowsDeleted:0} json_result = commit_cached_featuregroup(@project[:id], @featurestore_id, @stream_feature_group["id"], commit_metadata: commit_metadata) expect_status_details(200) + # feature view - cached feature group - no time travel + json_result = create_feature_view_from_feature_group(@project[:id], @featurestore_id, @cached_feature_group) + expect_status_details(201) + @cached_feature_view = JSON.parse(json_result) + # feature view - stream feature group - time travel + json_result = create_feature_view_from_feature_group(@project[:id], @featurestore_id, @stream_feature_group) + expect_status_details(201) + @stream_feature_view = JSON.parse(json_result) # training datasets - with and without splits all_metadata = create_featureview_training_dataset_from_project(@project) @training_dataset = all_metadata["response"] @@ -99,6 +107,36 @@ # all query parameter combinations (including window times) are covered in the unit tests end + # feature view - left feature group - no time travel (for feature monitoring) + + it "should be able to add statistics as a commit to a feature view with a left feature group with time travel disabled (feature monitoring)" do + create_statistics_commit_fv(@project[:id], @featurestore_id, @cached_feature_view["name"], @cached_feature_view["version"]) + expect_status_details(200) + end + + it "should fail to add statistics as a commit to a feature view with a left feature group with time travel disabled and window times (feature monitoring)" do + create_statistics_commit_fv(@project[:id], @featurestore_id, @cached_feature_view["name"], @cached_feature_view["version"], computation_time: 1597903688010, window_start_commit_time: 1597903688000, window_end_commit_time: 1597903688010) + expect_status_details(400, error_code: 270229) + # all query parameter combinations (including window times) are covered in the unit tests + end + + # feature view - left stream feature group - time travel enable (for feature monitoring) + + it "should be able to add statistics as a commit to a feature view with a left stream feature group (feature monitoring)" do + create_statistics_commit_fv(@project[:id], @featurestore_id, @stream_feature_view["name"], @stream_feature_view["version"]) + expect_status_details(200) + end + + it "should be able to add statistics as a commit to a feature view with a left stream feature group with window times (feature monitoring)" do + # on two commits + create_statistics_commit_fv(@project[:id], @featurestore_id, @stream_feature_view["name"], @stream_feature_view["version"], computation_time: 1603667485000, window_start_commit_time: 1603577485000, window_end_commit_time: 1603667485000) + expect_status_details(200) + # on a single commit + create_statistics_commit_fv(@project[:id], @featurestore_id, @stream_feature_view["name"], @stream_feature_view["version"], computation_time: 1603667485000, window_start_commit_time: 1603667485000, window_end_commit_time: 1603667485000) + expect_status_details(200) + # all query parameter combinations (including window times) are covered in the unit tests + end + # training dataset it "should be able to add statistics as a commit to a training dataset" do diff --git a/hopsworks-IT/src/test/ruby/spec/helpers/alert_helper.rb b/hopsworks-IT/src/test/ruby/spec/helpers/alert_helper.rb index ba947190e8..b24b2e062d 100644 --- a/hopsworks-IT/src/test/ruby/spec/helpers/alert_helper.rb +++ b/hopsworks-IT/src/test/ruby/spec/helpers/alert_helper.rb @@ -299,6 +299,10 @@ def with_receivers(project) pagerdutyConfigs: [create_pager_duty_config])) create_receivers_checked(project, create_receiver(project, name: "#{project[:projectname]}__slack1", slackConfigs: [create_slack_config])) + create_receivers_checked(project, create_receiver(project, name: "#{project[:projectname]}__pagerduty1", + pagerdutyConfigs: [create_pager_duty_config])) + create_receivers_checked(project, create_receiver(project, name: "#{project[:projectname]}__slack2", + slackConfigs: [create_slack_config])) end def get_alerts(project, query: "") @@ -582,6 +586,16 @@ def create_route_match_query(project, receiver, status, job: nil, fg: nil) return query end + def create_fm_route_match_query(project, receiver, status, fg:nil, fv: nil) + query = "?match=status:#{status}" + query = receiver.start_with?("global-receiver__") ? + "#{query}&match=type:global-alert-#{receiver.partition('__')[2]}" : + "#{query}&match=project:#{project[:projectname]}&match=type:project-alert" + query = fv ? "#{query}&match=featureViewName:#{fv['name']}" : query + query = fg ? "#{query}&match=featureGroup:#{fg['name']}" : query + return query + end + def check_route_created(project, receiver, status, job: nil, fg: nil) query = create_route_match_query(project, receiver, status, job: job, fg: fg) get_routes_by_receiver(project, receiver, query: query) @@ -603,4 +617,19 @@ def check_route_deleted(project, receiver, status, job: nil, fg: nil) expect_status_details(400) end -end + def check_route_created_fm(project, receiver, status, fg: nil, fv: nil) + query = create_fm_route_match_query(project, receiver, status, fg: fg, fv: fv) + get_routes_by_receiver(project, receiver, query:query) + expect_status_details(200) + check_backup_contains_route(json_body) + check_alert_receiver_created(receiver) + expect(json_body[:receiver]).to eq(receiver) + end + + def check_route_deleted_fm(project, receiver, status, fg: nil, fv: nil) + query = create_fm_route_match_query(project, receiver, status, fg: fg, fv: fv) + get_routes_by_receiver(project, receiver, query: query) + expect_status_details(400) + end + +end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/helpers/feature_group_alert_helper.rb b/hopsworks-IT/src/test/ruby/spec/helpers/feature_group_alert_helper.rb index 00dae7af0d..25f5ec39d9 100644 --- a/hopsworks-IT/src/test/ruby/spec/helpers/feature_group_alert_helper.rb +++ b/hopsworks-IT/src/test/ruby/spec/helpers/feature_group_alert_helper.rb @@ -16,11 +16,14 @@ module FeatureGroupAlertHelper @@fg_alert_resource = "#{ENV['HOPSWORKS_API']}/project/%{projectId}/featurestores/%{fsId}/featuregroups/%{fgId}/alerts" - @@alert_success = {"status": "SUCCESS", "receiver": "global-receiver__email", "severity": "INFO"} @@alert_warning = {"status": "WARNING", "receiver": "global-receiver__slack", "severity": "WARNING"} @@alert_failed = {"status": "FAILURE", "receiver": "global-receiver__pagerduty", "severity": "CRITICAL"} - + # feature monitor specific + @@featureview_alert_resource = "#{ENV['HOPSWORKS_API']}/project/%{projectId}/featurestores/%{fsId}/featureview/%{name}/version/%{version}/alerts" + @@fm_alert_success = {"status": "FEATURE_MONITOR_SHIFT_UNDETECTED", "receiver": "global-receiver__email", "severity": + "INFO"} + @@fm_alert_failed = {"status": "FEATURE_MONITOR_SHIFT_DETECTED", "receiver": "global-receiver__pagerduty", "severity": "CRITICAL"} def get_fg_alert_success(project) success = @@alert_success.clone success[:receiver] = "#{project[:projectname]}__email" @@ -80,6 +83,8 @@ def create_fg_alerts_global(project, featuregroup) create_fg_alert(project, featuregroup, @@alert_success.clone) create_fg_alert(project, featuregroup, @@alert_warning.clone) create_fg_alert(project, featuregroup, @@alert_failed.clone) + create_fg_alert(project, featuregroup, @@fm_alert_failed.clone) + create_fg_alert(project, featuregroup, @@fm_alert_success.clone) end def with_valid_fg(project) @@ -87,4 +92,44 @@ def with_valid_fg(project) json_result, featuregroup_name = create_cached_featuregroup(project[:id], featurestore_id) return JSON.parse(json_result) end -end + + def get_fm_alert_failure(project) + failed = @@fm_alert_failed.clone + failed[:receiver] = "#{project[:projectname]}__pagerduty" + return failed + end + + def get_fm_alert_success(project) + success = @@fm_alert_success.clone + success[:receiver] = "#{project[:projectname]}__email" + return success + end + + def create_fm_alerts(project, featuregroup, featureview) + create_fg_alert(project, featuregroup, get_fm_alert_failure(project)) + create_feature_view_alert(project, featureview, get_fm_alert_failure(project)) + end + + def create_feature_view_alert(project, featureview, alert) + post "#{@@featureview_alert_resource}" % { projectId: project[:id], + fsId: featureview["featurestoreId"], + name: featureview["name"], + version: featureview["version"]}, alert.to_json + end + + def update_featureview_alert(project, featureview, id, alert) + put "#{@@featureview_alert_resource}/#{id}" % { projectId: project[:id], + fsId: featureview["featurestoreId"], + name: featureview["name"], + version: featureview["version"]}, alert.to_json + end + + def get_featureview_alerts(project, featureview) + get "#{@@featureview_alert_resource}" % {projectId: project[:id], + fsId: featureview["featurestoreId"], + name: featureview["name"], + version: featureview["version"]} + end + + +end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/helpers/feature_monitoring_helper.rb b/hopsworks-IT/src/test/ruby/spec/helpers/feature_monitoring_helper.rb new file mode 100644 index 0000000000..295a30017b --- /dev/null +++ b/hopsworks-IT/src/test/ruby/spec/helpers/feature_monitoring_helper.rb @@ -0,0 +1,381 @@ +# This file is part of Hopsworks +# Copyright (C) 2023, Hopsworks AB. All rights reserved +# +# Hopsworks is free software: you can redistribute it and/or modify it under the terms of +# the GNU Affero General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. +# If not, see . +# + +module FeatureMonitoringHelper + def generate_template_descriptive_statistics_comparison_config() + { + "threshold": 1, + "strict": true, + "relative": true, + "metric": "MEAN" + } + end + + def generate_template_monitoring_scheduler_config() + { + "startDateTime": 1677670460000, # Wed Mar 01 2023 11:30:00 GMT+0000 + "cronExpression": "0 0 * ? * * *", + "enabled": true, + + } + end + + def generate_template_detection_window_config(window_type: "ROLLING_TIME") + window = { "windowConfigType": window_type } + if window_type == "ROLLING_TIME" + window = window.merge({ + "rowPercentage": 0.13, + "windowLength": "1h", + "timeOffset": "1d" + }) + end + # TODO: add other types of detection window + window + end + + def generate_template_reference_window_config(window_type: "TRAINING_DATASET") + if window_type.nil? + return nil + end + + window = { "windowConfigType": window_type } + if window_type == "TRAINING_DATASET" + window = window.merge({ "trainingDatasetVersion": 12 }) + end + if window_type == "SPECIFIC_VALUE" + window = window.merge({ "specificValue": 20 }) + end + if window_type == "ROLLING_TIME" + window = window.merge({ + "rowPercentage": 1.0, + "windowLength": "1h", + "timeOffset": "1d" + }) + end + # TODO: add other types of reference window + window + end + + def generate_template_stats_monitoring_config(featurestore_id, entity_id, entity_name, entity_version, is_entity_fg) + detection_window = generate_template_detection_window_config() + schedule = generate_template_monitoring_scheduler_config() + config = { + "jobSchedule": schedule, + "featureStoreId": featurestore_id, + "featureName": "testfeature", + "name": "test_stats_config", + "description": "Configuration created for ruby integration test", + "featureMonitoringType": "STATISTICS_COMPUTATION", + "detectionWindowConfig": detection_window, + } + if is_entity_fg + config[:featureGroupId] = entity_id + else + config[:featureViewName] = entity_name + config[:featureViewVersion] = entity_version + end + config + end + + def generate_template_feature_monitoring_config(featurestore_id, entity_id, entity_name, entity_version, is_entity_fg, \ + det_window_type: "ROLLING_TIME", ref_window_type: "TRAINING_DATASET", name: "test_config") + stats_comparison_config = generate_template_descriptive_statistics_comparison_config() + detection_window = generate_template_detection_window_config(window_type: det_window_type) + reference_window = generate_template_reference_window_config(window_type: ref_window_type) + schedule = generate_template_monitoring_scheduler_config() + config = { + "jobSchedule": schedule, + "featureStoreId": featurestore_id, + "featureName": "testfeature", + "name": name, + "description": "Configuration created for ruby integration test", + "featureMonitoringType": "STATISTICS_COMPARISON", + "statisticsComparisonConfig": stats_comparison_config, + "detectionWindowConfig": detection_window, + "referenceWindowConfig": reference_window + } + if is_entity_fg + config[:featureGroupId] = entity_id + else + config[:featureViewName] = entity_name + config[:featureViewVersion] = entity_version + end + config + end + + def expect_window_config_equal(actual, expected) + expect(actual.has_key?("id")).to be true + expect(actual["windowConfigType"]).to eq(expected[:windowConfigType]) + expect(actual["specificValue"]).to eq(expected[:specificValue]) + expect(actual["rowPercentage"]).to eq(expected[:rowPercentage]) + expect(actual["windowLength"]).to eq(expected[:windowLength]) + expect(actual["timeOffset"]).to eq(expected[:timeOffset]) + expect(actual["trainingDatasetVersion"]).to eq(expected[:trainingDatasetVersion]) + end + + def expect_stats_comparison_equal(actual, expected, threshold: nil) + expect(actual.has_key?("id")).to be true + expect(actual["strict"]).to eq(expected[:strict]) + expect(actual["relative"]).to eq(expected[:relative]) + expect(actual["metric"]).to eq(expected[:metric]) + if threshold.nil? + expect(actual["threshold"]).to eq(expected[:threshold]) + else + expect(actual["threshold"]).to eq(threshold) + end + end + + def expect_monitoring_scheduler_equal(actual, expected) + expect(actual.has_key?("id")).to be true + expect(actual["startDateTime"]).to eq(1677670460000) # Wed Mar 01 2023 11:30:00 GMT+0000 + expect(actual["cronExpression"]).to eq(expected[:cronExpression]) + expect(actual["enabled"]).to eq(expected[:enabled]) + end + + def expect_partial_feature_monitoring_equal(actual, expected, job_name, name, fg_id: nil, fv_name: nil, fv_version: nil) + expect(actual.has_key?("id")).to be true + expect(actual["featureStoreId"]).to eq(expected[:featureStoreId]) + expect(actual["featureName"]).to eq(expected[:featureName]) + expect(actual["name"]).to eq(name) + expect(actual["description"]).to eq(expected[:description]) + expect(actual["featureMonitoringType"]).to eq(expected[:featureMonitoringType]) + expect(actual["featureGroupId"]).to eq(fg_id) + expect(actual["featureViewName"]).to eq(fv_name) + expect(actual["featureViewVersion"]).to eq(fv_version) + expect(actual["jobName"]).to eq(job_name) + end + + def expect_feature_monitoring_equal(actual, expected, job_name, name, fg_id: nil, fv_name: nil, fv_version: nil) + expect_partial_feature_monitoring_equal(actual, expected, job_name, name, fg_id: fg_id, fv_name: fv_name, fv_version: fv_version) + expect_monitoring_scheduler_equal(actual["jobSchedule"], expected[:jobSchedule]) + expect_window_config_equal(actual["detectionWindowConfig"], expected[:detectionWindowConfig]) + if expected.has_key?(:referenceWindowConfig) + expect_stats_comparison_equal(actual["statisticsComparisonConfig"], expected[:statisticsComparisonConfig]) + expect_window_config_equal(actual["referenceWindowConfig"], expected[:referenceWindowConfig]) + else + expect(actual.has_key?("statisticsComparisonConfig")).to eq(false) + end + end + + + def create_feature_monitoring_configuration_fg(project_id, featurestore_id, featuregroup_id, feature_monitoring_config) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/config" + post endpoint, feature_monitoring_config + end + + def get_feature_monitoring_configuration_by_feature_name_fg(project_id, featurestore_id, featuregroup_id, feature_name) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/config/feature/#{feature_name}" + get endpoint + end + + def get_feature_monitoring_configuration_by_entity_fg(project_id, featurestore_id, featuregroup_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/config/entity" + get endpoint + end + + def get_feature_monitoring_configuration_by_id_fg(project_id, featurestore_id, featuregroup_id, config_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/config/#{config_id}" + get endpoint + end + + def get_feature_monitoring_configuration_by_name_fg(project_id, featurestore_id, featuregroup_id, config_name) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/config/name/#{config_name}" + get endpoint + end + + def update_feature_monitoring_configuration_fg(project_id, featurestore_id, featuregroup_id, feature_monitoring_config) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/config/#{feature_monitoring_config[:id]}" + put endpoint, feature_monitoring_config + end + + def delete_feature_monitoring_configuration_by_id_fg(\ + project_id, featurestore_id, featuregroup_id, config_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/config/#{config_id}" + delete endpoint + end + + def create_feature_monitoring_configuration_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, feature_monitoring_config) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/featuremonitoring/config" + post endpoint, feature_monitoring_config + end + + def get_feature_monitoring_configuration_by_feature_name_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, feature_name) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/featuremonitoring/config/" + endpoint += "feature/#{feature_name}" + get endpoint + end + + def get_feature_monitoring_configuration_by_entity_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/featuremonitoring/config/" + endpoint += "entity" + get endpoint + end + + def get_feature_monitoring_configuration_by_id_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, config_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/featuremonitoring/config/#{config_id}" + get endpoint + end + + def get_feature_monitoring_configuration_by_name_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, config_name) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/" + endpoint += "featuremonitoring/config/name/#{config_name}" + get endpoint + end + + def update_feature_monitoring_configuration_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, config) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/featuremonitoring/config/" + endpoint += "#{config[:id]}" + put endpoint, config + end + + def delete_feature_monitoring_configuration_by_id_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, config_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/featuremonitoring/config/#{config_id}" + delete endpoint + end + + def generate_template_feature_monitoring_result(\ + featurestore_id, config_id, monitoring_time, detection_statistics_id, reference_statistics_id: nil, specific_value: nil) + result = { + "featureStoreId": featurestore_id, + "configId": config_id, + "featureName": "testfeature", + "executionId": 1, + "difference": -1, + "specificValue": specific_value, + "shiftDetected": false, + "monitoringTime": monitoring_time.round(-3), + "detectionStatisticsId": detection_statistics_id, + "referenceStatisticsId": reference_statistics_id, + "raisedException": false, + "emptyDetectionWindow": false, + "emptyReferenceWindow": false, + } + if detection_statistics_id.nil? + result[:emptyDetectionWindow] = true + end + if reference_statistics_id.nil? + result[:emptyReferenceWindow] = true + end + result + end + + def create_feature_monitoring_result_fg(project_id, featurestore_id, featuregroup_id, feature_monitoring_result) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/result" + post endpoint, feature_monitoring_result + end + + def get_all_feature_monitoring_results_by_config_id_fg(\ + project_id, featurestore_id, featuregroup_id, config_id, with_stats: false) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/result/" + endpoint += "byconfig/#{config_id}?sort_by=monitoring_time:desc" + if with_stats + endpoint += "&expand=statistics" + end + get endpoint + end + + def get_feature_monitoring_result_by_id_fg(project_id, featurestore_id, featuregroup_id, result_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/result/#{result_id}" + get endpoint + end + + def delete_feature_monitoring_result_by_id_fg(project_id, featurestore_id, featuregroup_id, result_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featuregroups/#{featuregroup_id}/featuremonitoring/result/#{result_id}" + delete endpoint + end + + def create_feature_monitoring_result_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, feature_monitoring_result) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/" + endpoint += "featuremonitoring/result" + post endpoint, feature_monitoring_result + end + + def get_all_feature_monitoring_results_by_config_id_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, config_id, with_stats: false) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/" + endpoint += "featuremonitoring/result/byconfig/#{config_id}?sort_by=monitoring_time:desc" + if with_stats + endpoint += "&expand=statistics" + end + get endpoint + end + + def get_feature_monitoring_result_by_id_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, result_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/" + endpoint += "featuremonitoring/result/#{result_id}" + get endpoint + end + + def delete_feature_monitoring_result_by_id_fv(\ + project_id, featurestore_id, feature_view_name, feature_view_version, result_id) + endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/" + endpoint += "featureview/#{feature_view_name}/version/#{feature_view_version}/" + endpoint += "featuremonitoring/result/#{result_id}" + delete endpoint + end + + def expect_desc_statistics_to_be_eq(stats_1, stats_2) + expect(stats_1).not_to be_nil + expect(stats_2).not_to be_nil + + # sanity ruby hashes so they both use string keys + stats_1 = JSON.parse(JSON.generate(stats_1)) + stats_2 = JSON.parse(JSON.generate(stats_2)) + if stats_1.key?("id") || stats_2.key?("id") + stats_1["id"] = nil + stats_2["id"] = nil + end + + # start / end times are not included when retrieving statistics + stats_1.delete("startTime") unless stats_1["startTime"].nil? + stats_1.delete("endTime") unless stats_1["endTime"].nil? + stats_1.delete("rowPercentage") unless stats_1["rowPercentage"].nil? + stats_2.delete("startTime") unless stats_2["startTime"].nil? + stats_2.delete("endTime") unless stats_2["endTime"].nil? + stats_2.delete("rowPercentage") unless stats_2["rowPercentage"].nil? + + expect(stats_1).to eq(stats_2) + end +end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/helpers/featurestore_statistics_helper.rb b/hopsworks-IT/src/test/ruby/spec/helpers/featurestore_statistics_helper.rb index 6b8b431990..2b0d96402a 100644 --- a/hopsworks-IT/src/test/ruby/spec/helpers/featurestore_statistics_helper.rb +++ b/hopsworks-IT/src/test/ruby/spec/helpers/featurestore_statistics_helper.rb @@ -15,7 +15,7 @@ module FeatureStoreStatisticsHelper - def generate_template_feature_descriptive_statistics(monitoring_time, exact_uniqueness: false, shift_delta: 0.0) + def generate_template_feature_descriptive_statistics(exact_uniqueness: false, shift_delta: 0.0) desc_stats = { featureName: "testfeature", featureType: "Fractional", @@ -43,7 +43,7 @@ def generate_template_feature_descriptive_statistics(monitoring_time, exact_uniq exactNumDistinctValues: 490, }) end - desc_stats + [desc_stats] end def generate_template_feature_group_statistics(feature_descriptive_statistics: nil, split_statistics: nil, computation_time: 1597903688000, window_start_commit_time: nil, window_end_commit_time: nil, before_transformation: false) @@ -59,7 +59,7 @@ def generate_template_feature_group_statistics(feature_descriptive_statistics: n json_data[:splitStatistics] = split_statistics end else - json_data[:featureDescriptiveStatistics] = feature_descriptive_statistics # JSON.parse(feature_descriptive_statistics.to_json) + json_data[:featureDescriptiveStatistics] = JSON.parse(feature_descriptive_statistics.to_json) end json_data[:windowStartCommitTime] = window_start_commit_time @@ -69,29 +69,15 @@ def generate_template_feature_group_statistics(feature_descriptive_statistics: n return json_data end - def create_feature_descriptive_statistics_fg(project_id, featurestore_id, fg_id, monitoring_time, exact_uniqueness: false, shift_delta: 0.0) - desc_stats = generate_template_feature_descriptive_statistics(monitoring_time, exact_uniqueness: exact_uniqueness, shift_delta: shift_delta) - create_statistics_commit_fg(project_id, featurestore_id, fg_id, feature_descriptive_statistics: [desc_stats]) - expect_status_details(200) - json_result = get_statistics_commit_fg(project_id, featurestore_id, fg_id) - expect_status_details(200) - parsed_json = JSON.parse(json_result) - return parsed_json["items"][0]["featureDescriptiveStatistics"][0]["id"].to_i - end - - def create_feature_descriptive_statistics_td(project_id, featurestore_id, fv_name, fv_version, td_version, monitoring_time, exact_uniqueness: false, shift_delta: 0.0) - desc_stats = generate_template_feature_descriptive_statistics(monitoring_time, exact_uniqueness: exact_uniqueness, shift_delta: shift_delta) - create_statistics_commit_td(project_id, featurestore_id, fv_name, fv_version, td_version, feature_descriptive_statistics: [desc_stats]) - expect_status_details(200) - json_result = get_statistics_commit_td(project_id, featurestore_id, fv_name, fv_version, td_version) - expect_status_details(200) - parsed_json = JSON.parse(json_result) - return parsed_json["items"][0]["featureDescriptiveStatistics"][0]["id"].to_i + def create_statistics_commit_fg(project_id, featurestore_id, fg_id, feature_descriptive_statistics: nil, computation_time: 1597903688000, window_start_commit_time: nil, window_end_commit_time: nil) + post_statistics_endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/featuregroups/#{fg_id}/statistics" + post_statistics_commit(post_statistics_endpoint, feature_descriptive_statistics: feature_descriptive_statistics, computation_time: computation_time, window_start_commit_time: window_start_commit_time, window_end_commit_time: window_end_commit_time) end - def create_statistics_commit_fg(project_id, featurestore_id, fg_id, feature_descriptive_statistics: nil, split_statistics: nil, computation_time: 1597903688000, window_start_commit_time: nil, window_end_commit_time: nil) - post_statistics_endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/featuregroups/#{fg_id}/statistics" - post_statistics_commit(post_statistics_endpoint, feature_descriptive_statistics: feature_descriptive_statistics, split_statistics: split_statistics, computation_time: computation_time, window_start_commit_time: window_start_commit_time, window_end_commit_time: window_end_commit_time) + # for feature monitoring test only + def create_statistics_commit_fv(project_id, featurestore_id, fv_name, fv_version, feature_descriptive_statistics: nil, computation_time: 1597903688000, window_start_commit_time: nil, window_end_commit_time: nil) + post_statistics_endpoint = "#{ENV['HOPSWORKS_API']}/project/#{project_id}/featurestores/#{featurestore_id}/featureview/#{fv_name}/version/#{fv_version}/statistics" + post_statistics_commit(post_statistics_endpoint, feature_descriptive_statistics: feature_descriptive_statistics, computation_time: computation_time, window_start_commit_time: window_start_commit_time, window_end_commit_time: window_end_commit_time) end def create_statistics_commit_td(project_id, featurestore_id, fv_name, fv_version, td_version, feature_descriptive_statistics: nil, split_statistics: nil, computation_time: 1597903688000, window_start_commit_time: nil, window_end_commit_time: nil, before_transformation: false) diff --git a/hopsworks-IT/src/test/ruby/spec/helpers/job_helper.rb b/hopsworks-IT/src/test/ruby/spec/helpers/job_helper.rb index 8cf873d9f1..2ff6aa638a 100644 --- a/hopsworks-IT/src/test/ruby/spec/helpers/job_helper.rb +++ b/hopsworks-IT/src/test/ruby/spec/helpers/job_helper.rb @@ -239,9 +239,9 @@ def get_job(project_id, job_name, query: "", expected_status: nil, error_code: n expect_status_details(expected_status, error_code: error_code) unless expected_status.nil? end - def delete_job(project_id, job_name, expected_status: 204) + def delete_job(project_id, job_name, expected_status: 204, error_code: nil) delete "#{ENV['HOPSWORKS_API']}/project/#{project_id}/jobs/#{job_name}" - expect_status_details(expected_status) + expect_status_details(expected_status, error_code: error_code) end def get_job_from_db(job_id) diff --git a/hopsworks-IT/src/test/ruby/spec/helpers/project_alert_helper.rb b/hopsworks-IT/src/test/ruby/spec/helpers/project_alert_helper.rb index fda452f296..114ef2c3fb 100644 --- a/hopsworks-IT/src/test/ruby/spec/helpers/project_alert_helper.rb +++ b/hopsworks-IT/src/test/ruby/spec/helpers/project_alert_helper.rb @@ -25,6 +25,11 @@ module ProjectServiceAlertHelper @@project_alert_failed = {"service": "JOBS", "status": "JOB_FAILED", "receiver": "global-receiver__slack", "severity": "WARNING"} @@project_alert_killed = {"service": "JOBS", "status": "JOB_KILLED", "receiver": "global-receiver__pagerduty", "severity": "CRITICAL"} + @@project_alert_fm_detected = { "service": "FEATURESTORE", "status": "FEATURE_MONITOR_SHIFT_DETECTED", "receiver": + "global-receiver__pagerduty","severity": "CRITICAL" } + @@project_alert_fm_undetected = { "service": "FEATURESTORE", "status": "FEATURE_MONITOR_SHIFT_UNDETECTED", + "receiver":"global-receiver__slack","severity": "INFO" } + def get_project_alert_finished(project) finished = @@project_alert_finished.clone finished[:receiver] = "#{project[:projectname]}__email" @@ -91,6 +96,8 @@ def create_project_alerts(project) create_project_alert(project, get_project_alert_killed(project)) create_project_alert(project, get_project_alert_success(project)) create_project_alert(project, get_project_alert_failure(project)) + create_project_alert(project, get_project_alert_fm_detected(project)) + create_project_alert(project, get_project_alert_fm_undetected(project)) end def create_project_alerts_global(project) @@ -99,4 +106,16 @@ def create_project_alerts_global(project) create_project_alert(project, @@project_alert_success.clone) create_project_alert(project, @@project_alert_failure.clone) end -end + + def get_project_alert_fm_detected(project) + failure = @@project_alert_fm_detected.clone + failure[:receiver] = "#{project[:projectname]}__pagerduty1" + return failure + end + + def get_project_alert_fm_undetected(project) + alert = @@project_alert_fm_undetected.clone + alert[:receiver] = "#{project[:projectname]}__slack2" + return alert + end +end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/project_alert_spec.rb b/hopsworks-IT/src/test/ruby/spec/project_alert_spec.rb index e3567d9bed..295ba9b11a 100644 --- a/hopsworks-IT/src/test/ruby/spec/project_alert_spec.rb +++ b/hopsworks-IT/src/test/ruby/spec/project_alert_spec.rb @@ -40,7 +40,7 @@ it "should get" do get_project_alerts(@project) expect_status_details(200) - expect(json_body[:count]).to eq(4) + expect(json_body[:count]).to eq(6) end it "should update" do get_project_alerts(@project) @@ -67,7 +67,7 @@ expect_status_details(201) check_route_created(@project, alert[:receiver], alert[:status]) get_project_alerts(@project) - expect(json_body[:count]).to eq(5) + expect(json_body[:count]).to eq(7) end it "should fail to create duplicate" do create_project_alert(@project, get_project_alert_failed(@project)) @@ -80,7 +80,7 @@ expect_status_details(204) get_project_alerts(@project) - expect(json_body[:count]).to eq(4) + expect(json_body[:count]).to eq(6) end it "should delete route if not used" do project = create_project @@ -104,8 +104,12 @@ fg_alert2 = alerts[:items].detect { |a| a[:status] == "VALIDATION_FAILURE" and a[:service] == "FEATURESTORE" } delete_project_alert(project, fg_alert2[:id]) expect_status_details(204) - check_route_deleted(project, fg_alert2[:receiver], fg_alert2[:status]) + fm_alert = alerts[:items].detect { |a| a[:status] == "FEATURE_MONITOR_SHIFT_UNDETECTED" and a[:service] == + "FEATURESTORE" } + delete_project_alert(project, fm_alert[:id]) + expect_status_details(204) + check_route_deleted_fm(project, fm_alert[:receiver], fm_alert[:status]) end it "should cleanup receivers and routes when deleting project" do project = create_project @@ -189,4 +193,4 @@ end end end -end +end \ No newline at end of file diff --git a/hopsworks-IT/src/test/ruby/spec/spec_helper.rb b/hopsworks-IT/src/test/ruby/spec/spec_helper.rb index dbbac464b9..81d6fe0f97 100644 --- a/hopsworks-IT/src/test/ruby/spec/spec_helper.rb +++ b/hopsworks-IT/src/test/ruby/spec/spec_helper.rb @@ -106,6 +106,7 @@ config.include ProvHelper config.include ProvOpsHelper config.include FeatureStoreStatisticsHelper + config.include FeatureMonitoringHelper config.include FeatureStoreCodeHelper config.include StorageConnectorHelper config.include ProvAppHelper diff --git a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/FixReceiversTimer.java b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/FixReceiversTimer.java index 22dd34b0e5..17e69f7f3c 100644 --- a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/FixReceiversTimer.java +++ b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/FixReceiversTimer.java @@ -29,6 +29,7 @@ import io.hops.hopsworks.persistence.entity.alertmanager.AlertReceiver; import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; import io.hops.hopsworks.persistence.entity.jobs.description.JobAlert; import io.hops.hopsworks.persistence.entity.project.alert.ProjectServiceAlert; @@ -166,6 +167,7 @@ private List getRoutesToAdd(AlertReceiver alertReceiver, List rout Collection jobAlerts = alertReceiver.getJobAlertCollection(); Collection featureGroupAlerts = alertReceiver.getFeatureGroupAlertCollection(); Collection projectServiceAlerts = alertReceiver.getProjectServiceAlertCollection(); + Collection featureViewAlerts = alertReceiver.getFeatureViewAlertCollection(); List routesToAdd = new ArrayList<>(); for (JobAlert jobAlert : jobAlerts) { Route route = jobAlert.getAlertType().isGlobal() ? ConfigUtil.getRoute(jobAlert.getAlertType()) : @@ -190,6 +192,16 @@ private List getRoutesToAdd(AlertReceiver alertReceiver, List rout routesToAdd.add(route); } } + if (featureViewAlerts!=null) { + for (FeatureViewAlert featureViewAlert : featureViewAlerts) { + Route route = + featureViewAlert.getAlertType().isGlobal() ? ConfigUtil.getRoute(featureViewAlert.getAlertType()) : + ConfigUtil.getRoute(featureViewAlert); + if (!routes.contains(route) && !routesToAdd.contains(route)) { + routesToAdd.add(route); + } + } + } return routesToAdd; } diff --git a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/ConfigUtil.java b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/ConfigUtil.java index 47eb08882d..646b391028 100644 --- a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/ConfigUtil.java +++ b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/ConfigUtil.java @@ -20,6 +20,7 @@ import io.hops.hopsworks.alerting.config.dto.Route; import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; import io.hops.hopsworks.persistence.entity.jobs.description.JobAlert; import io.hops.hopsworks.persistence.entity.project.Project; import io.hops.hopsworks.persistence.entity.project.alert.ProjectServiceAlert; @@ -161,4 +162,29 @@ public static Route getRoute(AlertType alertType) { .withMatch(match) .withGroupBy(groupBy); } + + public static Route getRoute(FeatureViewAlert alert) { + if (alert.getAlertType().isGlobal()) { + return getRoute(alert.getAlertType()); + } + Map match = getMatch(alert); + List groupBy = new ArrayList<>(); + groupBy.add(Constants.LABEL_PROJECT); + groupBy.add(Constants.LABEL_FEATURE_VIEW_NAME); + groupBy.add(Constants.LABEL_STATUS); + return new Route(alert.getReceiver().getName()) + .withContinue(true) + .withMatch(match) + .withGroupBy(groupBy); + } + + public static Map getMatch(FeatureViewAlert alert) { + Project project = alert.getFeatureView().getFeaturestore().getProject(); + Map match = new HashMap<>(); + match.put(Constants.ALERT_TYPE_LABEL, alert.getAlertType().getValue()); + match.put(Constants.LABEL_PROJECT, project.getName()); + match.put(Constants.LABEL_FEATURE_VIEW_NAME, alert.getFeatureView().getName()); + match.put(Constants.LABEL_STATUS, alert.getStatus().getName()); + return match; + } } diff --git a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/Constants.java b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/Constants.java index 2da91cee2f..93e5914e5a 100644 --- a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/Constants.java +++ b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/Constants.java @@ -52,7 +52,13 @@ public class Constants { public static final String LABEL_TITLE = "title"; public static final String LABEL_SUMMARY = "summary"; public static final String LABEL_DESCRIPTION = "description"; - + + public final static String ALERT_NAME_FEATURE_MONITOR = "FeatureMonitoring"; + public final static String LABEL_FM_CONFIG = "featureMonitorConfig"; + public final static String LABEL_FM_RESULT_ID = "featureMonitorResultId"; + public final static String LABEL_FEATURE_VIEW_NAME = "featureViewName"; + public final static String LABEL_FEATURE_VIEW_VERSION = "featureViewVersion"; + public static final String FILTER_BY_PROJECT_LABEL = LABEL_PROJECT + "=\""; public static final String FILTER_BY_PROJECT_FORMAT = FILTER_BY_PROJECT_LABEL + PROJECT_PLACE_HOLDER + "\""; @@ -73,17 +79,27 @@ public class Constants { public static final String TEST_ALERT_FG_NAME = "AlertTestFeatureGroup"; public static final String TEST_ALERT_FG_DESCRIPTION = "Alert test description."; public static final String TEST_ALERT_FG_SUMMARY = "Alert test summary."; - + public static final String TEST_ALERT_FM_NAME = "AlertTestFeatureMonitor"; public static final Integer TEST_ALERT_EXECUTION_ID = 1; public static final Integer TEST_ALERT_FG_ID = 1; public static final Integer TEST_ALERT_FG_VERSION = 1; - + + public static final String FM_RESULT_ID_LABEL = "featureMonitorResultId"; + public static final String FM_ID_PLACE_HOLDER = "%%fmResultId%%"; + public static final String FILTER_BY_FM_RESULT_ID = FM_RESULT_ID_LABEL + "=\""; + public static final String FILTER_BY_FM_RESULT_FORMAT = FILTER_BY_FM_RESULT_ID + FM_ID_PLACE_HOLDER + "\""; + public static final String FM_CONFIG_NAME_LABEL ="featureMonitorConfig"; + public static final String FM_NAME_PLACE_HOLDER ="%%configName%%"; + public static final String FILTER_BY_FV_LABEL = FM_CONFIG_NAME_LABEL + "=\""; + public static final String FILTER_BY_FM_NAME_FORMAT = FILTER_BY_FV_LABEL + FM_NAME_PLACE_HOLDER + "\""; + public static final String DEFAULT_EMAIL_HTML = "{{ template \"hopsworks.email.default.html\" . }}"; public static final String DEFAULT_SLACK_ICON_URL = "https://uploads-ssl.webflow.com/5f6353590bb01cacbcecfbac/" + "633ed0f0ed7662f8621ce701_Hopsworks%20Logo%20Social%20.png"; public static final String DEFAULT_SLACK_TEXT = "{{ template \"hopsworks.slack.default.text\" . }}"; public static final String DEFAULT_SLACK_TITLE = "{{ template \"hopsworks.slack.default.title\" . }}"; - + public static final String NO_PAYLOAD = "No payload."; + public static final String AM_CONFIG_UPDATED_TOPIC_NAME = "alertmanager_config_updated"; public enum TimerType { diff --git a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/PostableAlertBuilder.java b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/PostableAlertBuilder.java index 11f9b8e3b2..d0ddbbc4a0 100644 --- a/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/PostableAlertBuilder.java +++ b/hopsworks-alert/src/main/java/io/hops/hopsworks/alert/util/PostableAlertBuilder.java @@ -42,7 +42,11 @@ public static class Builder { private String summary; private String description; private URL generatorURL; - + private String featureMonitorConfigName; + private Integer featureMonitorResultId; + private String featureViewName; + private Integer featureViewVersion; + public Builder(String projectName, AlertType type, AlertSeverity severity, String status) { this.projectName = projectName; this.type = type; @@ -97,7 +101,32 @@ public Builder withFeatureGroupVersion(Integer featureGroupVersion) { this.featureGroupVersion = featureGroupVersion; return this; } - + + public Builder withFeatureViewVersion(Integer version) { + if (jobName != null || executionId != null || featureGroupId != null) { + throw new IllegalArgumentException("Alert can be either job, feature group or feature view."); + } + this.featureViewVersion = version; + return this; + } + + public Builder withFeatureViewName(String name) { + if (jobName != null || executionId != null || featureGroupId != null) { + throw new IllegalArgumentException("Alert can be either job, feature group or feature view."); + } + this.featureViewName = name; + return this; + } + + public Builder withFeatureMonitorConfig(String configName, Integer resultId) { + if (jobName != null || executionId != null) { + throw new IllegalArgumentException("Alert can be either job or feature group validation."); + } + this.featureMonitorConfigName = configName; + this.featureMonitorResultId = resultId; + return this; + } + public Builder withSummary(String summary) { this.summary = summary; return this; @@ -149,6 +178,21 @@ public PostableAlert build() { if (Strings.isNullOrEmpty(this.summary) || Strings.isNullOrEmpty(this.description)) { throw new IllegalArgumentException("Summary and description can not be empty."); } + // if alert if feature monitoring config change title + if (!Strings.isNullOrEmpty(this.featureMonitorConfigName)) { + labels.put(Constants.ALERT_NAME_LABEL, Constants.ALERT_NAME_FEATURE_MONITOR); + labels.put(Constants.LABEL_FM_CONFIG, this.featureMonitorConfigName); + annotations.put(Constants.LABEL_TITLE, this.featureMonitorConfigName); + } + if (this.featureMonitorResultId != null) { + labels.put(Constants.LABEL_FM_RESULT_ID, this.featureMonitorResultId.toString()); + } + if (this.featureViewName != null) { + labels.put(Constants.LABEL_FEATURE_VIEW_NAME, this.featureViewName); + } + if (this.featureViewVersion != null) { + labels.put(Constants.LABEL_FEATURE_VIEW_VERSION, this.featureViewVersion.toString()); + } annotations.put(Constants.LABEL_SUMMARY, this.summary); annotations.put(Constants.LABEL_DESCRIPTION, this.description); PostableAlert postableAlert = new PostableAlert(labels, annotations); diff --git a/hopsworks-alert/src/test/java/TestAlertManagerConfigTimer.java b/hopsworks-alert/src/test/java/TestAlertManagerConfigTimer.java index f51e033bb0..59241e8495 100644 --- a/hopsworks-alert/src/test/java/TestAlertManagerConfigTimer.java +++ b/hopsworks-alert/src/test/java/TestAlertManagerConfigTimer.java @@ -47,7 +47,7 @@ import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.ValidationRuleAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import io.hops.hopsworks.persistence.entity.jobs.description.JobAlert; import io.hops.hopsworks.persistence.entity.jobs.description.JobAlertStatus; import io.hops.hopsworks.persistence.entity.jobs.description.Jobs; @@ -161,9 +161,9 @@ private AlertReceiver createAlertReceiver(Integer id, Receiver receiver, AlertTy new Date(), job, alertReceiver)); alertReceiver.setJobAlertCollection(jobAlerts); List featureGroupAlerts = new ArrayList<>(); - featureGroupAlerts.add(new FeatureGroupAlert(random.nextInt(1000), ValidationRuleAlertStatus.FAILURE, alertType, + featureGroupAlerts.add(new FeatureGroupAlert(random.nextInt(1000), FeatureStoreAlertStatus.FAILURE, alertType, AlertSeverity.CRITICAL, new Date(), featuregroup, alertReceiver)); - featureGroupAlerts.add(new FeatureGroupAlert(random.nextInt(1000), ValidationRuleAlertStatus.WARNING, alertType, + featureGroupAlerts.add(new FeatureGroupAlert(random.nextInt(1000), FeatureStoreAlertStatus.WARNING, alertType, AlertSeverity.WARNING, new Date(), featuregroup, alertReceiver)); alertReceiver.setFeatureGroupAlertCollection(featureGroupAlerts); List projectServiceAlerts = new ArrayList<>(); diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertController.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertController.java new file mode 100644 index 0000000000..c025d11545 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertController.java @@ -0,0 +1,219 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.alert; + +import com.google.common.base.Strings; +import io.hops.hopsworks.alert.dao.AlertReceiverFacade; +import io.hops.hopsworks.alert.exception.AlertManagerAccessControlException; +import io.hops.hopsworks.alert.exception.AlertManagerUnreachableException; +import io.hops.hopsworks.alerting.exceptions.AlertManagerClientCreateException; +import io.hops.hopsworks.alerting.exceptions.AlertManagerConfigCtrlCreateException; +import io.hops.hopsworks.alerting.exceptions.AlertManagerConfigReadException; +import io.hops.hopsworks.alerting.exceptions.AlertManagerConfigUpdateException; +import io.hops.hopsworks.alerting.exceptions.AlertManagerNoSuchElementException; +import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureGroupAlertDTO; +import io.hops.hopsworks.api.featurestore.datavalidation.alert.PostableFeatureStoreAlerts; +import io.hops.hopsworks.api.featurestore.featureview.FeatureViewAlertDTO; +import io.hops.hopsworks.common.alert.AlertController; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.common.featurestore.datavalidation.FeatureGroupAlertFacade; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewAlertFacade; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertReceiver; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.restutils.RESTCodes; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.stream.Collectors; +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureStoreAlertController { + @EJB + FeatureViewAlertFacade featureViewAlertFacade; + @EJB + AlertController alertController; + @EJB + AlertReceiverFacade alertReceiverFacade; + @EJB + FeatureGroupAlertFacade featureGroupAlertFacade; + + private AlertReceiver getReceiver(String name) throws FeaturestoreException { + if (Strings.isNullOrEmpty(name)) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + "Receiver can not be empty."); + } + Optional alertReceiver = alertReceiverFacade.findByName(name); + if (alertReceiver.isPresent()) { + return alertReceiver.get(); + } + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + "Alert receiver not found " + name); + } + + public void createRoute(Project project, FeatureGroupAlert featureGroupAlert) throws FeaturestoreException { + try { + alertController.createRoute(project, featureGroupAlert); + } catch (AlertManagerClientCreateException | AlertManagerConfigReadException | + AlertManagerConfigCtrlCreateException | AlertManagerConfigUpdateException | + AlertManagerNoSuchElementException | + AlertManagerAccessControlException | AlertManagerUnreachableException e) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.FAILED_TO_CREATE_ROUTE, Level.FINE, + e.getMessage()); + } + } + + public void createRoute(Project project, FeatureViewAlert featureViewAlert) throws FeaturestoreException { + try { + alertController.createRoute(project, featureViewAlert); + } catch (AlertManagerClientCreateException | AlertManagerConfigReadException | + AlertManagerConfigCtrlCreateException | AlertManagerConfigUpdateException | + AlertManagerNoSuchElementException | + AlertManagerAccessControlException | AlertManagerUnreachableException e) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.FAILED_TO_CREATE_ROUTE, Level.FINE, + e.getMessage()); + } + } + + public void deleteRoute(FeatureGroupAlert featureGroupAlert, Project project) throws FeaturestoreException { + try { + alertController.deleteRoute(project, featureGroupAlert); + } catch (AlertManagerUnreachableException | AlertManagerAccessControlException | AlertManagerConfigUpdateException | + AlertManagerConfigCtrlCreateException | AlertManagerConfigReadException | + AlertManagerClientCreateException e) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.FAILED_TO_DELETE_ROUTE, Level.FINE, + e.getMessage()); + } + } + + public void deleteRoute(FeatureViewAlert featureViewAlert, Project project) throws FeaturestoreException { + try { + alertController.deleteRoute(project, featureViewAlert); + } catch (AlertManagerUnreachableException | AlertManagerAccessControlException | AlertManagerConfigUpdateException | + AlertManagerConfigCtrlCreateException | AlertManagerConfigReadException | + AlertManagerClientCreateException e) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.FAILED_TO_DELETE_ROUTE, Level.FINE, + e.getMessage()); + } + } + + public FeatureGroupAlert updateAlert(FeatureGroupAlertDTO dto, FeatureGroupAlert featureGroupAlert, + Project project) + throws FeaturestoreException { + if (dto.getStatus() != null) { + featureGroupAlert.setStatus(dto.getStatus()); + } + if (dto.getSeverity() != null) { + featureGroupAlert.setSeverity(dto.getSeverity()); + } + if (!featureGroupAlert.getReceiver().getName().equals(dto.getReceiver())) { + deleteRoute(featureGroupAlert, project); + featureGroupAlert.setReceiver(getReceiver(dto.getReceiver())); + createRoute(project, featureGroupAlert); + } + featureGroupAlert.setAlertType(alertController.getAlertType(featureGroupAlert.getReceiver())); + featureGroupAlert = featureGroupAlertFacade.update(featureGroupAlert); + return featureGroupAlert; + } + + public FeatureViewAlert updateAlert(FeatureViewAlertDTO dto, FeatureViewAlert featureViewAlert, Project project) + throws FeaturestoreException { + if (dto.getStatus() != null) { + featureViewAlert.setStatus(dto.getStatus()); + } + if (dto.getSeverity() != null) { + featureViewAlert.setSeverity(dto.getSeverity()); + } + if (!featureViewAlert.getReceiver().getName().equals(dto.getReceiver())) { + deleteRoute(featureViewAlert, project); + featureViewAlert.setReceiver(getReceiver(dto.getReceiver())); + createRoute(project, featureViewAlert); + } + featureViewAlert.setAlertType(alertController.getAlertType(featureViewAlert.getReceiver())); + featureViewAlert = featureViewAlertFacade.update(featureViewAlert); + return featureViewAlert; + } + + + + public FeatureViewAlert persistFeatureViewEntityValues(PostableFeatureStoreAlerts dto, + FeatureView featureView) throws FeaturestoreException { + FeatureViewAlert featureViewAlert = new FeatureViewAlert(); + featureViewAlert.setStatus(dto.getStatus()); + featureViewAlert.setSeverity(dto.getSeverity()); + featureViewAlert.setCreated(new Date()); + featureViewAlert.setFeatureView(featureView); + featureViewAlert.setReceiver(getReceiver(dto.getReceiver())); + featureViewAlert.setAlertType(alertController.getAlertType(featureViewAlert.getReceiver())); + featureViewAlert = featureViewAlertFacade.update(featureViewAlert); + return featureViewAlert; + } + + public FeatureGroupAlert persistFeatureGroupEntityValues(PostableFeatureStoreAlerts dto, + Featuregroup featureGroup) throws FeaturestoreException { + FeatureGroupAlert featureGroupAlert = new FeatureGroupAlert(); + featureGroupAlert.setStatus(dto.getStatus()); + featureGroupAlert.setSeverity(dto.getSeverity()); + featureGroupAlert.setCreated(new Date()); + featureGroupAlert.setFeatureGroup(featureGroup); + featureGroupAlert.setReceiver(getReceiver(dto.getReceiver())); + featureGroupAlert.setAlertType(alertController.getAlertType(featureGroupAlert.getReceiver())); + featureGroupAlert = featureGroupAlertFacade.update(featureGroupAlert); + return featureGroupAlert; + } + + public FeatureViewAlert retrieveSingleAlert( Integer id , FeatureView featureView) throws FeaturestoreException { + + FeatureViewAlert featureViewAlert = featureViewAlertFacade.findByFeatureViewAndId(featureView, id); + if (featureViewAlert == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_NOT_FOUND, Level.FINE); + } + return featureViewAlert; + } + + public List retrieveManyAlerts(ResourceRequest resourceRequest, FeatureView featureView) + throws FeaturestoreException { + AbstractFacade.CollectionInfo collectionInfo; + collectionInfo = + featureViewAlertFacade.findFeatureViewAlerts(resourceRequest.getOffset(), resourceRequest.getLimit(), + resourceRequest.getFilter(), resourceRequest.getSort(), featureView); + if (collectionInfo.getItems().isEmpty()) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_NOT_FOUND, Level.FINE, "No alerts found " + + "for feature view "+featureView.getName()); + } + return collectionInfo.getItems(); + } + + public List retrieveAllFeatureViewAlerts(Project project) { + List projectAlerts = featureViewAlertFacade.findAll(); + projectAlerts = projectAlerts.stream() + .filter(featureViewAlert -> featureViewAlert.getFeatureView().getFeaturestore().getProject().equals(project)) + .collect(Collectors.toList()); + return projectAlerts; + + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertValidation.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertValidation.java new file mode 100644 index 0000000000..e31543b451 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/alert/FeatureStoreAlertValidation.java @@ -0,0 +1,140 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.alert; + +import io.hops.hopsworks.alert.util.Constants; +import io.hops.hopsworks.api.featurestore.datavalidation.alert.PostableFeatureStoreAlerts; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.datavalidation.FeatureGroupAlertFacade; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewAlertFacade; +import io.hops.hopsworks.exceptions.AlertException; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; +import io.hops.hopsworks.restutils.RESTCodes; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureStoreAlertValidation { + @EJB + private FeatureGroupAlertFacade featureGroupAlertFacade; + @EJB + private FeatureViewAlertFacade featureViewAlertFacade; + + public void validate(PostableFeatureStoreAlerts dto, Featuregroup featuregroup, FeatureView featureView) + throws FeaturestoreException { + if (dto == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + Constants.NO_PAYLOAD); + } + if (dto.getStatus() == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + "Status can not be empty."); + } + if (dto.getSeverity() == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + "Severity can not be empty."); + } + FeatureGroupAlert featureGroupAlert = + featureGroupAlertFacade.findByFeatureGroupAndStatus(featuregroup, dto.getStatus()); + if (featureGroupAlert != null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ALREADY_EXISTS, Level.FINE, + String.format("Feature Group Alert with FeatureGroupName=%s status=%s already exists.", + featuregroup.getName(), dto.getStatus())); + } + FeatureViewAlert existingAlert = + featureViewAlertFacade.findByFeatureViewAndStatus(featureView, dto.getStatus()); + if (existingAlert != null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ALREADY_EXISTS, Level.FINE, + String.format("Feature View Alert with feature view=%s status=%s already exists.", featureView.getName(), + dto.getStatus())); + } + } + + public void validateUpdate(FeatureViewAlert featureViewAlert, + FeatureStoreAlertStatus status, FeatureView featureView) + throws FeaturestoreException { + if (featureViewAlert == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_NOT_FOUND, Level.FINE); + } + if (!status.equals(featureViewAlert.getStatus()) && + featureViewAlertFacade.findByFeatureViewAndStatus(featureView, status) != null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ALREADY_EXISTS, Level.FINE, + "Feature Group Alert with name=" + featureView.getName() + " status=" + + status + " already exists."); + } + } + + public void validateUpdate(FeatureGroupAlert featureGroupAlert, + FeatureStoreAlertStatus status, Featuregroup featuregroup) + throws FeaturestoreException { + if (featureGroupAlert == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_NOT_FOUND, Level.FINE); + } + if (!status.equals(featureGroupAlert.getStatus()) && + featureGroupAlertFacade.findByFeatureGroupAndStatus(featuregroup, status) != null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ALREADY_EXISTS, Level.FINE, + "Feature Group Alert with name=" + featuregroup.getName() + " status=" + + status + " already exists."); + } + } + + public void validateBulk(PostableFeatureStoreAlerts featureGroupAlertDTO) throws FeaturestoreException { + if (featureGroupAlertDTO.getItems() == null || featureGroupAlertDTO.getItems().isEmpty()) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + Constants.NO_PAYLOAD); + } + Set statusSet = new HashSet<>(); + for (PostableFeatureStoreAlerts dto : featureGroupAlertDTO.getItems()) { + statusSet.add(dto.getStatus()); + } + if (statusSet.size() < featureGroupAlertDTO.getItems().size()) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + "Duplicate alert."); + } + } + + public void validateFeatureViewRequest(PostableFeatureStoreAlerts dto, ResourceRequest.Name resourceName) + throws AlertException { + if (resourceName.equals(ResourceRequest.Name.FEATUREVIEW) && + !FeatureStoreAlertStatus.isFeatureMonitoringStatus(dto.getStatus())) { + throw new AlertException(RESTCodes.AlertErrorCode.ILLEGAL_ARGUMENT, Level.FINE, + String.format("Alert status %s not allowed for feature view alerts", dto.getStatus())); + } + } + + public void validateEntityType(ResourceRequest.Name requestName, Featuregroup featuregroup, FeatureView featureView) { + if (requestName.equals(ResourceRequest.Name.FEATUREGROUPS) && featuregroup == null) { + throw new IllegalArgumentException( + "Feature group id cannot be null if alert entity type is " + ResourceRequest.Name.FEATUREGROUPS); + } + if (requestName.equals(ResourceRequest.Name.FEATUREVIEW) && featureView == null) { + throw new IllegalArgumentException( + "Feature view cannot be null if alert entity type is " + ResourceRequest.Name.FEATUREVIEW); + } + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertDTO.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertDTO.java index e6ec22095f..cade40f1a7 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertDTO.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertDTO.java @@ -19,7 +19,7 @@ import io.hops.hopsworks.common.api.RestDTO; import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.ValidationRuleAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import javax.xml.bind.annotation.XmlRootElement; import java.util.Date; @@ -30,7 +30,7 @@ public class FeatureGroupAlertDTO extends RestDTO { private Integer featureGroupId; private String featureStoreName; private String featureGroupName; - private ValidationRuleAlertStatus status; + private FeatureStoreAlertStatus status; private AlertType alertType; private AlertSeverity severity; private String receiver; @@ -70,13 +70,12 @@ public String getFeatureGroupName() { public void setFeatureGroupName(String featureGroupName) { this.featureGroupName = featureGroupName; } - - public ValidationRuleAlertStatus getStatus() { + + public FeatureStoreAlertStatus getStatus() { return status; } - - public void setStatus( - ValidationRuleAlertStatus status) { + + public void setStatus(FeatureStoreAlertStatus status) { this.status = status; } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertResource.java deleted file mode 100644 index db39e72e17..0000000000 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertResource.java +++ /dev/null @@ -1,367 +0,0 @@ -/* - * This file is part of Hopsworks - * Copyright (C) 2021, Logical Clocks AB. All rights reserved - * - * Hopsworks is free software: you can redistribute it and/or modify it under the terms of - * the GNU Affero General Public License as published by the Free Software Foundation, - * either version 3 of the License, or (at your option) any later version. - * - * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see . - */ -package io.hops.hopsworks.api.featurestore.datavalidation.alert; - -import com.google.common.base.Strings; -import io.hops.hopsworks.alert.dao.AlertReceiverFacade; -import io.hops.hopsworks.alert.exception.AlertManagerAccessControlException; -import io.hops.hopsworks.alert.exception.AlertManagerUnreachableException; -import io.hops.hopsworks.alerting.api.alert.dto.Alert; -import io.hops.hopsworks.alerting.exceptions.AlertManagerClientCreateException; -import io.hops.hopsworks.alerting.exceptions.AlertManagerConfigCtrlCreateException; -import io.hops.hopsworks.alerting.exceptions.AlertManagerConfigReadException; -import io.hops.hopsworks.alerting.exceptions.AlertManagerConfigUpdateException; -import io.hops.hopsworks.alerting.exceptions.AlertManagerNoSuchElementException; -import io.hops.hopsworks.alerting.exceptions.AlertManagerResponseException; -import io.hops.hopsworks.api.alert.AlertBuilder; -import io.hops.hopsworks.api.alert.AlertDTO; -import io.hops.hopsworks.api.filter.AllowedProjectRoles; -import io.hops.hopsworks.api.filter.Audience; -import io.hops.hopsworks.api.auth.key.ApiKeyRequired; -import io.hops.hopsworks.api.project.alert.ProjectAlertsDTO; -import io.hops.hopsworks.api.util.Pagination; -import io.hops.hopsworks.common.alert.AlertController; -import io.hops.hopsworks.common.api.ResourceRequest; -import io.hops.hopsworks.common.featurestore.datavalidation.FeatureGroupAlertFacade; -import io.hops.hopsworks.exceptions.AlertException; -import io.hops.hopsworks.exceptions.FeaturestoreException; -import io.hops.hopsworks.jwt.annotation.JWTRequired; -import io.hops.hopsworks.persistence.entity.alertmanager.AlertReceiver; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.ValidationRuleAlertStatus; -import io.hops.hopsworks.persistence.entity.user.security.apiKey.ApiScope; -import io.hops.hopsworks.restutils.RESTCodes; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; - -import javax.ejb.EJB; -import javax.ejb.TransactionAttribute; -import javax.ejb.TransactionAttributeType; -import javax.enterprise.context.RequestScoped; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.BeanParam; -import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.core.UriInfo; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -@Api(value = "FeatureGroupAlert Resource") -@RequestScoped -@TransactionAttribute(TransactionAttributeType.NEVER) -public class FeatureGroupAlertResource { - - private static final Logger LOGGER = Logger.getLogger(FeatureGroupAlertResource.class.getName()); - - @EJB - private FeatureGroupAlertBuilder featureGroupAlertBuilder; - @EJB - private FeatureGroupAlertFacade featureGroupAlertFacade; - @EJB - private AlertController alertController; - @EJB - private AlertBuilder alertBuilder; - @EJB - private AlertReceiverFacade alertReceiverFacade; - - private Featuregroup featuregroup; - - public void setFeatureGroup(Featuregroup featuregroup) { - this.featuregroup = featuregroup; - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Get all feature group alerts.", response = FeatureGroupAlertDTO.class) - @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) - @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, - allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - public Response get(@BeanParam Pagination pagination, - @BeanParam FeatureGroupAlertBeanParam featureGroupAlertBeanParam, - @Context UriInfo uriInfo, - @Context SecurityContext sc) { - ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); - resourceRequest.setOffset(pagination.getOffset()); - resourceRequest.setLimit(pagination.getLimit()); - resourceRequest.setSort(featureGroupAlertBeanParam.getSortBySet()); - resourceRequest.setFilter(featureGroupAlertBeanParam.getFilter()); - FeatureGroupAlertDTO dto = featureGroupAlertBuilder.buildItems(uriInfo, resourceRequest, this.featuregroup); - return Response.ok().entity(dto).build(); - } - - @GET - @Path("{id}") - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Find feature group alert by Id.", response = FeatureGroupAlertDTO.class) - @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) - @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, - allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - public Response getById(@PathParam("id") Integer id, - @Context UriInfo uriInfo, - @Context HttpServletRequest req, - @Context SecurityContext sc) - throws FeaturestoreException { - ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); - FeatureGroupAlertDTO dto = featureGroupAlertBuilder.build(uriInfo, resourceRequest, this.featuregroup, id); - return Response.ok().entity(dto).build(); - } - - @GET - @Path("values") - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Get values for feature group alert.", response = FeatureGroupAlertValues.class) - @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) - @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, - allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - public Response getAvailableServices(@Context UriInfo uriInfo, - @Context HttpServletRequest req, - @Context SecurityContext sc) { - FeatureGroupAlertValues values = new FeatureGroupAlertValues(); - return Response.ok().entity(values).build(); - } - - @PUT - @Path("{id}") - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Update a feature group alert.", response = FeatureGroupAlertDTO.class) - @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) - @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, - allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - public Response createOrUpdate(@PathParam("id") Integer id, - FeatureGroupAlertDTO dto, - @Context UriInfo uriInfo, - @Context SecurityContext sc) throws FeaturestoreException { - FeatureGroupAlert featureGroupAlert = featureGroupAlertFacade.findByFeatureGroupAndId(this.featuregroup, id); - if (featureGroupAlert == null) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_NOT_FOUND, Level.FINE); - } - if (dto == null) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "No payload."); - } - if (dto.getStatus() != null) { - if (!dto.getStatus().equals(featureGroupAlert.getStatus()) && - featureGroupAlertFacade.findByFeatureGroupAndStatus(this.featuregroup, dto.getStatus()) != null) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ALREADY_EXISTS, Level.FINE, - "Feature Group Alert with FeatureGroupName=" + this.featuregroup.getName() + " status=" + - dto.getStatus() + " already exists."); - } - featureGroupAlert.setStatus(dto.getStatus()); - } - if (dto.getSeverity() != null) { - featureGroupAlert.setSeverity(dto.getSeverity()); - } - if (!featureGroupAlert.getReceiver().getName().equals(dto.getReceiver())) { - deleteRoute(featureGroupAlert); - featureGroupAlert.setReceiver(getReceiver(dto.getReceiver())); - createRoute(featureGroupAlert); - } - featureGroupAlert.setAlertType(alertController.getAlertType(featureGroupAlert.getReceiver())); - featureGroupAlert = featureGroupAlertFacade.update(featureGroupAlert); - ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); - dto = featureGroupAlertBuilder.build(uriInfo, resourceRequest, featureGroupAlert); - return Response.ok().entity(dto).build(); - } - - @POST - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Create a feature group alert.", response = PostableFeatureGroupAlerts.class) - @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) - @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, - allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - public Response create(PostableFeatureGroupAlerts dto, - @QueryParam("bulk") @DefaultValue("false") Boolean bulk, - @Context UriInfo uriInfo, - @Context SecurityContext sc) throws FeaturestoreException { - ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); - FeatureGroupAlertDTO featureGroupAlertDTO = createAlert(dto, bulk, uriInfo, resourceRequest); - return Response.created(featureGroupAlertDTO.getHref()).entity(featureGroupAlertDTO).build(); - } - - private FeatureGroupAlertDTO createAlert(PostableFeatureGroupAlerts featureGroupAlertDTO, Boolean bulk, - UriInfo uriInfo, ResourceRequest resourceRequest) throws FeaturestoreException { - FeatureGroupAlertDTO dto; - if (bulk) { - validateBulk(featureGroupAlertDTO); - dto = new FeatureGroupAlertDTO(); - for (PostableFeatureGroupAlerts pa : featureGroupAlertDTO.getItems()) { - dto.addItem(createAlert(pa, uriInfo, resourceRequest)); - } - dto.setCount((long) featureGroupAlertDTO.getItems().size()); - } else { - dto = createAlert(featureGroupAlertDTO, uriInfo, resourceRequest); - } - return dto; - } - - private void validateBulk(PostableFeatureGroupAlerts featureGroupAlertDTO) throws FeaturestoreException { - if (featureGroupAlertDTO.getItems() == null || featureGroupAlertDTO.getItems().size() < 1) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "No payload."); - } - Set statusSet = new HashSet<>(); - for (PostableFeatureGroupAlerts dto : featureGroupAlertDTO.getItems()) { - statusSet.add(dto.getStatus()); - } - if (statusSet.size() < featureGroupAlertDTO.getItems().size()) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "Duplicate alert."); - } - } - - private FeatureGroupAlertDTO createAlert(PostableFeatureGroupAlerts dto, UriInfo uriInfo, - ResourceRequest resourceRequest) throws FeaturestoreException { - validate(dto); - FeatureGroupAlert featureGroupAlert = new FeatureGroupAlert(); - featureGroupAlert.setStatus(dto.getStatus()); - featureGroupAlert.setSeverity(dto.getSeverity()); - featureGroupAlert.setCreated(new Date()); - featureGroupAlert.setFeatureGroup(this.featuregroup); - featureGroupAlert.setReceiver(getReceiver(dto.getReceiver())); - featureGroupAlert.setAlertType(alertController.getAlertType(featureGroupAlert.getReceiver())); - createRoute(featureGroupAlert); - featureGroupAlertFacade.save(featureGroupAlert); - featureGroupAlert = featureGroupAlertFacade.findByFeatureGroupAndStatus(this.featuregroup, dto.getStatus()); - return featureGroupAlertBuilder.buildItems(uriInfo, resourceRequest, featureGroupAlert); - } - - @POST - @Path("{id}/test") - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation(value = "Test alert by Id.", response = ProjectAlertsDTO.class) - @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) - @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, - allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - public Response getTestById(@PathParam("id") Integer id, - @Context UriInfo uriInfo, - @Context HttpServletRequest req, - @Context SecurityContext sc) throws AlertException { - FeatureGroupAlert featureGroupAlert = featureGroupAlertFacade.findByFeatureGroupAndId(featuregroup, id); - List alerts; - try { - alerts = alertController.testAlert(featuregroup.getFeaturestore().getProject(), featureGroupAlert); - } catch (AlertManagerUnreachableException | AlertManagerClientCreateException e) { - throw new AlertException(RESTCodes.AlertErrorCode.FAILED_TO_CONNECT, Level.FINE, e.getMessage()); - } catch (AlertManagerAccessControlException e) { - throw new AlertException(RESTCodes.AlertErrorCode.ACCESS_CONTROL_EXCEPTION, Level.FINE, e.getMessage()); - } catch (AlertManagerResponseException e) { - throw new AlertException(RESTCodes.AlertErrorCode.RESPONSE_ERROR, Level.FINE, e.getMessage()); - } - ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); - AlertDTO alertDTO = - alertBuilder.getAlertDTOs(uriInfo, resourceRequest, alerts, featuregroup.getFeaturestore().getProject()); - return Response.ok().entity(alertDTO).build(); - } - - @DELETE - @Path("{id}") - @ApiOperation(value = "Delete feature group alert by Id.") - @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) - @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, - allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) - public Response deleteById(@PathParam("id") Integer id, - @Context UriInfo uriInfo, - @Context HttpServletRequest req, - @Context SecurityContext sc) throws FeaturestoreException { - FeatureGroupAlert featureGroupAlert = featureGroupAlertFacade.findByFeatureGroupAndId(this.featuregroup, id); - if (featureGroupAlert != null) { - deleteRoute(featureGroupAlert); - featureGroupAlertFacade.remove(featureGroupAlert); - } - return Response.noContent().build(); - } - - - private void validate(PostableFeatureGroupAlerts dto) throws FeaturestoreException { - if (dto == null) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "No payload."); - } - if (dto.getStatus() == null) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "Status can not be empty."); - } - if (dto.getSeverity() == null) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "Severity can not be empty."); - } - FeatureGroupAlert featuregroupexpectationalert = - featureGroupAlertFacade.findByFeatureGroupAndStatus(this.featuregroup, dto.getStatus()); - if (featuregroupexpectationalert != null) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ALREADY_EXISTS, Level.FINE, - "Feature Group Alert with FeatureGroupName=" + this.featuregroup.getName() + " status=" + - dto.getStatus() + " already exists."); - } - } - - private AlertReceiver getReceiver(String name) throws FeaturestoreException { - if (Strings.isNullOrEmpty(name)) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "Receiver can not be empty."); - } - Optional alertReceiver = alertReceiverFacade.findByName(name); - if (alertReceiver.isPresent()) { - return alertReceiver.get(); - } - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, - "Alert receiver not found " + name); - } - - private void createRoute(FeatureGroupAlert featureGroupAlert) throws FeaturestoreException { - try { - alertController.createRoute(featureGroupAlert); - } catch (AlertManagerClientCreateException | AlertManagerConfigReadException | - AlertManagerConfigCtrlCreateException | AlertManagerConfigUpdateException | AlertManagerNoSuchElementException | - AlertManagerAccessControlException | AlertManagerUnreachableException e) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.FAILED_TO_CREATE_ROUTE, Level.FINE, - e.getMessage()); - } - } - - private void deleteRoute(FeatureGroupAlert featureGroupAlert) throws FeaturestoreException { - try { - alertController.deleteRoute(featureGroupAlert); - } catch (AlertManagerUnreachableException | AlertManagerAccessControlException | AlertManagerConfigUpdateException | - AlertManagerConfigCtrlCreateException | AlertManagerConfigReadException | AlertManagerClientCreateException e) { - throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.FAILED_TO_DELETE_ROUTE, Level.FINE, - e.getMessage()); - } - } -} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertValues.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertValues.java index a136256116..255267494d 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertValues.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupAlertValues.java @@ -17,7 +17,7 @@ import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.ValidationRuleAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; @@ -26,23 +26,22 @@ @XmlRootElement public class FeatureGroupAlertValues { - private List status; + private List status; private List alertType; private List severity; public FeatureGroupAlertValues() { - status = Arrays.asList(ValidationRuleAlertStatus.values()); + status = Arrays.asList(FeatureStoreAlertStatus.values()); severity = Arrays.asList(AlertSeverity.values()); alertType = new ArrayList<>(Arrays.asList(AlertType.values())); alertType.removeIf(a -> a.equals(AlertType.SYSTEM_ALERT)); } - - public List getStatus() { + + public List getStatus() { return status; } - - public void setStatus( - List status) { + + public void setStatus(List status) { this.status = status; } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupValidationAlertResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupValidationAlertResource.java new file mode 100644 index 0000000000..1c9ffa9222 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureGroupValidationAlertResource.java @@ -0,0 +1,90 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.datavalidation.alert; + +import io.hops.hopsworks.alert.util.Constants; +import io.hops.hopsworks.api.filter.AllowedProjectRoles; +import io.hops.hopsworks.api.filter.Audience; +import io.hops.hopsworks.api.auth.key.ApiKeyRequired; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.datavalidation.FeatureGroupAlertFacade; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.jwt.annotation.JWTRequired; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.user.security.apiKey.ApiScope; +import io.hops.hopsworks.restutils.RESTCodes; +import io.swagger.annotations.ApiOperation; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.enterprise.context.RequestScoped; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import java.util.logging.Level; + +@RequestScoped +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureGroupValidationAlertResource extends FeatureStoreAlertResource { + @Override + protected ResourceRequest.Name getEntityType() { + return ResourceRequest.Name.FEATUREGROUPS; + } + + @EJB + FeatureGroupAlertBuilder featureGroupAlertBuilder; + @EJB + FeatureGroupAlertFacade featureGroupAlertFacade; + + @PUT + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Update a feature group alert.", response = FeatureGroupAlertDTO.class) + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response createOrUpdate( + @PathParam("id") + Integer id, + FeatureGroupAlertDTO dto, + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) throws FeaturestoreException { + featureStoreAlertValidation.validateEntityType(getEntityType(), this.featuregroup, this.featureView); + if (dto == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + Constants.NO_PAYLOAD); + } + FeatureGroupAlert featureGroupAlert = featureGroupAlertFacade.findByFeatureGroupAndId(this.featuregroup, id); + featureStoreAlertValidation.validateUpdate(featureGroupAlert, dto.getStatus(), featuregroup); + featureGroupAlert = featureStoreAlertController.updateAlert(dto, featureGroupAlert, project); + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); + dto = featureGroupAlertBuilder.build(uriInfo, resourceRequest, featureGroupAlert); + return Response.ok().entity(dto).build(); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureStoreAlertResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureStoreAlertResource.java new file mode 100644 index 0000000000..d63f53e036 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/FeatureStoreAlertResource.java @@ -0,0 +1,379 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.datavalidation.alert; + +import io.hops.hopsworks.alert.dao.AlertReceiverFacade; +import io.hops.hopsworks.alert.exception.AlertManagerAccessControlException; +import io.hops.hopsworks.alert.exception.AlertManagerUnreachableException; +import io.hops.hopsworks.alerting.api.alert.dto.Alert; +import io.hops.hopsworks.alerting.exceptions.AlertManagerClientCreateException; +import io.hops.hopsworks.alerting.exceptions.AlertManagerResponseException; +import io.hops.hopsworks.api.alert.AlertBuilder; +import io.hops.hopsworks.api.alert.AlertDTO; +import io.hops.hopsworks.api.alert.FeatureStoreAlertController; +import io.hops.hopsworks.api.alert.FeatureStoreAlertValidation; +import io.hops.hopsworks.api.featurestore.featureview.FeatureViewAlertBuilder; +import io.hops.hopsworks.api.featurestore.featureview.FeatureViewAlertDTO; +import io.hops.hopsworks.api.filter.AllowedProjectRoles; +import io.hops.hopsworks.api.filter.Audience; +import io.hops.hopsworks.api.auth.key.ApiKeyRequired; +import io.hops.hopsworks.api.project.alert.ProjectAlertsDTO; +import io.hops.hopsworks.api.util.Pagination; +import io.hops.hopsworks.common.alert.AlertController; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.api.RestDTO; +import io.hops.hopsworks.common.featurestore.datavalidation.FeatureGroupAlertFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.alert.FeatureMonitoringAlertController; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewAlertFacade; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; +import io.hops.hopsworks.exceptions.AlertException; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.jwt.annotation.JWTRequired; +import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.persistence.entity.user.security.apiKey.ApiScope; +import io.hops.hopsworks.restutils.RESTCodes; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BeanParam; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Api(value = "FeatureStoreAlert Resource") +@TransactionAttribute(TransactionAttributeType.NEVER) +public abstract class FeatureStoreAlertResource { + + private static final Logger LOGGER = Logger.getLogger(FeatureStoreAlertResource.class.getName()); + @EJB + private FeatureGroupAlertBuilder featureGroupAlertBuilder; + @EJB + private FeatureGroupAlertFacade featureGroupAlertFacade; + @EJB + private AlertController alertController; + @EJB + private AlertBuilder alertBuilder; + @EJB + private AlertReceiverFacade alertReceiverFacade; + @EJB + private FeatureViewController featureViewController; + @EJB + private FeatureViewAlertBuilder featureViewAlertBuilder; + @EJB + protected FeatureViewAlertFacade featureViewAlertFacade; + @EJB + protected FeatureMonitoringAlertController featureMonitoringAlertController; + @EJB + protected FeatureStoreAlertController featureStoreAlertController; + @EJB + protected FeatureStoreAlertValidation featureStoreAlertValidation; + + protected Featuregroup featuregroup; + protected FeatureView featureView; + protected Featurestore featureStore; + protected Project project; + + public void setFeatureGroup(Featuregroup featuregroup) { + this.featuregroup = featuregroup; + } + + public void setFeatureView(String name, Integer version, Featurestore featurestore) throws FeaturestoreException { + this.featureView = featureViewController.getByNameVersionAndFeatureStore(name, version, featurestore); + } + + public void setFeatureStore(Featurestore featureStore) { + this.featureStore = featureStore; + } + + public void setProject(Project project) { + this.project = project; + } + protected abstract ResourceRequest.Name getEntityType(); + + @GET + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Get all feature store alerts.", response = RestDTO.class) + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response get( + @BeanParam + Pagination pagination, + @BeanParam + FeatureGroupAlertBeanParam featureGroupAlertBeanParam, + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) throws FeaturestoreException { + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); + resourceRequest.setOffset(pagination.getOffset()); + resourceRequest.setLimit(pagination.getLimit()); + resourceRequest.setSort(featureGroupAlertBeanParam.getSortBySet()); + resourceRequest.setFilter(featureGroupAlertBeanParam.getFilter()); + featureStoreAlertValidation.validateEntityType(getEntityType(), this.featuregroup, this.featureView); + FeatureGroupAlertDTO dto; + FeatureViewAlertDTO featureViewAlertDto; + if (getEntityType().equals(ResourceRequest.Name.FEATUREGROUPS)) { + dto = featureGroupAlertBuilder.buildItems(uriInfo, resourceRequest, this.featuregroup); + return Response.ok().entity(dto).build(); + } else { + featureViewAlertDto = featureViewAlertBuilder.buildMany(uriInfo, resourceRequest, + featureStoreAlertController.retrieveManyAlerts(resourceRequest, this.featureView)); + return Response.ok().entity(featureViewAlertDto).build(); + } + } + + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Find feature store alert by Id.", response = RestDTO.class) + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getById( + @PathParam("id") + Integer id, + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) + throws FeaturestoreException { + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); + featureStoreAlertValidation.validateEntityType(getEntityType(), this.featuregroup, this.featureView); + if (getEntityType().equals(ResourceRequest.Name.FEATUREGROUPS)) { + FeatureGroupAlertDTO dto = featureGroupAlertBuilder.build(uriInfo, resourceRequest, this.featuregroup, id); + return Response.ok().entity(dto).build(); + } else { + FeatureViewAlertDTO dto = featureViewAlertBuilder.buildFeatureViewAlertDto(uriInfo, resourceRequest, + featureStoreAlertController.retrieveSingleAlert(id, this.featureView)); + return Response.ok().entity(dto).build(); + } + } + + @GET + @Path("values") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Get values for feature store alert.", response = FeatureGroupAlertValues.class) + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getAvailableServices( + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) { + FeatureGroupAlertValues values = new FeatureGroupAlertValues(); + return Response.ok().entity(values).build(); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Create a feature store alert.", response = PostableFeatureStoreAlerts.class) + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response create(PostableFeatureStoreAlerts dto, + @QueryParam("bulk") + @DefaultValue("false") + Boolean bulk, + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) throws FeaturestoreException, AlertException { + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); + featureStoreAlertValidation.validateEntityType(getEntityType(), this.featuregroup, this.featureView); + if (getEntityType().equals(ResourceRequest.Name.FEATUREGROUPS)) { + FeatureGroupAlertDTO featureGroupAlertDTO = createAlert(dto, bulk, uriInfo, resourceRequest); + return Response.created(featureGroupAlertDTO.getHref()).entity(featureGroupAlertDTO).build(); + } else { + featureStoreAlertValidation.validateFeatureViewRequest(dto, getEntityType()); + FeatureViewAlertDTO fvDTO = createFeatureViewAlert(dto, bulk, uriInfo, resourceRequest); + return Response.created(fvDTO.getHref()).entity(fvDTO).build(); + } + } + + + + private FeatureGroupAlertDTO createAlert(PostableFeatureStoreAlerts featureGroupAlertDTO, Boolean bulk, + UriInfo uriInfo, ResourceRequest resourceRequest) throws FeaturestoreException { + FeatureGroupAlertDTO dto; + if (bulk) { + featureStoreAlertValidation.validateBulk(featureGroupAlertDTO); + dto = new FeatureGroupAlertDTO(); + for (PostableFeatureStoreAlerts pa : featureGroupAlertDTO.getItems()) { + dto.addItem(createAlert(pa, uriInfo, resourceRequest)); + } + dto.setCount((long) featureGroupAlertDTO.getItems().size()); + } else { + dto = createAlert(featureGroupAlertDTO, uriInfo, resourceRequest); + } + return dto; + } + + + private FeatureGroupAlertDTO createAlert(PostableFeatureStoreAlerts dto, UriInfo uriInfo, + ResourceRequest resourceRequest) throws FeaturestoreException { + featureStoreAlertValidation.validate(dto, this.featuregroup, this.featureView); + FeatureGroupAlert featureGroupAlert = + featureStoreAlertController.persistFeatureGroupEntityValues(dto, this.featuregroup); + featureStoreAlertController.createRoute(project,featureGroupAlert); + return featureGroupAlertBuilder.buildItems(uriInfo, resourceRequest, featureGroupAlert); + } + + @POST + @Path("{id}/test") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Test alert by Id.", response = ProjectAlertsDTO.class) + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getTestById( + @PathParam("id") + Integer id, + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) throws AlertException { + List alerts; + FeatureGroupAlert featureGroupAlert=null; + FeatureViewAlert featureViewAlert=null; + try { + if (getEntityType().equals(ResourceRequest.Name.FEATUREGROUPS)) { + featureGroupAlert = featureGroupAlertFacade.find(id); + if (featureGroupAlert == null) { + throw new AlertException(RESTCodes.AlertErrorCode.ILLEGAL_ARGUMENT, Level.FINE, + "Alert not found " + id); + } + alerts = alertController.testAlert(project, featureGroupAlert); + } else { + featureViewAlert = featureViewAlertFacade.find(id); + if (featureViewAlert == null) { + throw new AlertException(RESTCodes.AlertErrorCode.ILLEGAL_ARGUMENT, Level.FINE, + "Alert not found " + id); + } + alerts = alertController.testAlert(project, featureViewAlert); + } + } catch (AlertManagerUnreachableException | AlertManagerClientCreateException e) { + throw new AlertException(RESTCodes.AlertErrorCode.FAILED_TO_CONNECT, Level.SEVERE, e.getMessage()); + } catch (AlertManagerAccessControlException e) { + throw new AlertException(RESTCodes.AlertErrorCode.ACCESS_CONTROL_EXCEPTION, Level.SEVERE, e.getMessage()); + } catch (AlertManagerResponseException e) { + throw new AlertException(RESTCodes.AlertErrorCode.RESPONSE_ERROR, Level.SEVERE, e.getMessage()); + } + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); + AlertDTO alertDTO = + alertBuilder.getAlertDTOs(uriInfo, resourceRequest, alerts, project); + return Response.ok().entity(alertDTO).build(); + } + + @DELETE + @Path("{id}") + @ApiOperation(value = "Delete feature store alert by Id.") + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response deleteById( + @PathParam("id") + Integer id, + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) throws FeaturestoreException { + featureStoreAlertValidation.validateEntityType(getEntityType(), this.featuregroup, this.featureView); + if (getEntityType().equals(ResourceRequest.Name.FEATUREGROUPS)) { + FeatureGroupAlert featureGroupAlert = featureGroupAlertFacade.findByFeatureGroupAndId(this.featuregroup, id); + if (featureGroupAlert != null) { + featureStoreAlertController.deleteRoute(featureGroupAlert, project); + featureGroupAlertFacade.remove(featureGroupAlert); + } + return Response.noContent().build(); + } else { + FeatureViewAlert featureViewAlert = featureViewAlertFacade.findByFeatureViewAndId(this.featureView, id); + if (featureViewAlert != null) { + featureStoreAlertController.deleteRoute(featureViewAlert, project); + featureViewAlertFacade.remove(featureViewAlert); + } + return Response.noContent().build(); + } + } + + private FeatureViewAlertDTO createFeatureViewAlert(PostableFeatureStoreAlerts paDTO, Boolean bulk, + UriInfo uriInfo, ResourceRequest resourceRequest) throws FeaturestoreException { + FeatureViewAlertDTO dto; + if (bulk) { + featureStoreAlertValidation.validateBulk(paDTO); + dto = new FeatureViewAlertDTO(); + for (PostableFeatureStoreAlerts pa : paDTO.getItems()) { + dto.addItem(createFeatureViewAlert(pa, uriInfo, resourceRequest)); + } + dto.setCount((long) paDTO.getItems().size()); + } else { + dto = createFeatureViewAlert(paDTO, uriInfo, resourceRequest); + } + return dto; + } + + private FeatureViewAlertDTO createFeatureViewAlert(PostableFeatureStoreAlerts dto, UriInfo uriInfo, + ResourceRequest resourceRequest) throws FeaturestoreException { + featureStoreAlertValidation.validate(dto, this.featuregroup, this.featureView); + FeatureViewAlert featureViewAlert; + FeatureViewAlertDTO fvDTO; + featureViewAlert = featureStoreAlertController.persistFeatureViewEntityValues(dto, this.featureView); + fvDTO = featureViewAlertBuilder.buildFeatureViewAlertDto(uriInfo, resourceRequest, featureViewAlert); + featureStoreAlertController.createRoute(project,featureViewAlert); + return fvDTO; + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/PostableFeatureGroupAlerts.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/PostableFeatureStoreAlerts.java similarity index 70% rename from hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/PostableFeatureGroupAlerts.java rename to hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/PostableFeatureStoreAlerts.java index c420b431ee..5a74f83e9c 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/PostableFeatureGroupAlerts.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/datavalidation/alert/PostableFeatureStoreAlerts.java @@ -1,6 +1,6 @@ /* * This file is part of Hopsworks - * Copyright (C) 2021, Logical Clocks AB. All rights reserved + * Copyright (C) 2024, Hopsworks AB. All rights reserved * * Hopsworks is free software: you can redistribute it and/or modify it under the terms of * the GNU Affero General Public License as published by the Free Software Foundation, @@ -16,27 +16,26 @@ package io.hops.hopsworks.api.featurestore.datavalidation.alert; import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.ValidationRuleAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement -public class PostableFeatureGroupAlerts { - private ValidationRuleAlertStatus status; +public class PostableFeatureStoreAlerts { + private FeatureStoreAlertStatus status; private AlertSeverity severity; private String receiver; - private List items; - - public PostableFeatureGroupAlerts() { + private List items; + + public PostableFeatureStoreAlerts() { } - - public ValidationRuleAlertStatus getStatus() { + + public FeatureStoreAlertStatus getStatus() { return status; } - - public void setStatus( - ValidationRuleAlertStatus status) { + + public void setStatus(FeatureStoreAlertStatus status) { this.status = status; } @@ -55,13 +54,13 @@ public String getReceiver() { public void setReceiver(String receiver) { this.receiver = receiver; } - - public List getItems() { + + public List getItems() { return items; } public void setItems( - List items) { + List items) { this.items = items; } } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringConfigurationResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringConfigurationResource.java new file mode 100644 index 0000000000..f775de3828 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringConfigurationResource.java @@ -0,0 +1,66 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuregroup; + +import io.hops.hopsworks.api.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationResource; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.enterprise.context.RequestScoped; + +@RequestScoped +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureGroupFeatureMonitoringConfigurationResource extends FeatureMonitoringConfigurationResource { + + @EJB + private FeaturegroupController featuregroupController; + + private Featuregroup featureGroup; + + /** + * Sets the feature group of the tag resource + * + * @param featureGroupId + */ + public void setFeatureGroup(Integer featureGroupId) throws FeaturestoreException { + this.featureGroup = featuregroupController.getFeaturegroupById(featureStore, featureGroupId); + } + + @Override + protected Integer getItemId() { + return featureGroup.getId(); + } + + @Override + protected ResourceRequest.Name getItemType() { + return ResourceRequest.Name.FEATUREGROUPS; + } + + @Override + protected String getItemName() { + return featureGroup.getName(); + } + + @Override + protected Integer getItemVersion() { + return featureGroup.getVersion(); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringResultResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringResultResource.java new file mode 100644 index 0000000000..f26feb8b47 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeatureGroupFeatureMonitoringResultResource.java @@ -0,0 +1,45 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuregroup; + +import io.hops.hopsworks.api.featurestore.featuremonitoring.result.FeatureMonitoringResultResource; +import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.enterprise.context.RequestScoped; + +@RequestScoped +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureGroupFeatureMonitoringResultResource extends FeatureMonitoringResultResource { + + @EJB + private FeaturegroupController featuregroupController; + + private Featuregroup featureGroup; + + /** + * Sets the feature group of the tag resource + * + * @param featureGroupId + */ + public void setFeatureGroup(Integer featureGroupId) throws FeaturestoreException { + this.featureGroup = featuregroupController.getFeaturegroupById(featureStore, featureGroupId); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeaturegroupService.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeaturegroupService.java index 1079406066..c6966b23fd 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeaturegroupService.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuregroup/FeaturegroupService.java @@ -21,7 +21,8 @@ import io.hops.hopsworks.api.featurestore.activities.ActivityResource; import io.hops.hopsworks.api.featurestore.code.CodeResource; import io.hops.hopsworks.api.featurestore.commit.CommitResource; -import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureGroupAlertResource; +import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureGroupValidationAlertResource; +import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureStoreAlertResource; import io.hops.hopsworks.api.featurestore.datavalidationv2.reports.ValidationReportResource; import io.hops.hopsworks.api.featurestore.datavalidationv2.results.ValidationResultResource; import io.hops.hopsworks.api.featurestore.datavalidationv2.suites.ExpectationSuiteResource; @@ -45,6 +46,7 @@ import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupDTO; import io.hops.hopsworks.common.featurestore.featuregroup.IngestionJob; +import io.hops.hopsworks.common.util.Settings; import io.hops.hopsworks.exceptions.DatasetException; import io.hops.hopsworks.exceptions.FeaturestoreException; import io.hops.hopsworks.exceptions.GenericException; @@ -134,7 +136,7 @@ public class FeaturegroupService { @Inject private ActivityResource activityResource; @Inject - private FeatureGroupAlertResource featureGroupAlertResource; + private FeatureGroupValidationAlertResource featureGroupDataValidationAlertResource; @Inject private ExpectationSuiteResource expectationSuiteResource; @Inject @@ -149,6 +151,12 @@ public class FeaturegroupService { private FeatureGroupProvenanceResource provenanceResource; @EJB private FeaturegroupBuilder featuregroupBuilder; + @Inject + private FeatureGroupFeatureMonitoringConfigurationResource featureMonitoringConfigurationResource; + @Inject + private FeatureGroupFeatureMonitoringResultResource featureMonitoringResultResource; + @EJB + private Settings settings; private Project project; private Featurestore featurestore; @@ -658,11 +666,12 @@ public ActivityResource activity(@ApiParam(value = "Id of the feature group") } @Path("/{featureGroupId}/alerts") - public FeatureGroupAlertResource alerts(@PathParam("featureGroupId") Integer featureGroupId) + public FeatureStoreAlertResource alerts(@PathParam("featureGroupId") Integer featureGroupId) throws FeaturestoreException { - Featuregroup featuregroup = featuregroupController.getFeaturegroupById(featurestore, featureGroupId); - featureGroupAlertResource.setFeatureGroup(featuregroup); - return featureGroupAlertResource; + featureGroupDataValidationAlertResource.setFeatureGroup( + featuregroupController.getFeaturegroupById(featurestore, featureGroupId)); + featureGroupDataValidationAlertResource.setProject(project); + return featureGroupDataValidationAlertResource; } @POST @@ -744,4 +753,36 @@ public ValidationResultResource validationResultResource( return validationResultResource; } + + @Path("/{featureGroupId}/featuremonitoring/config") + public FeatureGroupFeatureMonitoringConfigurationResource featureGroupFeatureMonitoringConfigurationResource( + @PathParam("featureGroupId") Integer featureGroupId) + throws FeaturestoreException { + if (!settings.isFeatureMonitoringEnabled()) { + throw new FeaturestoreException( + RESTCodes.FeaturestoreErrorCode.FEATURE_MONITORING_NOT_ENABLED, + Level.FINE + ); + } + this.featureMonitoringConfigurationResource.setProject(project); + this.featureMonitoringConfigurationResource.setFeatureStore(featurestore); + this.featureMonitoringConfigurationResource.setFeatureGroup(featureGroupId); + return featureMonitoringConfigurationResource; + } + + @Path("/{featureGroupId}/featuremonitoring/result") + public FeatureGroupFeatureMonitoringResultResource featureMonitoringResultResource( + @PathParam("featureGroupId") Integer featureGroupId) + throws FeaturestoreException { + if (!settings.isFeatureMonitoringEnabled()) { + throw new FeaturestoreException( + RESTCodes.FeaturestoreErrorCode.FEATURE_MONITORING_NOT_ENABLED, + Level.FINE + ); + } + this.featureMonitoringResultResource.setProject(project); + this.featureMonitoringResultResource.setFeatureStore(featurestore); + this.featureMonitoringResultResource.setFeatureGroup(featureGroupId); + return featureMonitoringResultResource; + } } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationBuilder.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationBuilder.java new file mode 100644 index 0000000000..51ee3b6bbb --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationBuilder.java @@ -0,0 +1,217 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuremonitoring.config; + +import io.hops.hopsworks.api.jobs.scheduler.JobScheduleV2Builder; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.app.FsJobManagerController; +import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.DescriptiveStatisticsComparisonConfigurationDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration.MonitoringWindowConfigurationDTO; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; +import io.hops.hopsworks.common.jobs.JobController; +import io.hops.hopsworks.exceptions.JobException; +import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.MonitoringWindowConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics.DescriptiveStatisticsComparisonConfig; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.project.Project; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; + + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureMonitoringConfigurationBuilder { + @EJB + private JobController jobController; + @EJB + private JobScheduleV2Builder jobScheduleV2Builder; + @EJB + private FeaturegroupController featureGroupController; + @EJB + private FeatureViewController featureViewController; + @EJB + private FsJobManagerController fsJobManagerController; + + + public URI uri(UriInfo uriInfo, Project project, Featurestore featureStore, ResourceRequest.Name entityType, + Integer entityId) { + return uriInfo.getBaseUriBuilder().path(ResourceRequest.Name.PROJECT.toString().toLowerCase()) + .path(Integer.toString(project.getId())).path(ResourceRequest.Name.FEATURESTORES.toString().toLowerCase()) + .path(Integer.toString(featureStore.getId())).path(entityType.toString().toLowerCase()) + .path(Integer.toString(entityId)).path(ResourceRequest.Name.FEATURE_MONITORING.toString().toLowerCase()) + .path("config").build(); + } + + public FeatureMonitoringConfigurationDTO buildMany(UriInfo uri, Featurestore featureStore, + ResourceRequest.Name entityType, Integer entityId, String entityName, Integer entityVersion, + List configs) { + FeatureMonitoringConfigurationDTO configsDto = new FeatureMonitoringConfigurationDTO(); + configsDto.setHref(uri(uri, featureStore.getProject(), featureStore, entityType, entityId)); + + configsDto.setItems( + configs.stream().map(config -> build( + uri, featureStore, entityType, entityId, entityName, entityVersion, config) + ).collect(Collectors.toList())); + configsDto.setCount(((long) configsDto.getItems().size())); + + return configsDto; + } + + public FeatureMonitoringConfigurationDTO build(UriInfo uri, Featurestore featureStore, + ResourceRequest.Name entityType, Integer entityId, String entityName, Integer entityVersion, + FeatureMonitoringConfiguration config) { + FeatureMonitoringConfigurationDTO configDto = new FeatureMonitoringConfigurationDTO(); + + configDto.setHref(uri(uri, featureStore.getProject(), featureStore, entityType, entityId)); + + configDto.setId(config.getId()); + configDto.setFeatureStoreId(featureStore.getId()); + configDto.setFeatureName(config.getFeatureName()); + configDto.setJobName(config.getJob().getName()); + configDto.setName(config.getName()); + configDto.setDescription(config.getDescription()); + configDto.setFeatureMonitoringType(config.getFeatureMonitoringType()); + + if (entityType == ResourceRequest.Name.FEATUREVIEW) { + configDto.setFeatureViewName(entityName); + configDto.setFeatureViewVersion(entityVersion); + } else if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + configDto.setFeatureGroupId(entityId); + } + + configDto.setStatisticsComparisonConfig( + buildDescriptiveStatisticsComparisonConfigurationDTO(config.getDsComparisonConfig())); + configDto.setJobSchedule(jobScheduleV2Builder.build(uri, config.getJobSchedule())); + configDto.setDetectionWindowConfig(buildMonitoringWindowConfigurationDTO(config.getDetectionWindowConfig())); + configDto.setReferenceWindowConfig(buildMonitoringWindowConfigurationDTO(config.getReferenceWindowConfig())); + + return configDto; + } + + public MonitoringWindowConfigurationDTO buildMonitoringWindowConfigurationDTO( + MonitoringWindowConfiguration monitoringWindowConfiguration) { + if (monitoringWindowConfiguration == null) { + return null; + } + MonitoringWindowConfigurationDTO dto = new MonitoringWindowConfigurationDTO(); + + dto.setId(monitoringWindowConfiguration.getId()); + dto.setRowPercentage(monitoringWindowConfiguration.getRowPercentage()); + dto.setTrainingDatasetVersion(monitoringWindowConfiguration.getTrainingDatasetVersion()); + dto.setTimeOffset(monitoringWindowConfiguration.getTimeOffset()); + dto.setWindowLength(monitoringWindowConfiguration.getWindowLength()); + dto.setWindowConfigType(monitoringWindowConfiguration.getWindowConfigType()); + dto.setSpecificValue(monitoringWindowConfiguration.getSpecificValue()); + + return dto; + } + + public DescriptiveStatisticsComparisonConfigurationDTO buildDescriptiveStatisticsComparisonConfigurationDTO( + DescriptiveStatisticsComparisonConfig statsConfig) { + if (statsConfig == null) { + return null; + } + + DescriptiveStatisticsComparisonConfigurationDTO statsDto = new DescriptiveStatisticsComparisonConfigurationDTO(); + statsDto.setId(statsConfig.getId()); + statsDto.setMetric(statsConfig.getMetric()); + statsDto.setRelative(statsConfig.getRelative()); + statsDto.setStrict(statsConfig.getStrict()); + statsDto.setThreshold(statsConfig.getThreshold()); + + return statsDto; + } + + public FeatureMonitoringConfiguration buildFromDTO(Project project, Featuregroup featureGroup, + FeatureView featureView, FeatureMonitoringConfigurationDTO dto) throws JobException { + + FeatureMonitoringConfiguration config = new FeatureMonitoringConfiguration(); + if (featureGroup != null) { + config.setFeatureGroup(featureGroup); + } else if (featureView != null) { + config.setFeatureView(featureView); + } + + config.setFeatureName(dto.getFeatureName()); + config.setFeatureMonitoringType(dto.getFeatureMonitoringType()); + config.setName(dto.getName()); + config.setDescription(dto.getDescription()); + + config.setDetectionWindowConfig(buildWindowConfigurationFromDTO(dto.getDetectionWindowConfig())); + config.setReferenceWindowConfig(buildWindowConfigurationFromDTO(dto.getReferenceWindowConfig())); + + // Descriptive Statistics specific + if (dto.getStatisticsComparisonConfig() != null) { + config.setDsComparisonConfig( + buildDescriptiveStatisticsComparisonConfigurationFromDTO(dto.getStatisticsComparisonConfig()) + ); + } + + // Integration with other Hopsworks services + if (dto.getId() != null) { + config.setJob(jobController.getJob(project, dto.getJobName())); + config.setJobSchedule(jobScheduleV2Builder.validateAndConvertOnUpdate(config.getJob(), dto.getJobSchedule())); + } else { + config.setJobSchedule( + jobScheduleV2Builder.validateAndConvertOnCreate(null, dto.getJobSchedule()) + ); + } + return config; + } + + public MonitoringWindowConfiguration buildWindowConfigurationFromDTO(MonitoringWindowConfigurationDTO windowDto) { + if (windowDto == null) { + return null; + } + + MonitoringWindowConfiguration window = new MonitoringWindowConfiguration(); + + window.setId(windowDto.getId()); + window.setWindowLength(windowDto.getWindowLength()); + window.setTimeOffset(windowDto.getTimeOffset()); + window.setWindowConfigType(windowDto.getWindowConfigType()); + window.setTrainingDatasetVersion(windowDto.getTrainingDatasetVersion()); + window.setRowPercentage(windowDto.getRowPercentage()); + window.setSpecificValue(windowDto.getSpecificValue()); + + return window; + } + + public DescriptiveStatisticsComparisonConfig buildDescriptiveStatisticsComparisonConfigurationFromDTO( + DescriptiveStatisticsComparisonConfigurationDTO statsDto) { + DescriptiveStatisticsComparisonConfig statsConfig = new DescriptiveStatisticsComparisonConfig(); + + statsConfig.setId(statsDto.getId()); + statsConfig.setThreshold(statsDto.getThreshold()); + statsConfig.setMetric(statsDto.getMetric()); + statsConfig.setRelative(statsDto.getRelative()); + statsConfig.setStrict(statsDto.getStrict()); + + return statsConfig; + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationResource.java new file mode 100644 index 0000000000..51eec42646 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationResource.java @@ -0,0 +1,345 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuremonitoring.config; + +import io.hops.hopsworks.api.filter.AllowedProjectRoles; +import io.hops.hopsworks.api.filter.Audience; +import io.hops.hopsworks.api.auth.key.ApiKeyRequired; +import io.hops.hopsworks.api.jobs.JobsBuilder; +import io.hops.hopsworks.api.jwt.JWTHelper; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationInputValidation; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.exceptions.JobException; +import io.hops.hopsworks.jwt.annotation.JWTRequired; +import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.persistence.entity.user.Users; +import io.hops.hopsworks.persistence.entity.user.security.apiKey.ApiScope; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import java.util.List; + +@TransactionAttribute(TransactionAttributeType.NEVER) +@Api(value = "Feature Monitoring Configuration resource") +public abstract class FeatureMonitoringConfigurationResource { + + @EJB + private FeatureMonitoringConfigurationController featureMonitoringConfigurationController; + @EJB + private FeaturegroupController featureGroupController; + @EJB + private FeatureViewController featureViewController; + @EJB + private FeatureMonitoringConfigurationBuilder featureMonitoringConfigurationBuilder; + @EJB + private JobsBuilder jobsBuilder; + @EJB + private JWTHelper jWTHelper; + @EJB + private FeatureMonitoringConfigurationInputValidation featureMonitoringConfigurationInputValidation; + + protected Project project; + protected Featurestore featureStore; + + public void setProject(Project project) { + this.project = project; + } + + public void setFeatureStore(Featurestore featureStore) { + this.featureStore = featureStore; + } + + protected abstract Integer getItemId(); + protected abstract ResourceRequest.Name getItemType(); + + // Added to init engine class for FV only + protected abstract String getItemName(); + protected abstract Integer getItemVersion(); + + /** + * Endpoint to fetch the feature monitoring configuration with a given id. + * + * @param configId id of the configuration to fetch + * @return JSON information regarding the monitoring configuration connected to a Feature + * @throws FeaturestoreException + */ + @ApiOperation(value = "Fetch the feature monitoring configuration connected to a Feature", + response = FeatureMonitoringConfigurationDTO.class) + @GET + @Path("/{configId: [0-9]+}") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getById(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @ApiParam(value = "Id of the Feature Monitoring Configuration", required = true) + @PathParam("configId") Integer configId + ) throws FeaturestoreException { + + FeatureMonitoringConfiguration config = + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByConfigId(configId); + + FeatureMonitoringConfigurationDTO dto = + featureMonitoringConfigurationBuilder.build( + uriInfo, featureStore, getItemType(), getItemId(), getItemName(), getItemVersion(), config); + + return Response.ok().entity(dto).build(); + } + + /** + * Endpoint to fetch the feature monitoring configuration with a given name. + * + * @param name name of the configuration to fetch + * @return JSON information regarding the monitoring configuration connected to a Feature + * @throws FeaturestoreException + */ + @ApiOperation(value = "Fetch the feature monitoring configuration by its name connected to a Feature", + response = FeatureMonitoringConfigurationDTO.class) + @GET + @Path("/name/{name: [a-z0-9_]*(?=[a-z])[a-z0-9_]+}") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getByName(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @ApiParam(value = "Name of the Feature Monitoring Configuration", required = true) + @PathParam("name") String name + ) throws FeaturestoreException { + + FeatureMonitoringConfiguration config = + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByEntityAndName( + featureStore, getItemType(), getItemId(), name); + + FeatureMonitoringConfigurationDTO dto = featureMonitoringConfigurationBuilder.build( + uriInfo, featureStore, getItemType(), getItemId(), getItemName(), getItemVersion(), config); + + + return Response.ok().entity(dto).build(); + } + + /** + * Endpoint to fetch the feature monitoring configuration attached to a Feature. + * + * @param featureName name of the feature to monitor + * @return JSON information about the monitoring configuration connected to featureName + * @throws FeaturestoreException + */ + @ApiOperation(value = "Fetch a feature monitoring configuration connected to a Feature", + response = FeatureMonitoringConfigurationDTO.class) + @GET + @Path("feature/{featureName: [a-z0-9_]*(?=[a-z])[a-z0-9_]+}") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getByFeatureName(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @ApiParam(value = "Name of the feature", required = true) + @PathParam("featureName")String featureName + ) throws FeaturestoreException { + + List configs = + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByEntityAndFeatureName(featureStore, + getItemType(), getItemId(), featureName); + + FeatureMonitoringConfigurationDTO dto =featureMonitoringConfigurationBuilder.buildMany( + uriInfo, featureStore, getItemType(), getItemId(), getItemName(), getItemVersion(), configs); + + return Response.ok().entity(dto).build(); + } + + /** + * Endpoint to fetch the feature monitoring configuration connected to a Feature Group or Feature View. + * + * @return JSON information about the monitoring configuration connected to the entity + * @throws FeaturestoreException + */ + @ApiOperation( + value = "Fetch a feature monitoring configuration connected to a Feature Group or Feature View", + response = FeatureMonitoringConfigurationDTO.class) + @GET + @Path("entity") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getByEntityId(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo + ) throws FeaturestoreException { + + List configs = + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByEntity( + featureStore, getItemType(), getItemId()); + + FeatureMonitoringConfigurationDTO dto = featureMonitoringConfigurationBuilder.buildMany( + uriInfo, featureStore, getItemType(), getItemId(), getItemName(), getItemVersion(), configs); + + return Response.ok().entity(dto).build(); + } + + /** + * Endpoint to persist a configuration connected to the monitoring of a Feature + * + * @param configDTO json representation of the feature monitoring configuration + * @return JSON information about the created configuration + * @throws FeaturestoreException + */ + @ApiOperation(value = "Persist the configuration connected to the monitoring of a Feature", + response = FeatureMonitoringConfigurationDTO.class) + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response createFeatureMonitoringConfiguration(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + FeatureMonitoringConfigurationDTO configDTO + ) throws FeaturestoreException, JobException { + Users user = jWTHelper.getUserPrincipal(sc); + + Featuregroup featureGroup = null; + FeatureView featureView = null; + if (getItemType() == ResourceRequest.Name.FEATUREGROUPS) { + featureGroup = featureGroupController.getFeaturegroupById(featureStore, getItemId()); + } else if (getItemType() == ResourceRequest.Name.FEATUREVIEW) { + featureView = featureViewController.getByIdAndFeatureStore(getItemId(), featureStore); + } + featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, configDTO, featureGroup, featureView); + FeatureMonitoringConfiguration config = + featureMonitoringConfigurationBuilder.buildFromDTO(project, featureGroup, featureView, configDTO); + config = featureMonitoringConfigurationController.createFeatureMonitoringConfiguration( + featureStore, user, getItemType(), config); + + FeatureMonitoringConfigurationDTO dto = featureMonitoringConfigurationBuilder.build( + uriInfo, featureStore, getItemType(), getItemId(), getItemName(), getItemVersion(), config); + return Response.created(dto.getHref()).entity(dto).build(); + } + + /** + * Endpoint to edit a configuration connected to the monitoring of a Feature + * + * @param configId id of the feature monitoring configuration to edit + * @param configDTO json representation of the feature monitoring configuration to edit + * @return JSON information about the edited configuration + * @throws FeaturestoreException + */ + @ApiOperation(value = "Edit the configuration connected to the monitoring of a Feature", + response = FeatureMonitoringConfigurationDTO.class) + @PUT + @Path("/{configId : [0-9]+}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response updateFeatureMonitoringConfiguration(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @PathParam("configId") Integer configId, + FeatureMonitoringConfigurationDTO configDTO + ) throws FeaturestoreException, JobException { + + Featuregroup featureGroup = null; + FeatureView featureView = null; + if (getItemType() == ResourceRequest.Name.FEATUREGROUPS) { + featureGroup = featureGroupController.getFeaturegroupById(featureStore, getItemId()); + } else if (getItemType() == ResourceRequest.Name.FEATUREVIEW) { + featureView = featureViewController.getByIdAndFeatureStore(getItemId(), featureStore); + } + featureMonitoringConfigurationInputValidation.validateConfigOnUpdate(configDTO, featureGroup, featureView); + FeatureMonitoringConfiguration config = + featureMonitoringConfigurationBuilder.buildFromDTO(project, featureGroup, featureView, configDTO); + config = featureMonitoringConfigurationController.updateFeatureMonitoringConfiguration(configId, config); + + FeatureMonitoringConfigurationDTO dto = featureMonitoringConfigurationBuilder.build( + uriInfo, featureStore, getItemType(), getItemId(), getItemName(), getItemVersion(), config); + return Response.ok(dto.getHref()).entity(dto).build(); + } + + /** + * Endpoint to delete a config connected to the monitoring of a Feature + * + * @param configId id of the configuration to delete + * @return Response with 204 status to indicate config has been deleted + * @throws FeaturestoreException + */ + @ApiOperation(value = "Delete a configuration connected to the monitoring of a Feature") + @DELETE + @Path("/{configId: [0-9]+}") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response deleteFeatureMonitoringConfiguration(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @PathParam("configId") Integer configId + ) throws FeaturestoreException { + + featureMonitoringConfigurationController.deleteFeatureMonitoringConfiguration(configId); + + return Response.noContent().build(); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBeanParam.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBeanParam.java new file mode 100644 index 0000000000..477312323a --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBeanParam.java @@ -0,0 +1,88 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuremonitoring.result; + +import io.swagger.annotations.ApiParam; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.QueryParam; +import java.util.LinkedHashSet; +import java.util.Set; + +public class FeatureMonitoringResultBeanParam { + @QueryParam("sort_by") + @ApiParam(value = "ex. sort_by=monitoring_time:desc", allowableValues = "monitoring_time:asc,monitoring_time:desc,") + private String sortBy; + private final Set sortBySet; + + @QueryParam("filter_by") + @ApiParam(value = "ex. filter_by=monitoring_time_lt:1610471222000", + allowableValues = "filter_by=monitoring_time_lt:1610471222000,filter_by=monitoring_time_gt:1610471222000", + allowMultiple = true) + private Set filter; + + @BeanParam + private FeatureMonitoringResultExpansionBeanParam expansion; + + public FeatureMonitoringResultBeanParam( + @QueryParam("sort_by") String sortBy, + @QueryParam("filter_by") Set filter) { + this.sortBy = sortBy; + this.sortBySet = getSortBy(sortBy); + this.filter = filter; + } + + private Set getSortBy(String param) { + if (param == null || param.isEmpty()) { + return new LinkedHashSet<>(); + } + String[] params = param.split(","); + //Hash table and linked list implementation of the Set interface, with predictable iteration order + Set sortBys = new LinkedHashSet<>();//make ordered + FeatureMonitoringResultSortBy sort; + for (String s : params) { + sort = new FeatureMonitoringResultSortBy(s.trim()); + sortBys.add(sort); + } + return sortBys; + } + + public String getSortBy() { + return sortBy; + } + + public void setSortBy(String sortBy) { + this.sortBy = sortBy; + } + + public Set getSortBySet() { + return sortBySet; + } + + public Set getFilter() { + return filter; + } + + public void setFilter(Set filter) { + this.filter = filter; + } + + public FeatureMonitoringResultExpansionBeanParam getExpansion() { return expansion; } + + public void setExpansion(FeatureMonitoringResultExpansionBeanParam expansion) { + this.expansion = expansion; + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBuilder.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBuilder.java new file mode 100644 index 0000000000..d84c1a1088 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultBuilder.java @@ -0,0 +1,114 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.api.featurestore.statistics.FeatureDescriptiveStatisticsBuilder; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultDTO; +import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult; +import io.hops.hopsworks.persistence.entity.project.Project; + +import javax.ejb.EJB; +import javax.ws.rs.core.UriInfo; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.net.URI; +import java.util.Date; +import java.util.stream.Collectors; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureMonitoringResultBuilder { + + @EJB + private FeatureDescriptiveStatisticsBuilder featureDescriptiveStatisticsBuilder; + + public URI uri(UriInfo uriInfo, Project project, Featurestore featureStore) { + return uriInfo.getBaseUriBuilder().path(ResourceRequest.Name.PROJECT.toString().toLowerCase()) + .path(Integer.toString(project.getId())).path(ResourceRequest.Name.FEATURESTORES.toString().toLowerCase()) + .path(Integer.toString(featureStore.getId())) + .path(ResourceRequest.Name.FEATURE_MONITORING.toString().toLowerCase()).path("result").build(); + } + + public FeatureMonitoringResultDTO buildWithStats(UriInfo uri, Project project, Featurestore featurestore, + FeatureMonitoringResult result) { + return build(uri, project, featurestore, result, new ResourceRequest(ResourceRequest.Name.STATISTICS)); + } + + public FeatureMonitoringResultDTO build(UriInfo uri, Project project, Featurestore featureStore, + FeatureMonitoringResult result, ResourceRequest resourceRequest) { + FeatureMonitoringResultDTO resultDto = new FeatureMonitoringResultDTO(); + resultDto.setHref(uri(uri, project, featureStore)); + + resultDto.setId(result.getId()); + resultDto.setConfigId(result.getFeatureMonitoringConfig().getId()); + resultDto.setFeatureStoreId(featureStore.getId()); + resultDto.setExecutionId(result.getExecutionId()); + resultDto.setDifference(result.getDifference()); + resultDto.setMonitoringTime(result.getMonitoringTime().getTime()); + resultDto.setShiftDetected(result.getShiftDetected()); + resultDto.setFeatureName(result.getFeatureName()); + resultDto.setSpecificValue(result.getSpecificValue()); + resultDto.setRaisedException(result.getRaisedException()); + resultDto.setEmptyDetectionWindow(result.getEmptyDetectionWindow()); + resultDto.setEmptyReferenceWindow(result.getEmptyReferenceWindow()); + + if (resourceRequest != null && resourceRequest.contains(ResourceRequest.Name.STATISTICS)) { + resultDto.setDetectionStatistics(featureDescriptiveStatisticsBuilder.build(result.getDetectionStatistics())); + resultDto.setReferenceStatistics(featureDescriptiveStatisticsBuilder.build(result.getReferenceStatistics())); + } else { + resultDto.setDetectionStatisticsId(result.getDetectionStatsId()); + resultDto.setReferenceStatisticsId(result.getReferenceStatsId()); + } + + return resultDto; + } + + public FeatureMonitoringResultDTO buildMany(UriInfo uri, Project project, Featurestore featureStore, + AbstractFacade.CollectionInfo results, ResourceRequest resourceRequest) { + FeatureMonitoringResultDTO dtos = new FeatureMonitoringResultDTO(); + + dtos.setCount(results.getCount()); + dtos.setHref(uri(uri, project, featureStore)); + + dtos.setItems(results.getItems().stream().map(result -> build(uri, project, featureStore, result, resourceRequest)) + .collect(Collectors.toList())); + + return dtos; + } + + public FeatureMonitoringResult buildFromDTO(FeatureMonitoringResultDTO dto) { + FeatureMonitoringResult result = new FeatureMonitoringResult(); + + result.setExecutionId(dto.getExecutionId()); + result.setShiftDetected(dto.getShiftDetected()); + result.setDifference(dto.getDifference()); + result.setMonitoringTime(new Date(dto.getMonitoringTime())); + result.setFeatureName(dto.getFeatureName()); + result.setSpecificValue(dto.getSpecificValue()); + result.setRaisedException(dto.getRaisedException()); + result.setEmptyDetectionWindow(dto.getEmptyDetectionWindow()); + result.setEmptyReferenceWindow(dto.getEmptyReferenceWindow()); + + result.setDetectionStatsId(dto.getDetectionStatisticsId()); + result.setReferenceStatsId(dto.getReferenceStatisticsId()); + + return result; + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansionBeanParam.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansionBeanParam.java new file mode 100644 index 0000000000..5c2f43df15 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansionBeanParam.java @@ -0,0 +1,47 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.api.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.common.api.ResourceRequest; +import io.swagger.annotations.ApiParam; + +import javax.ws.rs.QueryParam; +import java.util.Set; +import java.util.stream.Collectors; + +public class FeatureMonitoringResultExpansionBeanParam { + @QueryParam("expand") + @ApiParam(value = "ex. expand=statistics", allowableValues = "expand=statistics") + private Set expansions; + + public FeatureMonitoringResultExpansionBeanParam( + @QueryParam("expand") Set expansions) { + this.expansions = expansions; + } + + public Set getExpansions() { + return expansions; + } + + public void setExpansions(Set expansions) { + this.expansions = expansions; + } + + public Set getResources() { + return expansions.stream().map(FeatureMonitoringResultExpansions::getResourceRequest).collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansions.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansions.java new file mode 100644 index 0000000000..cad55ee1b9 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultExpansions.java @@ -0,0 +1,39 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.api.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.common.api.Expansions; +import io.hops.hopsworks.common.api.ResourceRequest; + +public class FeatureMonitoringResultExpansions implements Expansions { + + private ResourceRequest resourceRequest; + + public FeatureMonitoringResultExpansions(String queryParam) { + resourceRequest = new ResourceRequest(queryParam); + } + + @Override + public ResourceRequest getResourceRequest() { + return resourceRequest; + } + + @Override + public void setResourceRequest(ResourceRequest resourceRequest) { + this.resourceRequest = resourceRequest; + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultFilterBy.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultFilterBy.java new file mode 100644 index 0000000000..5d599b16b9 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultFilterBy.java @@ -0,0 +1,60 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultFacade; + +public class FeatureMonitoringResultFilterBy implements AbstractFacade.FilterBy { + + private final FeatureMonitoringResultFacade.Filters filter; + private final String param; + + public FeatureMonitoringResultFilterBy(String param) { + if (param.contains(":")) { + this.filter = FeatureMonitoringResultFacade.Filters.valueOf(param.substring(0, param.indexOf(':')).toUpperCase()); + this.param = param.substring(param.indexOf(':') + 1); + } else { + this.filter = FeatureMonitoringResultFacade.Filters.valueOf(param); + this.param = this.filter.getDefaultParam(); + } + } + + @Override + public String getParam() { + return param; + } + + @Override + public String getValue() { + return this.filter.getValue(); + } + + @Override + public String getSql() { + return this.filter.getSql(); + } + + @Override + public String getField() { + return this.filter.getField(); + } + + @Override + public String toString() { + return filter.toString(); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultResource.java new file mode 100644 index 0000000000..0ab121b68a --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultResource.java @@ -0,0 +1,217 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.api.filter.AllowedProjectRoles; +import io.hops.hopsworks.api.filter.Audience; +import io.hops.hopsworks.api.auth.key.ApiKeyRequired; +import io.hops.hopsworks.api.util.Pagination; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultInputValidation; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.jwt.annotation.JWTRequired; +import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.persistence.entity.user.security.apiKey.ApiScope; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BeanParam; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; + +@TransactionAttribute(TransactionAttributeType.NEVER) +@Api(value = "Feature Monitoring Result resource") +public abstract class FeatureMonitoringResultResource { + + @EJB + private FeatureMonitoringResultController featureMonitoringResultController; + @EJB + private FeatureMonitoringResultInputValidation featureMonitoringResultInputValidation; + @EJB + private FeatureMonitoringResultBuilder featureMonitoringResultBuilder; + @EJB + private FeatureMonitoringConfigurationController featureMonitoringConfigurationController; + + protected Project project; + protected Featurestore featureStore; + + public void setProject(Project project) { + this.project = project; + } + + public void setFeatureStore(Featurestore featureStore) { + this.featureStore = featureStore; + } + + /** + * Endpoint to fetch a list of feature monitoring results attached to a Feature. + * + * @param pagination + * @param featureMonitoringResultBeanParam + * @return JSON-array of feature monitoring results + * @throws FeaturestoreException + */ + @ApiOperation(value = "Fetch all feature monitoring results connected to a feature monitoring configuration", + response = FeatureMonitoringResultDTO.class, responseContainer = "List") + @GET + @Path("/byconfig/{configId: [0-9]+}") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getAllByConfigId(@BeanParam Pagination pagination, + @BeanParam FeatureMonitoringResultBeanParam featureMonitoringResultBeanParam, + @Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @ApiParam(value = "Id of the configuration", required = true) + @PathParam("configId") Integer configId + ) throws FeaturestoreException { + + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.FEATURE_MONITORING); + resourceRequest.setOffset(pagination.getOffset()); + resourceRequest.setLimit(pagination.getLimit()); + resourceRequest.setSort(featureMonitoringResultBeanParam.getSortBySet()); + resourceRequest.setFilter(featureMonitoringResultBeanParam.getFilter()); + resourceRequest.setExpansions(featureMonitoringResultBeanParam.getExpansion().getResources()); + + AbstractFacade.CollectionInfo results = + featureMonitoringResultController.getAllFeatureMonitoringResultByConfigId( + pagination.getOffset(), pagination.getLimit(), featureMonitoringResultBeanParam.getSortBySet(), + featureMonitoringResultBeanParam.getFilter(), configId); + + FeatureMonitoringResultDTO dtos = + featureMonitoringResultBuilder.buildMany(uriInfo, project, featureStore, results, resourceRequest); + + return Response.ok().entity(dtos).build(); + } + + /** + * Endpoint to fetch a list of feature monitoring results attached to a Feature. + * + * @param resultId the id of the result + * @return JSON-array of feature monitoring results + * @throws FeaturestoreException + */ + @ApiOperation(value = "Fetch all feature monitoring result connected to a Feature", + response = FeatureMonitoringResultDTO.class, responseContainer = "List") + @GET + @Path("/{resultId: [0-9]+}") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response getSingle(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @ApiParam(value = "Id of the result", required = true) + @PathParam("resultId") Integer resultId + ) throws FeaturestoreException { + + FeatureMonitoringResult result = featureMonitoringResultController.getFeatureMonitoringResultById(resultId); + FeatureMonitoringResultDTO dtos = featureMonitoringResultBuilder.buildWithStats( + uriInfo, project, featureStore, result + ); + + return Response.ok().entity(dtos).build(); + } + + /** + * Endpoint to persist a result connected to the monitoring of a Feature + * + * @param resultDTO json representation of the result generated during feature monitoring comparison. + * @return JSON information about the created result + * @throws FeaturestoreException + */ + @ApiOperation(value = "Persist a result connected to the monitoring of a Feature", + response = FeatureMonitoringResultDTO.class) + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response createFeatureMonitoringResult(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + FeatureMonitoringResultDTO resultDTO) throws FeaturestoreException { + + featureMonitoringResultInputValidation.validateOnCreate(resultDTO); + FeatureMonitoringResult result = featureMonitoringResultBuilder.buildFromDTO(resultDTO); + result.setFeatureMonitoringConfig( + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByConfigId(resultDTO.getConfigId())); + featureMonitoringResultController.createFeatureMonitoringResult(result); + FeatureMonitoringResultDTO dto = featureMonitoringResultBuilder.buildWithStats( + uriInfo, project, featureStore, result + ); + + return Response.created(dto.getHref()).entity(dto).build(); + } + + /** + * Endpoint to delete a result connected to the monitoring of a Feature + * + * @param resultId id of the result to delete + * @return JSON information about the created result + * @throws FeaturestoreException + */ + @ApiOperation(value = "Delete a result connected to the monitoring of a Feature") + @DELETE + @Path("/{resultId: [0-9]+}") + @Produces(MediaType.APPLICATION_JSON) + @AllowedProjectRoles({AllowedProjectRoles.DATA_SCIENTIST, AllowedProjectRoles.DATA_OWNER}) + @JWTRequired(acceptedTokens = {Audience.API, Audience.JOB}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response deleteFeatureMonitoringResult(@Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo, + @PathParam("resultId")Integer resultId + ) throws FeaturestoreException { + + featureMonitoringResultController.deleteFeatureMonitoringResult(resultId); + + return Response.noContent().build(); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultSortBy.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultSortBy.java new file mode 100644 index 0000000000..14232401f6 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featuremonitoring/result/FeatureMonitoringResultSortBy.java @@ -0,0 +1,62 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultFacade; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +public class FeatureMonitoringResultSortBy implements AbstractFacade.SortBy { + private final FeatureMonitoringResultFacade.Sorts sortBy; + private final AbstractFacade.OrderBy param; + + public FeatureMonitoringResultSortBy(String param) { + String[] sortByParams = param.split(":"); + String sort = ""; + try { + sort = sortByParams[0].toUpperCase(); + this.sortBy = FeatureMonitoringResultFacade.Sorts.valueOf(sort); + } catch (IllegalArgumentException iae) { + throw new WebApplicationException("Sort by needs to set a valid sort parameter, but found: " + sort, + Response.Status.NOT_FOUND); + } + String order = ""; + try { + order = sortByParams.length > 1 ? sortByParams[1].toUpperCase() : this.sortBy.getDefaultParam(); + this.param = AbstractFacade.OrderBy.valueOf(order); + } catch (IllegalArgumentException iae) { + throw new WebApplicationException( + "Sort by " + sort + " needs to set a valid order(asc|desc), but found: " + order, Response.Status.NOT_FOUND); + } + } + + @Override + public String getValue() { + return this.sortBy.getValue(); + } + + @Override + public AbstractFacade.OrderBy getParam() { + return this.param; + } + + @Override + public String getSql() { + return this.sortBy.getSql(); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertBuilder.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertBuilder.java new file mode 100644 index 0000000000..d8d957e354 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertBuilder.java @@ -0,0 +1,134 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featureview; + +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.FeaturestoreController; +import io.hops.hopsworks.common.featurestore.FeaturestoreFacade; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewAlertFacade; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.util.List; +import java.util.logging.Logger; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureViewAlertBuilder { + private static final Logger LOGGER = Logger.getLogger(FeatureViewAlertBuilder.class.getName()); + @EJB + private FeaturestoreController featurestoreController; + @EJB + private FeatureViewAlertFacade featureViewAlertFacade; + @EJB + private FeaturestoreFacade featurestoreFacade; + + public FeatureViewAlertDTO build(UriInfo uriInfo, ResourceRequest resourceRequest, + FeatureViewAlert featureViewAlert) { + FeatureViewAlertDTO dto = new FeatureViewAlertDTO(); + dto.setHref(uriInfo.getAbsolutePathBuilder().path(featureViewAlert.getId().toString()).build()); + setDtoValues(dto, resourceRequest, featureViewAlert); + return dto; + } + + public FeatureViewAlertDTO expand(FeatureViewAlertDTO dto, ResourceRequest resourceRequest) { + if (resourceRequest != null && resourceRequest.contains(ResourceRequest.Name.ALERTS)) { + dto.setExpand(true); + } + return dto; + } + + + public FeatureViewAlertDTO buildFeatureViewAlertDto(UriInfo uriInfo, ResourceRequest resourceRequest, + FeatureViewAlert featureViewAlert) { + FeatureViewAlertDTO dto = new FeatureViewAlertDTO(); + dto.setHref(uriInfo.getAbsolutePathBuilder().build()); + setDtoValues(dto, resourceRequest, featureViewAlert); + return dto; + } + + private void setDtoValues(FeatureViewAlertDTO dto, ResourceRequest resourceRequest, + FeatureViewAlert featureViewAlert) { + expand(dto, resourceRequest); + if (dto.isExpand()) { + dto.setFeatureViewId(featureViewAlert.getFeatureView().getId()); + dto.setFeatureViewName(featureViewAlert.getFeatureView().getName()); + dto.setFeatureViewVersion(featureViewAlert.getFeatureView().getVersion()); + dto.setFeatureStoreName( + featurestoreController.getOfflineFeaturestoreDbName(featureViewAlert.getFeatureView().getFeaturestore())); + dto.setId(featureViewAlert.getId()); + dto.setAlertType(featureViewAlert.getAlertType()); + dto.setStatus(featureViewAlert.getStatus()); + dto.setSeverity(featureViewAlert.getSeverity()); + dto.setCreated(featureViewAlert.getCreated()); + dto.setReceiver(featureViewAlert.getReceiver().getName()); + } + } + + public FeatureViewAlertDTO projectUri(FeatureViewAlertDTO dto, UriInfo uriInfo, + FeatureViewAlert featureViewAlert) { + UriBuilder uri = uriInfo.getBaseUriBuilder().path(ResourceRequest.Name.PROJECT.toString()); + uri.path(Integer.toString(featureViewAlert.getFeatureView().getFeaturestore().getProject().getId())) + .path(ResourceRequest.Name.FEATURESTORES.toString()) + .path(Integer.toString(featureViewAlert.getFeatureView().getFeaturestore().getId())) + .path(ResourceRequest.Name.FEATUREVIEW.toString()).path(featureViewAlert.getFeatureView().getName()) + .path(ResourceRequest.Name.VERSION.toString()) + .path(String.valueOf(featureViewAlert.getFeatureView().getVersion())) + .path(ResourceRequest.Name.ALERTS.toString()).path(featureViewAlert.getId().toString()); + dto.setHref(uri.build()); + return dto; + } + + public FeatureViewAlertDTO buildProjectItems(UriInfo uriInfo, ResourceRequest resourceRequest, + FeatureViewAlert featureViewAlert) { + FeatureViewAlertDTO dto = new FeatureViewAlertDTO(); + dto = projectUri(dto, uriInfo, featureViewAlert); + setDtoValues(dto, resourceRequest, featureViewAlert); + return dto; + } + + public FeatureViewAlertDTO buildMany(UriInfo uriInfo, ResourceRequest resourceRequest, + List featureViewAlerts) { + FeatureViewAlertDTO dto = new FeatureViewAlertDTO(); + dto.setHref(uriInfo.getAbsolutePathBuilder().build()); + expand(dto, resourceRequest); + if (dto.isExpand() && featureViewAlerts != null) { + dto.setCount((long) featureViewAlerts.size()); + featureViewAlerts.forEach(featureViewAlert -> dto.addItem(build(uriInfo, resourceRequest, featureViewAlert))); + return dto; + } + return dto; + } + + public FeatureViewAlertDTO buildManyProjectAlerts(UriInfo uriInfo, ResourceRequest resourceRequest, + List projectAlerts) { + FeatureViewAlertDTO dto = new FeatureViewAlertDTO(); + dto.setHref(uriInfo.getAbsolutePathBuilder().build()); + expand(dto, resourceRequest); + if (dto.isExpand() && projectAlerts != null) { + dto.setCount((long) projectAlerts.size()); + projectAlerts.forEach( + featureViewAlert -> dto.addItem(buildProjectItems(uriInfo, resourceRequest, featureViewAlert))); + return dto; + } + return dto; + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertDTO.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertDTO.java new file mode 100644 index 0000000000..eca2845be8 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewAlertDTO.java @@ -0,0 +1,62 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featureview; + +import io.hops.hopsworks.common.api.RestDTO; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; +import lombok.Getter; +import lombok.Setter; + +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Date; + +@XmlRootElement +@Getter +@Setter +public class FeatureViewAlertDTO extends RestDTO { + private Integer id; + private String featureStoreName; + private FeatureStoreAlertStatus status; + private AlertType alertType; + private AlertSeverity severity; + private String receiver; + private Date created; + private Integer featureViewId; + private Integer featureViewVersion; + private String featureViewName; + + public FeatureViewAlertDTO() { + } + + @Override + public String toString() { + return "FeatureViewAlertDTO{" + + "id=" + id + + ", featureStoreName='" + featureStoreName + '\'' + + ", status=" + status + + ", alertType=" + alertType + + ", severity=" + severity + + ", receiver='" + receiver + '\'' + + ", created=" + created + + ", featureViewId=" + featureViewId + + ", featureViewVersion=" + featureViewVersion + + ", featureViewName='" + featureViewName + '\'' + + '}'; + } + +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewBuilder.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewBuilder.java index f1bcd52d6b..c2831b04cb 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewBuilder.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewBuilder.java @@ -85,7 +85,7 @@ public class FeatureViewBuilder { @EJB private FeatureStoreTagBuilder tagsBuilder; @EJB - private FeatureViewInputValidator featureViewInputValidator; + private FeatureViewInputValidation featureViewInputValidation; @Inject private PitJoinController pitJoinController; @@ -94,7 +94,7 @@ public FeatureViewBuilder() { public FeatureView convertFromDTO(Project project, Featurestore featurestore, Users user, FeatureViewDTO featureViewDTO) throws FeaturestoreException { - featureViewInputValidator.validate(featureViewDTO, project, user); + featureViewInputValidation.validate(featureViewDTO, project, user); FeatureView featureView = new FeatureView(); featureView.setName(featureViewDTO.getName()); featureView.setFeaturestore(featurestore); diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitorAlertResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitorAlertResource.java new file mode 100644 index 0000000000..bdeb7808ea --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitorAlertResource.java @@ -0,0 +1,92 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featureview; + +import io.hops.hopsworks.alert.util.Constants; +import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureGroupAlertDTO; +import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureStoreAlertResource; +import io.hops.hopsworks.api.filter.AllowedProjectRoles; +import io.hops.hopsworks.api.filter.Audience; +import io.hops.hopsworks.api.auth.key.ApiKeyRequired; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.jwt.annotation.JWTRequired; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; +import io.hops.hopsworks.persistence.entity.user.security.apiKey.ApiScope; +import io.hops.hopsworks.restutils.RESTCodes; +import io.swagger.annotations.ApiOperation; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.enterprise.context.RequestScoped; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import java.util.logging.Level; + +@RequestScoped +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureViewFeatureMonitorAlertResource extends FeatureStoreAlertResource { + + @Override + protected ResourceRequest.Name getEntityType() { + return ResourceRequest.Name.FEATUREVIEW; + } + @EJB + FeatureViewAlertBuilder featureViewAlertBuilder; + + + @PUT + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Update a feature view alert.", response = FeatureGroupAlertDTO.class) + @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST}) + @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + @ApiKeyRequired(acceptedScopes = {ApiScope.FEATURESTORE}, + allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) + public Response createOrUpdate( + @PathParam("id") + Integer id, + FeatureViewAlertDTO dto, + @Context + UriInfo uriInfo, + @Context + HttpServletRequest req, + @Context + SecurityContext sc) throws FeaturestoreException { + featureStoreAlertValidation.validateEntityType(getEntityType(), this.featuregroup, this.featureView); + if (dto == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + Constants.NO_PAYLOAD); + } + FeatureViewAlert featureViewAlert = featureViewAlertFacade.findByFeatureViewAndId(this.featureView, id); + featureStoreAlertValidation.validateUpdate(featureViewAlert, dto.getStatus(), featureView); + featureViewAlert = featureStoreAlertController.updateAlert(dto, featureViewAlert, project); + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.ALERTS); + dto = featureViewAlertBuilder.buildFeatureViewAlertDto(uriInfo, resourceRequest, featureViewAlert); + return Response.ok().entity(dto).build(); + } + + +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringConfigurationResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringConfigurationResource.java new file mode 100644 index 0000000000..141d6aee65 --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringConfigurationResource.java @@ -0,0 +1,66 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featureview; + +import io.hops.hopsworks.api.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationResource; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.enterprise.context.RequestScoped; + +@RequestScoped +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureViewFeatureMonitoringConfigurationResource extends FeatureMonitoringConfigurationResource { + + private FeatureView featureView; + @EJB + private FeatureViewController featureViewController; + + /** + * Sets the feature view of the tag resource + * + * @param name + * @param version + */ + public void setFeatureView(String name, Integer version) throws FeaturestoreException { + this.featureView = featureViewController.getByNameVersionAndFeatureStore(name, version, featureStore); + } + + @Override + protected Integer getItemId() { + return featureView.getId(); + } + + @Override + protected ResourceRequest.Name getItemType() { + return ResourceRequest.Name.FEATUREVIEW; + } + + @Override + protected String getItemName() { + return featureView.getName(); + } + + @Override + protected Integer getItemVersion() { + return featureView.getVersion(); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringResultResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringResultResource.java new file mode 100644 index 0000000000..1172627ded --- /dev/null +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewFeatureMonitoringResultResource.java @@ -0,0 +1,45 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.api.featurestore.featureview; + +import io.hops.hopsworks.api.featurestore.featuremonitoring.result.FeatureMonitoringResultResource; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; + +import javax.ejb.EJB; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.enterprise.context.RequestScoped; + +@RequestScoped +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureViewFeatureMonitoringResultResource extends FeatureMonitoringResultResource { + + private FeatureView featureView; + @EJB + private FeatureViewController featureViewController; + + /** + * Sets the feature view of the feature monitoring ressource + * + * @param name of the Feature View + * @param version of the Feature View + */ + public void setFeatureView(String name, Integer version) throws FeaturestoreException { + this.featureView = featureViewController.getByNameVersionAndFeatureStore(name, version, featureStore); + } +} \ No newline at end of file diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewInputValidator.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewInputValidation.java similarity index 98% rename from hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewInputValidator.java rename to hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewInputValidation.java index 6f10372058..d764ba6b1d 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewInputValidator.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewInputValidation.java @@ -36,7 +36,7 @@ @Stateless @TransactionAttribute(TransactionAttributeType.NEVER) -public class FeatureViewInputValidator { +public class FeatureViewInputValidation { @EJB private TrainingDatasetInputValidation trainingDatasetInputValidation; diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewService.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewService.java index 42346496d2..a9e1a07fce 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewService.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/featureview/FeatureViewService.java @@ -20,16 +20,20 @@ import io.hops.hopsworks.api.featurestore.activities.ActivityResource; import io.hops.hopsworks.api.featurestore.preparestatement.PreparedStatementResource; import io.hops.hopsworks.api.featurestore.query.QueryResource; +import io.hops.hopsworks.api.featurestore.statistics.StatisticsResource; import io.hops.hopsworks.api.featurestore.tag.FeatureViewTagResource; import io.hops.hopsworks.api.featurestore.trainingdataset.TrainingDatasetResource; import io.hops.hopsworks.api.featurestore.transformation.TransformationResource; import io.hops.hopsworks.api.provenance.FeatureViewProvenanceResource; import io.hops.hopsworks.common.featurestore.FeaturestoreController; +import io.hops.hopsworks.common.util.Settings; import io.hops.hopsworks.exceptions.FeaturestoreException; import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.restutils.RESTCodes; import io.swagger.annotations.Api; import io.swagger.annotations.ApiParam; +import java.util.logging.Level; import javax.ejb.EJB; import javax.ejb.TransactionAttribute; @@ -64,6 +68,16 @@ public class FeatureViewService { private ActivityResource activityResource; @Inject private FeatureViewProvenanceResource provenanceResource; + @Inject + private StatisticsResource statisticsResource; + @Inject + private FeatureViewFeatureMonitoringConfigurationResource featureMonitoringConfigurationResource; + @Inject + private FeatureViewFeatureMonitoringResultResource featureMonitoringResultResource; + @EJB + private Settings settings; + @Inject + private FeatureViewFeatureMonitorAlertResource featureViewFeatureMonitorAlertResource; private Project project; private Featurestore featurestore; @@ -183,4 +197,83 @@ public FeatureViewProvenanceResource provenance( this.provenanceResource.setFeatureViewVersion(version); return provenanceResource; } + + @Path("/{name: [a-z0-9_]*(?=[a-z])[a-z0-9_]+}/version/{version: [0-9]+}/statistics") + public StatisticsResource statistics( + @ApiParam(value = "Name of the feature view", required = true) + @PathParam("name") + String name, + @ApiParam(value = "Version of the feature view", required = true) + @PathParam("version") + Integer version + ) throws FeaturestoreException { + this.statisticsResource.setProject(project); + this.statisticsResource.setFeaturestore(featurestore); + this.statisticsResource.setFeatureViewByNameAndVersion(name, version); + return statisticsResource; + } + + @Path("/{name: [a-z0-9_]*(?=[a-z])[a-z0-9_]+}/version/{version: [0-9]+}/featuremonitoring/config") + public FeatureViewFeatureMonitoringConfigurationResource featureMonitoringConfigurationResource( + @ApiParam(value = "Name of the feature view", required = true) + @PathParam("name") + String name, + @ApiParam(value = "Version of the feature view", required = true) + @PathParam("version") + Integer version + ) throws FeaturestoreException { + if (!settings.isFeatureMonitoringEnabled()) { + throw new FeaturestoreException( + RESTCodes.FeaturestoreErrorCode.FEATURE_MONITORING_NOT_ENABLED, + Level.FINE + ); + } + this.featureMonitoringConfigurationResource.setProject(project); + this.featureMonitoringConfigurationResource.setFeatureStore(featurestore); + this.featureMonitoringConfigurationResource.setFeatureView(name, version); + return this.featureMonitoringConfigurationResource; + } + + @Path("/{name: [a-z0-9_]*(?=[a-z])[a-z0-9_]+}/version/{version: [0-9]+}/featuremonitoring/result") + public FeatureViewFeatureMonitoringResultResource featureMonitoringResultResource( + @ApiParam(value = "Name of the feature view", required = true) + @PathParam("name") + String name, + @ApiParam(value = "Version of the feature view", required = true) + @PathParam("version") + Integer version + ) throws FeaturestoreException { + if (!settings.isFeatureMonitoringEnabled()) { + throw new FeaturestoreException( + RESTCodes.FeaturestoreErrorCode.FEATURE_MONITORING_NOT_ENABLED, + Level.FINE + ); + } + this.featureMonitoringResultResource.setProject(project); + this.featureMonitoringResultResource.setFeatureStore(featurestore); + this.featureMonitoringResultResource.setFeatureView(name, version); + return this.featureMonitoringResultResource; + } + + @Path("/{name: [a-z0-9_]*(?=[a-z])[a-z0-9_]+}/version/{version: [0-9]+}/alerts") + public FeatureViewFeatureMonitorAlertResource featureViewFeatureMonitorAlertResource( + @ApiParam(value = "Name of the feature view", required = true) + @PathParam("name") + String featureViewName, + @ApiParam(value = "Version of the feature view", required = true) + @PathParam("version") + Integer version + ) throws FeaturestoreException { + if (!settings.isFeatureMonitoringEnabled()) { + throw new FeaturestoreException( + RESTCodes.FeaturestoreErrorCode.FEATURE_MONITORING_NOT_ENABLED, + Level.FINE + ); + } + featureViewFeatureMonitorAlertResource.setFeatureStore(featurestore); + featureViewFeatureMonitorAlertResource.setFeatureView(featureViewName, version, featurestore); + featureViewFeatureMonitorAlertResource.setProject(project); + return featureViewFeatureMonitorAlertResource; + } + } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsBuilder.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsBuilder.java index 717183c26e..fb1353095a 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsBuilder.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsBuilder.java @@ -26,6 +26,7 @@ import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupStatistics; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.TrainingDatasetStatistics; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDataset; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.split.SplitName; @@ -52,6 +53,8 @@ public class StatisticsBuilder { @EJB private FeatureDescriptiveStatisticsBuilder featureDescriptiveStatisticsBuilder; + // Feature Store URI + private UriBuilder uriQueryParams(UriBuilder uriBuilder, ResourceRequest resourceRequest, Set featureNames) { // feature names if (featureNames != null && !featureNames.isEmpty()) { @@ -83,6 +86,8 @@ private UriBuilder uri(UriInfo uriInfo, Project project, Featurestore featuresto .path(ResourceRequest.Name.FEATURESTORES.toString().toLowerCase()).path(Integer.toString(featurestore.getId())); } + // Feature Group URI + private URI uri(UriInfo uriInfo, ResourceRequest resourceRequest, Project project, Featurestore featurestore, Featuregroup featuregroup, Set featureNames) { UriBuilder uriBuilder = uri(uriInfo, project, featurestore) @@ -104,6 +109,35 @@ private URI uri(UriInfo uriInfo, Project project, Featurestore featurestore, Fea .build(); } + // Feature View URI + + private URI uri(UriInfo uriInfo, ResourceRequest resourceRequest, Project project, Featurestore featurestore, + FeatureView featureView, Set featureNames) { + UriBuilder uriBuilder = uri(uriInfo, project, featurestore) + .path(ResourceRequest.Name.FEATUREVIEW.toString().toLowerCase()) + .path(featureView.getName()) + .path(Integer.toString(featureView.getVersion())) + .path(ResourceRequest.Name.STATISTICS.toString().toLowerCase()); + return uriQueryParams(uriBuilder, resourceRequest, featureNames).build(); + } + + private URI uri(UriInfo uriInfo, Project project, Featurestore featurestore, FeatureView featureView, + Long computationTime) { + String commitTimeEq = StatisticsFilters.Filters.COMPUTATION_TIME_EQ.getValue().toLowerCase(); + return uri(uriInfo, project, featurestore) + .path(ResourceRequest.Name.FEATUREVIEW.toString().toLowerCase()) + .path(featureView.getName()) + .path(Integer.toString(featureView.getVersion())) + .path(ResourceRequest.Name.STATISTICS.toString().toLowerCase()) + .queryParam("filter_by", commitTimeEq + ":" + computationTime) + .queryParam("fields", "content") + .queryParam("offset", 0L) + .queryParam("limit", 1L) + .build(); + } + + // Training Dataset URI + private URI uri(UriInfo uriInfo, ResourceRequest resourceRequest, Project project, Featurestore featurestore, TrainingDataset trainingDataset, Set featureNames) { FeatureView featureView = trainingDataset.getFeatureView(); @@ -195,6 +229,60 @@ public StatisticsDTO build(UriInfo uriInfo, ResourceRequest resourceRequest, Pro return dto; } + // Feature View + + public StatisticsDTO build(UriInfo uriInfo, ResourceRequest resourceRequest, Project project, Users user, + FeatureView featureView, FeatureViewStatistics statistics) throws FeaturestoreException { + StatisticsDTO dto = new StatisticsDTO(); + Long computationTime = statistics.getComputationTime().getTime(); + dto.setHref(uri(uriInfo, project, featureView.getFeaturestore(), featureView, computationTime)); + dto.setExpand(expand(resourceRequest)); + if (dto.isExpand()) { + dto.setComputationTime(computationTime); + dto.setRowPercentage(statistics.getRowPercentage()); + dto.setFeatureViewName(statistics.getFeatureView().getName()); + dto.setFeatureViewVersion(statistics.getFeatureView().getVersion()); + if (statistics.getWindowStartCommitTime() != null) { + dto.setWindowStartCommitTime(statistics.getWindowStartCommitTime()); + } + if (statistics.getWindowEndCommitTime() != null) { + dto.setWindowEndCommitTime(statistics.getWindowEndCommitTime()); + } + if (resourceRequest.getField() != null && resourceRequest.getField().contains("content")) { + statisticsController.appendExtendedStatistics(project, user, statistics.getFeatureDescriptiveStatistics()); + dto.setFeatureDescriptiveStatistics( + featureDescriptiveStatisticsBuilder.buildMany(statistics.getFeatureDescriptiveStatistics())); + } + } + return dto; + } + + public StatisticsDTO build(UriInfo uriInfo, ResourceRequest resourceRequest, Project project, Users user, + Featurestore featurestore, FeatureView featureView, Set featureNames) throws FeaturestoreException { + StatisticsDTO dto = new StatisticsDTO(); + dto.setHref(uri(uriInfo, resourceRequest, project, featurestore, featureView, featureNames)); + dto.setExpand(expand(resourceRequest)); + if (dto.isExpand()) { + AbstractFacade.CollectionInfo collectionInfo; + if (featureNames != null && !featureNames.isEmpty()) { + // if a set of feature names is provided, filter statistics by feature name. It returns feature view + // statistics with the filtered feature descriptive statistics + collectionInfo = statisticsController.getStatisticsByFeatureViewAndFeatureNames(resourceRequest.getOffset(), + resourceRequest.getLimit(), resourceRequest.getSort(), resourceRequest.getFilter(), featureNames, + featureView); + } else { + // otherwise, get all feature descriptive statistics by feature view. + collectionInfo = statisticsController.getStatisticsByFeatureView(resourceRequest.getOffset(), + resourceRequest.getLimit(), resourceRequest.getSort(), resourceRequest.getFilter(), featureView); + } + dto.setCount(collectionInfo.getCount()); + for (Object s : collectionInfo.getItems()) { + dto.addItem(build(uriInfo, resourceRequest, project, user, featureView, (FeatureViewStatistics) s)); + } + } + return dto; + } + // Training Dataset public StatisticsDTO build(UriInfo uriInfo, ResourceRequest resourceRequest, Project project, Users user, diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsResource.java index 11ec89de24..f06b5dae3c 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsResource.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/featurestore/statistics/StatisticsResource.java @@ -26,6 +26,7 @@ import io.hops.hopsworks.common.api.ResourceRequest; import io.hops.hopsworks.common.featurestore.app.FsJobManagerController; import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; import io.hops.hopsworks.common.featurestore.statistics.SplitStatisticsDTO; import io.hops.hopsworks.common.featurestore.statistics.StatisticsController; import io.hops.hopsworks.common.featurestore.statistics.StatisticsDTO; @@ -46,6 +47,7 @@ import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureDescriptiveStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupStatistics; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.TrainingDatasetStatistics; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDataset; import io.hops.hopsworks.persistence.entity.jobs.description.Jobs; @@ -98,6 +100,8 @@ public class StatisticsResource { @EJB private FeaturegroupController featuregroupController; @EJB + private FeatureViewController featureViewController; + @EJB private TrainingDatasetController trainingDatasetController; @EJB private FsJobManagerController fsJobManagerController; @@ -107,6 +111,7 @@ public class StatisticsResource { private Project project; private Featurestore featurestore; private Featuregroup featuregroup; + private FeatureView featureView; private TrainingDataset trainingDataset; public void setProject(Project project) { @@ -121,6 +126,12 @@ public void setFeatureGroupById(Integer featureGroupId) throws FeaturestoreExcep this.featuregroup = featuregroupController.getFeaturegroupById(featurestore, featureGroupId); } + public void setFeatureViewByNameAndVersion(String featureViewName, Integer featureViewVersion) + throws FeaturestoreException { + this.featureView = featureViewController.getByNameVersionAndFeatureStore( + featureViewName, featureViewVersion, featurestore); + } + public void setTrainingDatasetByVersion(FeatureView featureView, Integer trainingDatasetVersion) throws FeaturestoreException { this.trainingDataset = trainingDatasetController.getTrainingDatasetByFeatureViewAndVersion(featureView, @@ -172,9 +183,13 @@ public Response get(@BeanParam Pagination pagination, StatisticsDTO dto; if (featuregroup != null) { // feature group statistics - statisticsInputValidation.validateStatisticsFiltersForFeatureGroup((Set)statisticsBeanParam.getFilterSet()); + statisticsInputValidation.validateStatisticsFiltersForFeatureGroup((Set) statisticsBeanParam.getFilterSet()); statisticsInputValidation.validateGetForFeatureGroup(featuregroup, filters); dto = statisticsBuilder.build(uriInfo, resourceRequest, project, user, featurestore, featuregroup, featureNames); + } else if (featureView != null) { + statisticsInputValidation.validateStatisticsFiltersForFeatureView((Set) statisticsBeanParam.getFilterSet()); + statisticsInputValidation.validateGetForFeatureView(featureView, filters); + dto = statisticsBuilder.build(uriInfo, resourceRequest, project, user, featurestore, featureView, featureNames); } else { // training dataset statistics statisticsInputValidation.validateStatisticsFiltersForTrainingDataset((Set)statisticsBeanParam.getFilterSet()); dto = statisticsBuilder.build(uriInfo, resourceRequest, project, user, featurestore, trainingDataset, @@ -203,6 +218,8 @@ public Response register(@Context UriInfo uriInfo, StatisticsDTO dto; if (featuregroup != null) { dto = registerFeatureGroupStatistics(user, uriInfo, statisticsDTO); + } else if (featureView != null) { + dto = registerFeatureViewStatistics(user, uriInfo, statisticsDTO); } else { dto = registerTrainingDatasetStatistics(user, uriInfo, statisticsDTO); } @@ -242,6 +259,19 @@ private StatisticsDTO registerFeatureGroupStatistics(Users user, UriInfo uriInfo return statisticsBuilder.build(uriInfo, resourceRequest, project, user, featuregroup, featureGroupStatistics); } + private StatisticsDTO registerFeatureViewStatistics(Users user, UriInfo uriInfo, StatisticsDTO statisticsDTO) + throws FeaturestoreException, IOException, DatasetException, HopsSecurityException { + statisticsInputValidation.validateRegisterForFeatureView(featureView, statisticsDTO); + Collection stats = featureDescriptiveStatisticsBuilder.buildManyFromContentOrDTO( + statisticsDTO.getFeatureDescriptiveStatistics(), statisticsDTO.getContent()); + FeatureViewStatistics featureViewStatistics = statisticsController.registerFeatureViewStatistics(project, user, + statisticsDTO.getComputationTime(), statisticsDTO.getWindowStartCommitTime(), + statisticsDTO.getWindowEndCommitTime(), statisticsDTO.getRowPercentage(), stats, featureView); + ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.STATISTICS); + resourceRequest.setField(Collections.singleton("content")); + return statisticsBuilder.build(uriInfo, resourceRequest, project, user, featureView, featureViewStatistics); + } + private StatisticsDTO registerTrainingDatasetStatistics(Users user, UriInfo uriInfo, StatisticsDTO statisticsDTO) throws FeaturestoreException, IOException, DatasetException, HopsSecurityException { statisticsInputValidation.validateRegisterForTrainingDataset(trainingDataset, statisticsDTO); diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Builder.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Builder.java index 5cd5511fbd..1d2e80ae9b 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Builder.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Builder.java @@ -40,7 +40,8 @@ public JobScheduleV2 validateAndConvertOnCreate(Jobs job, JobScheduleV2DTO sched return convertJobScheduleDTO(job, scheduleDTO); } - public void validateOnUpdate(Jobs job, JobScheduleV2DTO scheduleDTO) throws IllegalArgumentException { + public JobScheduleV2 validateAndConvertOnUpdate(Jobs job, JobScheduleV2DTO scheduleDTO) + throws IllegalArgumentException { if (scheduleDTO.getId() == null) { // Need to specify schedule id because there can be multiple schedules linked to a job in the future. throw new IllegalArgumentException(String.format( @@ -48,6 +49,7 @@ public void validateOnUpdate(Jobs job, JobScheduleV2DTO scheduleDTO) throws Ille job.getName())); } inputValidation.validateJobScheduleDTO(scheduleDTO); + return convertJobScheduleDTO(job, scheduleDTO); } public JobScheduleV2 convertJobScheduleDTO(Jobs job, JobScheduleV2DTO scheduleDTO) throws IllegalArgumentException { diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Resource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Resource.java index 5081461a74..962dd6bc1d 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Resource.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jobs/scheduler/JobScheduleV2Resource.java @@ -110,15 +110,13 @@ public Response createSchedule(JobScheduleV2DTO scheduleDTO, @ApiKeyRequired(acceptedScopes = {ApiScope.JOB}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER", "HOPS_SERVICE_USER"}) public Response updateSchedule(JobScheduleV2DTO scheduleDTO, - @Context - SecurityContext sc, - @Context - HttpServletRequest req, - @Context - UriInfo uriInfo) throws JobException { - jobScheduleBuilder.validateOnUpdate(job, scheduleDTO); - JobScheduleV2 jobSchedule = jobScheduleController.updateSchedule(scheduleDTO); - scheduleDTO = jobScheduleBuilder.build(uriInfo, jobSchedule); + @Context SecurityContext sc, + @Context HttpServletRequest req, + @Context UriInfo uriInfo) + throws JobException { + JobScheduleV2 schedule = jobScheduleBuilder.validateAndConvertOnUpdate(job, scheduleDTO); + schedule = jobScheduleController.updateSchedule(schedule); + scheduleDTO = jobScheduleBuilder.build(uriInfo, schedule); return Response.ok().entity(scheduleDTO).build(); } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAlertsResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAlertsResource.java index dac16096bc..e681a352a2 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAlertsResource.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAlertsResource.java @@ -20,6 +20,7 @@ import io.hops.hopsworks.alert.dao.AlertReceiverFacade; import io.hops.hopsworks.alert.exception.AlertManagerAccessControlException; import io.hops.hopsworks.alert.exception.AlertManagerUnreachableException; +import io.hops.hopsworks.alert.util.Constants; import io.hops.hopsworks.alerting.api.alert.dto.Alert; import io.hops.hopsworks.alerting.exceptions.AlertManagerClientCreateException; import io.hops.hopsworks.alerting.exceptions.AlertManagerConfigCtrlCreateException; @@ -29,8 +30,11 @@ import io.hops.hopsworks.alerting.exceptions.AlertManagerResponseException; import io.hops.hopsworks.api.alert.AlertBuilder; import io.hops.hopsworks.api.alert.AlertDTO; +import io.hops.hopsworks.api.alert.FeatureStoreAlertController; import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureGroupAlertBuilder; import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureGroupAlertDTO; +import io.hops.hopsworks.api.featurestore.featureview.FeatureViewAlertBuilder; +import io.hops.hopsworks.api.featurestore.featureview.FeatureViewAlertDTO; import io.hops.hopsworks.api.filter.AllowedProjectRoles; import io.hops.hopsworks.api.filter.Audience; import io.hops.hopsworks.api.auth.key.ApiKeyRequired; @@ -106,7 +110,11 @@ public class ProjectAlertsResource { private FeatureGroupAlertBuilder featureGroupAlertBuilder; @EJB private AlertReceiverFacade alertReceiverFacade; - + @EJB + private FeatureViewAlertBuilder featureViewAlertBuilder; + @EJB + private FeatureStoreAlertController featureStoreAlertController; + private Integer projectId; private String projectName; @@ -193,11 +201,14 @@ public Response getAllAlerts(@Context UriInfo uriInfo, ProjectAlertsDTO projectAlertsDTO = projectAlertsBuilder.buildItemsAll(uriInfo, resourceRequest, getProject()); JobAlertsDTO jobAlertsDTO = jobAlertsBuilder.buildItems(uriInfo, resourceRequest, getProject()); FeatureGroupAlertDTO featureGroupAlertDTO = - featureGroupAlertBuilder.buildItems(uriInfo, resourceRequest, getProject()); + featureGroupAlertBuilder.buildItems(uriInfo, resourceRequest, getProject()); + FeatureViewAlertDTO featureViewAlertDTO = featureViewAlertBuilder.buildManyProjectAlerts(uriInfo, resourceRequest, + featureStoreAlertController.retrieveAllFeatureViewAlerts(getProject())); ProjectAllAlertsDTO projectAllAlertsDTO = new ProjectAllAlertsDTO(); projectAllAlertsDTO.setProjectAlerts(projectAlertsDTO); projectAllAlertsDTO.setJobAlerts(jobAlertsDTO); projectAllAlertsDTO.setFeatureGroupAlerts(featureGroupAlertDTO); + projectAllAlertsDTO.setFeatureViewAlertDTO(featureViewAlertDTO); return Response.ok().entity(projectAllAlertsDTO).build(); } @@ -219,7 +230,7 @@ public Response createOrUpdate(@PathParam("id") Integer id, ProjectAlertsDTO pro "Alert not found. Id=" + id.toString()); } if (projectAlertsDTO == null) { - throw new ProjectException(RESTCodes.ProjectErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, "No payload."); + throw new ProjectException(RESTCodes.ProjectErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, Constants.NO_PAYLOAD); } Project project = getProject(); if (projectAlertsDTO.getStatus() != null) { @@ -281,8 +292,8 @@ private ProjectAlertsDTO createAlert(PostableProjectAlerts projectAlertsDTO, Boo } private void validateBulk(PostableProjectAlerts projectAlertsDTO) throws ProjectException { - if (projectAlertsDTO.getItems() == null || projectAlertsDTO.getItems().size() < 1) { - throw new ProjectException(RESTCodes.ProjectErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, "No payload."); + if (projectAlertsDTO.getItems() == null || projectAlertsDTO.getItems().isEmpty()) { + throw new ProjectException(RESTCodes.ProjectErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, Constants.NO_PAYLOAD); } Set statusSet = new HashSet<>(); for (PostableProjectAlerts dto : projectAlertsDTO.getItems()) { @@ -335,7 +346,7 @@ private void createRoute(ProjectServiceAlert projectServiceAlert) throws Project private void validate(PostableProjectAlerts projectAlertsDTO) throws ProjectException { if (projectAlertsDTO == null) { - throw new ProjectException(RESTCodes.ProjectErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, "No payload."); + throw new ProjectException(RESTCodes.ProjectErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, Constants.NO_PAYLOAD); } if (projectAlertsDTO.getStatus() == null) { throw new ProjectException(RESTCodes.ProjectErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAllAlertsDTO.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAllAlertsDTO.java index ce75761c66..188fd91ca4 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAllAlertsDTO.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/project/alert/ProjectAllAlertsDTO.java @@ -16,6 +16,7 @@ package io.hops.hopsworks.api.project.alert; import io.hops.hopsworks.api.featurestore.datavalidation.alert.FeatureGroupAlertDTO; +import io.hops.hopsworks.api.featurestore.featureview.FeatureViewAlertDTO; import io.hops.hopsworks.api.jobs.alert.JobAlertsDTO; import javax.xml.bind.annotation.XmlRootElement; @@ -25,7 +26,9 @@ public class ProjectAllAlertsDTO { private ProjectAlertsDTO projectAlerts; private JobAlertsDTO jobAlerts; private FeatureGroupAlertDTO featureGroupAlerts; - + + private FeatureViewAlertDTO featureViewAlertDTO; + public ProjectAllAlertsDTO() { } @@ -52,4 +55,12 @@ public FeatureGroupAlertDTO getFeatureGroupAlerts() { public void setFeatureGroupAlerts(FeatureGroupAlertDTO featureGroupAlerts) { this.featureGroupAlerts = featureGroupAlerts; } + + public FeatureViewAlertDTO getFeatureViewAlertDTO() { + return featureViewAlertDTO; + } + + public void setFeatureViewAlertDTO(FeatureViewAlertDTO featureViewAlertDTO) { + this.featureViewAlertDTO = featureViewAlertDTO; + } } diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/alert/AlertController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/alert/AlertController.java index 1d05e6ed7d..e84e922938 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/alert/AlertController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/alert/AlertController.java @@ -34,11 +34,14 @@ import io.hops.hopsworks.alerting.exceptions.AlertManagerDuplicateEntryException; import io.hops.hopsworks.alerting.exceptions.AlertManagerNoSuchElementException; import io.hops.hopsworks.alerting.exceptions.AlertManagerResponseException; +import io.hops.hopsworks.common.api.ResourceRequest; import io.hops.hopsworks.persistence.entity.alertmanager.AlertReceiver; import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.FeatureGroupValidationStatus; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlert; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; import io.hops.hopsworks.persistence.entity.jobs.configuration.history.JobFinalStatus; import io.hops.hopsworks.persistence.entity.jobs.configuration.history.JobState; import io.hops.hopsworks.persistence.entity.jobs.description.JobAlert; @@ -109,7 +112,19 @@ public List testAlert(Project project, FeatureGroupAlert alert) throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerResponseException, AlertManagerClientCreateException { return sendFgTestAlert(project, alert.getAlertType(), alert.getSeverity(), - FeatureGroupValidationStatus.fromString(alert.getStatus().toString()), alert.getFeatureGroup().getName()); + FeatureStoreAlertStatus.fromString(alert.getStatus().toString()), alert.getFeatureGroup().getName()); + } + + /** + * Test feature view alert + * + * @param project + * @param alert + */ + public List testAlert(Project project, FeatureViewAlert alert) + throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerResponseException, + AlertManagerClientCreateException { + return sendFvTestAlert(project, alert); } /** @@ -135,7 +150,7 @@ public List testAlert(Project project, ProjectServiceAlert alert) List alerts = null; if (ProjectServiceEnum.FEATURESTORE.equals(alert.getService())) { alerts = sendFgTestAlert(project, alert.getAlertType(), alert.getSeverity(), - FeatureGroupValidationStatus.fromString(alert.getStatus().toString()), null); + FeatureStoreAlertStatus.fromString(alert.getStatus().toString()), null); } else if (ProjectServiceEnum.JOBS.equals(alert.getService())) { alerts = sendJobTestAlert(project, alert.getAlertType(), alert.getSeverity(), alert.getStatus().getName(), null); } @@ -155,7 +170,18 @@ public void sendFgAlert(List postableAlerts, Project project, Str sendAlert(postableAlerts, project); } catch (Exception e) { LOGGER.log(java.util.logging.Level.WARNING, "Failed to send alert. Featuregroup={0}. Exception: {1}", - new Object[] {name, e.getMessage()}); + new Object[] {name, e.getMessage()}); + } + } + + public void sendFeatureMonitorAlert(List postableAlerts, Project project, String name) { + try { + if (!postableAlerts.isEmpty()) { + sendAlert(postableAlerts, project); + } + } catch (Exception e) { + LOGGER.log(java.util.logging.Level.WARNING, "Failed to send alert. Feature Monitoring Config={0}. Exception: {1}", + new Object[]{name, e.getMessage()}); } } @@ -169,7 +195,7 @@ private void sendJobAlert(List postableAlerts, Project project, S } private List sendFgTestAlert(Project project, AlertType alertType, AlertSeverity severity, - FeatureGroupValidationStatus status, String fgName) throws AlertManagerUnreachableException, + FeatureStoreAlertStatus status, String fgName) throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerResponseException, AlertManagerClientCreateException { String testAlertFgName = Strings.isNullOrEmpty(fgName) ? Constants.TEST_ALERT_FG_NAME : fgName; List postableAlerts = new ArrayList<>(); @@ -182,7 +208,35 @@ private List sendFgTestAlert(Project project, AlertType alertType, AlertS Constants.FILTER_BY_FG_ID_FORMAT.replace(Constants.FG_ID_PLACE_HOLDER, Constants.TEST_ALERT_FG_ID.toString()); return getAlerts(project, fgFilter); } - + + /** + * create a test postable alert for feature view and send alert. Supports only for feature monitoring status. + * @param project + * @param alert + * @return + * @throws AlertManagerUnreachableException + * @throws AlertManagerAccessControlException + * @throws AlertManagerResponseException + * @throws AlertManagerClientCreateException + * @throws AlertException + */ + private List sendFvTestAlert(Project project, FeatureViewAlert alert) throws AlertManagerUnreachableException, + AlertManagerAccessControlException, AlertManagerResponseException, AlertManagerClientCreateException { + List postableAlerts = new ArrayList<>(); + PostableAlert postableAlert = + getPostableFeatureMonitorAlert(project, alert, ResourceRequest.Name.FEATUREVIEW, Constants.TEST_ALERT_FM_NAME, + Constants.TEST_ALERT_FG_VERSION, + Constants.TEST_ALERT_FG_SUMMARY, Constants.TEST_ALERT_FG_DESCRIPTION, Constants.TEST_ALERT_FS_NAME); + postableAlerts.add(postableAlert); + sendAlert(postableAlerts, project); + String fgFilter = + Constants.FILTER_BY_FM_NAME_FORMAT.replace(Constants.FM_NAME_PLACE_HOLDER, Constants.TEST_ALERT_FM_NAME) + + Constants.FILTER_BY_FM_RESULT_FORMAT.replace(Constants.FM_ID_PLACE_HOLDER, + Constants.TEST_ALERT_FG_VERSION.toString()); + return getAlerts(project, fgFilter); + } + + private List sendJobTestAlert(Project project, AlertType alertType, AlertSeverity severity, String status, String jobName) throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerResponseException, AlertManagerClientCreateException { @@ -207,8 +261,7 @@ private List getAlerts(Project project, String filter) project.getName()); filters.add(projectFilter); filters.add(filter); - List alerts = alertManager.getAlerts(true, null, null, null, filters, null, project); - return alerts; + return alertManager.getAlerts(true, null, null, null, filters, null, project); } public PostableAlert getPostableFgAlert(String projectName, AlertType alertType, AlertSeverity severity, @@ -224,7 +277,69 @@ public PostableAlert getPostableFgAlert(String projectName, AlertType alertType, .withDescription(description) .build(); } - + /** + * create a PostableAlert for FeatureMonitoring status for given ProjecServiceAlert + * @param project + * @param projectAlert + * @param fmConfigName + * @param fmResultId + * @param summary + * @param description + * @param featureStoreName + * @return PostableAlert + */ + public PostableAlert getPostableFeatureMonitorAlert(Project project, + ProjectServiceAlert projectAlert, String fmConfigName, Integer fmResultId, String summary, + String description, String featureStoreName) { + + PostableAlertBuilder.Builder builder = + new PostableAlertBuilder.Builder(project.getName(), projectAlert.getAlertType(), + projectAlert.getSeverity() + , projectAlert.getStatus().getName()) + .withFeatureStoreName(featureStoreName) + .withFeatureMonitorConfig(fmConfigName, fmResultId) + .withSummary(summary) + .withDescription(description); + return builder.build(); + } + + /** + * Create PostableAlert for FeatureMonitoring status from FeatureStoreAlert. + * @param project + * @param featureStoreAlert + * @param resourceName + * @param fmConfigName + * @param fmResultId + * @param summary + * @param description + * @param featureStoreName + * @return + */ + public PostableAlert getPostableFeatureMonitorAlert(Project project, FeatureStoreAlert featureStoreAlert, + ResourceRequest.Name resourceName, String fmConfigName, + Integer fmResultId, + String summary, + String description, + String featureStoreName) { + + PostableAlertBuilder.Builder builder = + new PostableAlertBuilder.Builder(project.getName(), featureStoreAlert.getAlertType(), + featureStoreAlert.getSeverity() + , featureStoreAlert.getStatus().getName()) + .withFeatureStoreName(featureStoreName) + .withFeatureMonitorConfig(fmConfigName, fmResultId) + .withSummary(summary) + .withDescription(description); + if (resourceName.equals(ResourceRequest.Name.FEATUREGROUPS)) { + builder.withFeatureGroupName(((FeatureGroupAlert) featureStoreAlert).getFeatureGroup().getName()) + .withFeatureGroupVersion(((FeatureGroupAlert) featureStoreAlert).getFeatureGroup().getVersion()); + } else if (resourceName.equals(ResourceRequest.Name.FEATUREVIEW)) { + builder.withFeatureViewVersion(((FeatureViewAlert) featureStoreAlert).getFeatureView().getVersion()) + .withFeatureViewName(((FeatureViewAlert) featureStoreAlert).getFeatureView().getName()); + } + return builder.build(); + } + private PostableAlert getPostableAlert(Project project, AlertType alertType, AlertSeverity severity, String status, String jobName, Integer id) { return new PostableAlertBuilder @@ -309,11 +424,10 @@ public void createRoute(ProjectServiceAlert alert) throws AlertManagerUnreachabl Route route = ConfigUtil.getRoute(alert); addRouteIfNotExist(alert.getAlertType(), route, project); } - - public void createRoute(FeatureGroupAlert alert) throws AlertManagerUnreachableException, + + public void createRoute(Project project, FeatureGroupAlert alert) throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerNoSuchElementException, AlertManagerConfigUpdateException, AlertManagerConfigCtrlCreateException, AlertManagerConfigReadException, AlertManagerClientCreateException { - Project project = alert.getFeatureGroup().getFeaturestore().getProject(); Route route = ConfigUtil.getRoute(alert); addRouteIfNotExist(alert.getAlertType(), route, project); } @@ -337,7 +451,14 @@ public void createRoute(AlertType alertType) // route exists } } - + + public void createRoute(Project project, FeatureViewAlert alert) throws AlertManagerUnreachableException, + AlertManagerAccessControlException, AlertManagerNoSuchElementException, AlertManagerConfigUpdateException, + AlertManagerConfigCtrlCreateException, AlertManagerConfigReadException, AlertManagerClientCreateException { + Route route = ConfigUtil.getRoute(alert); + addRouteIfNotExist(alert.getAlertType(), route, project); + } + public void deleteRoute(ProjectServiceAlert alert) throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerConfigUpdateException, AlertManagerConfigCtrlCreateException, AlertManagerConfigReadException, AlertManagerClientCreateException { @@ -347,11 +468,10 @@ public void deleteRoute(ProjectServiceAlert alert) alertManagerConfiguration.removeRoute(route, project); } } - - public void deleteRoute(FeatureGroupAlert alert) + + public void deleteRoute(Project project, FeatureGroupAlert alert) throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerConfigUpdateException, AlertManagerConfigCtrlCreateException, AlertManagerConfigReadException, AlertManagerClientCreateException { - Project project = alert.getFeatureGroup().getFeaturestore().getProject(); Route route = ConfigUtil.getRoute(alert); if (!isUsedByOtherAlerts(route, alert.getId())) { alertManagerConfiguration.removeRoute(route, project); @@ -367,7 +487,16 @@ public void deleteRoute(JobAlert alert) alertManagerConfiguration.removeRoute(route, project); } } - + + public void deleteRoute(Project project, FeatureViewAlert alert) + throws AlertManagerUnreachableException, AlertManagerAccessControlException, AlertManagerConfigUpdateException, + AlertManagerConfigCtrlCreateException, AlertManagerConfigReadException, AlertManagerClientCreateException { + Route route = ConfigUtil.getRoute(alert); + if (!isUsedByOtherAlerts(route, alert.getId())) { + alertManagerConfiguration.removeRoute(route, project); + } + } + private boolean isUsedByOtherAlerts(Route route, int id) { Optional alertReceiver = alertReceiverFacade.findByName(route.getReceiver()); if (!alertReceiver.isPresent()) { diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/api/ResourceRequest.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/api/ResourceRequest.java index fff260b891..e59c004a09 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/api/ResourceRequest.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/api/ResourceRequest.java @@ -243,7 +243,8 @@ public enum Name { PROVENANCE_ARTIFACTS, PROJECT_LDAP_GROUP_MAPPING, SCHEDULE, - ENVIRONMENT_HISTORY; + ENVIRONMENT_HISTORY, + FEATURE_MONITORING; public static Name fromString(String name) { return valueOf(name.toUpperCase()); diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/FeaturestoreConstants.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/FeaturestoreConstants.java index 79f95f42d8..3d4fba2fb7 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/FeaturestoreConstants.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/FeaturestoreConstants.java @@ -158,4 +158,16 @@ private FeaturestoreConstants() { public static final String PARTIAL_UNEXPECTED_LIST_KEY = "partial_unexpected_list"; public static final String UNEXPECTED_PERCENT_NONMISSING_KEY = "unexpected_percent_nonmissing"; public static final String GREAT_EXPECTATIONS_META = "{\"great_expectations_version\": \"0.15.12\"}"; + public static final int MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME = 128; + public static final int MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_DESCRIPTION = 2000; + public static final int MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_FEATURE_NAME = 63; + public static final int MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_TIME_OFFSET = 63; + public static final int MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_WINDOW_LENGTH = 63; + public static final List ALLOWED_METRICS_IN_STATISTICS_COMPARISON_CONFIG = Arrays.asList( + "COMPLETENESS", "NUM_RECORDS_NON_NULL", "NUM_RECORDS_NULL", "DISTINCTNESS", "ENTROPY", "UNIQUENESS", + "APPROXIMATE_NUM_DISTINCT_VALUES", "EXACT_NUM_DISTINCT_VALUES", "MEAN", "MAX", "MIN", "SUM", "STDDEV", + "COUNT"); + public static final Pattern INVALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX = Pattern.compile("([^dwh,0-9]+)"); + public static final Pattern VALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX = Pattern.compile( + "(?:(?\\d+w)()|(?\\d+d)()|(?\\d+h)()){1,3}"); } diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/activity/FeaturestoreActivityFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/activity/FeaturestoreActivityFacade.java index 48037c078f..1afcd6d137 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/activity/FeaturestoreActivityFacade.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/activity/FeaturestoreActivityFacade.java @@ -26,6 +26,7 @@ import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.ValidationReport; import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupStatistics; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.TrainingDatasetStatistics; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDataset; import io.hops.hopsworks.persistence.entity.jobs.history.Execution; @@ -97,6 +98,17 @@ public void logStatisticsActivity(Users user, Featuregroup featuregroup, Date ev fsActivity.setFeatureGroupStatistics(statistics); em.persist(fsActivity); } + + public void logStatisticsActivity(Users user, FeatureView featureView, Date eventTime, + FeatureViewStatistics statistics) { + FeaturestoreActivity fsActivity = new FeaturestoreActivity(); + fsActivity.setType(ActivityType.STATISTICS); + fsActivity.setFeatureView(featureView); + fsActivity.setUser(user); + fsActivity.setEventTime(eventTime); + fsActivity.setFeatureViewStatistics(statistics); + em.persist(fsActivity); + } public void logStatisticsActivity(Users user, TrainingDataset trainingDataset, Date eventTime, TrainingDatasetStatistics statistics) { diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/app/FsJobManagerController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/app/FsJobManagerController.java index 0c472202b6..07b3f4807f 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/app/FsJobManagerController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/app/FsJobManagerController.java @@ -17,6 +17,7 @@ package io.hops.hopsworks.common.featurestore.app; import com.fasterxml.jackson.databind.ObjectMapper; +import io.hops.hopsworks.common.api.ResourceRequest; import io.hops.hopsworks.common.dao.jobs.description.JobFacade; import io.hops.hopsworks.common.dataset.DatasetController; import io.hops.hopsworks.common.featurestore.FeaturestoreController; @@ -118,6 +119,7 @@ public class FsJobManagerController { private final static String DELTA_STREAMER_OP = "offline_fg_materialization"; private final static String GE_VALIDATE_OP = "ge_validate"; private final static String IMPORT_FEATUREGROUP_OP = "import_fg"; + private final static String FEATURE_MONITORING_OP = "run_feature_monitoring"; // td private final static String TRAINING_DATASET_OP = "create_td"; // fv @@ -162,6 +164,40 @@ public IngestionJob setupIngestionJob(Project project, Users user, Featuregroup dfs.closeDfsClient(udfso); } } + + public Jobs setupFeatureMonitoringJob( + Users user, Project project, ResourceRequest.Name entityType, String entityName, Integer entityVersion, + String configName) throws FeaturestoreException, JobException { + DistributedFileSystemOps udfso = dfs.getDfsOps(hdfsUsersController.getHdfsUserName(project, user)); + + try { + HashMap jobConfiguration = new HashMap<>(); + // Allows job access to the project's featurestore + jobConfiguration.put("feature_store", + featurestoreController.getOfflineFeaturestoreDbName(project)); + // Allows job to fetch the feature group or feature view to monitor + jobConfiguration.put("entity_type", entityType.toString()); + jobConfiguration.put("name", entityName); + jobConfiguration.put("version", String.valueOf(entityVersion)); + + // Allows job to fetch the config to use for monitoring + jobConfiguration.put("config_name", configName); + + String jobName = getMonitoringJobName(entityName, entityVersion, configName); + String jobFolder = createJobFolder(project, user, jobName); + String jobConfigurationPath = jobFolder + "/config.json"; + + writeToHDFS(jobConfigurationPath, objectMapper.writeValueAsString(jobConfiguration), udfso); + String jobArgs = getJobArgs(FEATURE_MONITORING_OP, jobConfigurationPath); + + return configureJob(user, project, null, jobName, jobArgs, JobType.PYSPARK); + } catch (IOException e) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ERROR_JOB_SETUP, Level.SEVERE, + "Error setting up feature monitoring job", e.getMessage(), e); + } finally { + dfs.closeDfsClient(udfso); + } + } // Create the directoy where the client should upload the data. private String getIngestionPath(Project project, Users user, Featuregroup featureGroup, @@ -460,6 +496,15 @@ private String getJobName(String op, String entityName, boolean withTimeStamp) { } return name; } + + // Added as single source of truth for monitoring jobs + public String getMonitoringJobName(String entityName, Integer entityVersion, String configName) { + return getJobName( + FEATURE_MONITORING_OP, + Utils.getFeatureStoreEntityName(entityName, entityVersion) + "_" + configName, + false + ); + } private String getJobArgs(String op, String jobConfigurationPath) { return "-op " + op + " -path " + jobConfigurationPath; @@ -494,8 +539,9 @@ private Jobs configureJob(Users user, Project project, SparkJobConfiguration spa public void deleteJobs(Project project, Users user, Featuregroup featuregroup) throws JobException { - String jobNameRegex = String.format("%s_(%s|%s|%s|%s|%s).*", Utils.getFeaturegroupName(featuregroup), - INSERT_FG_OP, COMPUTE_STATS_OP, DELTA_STREAMER_OP, GE_VALIDATE_OP, IMPORT_FEATUREGROUP_OP); + String jobNameRegex = String.format("%s_(%s|%s|%s|%s|%s|%s).*", Utils.getFeaturegroupName(featuregroup), + INSERT_FG_OP, COMPUTE_STATS_OP, DELTA_STREAMER_OP, GE_VALIDATE_OP, IMPORT_FEATUREGROUP_OP, + FEATURE_MONITORING_OP); deleteJobs(project, user, jobNameRegex); } @@ -508,8 +554,8 @@ public void deleteJobs(Project project, Users user, TrainingDataset trainingData public void deleteJobs(Project project, Users user, FeatureView featureView) throws JobException { - String jobNameRegex = String.format("%s_(%s).*", Utils.getFeatureViewName(featureView), - FEATURE_VIEW_TRAINING_DATASET_OP); + String jobNameRegex = String.format("%s_(%s|%s).*", Utils.getFeatureViewName(featureView), + FEATURE_VIEW_TRAINING_DATASET_OP, FEATURE_MONITORING_OP); deleteJobs(project, user, jobNameRegex); } @@ -522,11 +568,11 @@ private void deleteJobs(Project project, Users user, String jobPrefix) } public Jobs setupImportFgJob(Project project, Users user, Featurestore featurestore, ImportFgJobConf importFgJobConf) - throws FeaturestoreException, JobException, GenericException, ProjectException, ServiceException { - Map jobConfiguration =new HashMap<>(); + throws FeaturestoreException, JobException, GenericException, ProjectException, ServiceException { + Map jobConfiguration = new HashMap<>(); FeaturestoreStorageConnectorDTO storageConnector = importFgJobConf.getStorageConnectorDTO(); FeaturestoreConnectorType connectorType = storageConnector.getStorageConnectorType(); - HashMap options = new HashMap(); + HashMap options = new HashMap<>(); // set spark options as per connector type if (importFgJobConf.getTable() != null) { switch (connectorType) { @@ -574,7 +620,7 @@ public Jobs setupImportFgJob(Project project, Users user, Featurestore featurest "importData", jobConfiguration); } - public int getNewFeatureGroupVersion(Featurestore featurestore, String featureGroupName){ + public int getNewFeatureGroupVersion(Featurestore featurestore, String featureGroupName) { List fgPrevious = featuregroupFacade.findByNameAndFeaturestoreOrderedDescVersion( featureGroupName, featurestore); if (fgPrevious != null && !fgPrevious.isEmpty()) { diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidation/FeatureGroupAlertFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidation/FeatureGroupAlertFacade.java index 58eacf24ba..0d0fc70ca7 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidation/FeatureGroupAlertFacade.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidation/FeatureGroupAlertFacade.java @@ -20,7 +20,7 @@ import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.ValidationRuleAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import io.hops.hopsworks.persistence.entity.user.activity.Activity; import javax.ejb.Stateless; @@ -60,11 +60,11 @@ public FeatureGroupAlert findByFeatureGroupAndId(Featuregroup featureGroup, Inte } public FeatureGroupAlert findByFeatureGroupAndStatus( - Featuregroup featureGroup, ValidationRuleAlertStatus validationRuleAlertStatus) { + Featuregroup featureGroup, FeatureStoreAlertStatus featureStoreAlertStatus) { TypedQuery query = em.createNamedQuery("FeatureGroupAlert.findByFeatureGroupAndStatus", FeatureGroupAlert.class); query.setParameter("featureGroup", featureGroup) - .setParameter("status", validationRuleAlertStatus); + .setParameter("status", featureStoreAlertStatus); try { return query.getSingleResult(); } catch (NoResultException e) { @@ -107,7 +107,7 @@ private void setFilterQuery(AbstractFacade.FilterBy filterBy, Query q) { q.setParameter(filterBy.getField(), getEnumValues(filterBy, AlertType.class)); break; case STATUS: - q.setParameter(filterBy.getField(), getEnumValues(filterBy, ValidationRuleAlertStatus.class)); + q.setParameter(filterBy.getField(), getEnumValues(filterBy, FeatureStoreAlertStatus.class)); break; case SEVERITY: q.setParameter(filterBy.getField(), getEnumValues(filterBy, AlertSeverity.class)); @@ -161,7 +161,7 @@ public String toString() { public enum Filters { TYPE("TYPE", "a.alertType IN :alertType ", "alertType", AlertType.PROJECT_ALERT.toString()), - STATUS("STATUS", "a.status IN :status ", "status", ValidationRuleAlertStatus.SUCCESS.toString()), + STATUS("STATUS", "a.status IN :status ", "status", FeatureStoreAlertStatus.SUCCESS.toString()), SEVERITY("SEVERITY", "a.severity IN :severity ", "severity", AlertSeverity.INFO.toString()), CREATED("CREATED", "a.created = :created ","created",""), CREATED_GT("DATE_CREATED_GT", "a.created > :createdFrom ","createdFrom",""), diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidationv2/reports/ValidationReportController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidationv2/reports/ValidationReportController.java index fc7b95b093..97be9637d1 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidationv2/reports/ValidationReportController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/datavalidationv2/reports/ValidationReportController.java @@ -36,7 +36,7 @@ import io.hops.hopsworks.persistence.entity.dataset.DatasetAccessPermission; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.ValidationRuleAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.IngestionResult; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.ValidationReport; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.ValidationResult; @@ -145,10 +145,10 @@ private List getPostableAlerts(Featuregroup featureGroup, Validat if (featureGroup.getFeatureGroupAlerts() != null && !featureGroup.getFeatureGroupAlerts().isEmpty()) { String name = featurestoreController.getOfflineFeaturestoreDbName(featureGroup.getFeaturestore()); for (FeatureGroupAlert alert : featureGroup.getFeatureGroupAlerts()) { - if (alert.getStatus() == ValidationRuleAlertStatus.FAILURE + if (alert.getStatus() == FeatureStoreAlertStatus.FAILURE && validationReport.getIngestionResult() == IngestionResult.REJECTED) { postableAlerts.add(getPostableAlert(alert, name, featureGroup, validationReport)); - } else if (alert.getStatus() == ValidationRuleAlertStatus.SUCCESS + } else if (alert.getStatus() == FeatureStoreAlertStatus.SUCCESS && validationReport.getIngestionResult() == IngestionResult.INGESTED) { postableAlerts.add(getPostableAlert(alert, name, featureGroup, validationReport)); } diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/alert/FeatureMonitoringAlertController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/alert/FeatureMonitoringAlertController.java new file mode 100644 index 0000000000..dae000cf88 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/alert/FeatureMonitoringAlertController.java @@ -0,0 +1,178 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featuremonitoring.alert; + +import io.hops.hopsworks.alerting.api.alert.dto.PostableAlert; +import io.hops.hopsworks.common.alert.AlertController; +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.dao.project.alert.ProjectServiceAlertsFacade; +import io.hops.hopsworks.common.featurestore.FeaturestoreController; +import io.hops.hopsworks.common.featurestore.FeaturestoreFacade; +import io.hops.hopsworks.common.featurestore.datavalidation.FeatureGroupAlertFacade; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewAlertFacade; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlert; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.persistence.entity.project.alert.ProjectServiceAlert; +import io.hops.hopsworks.persistence.entity.project.alert.ProjectServiceAlertStatus; +import io.hops.hopsworks.restutils.RESTCodes; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureMonitoringAlertController { + @EJB + private FeaturestoreController featurestoreController; + @EJB + private AlertController alertController; + @EJB + private FeaturestoreFacade featurestoreFacade; + @EJB + private FeatureGroupAlertFacade featureGroupAlertFacade; + @EJB + private ProjectServiceAlertsFacade projectServiceAlertsFacade; + @EJB + private FeatureViewAlertFacade featureViewAlertFacade; + + public FeatureMonitoringAlertController() { + } + + /** + * This will retrieve alerts configs based on the feature group/feature view defined on feature monitoring. + * Posts the alert if it matches the shift detected filter. + * + * @param config + * @param result + */ + public void triggerAlertsByStatus(FeatureMonitoringConfiguration config, FeatureMonitoringResult result) + throws FeaturestoreException { + validateInput(config, result); + List postableAlerts; + Project project; + String fsName; + if (config.getFeatureGroup() != null) { + project = config.getFeatureGroup().getFeaturestore().getProject(); + fsName = featurestoreController.getOfflineFeaturestoreDbName(config.getFeatureGroup().getFeaturestore()); + postableAlerts = getValidPostableAlerts(project, fsName, config, result, + ResourceRequest.Name.FEATUREGROUPS); + } else { + project = config.getFeatureView().getFeaturestore().getProject(); + fsName = featurestoreController.getOfflineFeaturestoreDbName(config.getFeatureView().getFeaturestore()); + postableAlerts = getValidPostableAlerts(project, fsName, config, result, ResourceRequest.Name.FEATUREVIEW); + } + alertController.sendFeatureMonitorAlert(postableAlerts, project, config.getName()); + + } + + /** + * find alerts based on status with feature group or feature view and contruct PostableAlert + * + * @param configuration + * @param result + * @param entityType + * @return List of PostableAlerts to trigger + */ + private List getValidPostableAlerts(Project project, + String fsName, FeatureMonitoringConfiguration configuration, + FeatureMonitoringResult result, ResourceRequest.Name entityType) { + //TODO: add filter on alert and FM config severity + List postableAlerts = new ArrayList<>(); + ProjectServiceAlert projectAlert; + // get mapping status;if shift detected is FALSE status should be FEATURE_MONITOR_SHIFT_UNDETECTED + FeatureStoreAlertStatus alertStatus = + FeatureStoreAlertStatus.fromBooleanFeatureMonitorResultStatus(result.getShiftDetected()); + ProjectServiceAlertStatus projectAlertStatus = + ProjectServiceAlertStatus.fromBooleanFeatureMonitorResultStatus(result.getShiftDetected()); + // check project alerts + projectAlert = projectServiceAlertsFacade.findByProjectAndStatus(project, projectAlertStatus); + if (projectAlert != null) { + postableAlerts.add(geProjectPostableAlert(project, fsName, projectAlert, configuration, result)); + } + // check local alerts + FeatureStoreAlert featureStoreAlert = retrieveAlert(configuration, entityType, alertStatus); + if (featureStoreAlert !=null) { + postableAlerts.add(getFeatureMonitorAlert(project, fsName, featureStoreAlert, entityType, configuration, + result )); + } + return postableAlerts; + } + + private FeatureStoreAlert retrieveAlert(FeatureMonitoringConfiguration configuration, + ResourceRequest.Name entityType, FeatureStoreAlertStatus alertStatus) { + if (entityType.equals(ResourceRequest.Name.FEATUREGROUPS)) { + return featureGroupAlertFacade.findByFeatureGroupAndStatus(configuration.getFeatureGroup(), alertStatus); + } else { + return featureViewAlertFacade.findByFeatureViewAndStatus(configuration.getFeatureView(), alertStatus); + } + } + + private PostableAlert geProjectPostableAlert(Project project, String fsName, ProjectServiceAlert projectAlert, + FeatureMonitoringConfiguration configuration, + FeatureMonitoringResult result) { + + return alertController.getPostableFeatureMonitorAlert(project, projectAlert, + configuration.getName(), + result.getId(), constructAlertSummary(configuration, result), constructAlertDescription(configuration), + fsName); + } + + private PostableAlert getFeatureMonitorAlert(Project project, String fsName, FeatureStoreAlert alert, + ResourceRequest.Name entityType, + FeatureMonitoringConfiguration configuration, + FeatureMonitoringResult result) { + + return alertController.getPostableFeatureMonitorAlert(project, alert, entityType, + configuration.getName(), + result.getId(), constructAlertSummary(configuration, result), constructAlertDescription(configuration), + fsName); + } + + private String constructAlertSummary(FeatureMonitoringConfiguration configuration, FeatureMonitoringResult result) { + return String.format("Feature name: %s; Shift detected: %s; Difference: %s", + configuration.getFeatureName(), result.getShiftDetected(), result.getDifference()); + } + + private String constructAlertDescription(FeatureMonitoringConfiguration configuration) { + return String.format("FeatureMonitoring Config Name: %s; Monitoring Type: %s", + configuration.getName(), configuration.getFeatureMonitoringType()); + } + + public void validateInput(FeatureMonitoringConfiguration config, FeatureMonitoringResult result) + throws FeaturestoreException { + if (config == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.SEVERE, + "Feature Monitoring Config should not be null."); + } + if (result == null) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.SEVERE, + "Feature Monitoring Result should not be null."); + } + if (Boolean.FALSE.equals(config.getJobSchedule().getEnabled())) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.ALERT_ILLEGAL_ARGUMENT, Level.FINE, + "Feature Monitoring Config is disabled, skipping triggering alert."); + } + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/DescriptiveStatisticsComparisonConfigurationDTO.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/DescriptiveStatisticsComparisonConfigurationDTO.java new file mode 100644 index 0000000000..747064cda6 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/DescriptiveStatisticsComparisonConfigurationDTO.java @@ -0,0 +1,30 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featuremonitoring.config; + +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics.MetricDescriptiveStatistics; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DescriptiveStatisticsComparisonConfigurationDTO { + private Integer id; + private MetricDescriptiveStatistics metric; + private Double threshold; + private Boolean relative; + private Boolean strict; +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationController.java new file mode 100644 index 0000000000..d041deedf6 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationController.java @@ -0,0 +1,233 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.config; + +import io.hops.hopsworks.common.api.ResourceRequest; +import io.hops.hopsworks.common.featurestore.app.FsJobManagerController; +import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; +import io.hops.hopsworks.common.jobs.JobController; +import io.hops.hopsworks.common.jobs.scheduler.JobScheduleV2Controller; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.exceptions.JobException; +import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics.DescriptiveStatisticsComparisonConfig; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.jobs.description.Jobs; +import io.hops.hopsworks.persistence.entity.jobs.scheduler.JobScheduleV2; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.persistence.entity.user.Users; +import io.hops.hopsworks.restutils.RESTCodes.FeaturestoreErrorCode; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureMonitoringConfigurationController { + private static final Logger LOGGER = Logger.getLogger(FeatureMonitoringConfigurationController.class.getName()); + + @EJB + FeatureMonitoringConfigurationFacade featureMonitoringConfigurationFacade; + @EJB + FeaturegroupController featureGroupController; + @EJB + FeatureViewController featureViewController; + @EJB + FsJobManagerController fsJobManagerController; + @EJB + JobController jobController; + @EJB + JobScheduleV2Controller scheduleV2Controller; + + //////////////////////////////////////// + //// CRUD Operations + //////////////////////////////////////// + public List getFeatureMonitoringConfigurationByEntityAndFeatureName( + Featurestore featureStore, ResourceRequest.Name entityType, Integer entityId, String featureName) + throws FeaturestoreException { + Featuregroup featureGroup = null; + FeatureView featureView = null; + if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + featureGroup = featureGroupController.getFeaturegroupById(featureStore, entityId); + } else if (entityType == ResourceRequest.Name.FEATUREVIEW) { + featureView = featureViewController.getByIdAndFeatureStore(entityId, featureStore); + } + + List configs = new ArrayList<>(); + if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + configs = featureMonitoringConfigurationFacade.findByFeatureGroupAndFeatureName(featureGroup, featureName); + } else if (entityType == ResourceRequest.Name.FEATUREVIEW) { + configs = featureMonitoringConfigurationFacade.findByFeatureViewAndFeatureName(featureView, featureName); + } + + return configs; + } + + public List getFeatureMonitoringConfigurationByEntity(Featurestore featureStore, + ResourceRequest.Name entityType, Integer entityId) throws FeaturestoreException { + Featuregroup featureGroup = null; + FeatureView featureView = null; + if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + featureGroup = featureGroupController.getFeaturegroupById(featureStore, entityId); + } else if (entityType == ResourceRequest.Name.FEATUREVIEW) { + featureView = featureViewController.getByIdAndFeatureStore(entityId, featureStore); + } + + List configs = new ArrayList<>(); + if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + configs = featureMonitoringConfigurationFacade.findByFeatureGroup(featureGroup); + } else if (entityType == ResourceRequest.Name.FEATUREVIEW) { + configs = featureMonitoringConfigurationFacade.findByFeatureView(featureView); + } + + return configs; + } + + public FeatureMonitoringConfiguration getFeatureMonitoringConfigurationByConfigId(Integer configId) + throws FeaturestoreException { + + Optional optConfig = featureMonitoringConfigurationFacade.findById(configId); + + if (!optConfig.isPresent()) { + throw new FeaturestoreException(FeaturestoreErrorCode.FEATURE_MONITORING_ENTITY_NOT_FOUND, Level.WARNING, + String.format("Feature Monitoring Config with id %d not found.", configId)); + } + + return optConfig.get(); + } + + public FeatureMonitoringConfiguration getFeatureMonitoringConfigurationByEntityAndName(Featurestore featureStore, + ResourceRequest.Name entityType, Integer entityId, String name) throws FeaturestoreException { + Optional optConfig = Optional.empty(); + if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + Featuregroup featureGroup = featureGroupController.getFeaturegroupById(featureStore, entityId); + optConfig = featureMonitoringConfigurationFacade.findByFeatureGroupAndName(featureGroup, name); + } else if (entityType == ResourceRequest.Name.FEATUREVIEW) { + FeatureView featureView = featureViewController.getByIdAndFeatureStore(entityId, featureStore); + optConfig = featureMonitoringConfigurationFacade.findByFeatureViewAndName(featureView, name); + } + + if (!optConfig.isPresent()) { + throw new FeaturestoreException(FeaturestoreErrorCode.FEATURE_MONITORING_ENTITY_NOT_FOUND, Level.WARNING, + String.format("Feature Monitoring Config with name %s not found.", name)); + } + + return optConfig.get(); + } + + public FeatureMonitoringConfiguration createFeatureMonitoringConfiguration(Featurestore featureStore, Users user, + ResourceRequest.Name entityType, FeatureMonitoringConfiguration config) throws FeaturestoreException, JobException { + Featuregroup featureGroup = config.getFeatureGroup(); + FeatureView featureView = config.getFeatureView(); + String entityName = null; + Integer entityVersion = null; + if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + entityName = featureGroup.getName(); + entityVersion = featureGroup.getVersion(); + } else if (entityType == ResourceRequest.Name.FEATUREVIEW) { + entityName = featureView.getName(); + entityVersion = featureView.getVersion(); + } + + Jobs monitoringJob = + fsJobManagerController.setupFeatureMonitoringJob(user, featureStore.getProject(), entityType, entityName, + entityVersion, config.getName()); + config.setJob(monitoringJob); + JobScheduleV2 schedule = config.getJobSchedule(); + schedule.setJob(monitoringJob); + config.setJobSchedule(scheduleV2Controller.createSchedule(schedule)); + + return featureMonitoringConfigurationFacade.update(config); + } + + public void deleteFeatureMonitoringConfiguration(Integer configId) throws FeaturestoreException { + FeatureMonitoringConfiguration config = getFeatureMonitoringConfigurationByConfigId(configId); + featureMonitoringConfigurationFacade.remove(config); + } + + public FeatureMonitoringConfiguration updateFeatureMonitoringConfiguration(Integer configId, + FeatureMonitoringConfiguration config) throws FeaturestoreException, JobException { + Optional optConfig = featureMonitoringConfigurationFacade.findById(configId); + + if (!optConfig.isPresent()) { + throw new FeaturestoreException(FeaturestoreErrorCode.FEATURE_MONITORING_ENTITY_NOT_FOUND, Level.WARNING, + String.format("Feature Monitoring Config with id %d not found.", configId)); + } + + FeatureMonitoringConfiguration newConfig = setAllowedFeatureMonitoringConfigurationUpdates(optConfig.get(), config); + featureMonitoringConfigurationFacade.update(newConfig); + + return newConfig; + } + + public FeatureMonitoringConfiguration setAllowedFeatureMonitoringConfigurationUpdates( + FeatureMonitoringConfiguration config, FeatureMonitoringConfiguration newConfig) throws JobException { + // Allowed updates to the feature monitoring config metadata + // name is not editable as it would also require to change the job name + config.setDescription(newConfig.getDescription()); + + // Allowed updates to the scheduler config + JobScheduleV2 schedule = newConfig.getJobSchedule(); + schedule.setId(config.getJobSchedule().getId()); + schedule.setEnabled(newConfig.getJobSchedule().getEnabled()); + config.setJobSchedule(scheduleV2Controller.updateSchedule(schedule)); + + // No allowed updates to the detection window + + // Allowed updates to the comparison config + DescriptiveStatisticsComparisonConfig statsConfig = config.getDsComparisonConfig(); + statsConfig.setThreshold(newConfig.getDsComparisonConfig().getThreshold()); + statsConfig.setStrict(newConfig.getDsComparisonConfig().getStrict()); + // changing the metric is not allowed to keep historic data consistent + // changing the relative flag is not allowed to keep historic data consistent + config.setDsComparisonConfig(statsConfig); + + return config; + } + + //////////////////////////////////////// + //// Feature Monitoring Job methods + //////////////////////////////////////// + public Jobs setupFeatureMonitoringJob(Project project, Users user, Featurestore featureStore, + ResourceRequest.Name entityType, Integer entityId, String configName) throws FeaturestoreException, JobException { + + String entityName = ""; + Integer entityVersion = -1; + if (entityType == ResourceRequest.Name.FEATUREGROUPS) { + Featuregroup featureGroup = featureGroupController.getFeaturegroupById(featureStore, entityId); + entityName = featureGroup.getName(); + entityVersion = featureGroup.getVersion(); + } else if (entityType == ResourceRequest.Name.FEATUREVIEW) { + FeatureView featureView = featureViewController.getByIdAndFeatureStore(entityId, featureStore); + entityName = featureView.getName(); + entityVersion = featureView.getVersion(); + } + + return fsJobManagerController.setupFeatureMonitoringJob(user, project, entityType, entityName, entityVersion, + configName); + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationDTO.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationDTO.java new file mode 100644 index 0000000000..3a1a2bddda --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationDTO.java @@ -0,0 +1,50 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.config; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.hops.hopsworks.common.api.RestDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration.MonitoringWindowConfigurationDTO; +import io.hops.hopsworks.common.jobs.scheduler.JobScheduleV2DTO; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringType; +import lombok.Getter; +import lombok.Setter; + +@JsonTypeName("featureMonitoringConfigurationDTO") +@Getter +@Setter +public class FeatureMonitoringConfigurationDTO extends RestDTO { + + private Integer featureStoreId; + private String name; + private String description; + private String featureName; + private Integer id; + private FeatureMonitoringType featureMonitoringType; + + private Integer featureGroupId; + private String featureViewName; + private Integer featureViewVersion; + + private DescriptiveStatisticsComparisonConfigurationDTO statisticsComparisonConfig; + private MonitoringWindowConfigurationDTO detectionWindowConfig; + private MonitoringWindowConfigurationDTO referenceWindowConfig; + + // Integration with other Hopsworks services + private JobScheduleV2DTO jobSchedule; + private String jobName; +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationFacade.java new file mode 100644 index 0000000000..55b0a2e8b2 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationFacade.java @@ -0,0 +1,135 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.config; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; +import java.util.List; +import java.util.Optional; + +@Stateless +public class FeatureMonitoringConfigurationFacade extends AbstractFacade { + + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + @Override + protected EntityManager getEntityManager() { + return em; + } + + public FeatureMonitoringConfigurationFacade() { + super(FeatureMonitoringConfiguration.class); + } + + public List findByFeatureGroupAndFeatureName( + Featuregroup featureGroup, String featureName) { + return em.createNamedQuery( + "FeatureMonitoringConfiguration.findByFeatureGroupAndFeatureName", FeatureMonitoringConfiguration.class) + .setParameter("featureGroup", featureGroup) + .setParameter("featureName", featureName) + .getResultList(); + } + + public List findByFeatureViewAndFeatureName( + FeatureView featureView, String featureName) { + return em.createNamedQuery( + "FeatureMonitoringConfiguration.findByFeatureViewAndFeatureName", FeatureMonitoringConfiguration.class) + .setParameter("featureView", featureView) + .setParameter("featureName", featureName) + .getResultList(); + } + + public List findByFeatureGroup(Featuregroup featureGroup) { + return em.createNamedQuery( + "FeatureMonitoringConfiguration.findByFeatureGroup", + FeatureMonitoringConfiguration.class) + .setParameter("featureGroup", featureGroup) + .getResultList(); + } + + public List findByFeatureView(FeatureView featureView) { + return em.createNamedQuery( + "FeatureMonitoringConfiguration.findByFeatureView", + FeatureMonitoringConfiguration.class) + .setParameter("featureView", featureView) + .getResultList(); + } + + public Optional findById(Integer configId) { + try { + return Optional.of( + em.createNamedQuery("FeatureMonitoringConfiguration.findById", FeatureMonitoringConfiguration.class) + .setParameter("configId", configId).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findByJobId(Integer jobId) { + try { + return Optional.of( + em.createNamedQuery("FeatureMonitoringConfiguration.findByJobId", FeatureMonitoringConfiguration.class) + .setParameter("jobId", jobId).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findByName(String name) { + try { + return Optional.of( + em.createNamedQuery("FeatureMonitoringConfiguration.findByName", FeatureMonitoringConfiguration.class) + .setParameter("name", name).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findByFeatureGroupAndName(Featuregroup featureGroup, String name) { + try { + return Optional.of( + em.createNamedQuery("FeatureMonitoringConfiguration.findByFeatureGroupAndName", + FeatureMonitoringConfiguration.class) + .setParameter("name", name) + .setParameter("featureGroup", featureGroup) + .getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findByFeatureViewAndName(FeatureView featureView, String name) { + try { + return Optional.of( + em.createNamedQuery("FeatureMonitoringConfiguration.findByFeatureViewAndName", + FeatureMonitoringConfiguration.class) + .setParameter("name", name) + .setParameter("featureView", featureView) + .getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationInputValidation.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationInputValidation.java new file mode 100644 index 0000000000..e021e08489 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/config/FeatureMonitoringConfigurationInputValidation.java @@ -0,0 +1,218 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.config; + +import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration.MonitoringWindowConfigurationInputValidation; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringType; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDatasetFeature; +import io.hops.hopsworks.persistence.entity.user.Users; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.util.stream.Collectors; + +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.ALLOWED_METRICS_IN_STATISTICS_COMPARISON_CONFIG; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_DESCRIPTION; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_FEATURE_NAME; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureMonitoringConfigurationInputValidation { + @EJB + private FeatureMonitoringConfigurationFacade featureMonitoringConfigurationFacade; + @EJB + private FeaturegroupController featureGroupController; + @EJB + private FeatureViewController featureViewController; + @EJB + private MonitoringWindowConfigurationInputValidation monitoringWindowConfigurationInputValidation; + + public static final String FIELD_EXCEEDS_MAXIMAL_LENGTH_MESSAGE = + "%s exceeds maximum length of %d characters in feature monitoring congiguration %s"; + public static final String FIELD_MUST_BE_NOT_NULL = + "Field %s cannot be null if window config type is %s in feature monitoring congiguration %s"; + public static final String FIELD_MUST_BE_NULL = + "Field %s must be null if window config type is %s in feature monitoring congiguration %s"; + + public boolean validateConfigOnCreate(Users user, FeatureMonitoringConfigurationDTO dto, + Featuregroup featureGroup, FeatureView featureView) throws FeaturestoreException { + + // On creation it should be checked that the name is unique for the entity + validateUniqueConfigNameForEntity(dto.getName(), featureGroup, featureView,false); + validateConfigBasedOnFeatureMonitoringType(dto); + if (dto.getFeatureName() != null) { + validateFeatureNameExists(user, featureGroup, featureView, dto.getFeatureName()); + } + validateConfigDtoFieldMaximalLength(dto); + // validate window config + monitoringWindowConfigurationInputValidation.validateMonitoringWindowConfigDto( + dto.getName(), dto.getDetectionWindowConfig()); + if (dto.getReferenceWindowConfig() != null) { + monitoringWindowConfigurationInputValidation.validateMonitoringWindowConfigDto( + dto.getName(), dto.getReferenceWindowConfig()); + } + if (dto.getFeatureMonitoringType() == FeatureMonitoringType.STATISTICS_COMPARISON) { + validateStatisticsComparisonConfig(dto.getName(), dto.getStatisticsComparisonConfig()); + } + + return true; + } + + public boolean validateConfigOnUpdate(FeatureMonitoringConfigurationDTO dto, Featuregroup featureGroup, + FeatureView featureView) { + validateUniqueConfigNameForEntity(dto.getName(), featureGroup, featureView, true); + validateConfigDtoFieldMaximalLength(dto); + monitoringWindowConfigurationInputValidation.validateMonitoringWindowConfigDto( + dto.getName(), dto.getDetectionWindowConfig()); + if (dto.getReferenceWindowConfig() != null) { + monitoringWindowConfigurationInputValidation.validateMonitoringWindowConfigDto( + dto.getName(), dto.getReferenceWindowConfig()); + } + if (dto.getFeatureMonitoringType() == FeatureMonitoringType.STATISTICS_COMPARISON) { + validateStatisticsComparisonConfig(dto.getName(), dto.getStatisticsComparisonConfig()); + } + + return true; + } + + ////////////////////////// + ////// Config Metadata + ////////////////////////// + + public void validateUniqueConfigNameForEntity(String name, Featuregroup featureGroup, FeatureView featureView, + boolean onUpdate) { + // On creation it should throw an error if exists, on update it should throw an error if it does not exist + String userMessage = ""; + boolean toThrowOrNotToThrow = false; + if (onUpdate) { + userMessage += " does not exist for "; + } else { + userMessage += " already exists for "; + } + if (featureGroup != null) { + toThrowOrNotToThrow = + featureMonitoringConfigurationFacade.findByFeatureGroupAndName(featureGroup, name).isPresent(); + userMessage += "feature group " + featureGroup.getName(); + } else if (featureView != null) { + toThrowOrNotToThrow = + featureMonitoringConfigurationFacade.findByFeatureViewAndName(featureView, name).isPresent(); + userMessage += "feature view " + featureView.getName(); + } + if (onUpdate) { + toThrowOrNotToThrow = !toThrowOrNotToThrow; + } + + if (toThrowOrNotToThrow) { + throw new IllegalArgumentException("A feature monitoring configuration with name " + name + userMessage); + } + } + + public void validateConfigDtoFieldMaximalLength(FeatureMonitoringConfigurationDTO dto) { + if (dto.getName().length() > MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME) { + throw new IllegalArgumentException( + String.format(FIELD_EXCEEDS_MAXIMAL_LENGTH_MESSAGE, "Name", MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME, + dto.getName())); + } + + if (dto.getDescription() != null && + dto.getDescription().length() > MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_DESCRIPTION) { + throw new IllegalArgumentException(String.format(FIELD_EXCEEDS_MAXIMAL_LENGTH_MESSAGE, "Description", + MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_DESCRIPTION, dto.getName())); + } + + if (dto.getFeatureName() != null && + dto.getFeatureName().length() > MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_FEATURE_NAME) { + throw new IllegalArgumentException(String.format(FIELD_EXCEEDS_MAXIMAL_LENGTH_MESSAGE, "Feature name", + MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_FEATURE_NAME, dto.getName())); + } + } + + public void validateFeatureNameExists(Users user, Featuregroup featureGroup, FeatureView featureView, + String featureName) throws FeaturestoreException { + if (featureGroup != null && + !featureGroupController.getFeatureNames(featureGroup, featureGroup.getFeaturestore().getProject(), user) + .contains(featureName)) { + throw new IllegalArgumentException( + "The feature group " + featureGroup.getName() + " does not contain a feature with name " + featureName); + } else if (featureView != null && + !featureView.getFeatures().stream().map(TrainingDatasetFeature::getName).collect(Collectors.toList()) + .contains(featureName)) { + throw new IllegalArgumentException( + "The feature view " + featureView.getName() + " does not contain a feature with name " + featureName); + } + } + + public void validateConfigBasedOnFeatureMonitoringType(FeatureMonitoringConfigurationDTO dto) { + if (dto.getFeatureMonitoringType() == FeatureMonitoringType.STATISTICS_COMPUTATION) { + if (dto.getStatisticsComparisonConfig() != null) { + throw new IllegalArgumentException( + "Statistics comparison configuration of feature monitoring configuration " + dto.getName() + + " must be null if feature monitoring type is " + FeatureMonitoringType.STATISTICS_COMPUTATION + + ". Use feature monitoring API if you do not wish to compare statistics to a reference."); + } + + if (dto.getReferenceWindowConfig() != null) { + throw new IllegalArgumentException( + "Reference window configuration of feature monitoring configuration " + dto.getName() + + " must be null if feature monitoring type is " + FeatureMonitoringType.STATISTICS_COMPUTATION + + ". Use feature monitoring API if you do not wish to compare statistics to a reference."); + } + } else if (dto.getFeatureMonitoringType() == FeatureMonitoringType.STATISTICS_COMPARISON) { + if (dto.getReferenceWindowConfig() == null) { + throw new IllegalArgumentException( + "Reference window configuration of feature monitoring configuration " + dto.getName() + + " cannot be null if feature monitoring type is " + FeatureMonitoringType.STATISTICS_COMPARISON + + ". Use scheduled statistics API if you wish to compare statistics to a reference."); + } + + if (dto.getFeatureName() == null) { + throw new IllegalArgumentException( + "Feature name of feature monitoring configuration " + dto.getName() + + " cannot be null if feature monitoring type is " + FeatureMonitoringType.STATISTICS_COMPARISON + + ". Use monitoring statistics API to compute the statistics on a schedule of the whole entity or provide a" + + " feature name to use single feature monitoring."); + } + + if (dto.getStatisticsComparisonConfig() == null) { + throw new IllegalArgumentException( + "Statistics comparison configuration of feature monitoring configuration " + dto.getName() + + " cannot be null if feature monitoring type is " + FeatureMonitoringType.STATISTICS_COMPARISON + + ". Use scheduled statistics API if you wish to compare statistics to a reference."); + } + } + } + + //////////////////////////////////// + /////// Statistics Config + //////////////////////////////////// + public void validateStatisticsComparisonConfig(String configName, + DescriptiveStatisticsComparisonConfigurationDTO dto) { + if (!ALLOWED_METRICS_IN_STATISTICS_COMPARISON_CONFIG.contains(dto.getMetric().toString())) { + throw new IllegalArgumentException( + "The metric " + dto.getMetric() + " is not allowed in statistics comparison configuration " + configName + + " allowed metrics are: " + ALLOWED_METRICS_IN_STATISTICS_COMPARISON_CONFIG); + } + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationDTO.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationDTO.java new file mode 100644 index 0000000000..e2aa2c7e42 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationDTO.java @@ -0,0 +1,33 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration; + +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.WindowConfigurationType; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MonitoringWindowConfigurationDTO { + // Detection and Reference Window + private Integer id; + private WindowConfigurationType windowConfigType; + private String timeOffset; + private String windowLength; + private Integer trainingDatasetVersion; + private Float rowPercentage; + private Double specificValue; +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationInputValidation.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationInputValidation.java new file mode 100644 index 0000000000..a9152494c7 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/monitoringwindowconfiguration/MonitoringWindowConfigurationInputValidation.java @@ -0,0 +1,168 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration; + +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.WindowConfigurationType; + +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_TIME_OFFSET; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_WINDOW_LENGTH; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.INVALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.VALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class MonitoringWindowConfigurationInputValidation { + + private static final String FIELD_SPECIFIC_VALUE = "specificValue"; + private static final String FIELD_ROW_PERCENTAGE = "rowPercentage"; + private static final String FIELD_WINDOW_LENGTH = "windowLength"; + private static final String FIELD_TIME_OFFSET = "timeOffset"; + private static final String FIELD_TRAINING_DATASET_VERSION = "trainingDatasetVersion"; + private static final String FIELD_EXCEEDS_MAXIMAL_LENGTH_MESSAGE = + "%s exceeds maximum length of %d characters in feature monitoring congiguration %s"; + private static final String FIELD_MUST_BE_NOT_NULL = + "Field %s cannot be null if window config type is %s in feature monitoring congiguration %s"; + private static final String FIELD_MUST_BE_NULL = + "Field %s must be null if window config type is %s in feature monitoring congiguration %s"; + + public boolean validateMonitoringWindowConfigDto(String configName, MonitoringWindowConfigurationDTO dto) { + validateMonitoringWindowConfigDtoNullFieldBasedOnType(configName, dto); + if (dto.getWindowConfigType() == WindowConfigurationType.ROLLING_TIME) { + validateMonitoringWindowConfigDtoFieldMaximalLength(configName, dto); + validateTimeOffsetAndWindowLengthBasedOnRegex(configName, dto); + } + if (dto.getWindowConfigType() == WindowConfigurationType.ROLLING_TIME || + dto.getWindowConfigType() == WindowConfigurationType.ALL_TIME) { + validateRowPercentage(configName, dto.getRowPercentage()); + } + return true; + } + + public void validateMonitoringWindowConfigDtoNullFieldBasedOnType(String configName, + MonitoringWindowConfigurationDTO dto) { + if (dto.getWindowConfigType() == WindowConfigurationType.SPECIFIC_VALUE) { + fieldToBeOrNotToBeNull(configName, dto, FIELD_SPECIFIC_VALUE, false); + fieldToBeOrNotToBeNull(configName, dto, FIELD_ROW_PERCENTAGE, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_WINDOW_LENGTH, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_TIME_OFFSET, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_TRAINING_DATASET_VERSION, true); + } else if (dto.getWindowConfigType() == WindowConfigurationType.TRAINING_DATASET) { + fieldToBeOrNotToBeNull(configName, dto, FIELD_TRAINING_DATASET_VERSION, false); + fieldToBeOrNotToBeNull(configName, dto, FIELD_ROW_PERCENTAGE, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_WINDOW_LENGTH, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_TIME_OFFSET, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_SPECIFIC_VALUE, true); + } else if (dto.getWindowConfigType() == WindowConfigurationType.ALL_TIME) { + fieldToBeOrNotToBeNull(configName, dto, FIELD_ROW_PERCENTAGE, false); + fieldToBeOrNotToBeNull(configName, dto, FIELD_TIME_OFFSET, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_WINDOW_LENGTH, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_SPECIFIC_VALUE, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_TRAINING_DATASET_VERSION, true); + } else if (dto.getWindowConfigType() == WindowConfigurationType.ROLLING_TIME) { + fieldToBeOrNotToBeNull(configName, dto, FIELD_ROW_PERCENTAGE, false); + fieldToBeOrNotToBeNull(configName, dto, FIELD_TIME_OFFSET, false); + // windowLength is optional here + fieldToBeOrNotToBeNull(configName, dto, FIELD_TRAINING_DATASET_VERSION, true); + fieldToBeOrNotToBeNull(configName, dto, FIELD_SPECIFIC_VALUE, true); + } + } + + public void fieldToBeOrNotToBeNull(String configName, MonitoringWindowConfigurationDTO dto, String fieldName, + boolean mustBeNull) { + boolean isNull = false; + switch (fieldName) { + case FIELD_SPECIFIC_VALUE: + if (dto.getSpecificValue() != null) { + isNull = true; + } + break; + case FIELD_ROW_PERCENTAGE: + if (dto.getRowPercentage() != null) { + isNull = true; + } + break; + case FIELD_WINDOW_LENGTH: + if (dto.getWindowLength() != null) { + isNull = true; + } + break; + case FIELD_TIME_OFFSET: + if (dto.getTimeOffset() != null) { + isNull = true; + } + break; + case FIELD_TRAINING_DATASET_VERSION: + if (dto.getTrainingDatasetVersion() != null) { + isNull = true; + } + break; + default: + break; + } + if (isNull && mustBeNull) { + throw new IllegalArgumentException( + String.format(FIELD_MUST_BE_NULL, fieldName, dto.getWindowConfigType().toString(), configName)); + } else if (!isNull && !mustBeNull) { + throw new IllegalArgumentException( + String.format(FIELD_MUST_BE_NOT_NULL, fieldName, dto.getWindowConfigType().toString(), configName)); + } + } + + public void validateMonitoringWindowConfigDtoFieldMaximalLength(String configName, + MonitoringWindowConfigurationDTO dto) { + if (dto.getWindowLength() != null && + dto.getWindowLength().length() > MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_WINDOW_LENGTH) { + throw new IllegalArgumentException(String.format(FIELD_EXCEEDS_MAXIMAL_LENGTH_MESSAGE, "Window length", + MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_WINDOW_LENGTH, configName)); + } + + if (dto.getTimeOffset().length() > MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_TIME_OFFSET) { + throw new IllegalArgumentException(String.format(FIELD_EXCEEDS_MAXIMAL_LENGTH_MESSAGE, "Time offset", + MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_TIME_OFFSET, configName)); + } + } + + public void validateRowPercentage(String configName, Float rowPercentage) { + if (rowPercentage < 0 || rowPercentage > 1) { + throw new IllegalArgumentException( + "Row percentage of monitoring configuration " + configName + " must be a float between 0 and 1, not" + + rowPercentage); + } + } + + public void validateTimeOffsetAndWindowLengthBasedOnRegex(String configName, MonitoringWindowConfigurationDTO dto) { + if (dto.getWindowConfigType() == WindowConfigurationType.ROLLING_TIME) { + if (INVALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX.matcher(dto.getTimeOffset()).matches() || + !VALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX.matcher(dto.getTimeOffset()).matches()) { + throw new IllegalArgumentException( + "Time offset of monitoring configuration " + configName + + " must be in format 1w2d3h for 1 week 2 day 3 hours."); + } + if (dto.getWindowLength() != null && + (INVALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX.matcher(dto.getWindowLength()).matches() || + !VALID_MONITORING_WINDOW_CONFIG_TIME_RANGE_REGEX.matcher(dto.getWindowLength()).matches())) { + throw new IllegalArgumentException( + "Window length of monitoring configuration " + configName + + " must be in format 1w2d3h for 1 week 2 day 3 hours."); + } + } + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultController.java new file mode 100644 index 0000000000..b19d3be933 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultController.java @@ -0,0 +1,97 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.alert.FeatureMonitoringAlertController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult; +import io.hops.hopsworks.restutils.RESTCodes.FeaturestoreErrorCode; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureMonitoringResultController { + private static final Logger LOGGER = Logger.getLogger(FeatureMonitoringResultController.class.getName()); + + @EJB + FeatureMonitoringResultFacade featureMonitoringResultFacade; + @EJB + FeatureMonitoringConfigurationController featureMonitoringConfigurationController; + @EJB + FeatureMonitoringAlertController featureMonitoringAlertController; + + //////////////////////////////////////// + //// CRD Operations + //////////////////////////////////////// + public FeatureMonitoringResult createFeatureMonitoringResult(FeatureMonitoringResult result) { + featureMonitoringResultFacade.save(result); + try { + featureMonitoringAlertController.triggerAlertsByStatus(result.getFeatureMonitoringConfig(), result); + } catch (FeaturestoreException e) { + LOGGER.log(Level.SEVERE, + String.format("Error triggering alerts for Feature Monitoring Result with id: %d error message: %s", + result.getId(), e.getUsrMsg()), e); + } + return result; + } + + public FeatureMonitoringResult getFeatureMonitoringResultById(Integer resultId) + throws FeaturestoreException { + + Optional optResult = featureMonitoringResultFacade.findById(resultId); + + if (!optResult.isPresent()) { + throw new FeaturestoreException(FeaturestoreErrorCode.FEATURE_MONITORING_ENTITY_NOT_FOUND, Level.WARNING, + String.format("Feature Monitoring Result with id %d not found.", resultId)); + } + + return optResult.get(); + } + + public void deleteFeatureMonitoringResult(Integer resultId) throws FeaturestoreException { + Optional optResult = featureMonitoringResultFacade.findById(resultId); + + if (optResult.isPresent()) { + featureMonitoringResultFacade.remove(optResult.get()); + } else { + throw new FeaturestoreException(FeaturestoreErrorCode.FEATURE_MONITORING_ENTITY_NOT_FOUND, Level.WARNING, + String.format("Feature Monitoring Result with id %d not found.", resultId)); + } + } + + public AbstractFacade.CollectionInfo getAllFeatureMonitoringResultByConfigId( + Integer offset, Integer limit, Set sorts, + Set filters, Integer configId) + throws FeaturestoreException { + + FeatureMonitoringConfiguration config = + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByConfigId(configId); + + return featureMonitoringResultFacade.findByConfigId(offset, limit, sorts, filters, config); + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultDTO.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultDTO.java new file mode 100644 index 0000000000..4b66ba398a --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultDTO.java @@ -0,0 +1,48 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.result; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.hops.hopsworks.common.api.RestDTO; +import lombok.Getter; +import lombok.Setter; +import io.hops.hopsworks.common.featurestore.statistics.FeatureDescriptiveStatisticsDTO; + +@JsonTypeName("featureMonitoringResultDTO") +@Getter +@Setter +public class FeatureMonitoringResultDTO extends RestDTO { + private Integer id; + private Integer configId; + private String featureName; + private Integer featureStoreId; + private Integer executionId; + private Integer detectionStatisticsId; + private Integer referenceStatisticsId; + + private Boolean shiftDetected; + private Double difference; + private Double specificValue; + private Long monitoringTime; + private Boolean raisedException; + private Boolean emptyDetectionWindow; + private Boolean emptyReferenceWindow; + + // with statistics expansion + private FeatureDescriptiveStatisticsDTO detectionStatistics; + private FeatureDescriptiveStatisticsDTO referenceStatistics; +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultFacade.java new file mode 100644 index 0000000000..e98762357f --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultFacade.java @@ -0,0 +1,156 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.sql.Timestamp; +import java.util.Optional; +import java.util.Set; + +@Stateless +public class FeatureMonitoringResultFacade extends AbstractFacade { + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + @Override + protected EntityManager getEntityManager() { + return em; + } + + public FeatureMonitoringResultFacade() { + super(FeatureMonitoringResult.class); + } + + public Optional findById(Integer resultId) { + try { + return Optional.of(em.createNamedQuery("FeatureMonitoringResult.findByResultId", FeatureMonitoringResult.class) + .setParameter("resultId", resultId).getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public CollectionInfo findByConfigId(Integer offset, Integer limit, + Set sorts, Set filters, FeatureMonitoringConfiguration config) { + + String queryStr = buildQuery("SELECT result from FeatureMonitoringResult result ", filters, sorts, + "result.featureMonitoringConfig=:config"); + String queryCountStr = buildQuery("SELECT COUNT(result.id) from FeatureMonitoringResult result ", filters, sorts, + "result.featureMonitoringConfig=:config"); + + Query query = em.createQuery(queryStr, FeatureMonitoringResult.class) + .setParameter("config", config); + Query queryCount = + em.createQuery(queryCountStr, FeatureMonitoringResult.class) + .setParameter("config", config); + + setFilter(filters, query); + setFilter(filters, queryCount); + setOffsetAndLim(offset, limit, query); + + return new CollectionInfo((Long) queryCount.getSingleResult(), query.getResultList()); + } + + private void setFilter(Set filter, Query q) { + if (filter == null || filter.isEmpty()) { + return; + } + for (FilterBy aFilter : filter) { + q.setParameter(aFilter.getField(), new Timestamp(Long.parseLong(aFilter.getParam()))); + } + } + + public enum Sorts { + MONITORING_TIME("MONITORING_TIME", "result.monitoringTime ", "DESC"); + + private final String value; + private final String sql; + private final String defaultParam; + + Sorts(String value, String sql, String defaultParam) { + this.value = value; + this.sql = sql; + this.defaultParam = defaultParam; + } + + public String getValue() { + return value; + } + + public String getDefaultParam() { + return defaultParam; + } + + public String getSql() { + return sql; + } + + @Override + public String toString() { + return value; + } + } + + public enum Filters { + MONITORING_TIME_GT("MONITORING_TIME_GT", "result.monitoringTime > :monitoringTimeGt ", "monitoringTimeGt", ""), + MONITORING_TIME_LT("MONITORING_TIME_LT", "result.monitoringTime < :monitoringTimeLt ", "monitoringTimeLt", ""), + MONITORING_TIME_GTE("MONITORING_TIME_GTE", "result.monitoringTime >= :monitoringTimeGte ", "monitoringTimeGte", ""), + MONITORING_TIME_LTE("MONITORING_TIME_LTE", "result.monitoringTime <= :monitoringTimeLte ", "monitoringTimeLte", ""), + MONITORING_TIME_EQ("MONITORING_TIME_EQ", "result.monitoringTime = :monitoringTimeEq", "monitoringTimeEq", ""); + + private final String value; + private final String sql; + private final String field; + private final String defaultParam; + + Filters(String value, String sql, String field, String defaultParam) { + this.value = value; + this.sql = sql; + this.field = field; + this.defaultParam = defaultParam; + } + + public String getValue() { + return value; + } + + public String getDefaultParam() { + return defaultParam; + } + + public String getSql() { + return sql; + } + + public String getField() { + return field; + } + + @Override + public String toString() { + return value; + } + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultInputValidation.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultInputValidation.java new file mode 100644 index 0000000000..aeb635208f --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featuremonitoring/result/FeatureMonitoringResultInputValidation.java @@ -0,0 +1,132 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringType; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.WindowConfigurationType; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NEVER) +public class FeatureMonitoringResultInputValidation { + @EJB + private FeatureMonitoringConfigurationController featureMonitoringConfigurationController; + + public boolean validateOnCreate(FeatureMonitoringResultDTO resultDto) throws FeaturestoreException { + if (resultDto.getConfigId() == null) { + throw new IllegalArgumentException("Feature monitoring config ID not provided"); + } + + if (resultDto.getRaisedException().equals(true)) { + return true; + } + // If raising exception is false, then detection statistics id must be provided + validateDetectionStatsField(resultDto); + + FeatureMonitoringConfiguration config = + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByConfigId(resultDto.getConfigId()); + + validateReferenceStatsField(config, resultDto); + validateDifferenceField(config, resultDto); + + return true; + } + + private void validateDetectionStatsField(FeatureMonitoringResultDTO resultDto) { + if (resultDto.getDetectionStatisticsId() == null) { + throw new IllegalArgumentException("Descriptive statistics id not provided for the detection window."); + } + if (resultDto.getDetectionStatistics() != null) { + throw new IllegalArgumentException("Descriptive statistics for the detection window should be registered prior " + + "to the monitoring result."); + } + } + + private void validateReferenceStatsField( + FeatureMonitoringConfiguration config, FeatureMonitoringResultDTO resultDto) { + if (resultDto.getReferenceStatistics() != null) { + throw new IllegalArgumentException("Descriptive statistics for the reference window should be registered prior " + + "to the monitoring result."); + } + boolean hasReferenceField = ( + resultDto.getReferenceStatisticsId() != null || resultDto.getSpecificValue() != null + ); + + // Statistics only cannot have reference + if (config.getFeatureMonitoringType().equals(FeatureMonitoringType.STATISTICS_COMPUTATION) + && hasReferenceField) { + throw new IllegalArgumentException( + "Statistics Monitoring configuration " + config.getName() + + " cannot have results with specific value or reference statistics id field." + ); + } else if (config.getFeatureMonitoringType().equals(FeatureMonitoringType.STATISTICS_COMPUTATION) + && !hasReferenceField) + return; + + // Specific value only cannot have reference statistics and reference statistics cannot have specific value + if (config.getReferenceWindowConfig().getWindowConfigType().equals(WindowConfigurationType.SPECIFIC_VALUE) + && resultDto.getSpecificValue() == null) { + throw new IllegalArgumentException( + "Feature monitoring configuration " + config.getName() + + " result cannot have null specific value field when the reference window is configured to use specific value." + ); + } else if (!config.getReferenceWindowConfig().getWindowConfigType().equals(WindowConfigurationType.SPECIFIC_VALUE) + && resultDto.getSpecificValue() != null) { + throw new IllegalArgumentException( + "Feature monitoring configuration " + config.getName() + " result cannot have non-null specific value field" + + " when the reference window is configured to use descriptive statistics." + ); + } else if (config.getReferenceWindowConfig().getWindowConfigType().equals(WindowConfigurationType.SPECIFIC_VALUE) + && resultDto.getSpecificValue() != null) { + return; + } + + // + if (config.getReferenceWindowConfig().getWindowConfigType().equals(WindowConfigurationType.TRAINING_DATASET) && + resultDto.getReferenceStatisticsId() == null) { + throw new IllegalArgumentException( + "Feature monitoring configuration " + config.getName() + " result cannot have null reference statistics id" + + " field when the reference window is configured to use training dataset." + ); + } else if (config.getReferenceWindowConfig().getWindowConfigType().equals(WindowConfigurationType.TRAINING_DATASET)) + return; + } + + private void validateDifferenceField(FeatureMonitoringConfiguration config, FeatureMonitoringResultDTO resultDTO) { + if (config.getFeatureMonitoringType().equals(FeatureMonitoringType.STATISTICS_COMPUTATION) + && resultDTO.getDifference() != null) { + throw new IllegalArgumentException( + "Statistics Monitoring configuration " + config.getName() + + " cannot have results with non-null difference field."); + } + + // empty window and non-null difference field is not allowed, except for specific value + if ((resultDTO.getEmptyDetectionWindow() || resultDTO.getEmptyReferenceWindow()) + && resultDTO.getDifference() != null && resultDTO.getSpecificValue() == null) { + throw new IllegalArgumentException( + "Feature Monitoring configuration " + config.getName() + + " cannot have results with an empty window and non-null difference field."); + } + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewAlertFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewAlertFacade.java new file mode 100644 index 0000000000..5732760488 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewAlertFacade.java @@ -0,0 +1,174 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featureview; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.user.activity.Activity; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import javax.persistence.TypedQuery; +import java.util.Date; +import java.util.Set; + +@Stateless +public class FeatureViewAlertFacade extends AbstractFacade { + + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + public FeatureViewAlertFacade() { + super(FeatureViewAlert.class); + } + + @Override + protected EntityManager getEntityManager() { + return em; + } + + public FeatureViewAlert findByFeatureViewAndStatus( + FeatureView featureView, FeatureStoreAlertStatus featureStoreAlertStatus) { + TypedQuery query = + em.createNamedQuery("FeatureViewAlert.findByFeatureViewAndStatus", FeatureViewAlert.class); + query.setParameter("featureView", featureView) + .setParameter("status", featureStoreAlertStatus); + try { + return query.getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + + + public CollectionInfo findFeatureViewAlerts(Integer offset, Integer limit, + Set filter, + Set sort, FeatureView featureView) { + String queryStr = buildQuery("SELECT a FROM FeatureViewAlert a ", filter, sort, + "a.featureView = :featureView "); + String queryCountStr = + buildQuery("SELECT COUNT(a.id) FROM FeatureViewAlert a ", filter, sort, + "a.featureView = :featureView "); + Query query = + em.createQuery(queryStr, Activity.class).setParameter("featureView", featureView); + Query queryCount = + em.createQuery(queryCountStr, Activity.class).setParameter("featureView", featureView); + return findAll(offset, limit, filter, query, queryCount); + } + + public FeatureViewAlert findByFeatureViewAndId(FeatureView featureview, Integer id) { + TypedQuery query = + em.createNamedQuery("FeatureViewAlert.findByFeatureViewAndId", FeatureViewAlert.class); + query.setParameter("featureView", featureview) + .setParameter("id", id); + try { + return query.getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + private CollectionInfo findAll(Integer offset, Integer limit, + Set filter, Query query, Query queryCount) { + setFilter(filter, query); + setFilter(filter, queryCount); + setOffsetAndLim(offset, limit, query); + return new CollectionInfo((Long) queryCount.getSingleResult(), query.getResultList()); + } + + private void setFilter(Set filter, Query q) { + if (filter == null || filter.isEmpty()) { + return; + } + for (FilterBy aFilter : filter) { + setFilterQuery(aFilter, q); + } + } + + private void setFilterQuery(AbstractFacade.FilterBy filterBy, Query q) { + switch (FeatureViewAlertFacade.Filters.valueOf(filterBy.getValue())) { + case TYPE: + q.setParameter(filterBy.getField(), getEnumValues(filterBy, AlertType.class)); + break; + case STATUS: + q.setParameter(filterBy.getField(), getEnumValues(filterBy, FeatureStoreAlertStatus.class)); + break; + case SEVERITY: + q.setParameter(filterBy.getField(), getEnumValues(filterBy, AlertSeverity.class)); + break; + case CREATED: + case CREATED_GT: + case CREATED_LT: + Date date = getDate(filterBy.getField(), filterBy.getParam()); + q.setParameter(filterBy.getField(), date); + break; + default: + break; + } + } + + + + public enum Filters { + TYPE("TYPE", "a.alertType IN :alertType ", "alertType", AlertType.PROJECT_ALERT.toString()), + STATUS("STATUS", "a.status IN :status ", "status", FeatureStoreAlertStatus.SUCCESS.toString()), + SEVERITY("SEVERITY", "a.severity IN :severity ", "severity", AlertSeverity.INFO.toString()), + CREATED("CREATED", "a.created = :created ", "created", ""), + CREATED_GT("DATE_CREATED_GT", "a.created > :createdFrom ", "createdFrom", ""), + CREATED_LT("DATE_CREATED_LT", "a.created < :createdTo ", "createdTo", ""); + + private final String value; + private final String sql; + private final String field; + private final String defaultParam; + + Filters(String value, String sql, String field, String defaultParam) { + this.value = value; + this.sql = sql; + this.field = field; + this.defaultParam = defaultParam; + } + + public String getValue() { + return value; + } + + public String getDefaultParam() { + return defaultParam; + } + + public String getSql() { + return sql; + } + + public String getField() { + return field; + } + + @Override + public String toString() { + return value; + } + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewController.java index df9927c9ac..81fddefc77 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewController.java @@ -196,6 +196,15 @@ public List getByNameAndFeatureStore(String name, Featurestore feat return featureViews; } + public FeatureView getByIdAndFeatureStore(Integer id, Featurestore featureStore) + throws FeaturestoreException { + return featureViewFacade.findByIdAndFeatureStore(id, featureStore) + .orElseThrow(() -> new FeaturestoreException( + RESTCodes.FeaturestoreErrorCode.FEATURE_VIEW_NOT_FOUND, + Level.FINE, + String.format("There exists no feature view with the id %d.", id))); + } + public FeatureView getByNameVersionAndFeatureStore(String name, Integer version, Featurestore featurestore) throws FeaturestoreException { List featureViews = featureViewFacade.findByNameVersionAndFeaturestore(name, version, featurestore); @@ -408,6 +417,10 @@ private String getPrefixCheckCollision(Set prefixFeatureNames, String fe return prefix; } } + + public static Featuregroup getLeftFeatureGroup(FeatureView featureView) { + return featureView.getJoins().stream().findFirst().get().getFeatureGroup(); + } public List getByFeatureGroup(Integer featureGroupId) { return featureViewFacade.findByFeatureGroup(featureGroupId); diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewFacade.java index 3e1175ee52..d361c23686 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewFacade.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/featureview/FeatureViewFacade.java @@ -23,6 +23,7 @@ import javax.ejb.Stateless; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.persistence.TypedQuery; @@ -30,6 +31,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; @Stateless @@ -88,6 +90,18 @@ List retainLatestVersion(List featureViews) { return new ArrayList<>(latestVersion.values()); } + public Optional findByIdAndFeatureStore(Integer id, Featurestore featureStore) { + try { + return Optional.of( + em.createNamedQuery("FeatureView.findByIdAndFeaturestore", FeatureView.class) + .setParameter("id", id) + .setParameter("featurestore", featureStore) + .getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + public List findByNameAndFeaturestore(String name, Featurestore featurestore) { QueryParam queryParam = new QueryParam(); return findByNameAndFeaturestore(name, featurestore, queryParam); diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureDescriptiveStatisticsFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureDescriptiveStatisticsFacade.java index fd106688e1..e61903de3e 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureDescriptiveStatisticsFacade.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureDescriptiveStatisticsFacade.java @@ -70,12 +70,16 @@ public List findOrphaned(Pair ra String queryString = "SELECT fds.* FROM hopsworks.feature_descriptive_statistics fds " + "WHERE NOT EXISTS (SELECT id FROM hopsworks.feature_group_descriptive_statistics WHERE " + "feature_descriptive_statistics_id = fds.id) " + + "AND NOT EXISTS (SELECT id FROM hopsworks.feature_view_descriptive_statistics WHERE " + + "feature_descriptive_statistics_id = fds.id) " + "AND NOT EXISTS (SELECT id FROM hopsworks.training_dataset_descriptive_statistics WHERE " + "feature_descriptive_statistics_id = fds.id) " + "AND NOT EXISTS (SELECT id FROM hopsworks.test_dataset_descriptive_statistics WHERE " + "feature_descriptive_statistics_id = fds.id) " + "AND NOT EXISTS (SELECT id FROM hopsworks.val_dataset_descriptive_statistics WHERE " + - "feature_descriptive_statistics_id = fds.id)"; + "feature_descriptive_statistics_id = fds.id)" + + "AND NOT EXISTS (SELECT id FROM hopsworks.feature_monitoring_result WHERE " + + "(detection_stats_id = fds.id) OR (reference_stats_id = fds.id))"; Query q = em.createNativeQuery(queryString, FeatureDescriptiveStatistics.class); if (range != null) { q.setMaxResults(range.getValue1() - range.getValue0()); diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewDescriptiveStatisticsFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewDescriptiveStatisticsFacade.java new file mode 100644 index 0000000000..a9255ab524 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewDescriptiveStatisticsFacade.java @@ -0,0 +1,52 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.statistics; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewDescriptiveStatistics; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +/** + * A facade for the feature_view_descriptive_statistics table in the Hopsworks database, use this interface when + * performing database operations against the table. + */ +@Stateless +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class FeatureViewDescriptiveStatisticsFacade extends AbstractFacade { + + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + public FeatureViewDescriptiveStatisticsFacade() { + super(FeatureViewDescriptiveStatistics.class); + } + + /** + * Gets the entity manager of the facade + * + * @return entity manager + */ + @Override + protected EntityManager getEntityManager() { + return em; + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewStatisticsFacade.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewStatisticsFacade.java new file mode 100644 index 0000000000..d602771a31 --- /dev/null +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/FeatureViewStatisticsFacade.java @@ -0,0 +1,162 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.common.featurestore.statistics; + +import io.hops.hopsworks.common.dao.AbstractFacade; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewStatistics; +import io.hops.hopsworks.restutils.RESTCodes; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.util.Set; +import java.util.logging.Level; +import java.util.stream.Collectors; + +/** + * A facade for the feature_view_statistics table in the Hopsworks database, use this interface when performing database + * operations against the table. + */ +@Stateless +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class FeatureViewStatisticsFacade extends AbstractFacade { + + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + public FeatureViewStatisticsFacade() { + super(FeatureViewStatistics.class); + } + + /** + * Gets the entity manager of the facade + * + * @return entity manager + */ + @Override + protected EntityManager getEntityManager() { + return em; + } + + public CollectionInfo findByFeatureView(Integer offset, Integer limit, + Set sorts, Set filters, FeatureView featureView) { + String queryStr = + buildQuery("SELECT s from FeatureViewStatistics s ", filters, sorts, "s.featureView = :featureView"); + String queryCountStr = + buildQuery("SELECT COUNT(s.id) from FeatureViewStatistics s ", filters, sorts, "s.featureView = :featureView"); + Query query = em.createQuery(queryStr, FeatureViewStatistics.class).setParameter("featureView", featureView); + Query queryCount = + em.createQuery(queryCountStr, FeatureViewStatistics.class).setParameter("featureView", featureView); + StatisticsFilters.setFilter(filters, query); + StatisticsFilters.setFilter(filters, queryCount); + setOffsetAndLim(offset, limit, query); + + return new CollectionInfo<>((Long) queryCount.getSingleResult(), query.getResultList()); + } + + public CollectionInfo findByFeatureViewWithFeatureNames(Integer offset, Integer limit, + Set sorts, Set filters, Set featureNames, FeatureView featureView) + throws FeaturestoreException { + String fNsOperator; + Object fNsParamValue; + if (featureNames.size() > 1) { + fNsOperator = "IN"; + fNsParamValue = featureNames; + } else { + fNsOperator = "="; + fNsParamValue = featureNames.iterator().next(); + } + if (limit != null) { + // if limit is set, we need to run two sql queries + return findByFeatureViewWithFeatureNames(offset, limit, sorts, filters, fNsOperator, fNsParamValue, + featureView); + } else { + // otherwise, we can use a single query + return findByFeatureViewWithFeatureNames(sorts, filters, fNsOperator, fNsParamValue, featureView); + } + } + + private CollectionInfo findByFeatureViewWithFeatureNames(Set sorts, + Set filters, String fNsOperator, Object fNsParamValue, FeatureView featureView) + throws FeaturestoreException { + + String queryStr = buildQuery( + "SELECT DISTINCT s from FeatureViewStatistics s LEFT JOIN FETCH s.featureDescriptiveStatistics " + "fds ", + filters, sorts, "s.featureView = :featureView AND fds.featureName " + fNsOperator + " :featureName"); + String queryCountStr = buildQuery("SELECT COUNT(DISTINCT s.id) from FeatureViewStatistics s LEFT JOIN FETCH s" + + ".featureDescriptiveStatistics fds ", filters, sorts, + "s.featureView = :featureView AND fds.featureName " + fNsOperator + " :featureName"); + Query query = em.createQuery(queryStr, FeatureViewStatistics.class).setParameter("featureView", featureView) + .setParameter("featureName", fNsParamValue); + Query queryCount = + em.createQuery(queryCountStr, FeatureViewStatistics.class).setParameter("featureView", featureView) + .setParameter("featureName", fNsParamValue); + StatisticsFilters.setFilter(filters, query); + StatisticsFilters.setFilter(filters, queryCount); + + long count = (long) queryCount.getSingleResult(); + if (count == 0) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.STATISTICS_NOT_FOUND, Level.FINE, + "Statistics for feature view id '" + featureView.getId() + "' not found. Please, try again with different " + + "filters."); + } + return new CollectionInfo<>(count, query.getResultList()); + } + + private CollectionInfo findByFeatureViewWithFeatureNames(Integer offset, Integer limit, + Set sorts, Set filters, String fNsOperator, Object fNsParamValue, + FeatureView featureView) throws FeaturestoreException { + + // 1. fetch feature view statistics ids + String fgsQueryStr = + buildQuery("SELECT DISTINCT s.id from FeatureViewStatistics s LEFT JOIN s.featureDescriptiveStatistics fds ", + filters, sorts, "s.featureView = :featureView AND fds.featureName " + fNsOperator + " :featureName"); + String fgsQueryCountStr = buildQuery( + "SELECT COUNT(DISTINCT s.id) from FeatureViewStatistics s LEFT JOIN s.featureDescriptiveStatistics" + " fds ", + filters, sorts, "s.featureView = :featureView AND fds.featureName " + fNsOperator + " :featureName"); + Query fgsQuery = + em.createQuery(fgsQueryStr).setParameter("featureView", featureView).setParameter("featureName", fNsParamValue); + Query fgsQueryCount = em.createQuery(fgsQueryCountStr).setParameter("featureView", featureView) + .setParameter("featureName", fNsParamValue); + StatisticsFilters.setFilter(filters, fgsQuery); + setOffsetAndLim(offset, limit, fgsQuery); + StatisticsFilters.setFilter(filters, fgsQueryCount); + + long count = (long) fgsQueryCount.getSingleResult(); + if (count == 0) { + throw new FeaturestoreException(RESTCodes.FeaturestoreErrorCode.STATISTICS_NOT_FOUND, Level.FINE, + "Statistics for feature view id '" + featureView.getId() + "' not found. Please, try again with different " + + "filters."); + } + Set fgsIds = (Set) fgsQuery.getResultList().stream().collect(Collectors.toSet()); + + // 2. fetch feature view statistics with feature descriptive statistics + String queryStr = buildQuery( + "SELECT DISTINCT s from FeatureViewStatistics s LEFT JOIN FETCH s.featureDescriptiveStatistics " + "fds ", + filters, sorts, "s.id IN :fgsIds AND fds.featureName " + fNsOperator + " :featureName"); + Query query = em.createQuery(queryStr, FeatureViewStatistics.class).setParameter("fgsIds", fgsIds) + .setParameter("featureName", fNsParamValue); + StatisticsFilters.setFilter(filters, query); + + return new CollectionInfo<>(count, query.getResultList()); + } +} \ No newline at end of file diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsController.java index e461a97bfc..dc50a775b5 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsController.java @@ -20,6 +20,7 @@ import io.hops.hopsworks.common.featurestore.activity.FeaturestoreActivityFacade; import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; import io.hops.hopsworks.common.featurestore.featuregroup.cached.FeatureGroupCommitController; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; import io.hops.hopsworks.common.hdfs.DistributedFileSystemOps; import io.hops.hopsworks.common.hdfs.DistributedFsService; import io.hops.hopsworks.common.hdfs.HdfsUsersController; @@ -32,9 +33,12 @@ import io.hops.hopsworks.persistence.entity.dataset.Dataset; import io.hops.hopsworks.persistence.entity.dataset.DatasetAccessPermission; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureDescriptiveStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupDescriptiveStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupStatistics; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewDescriptiveStatistics; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.TrainingDatasetStatistics; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDataset; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.split.SplitName; @@ -76,8 +80,12 @@ public class StatisticsController { @EJB private FeatureGroupStatisticsFacade featureGroupStatisticsFacade; @EJB + private FeatureViewStatisticsFacade featureViewStatisticsFacade; + @EJB private FeatureGroupDescriptiveStatisticsFacade featureGroupDescriptiveStatisticsFacade; @EJB + private FeatureViewDescriptiveStatisticsFacade featureViewDescriptiveStatisticsFacade; + @EJB private TrainingDatasetStatisticsFacade trainingDatasetStatisticsFacade; @EJB private FeatureGroupCommitController featureGroupCommitController; @@ -124,11 +132,19 @@ public FeatureGroupStatistics registerFeatureGroupStatistics(Project project, Us new FeatureGroupStatistics(computationTimestamp, rowPercentage, descriptiveStatistics, featuregroup)); } else { // otherwise, time-travel enabled - Pair startEndCommitTimes = - featureGroupCommitController.getStartEndCommitTimesByWindowTime(featuregroup, windowStartCommitTime, - windowEndCommitTime); - windowStartCommitTime = startEndCommitTimes.getValue0(); - windowEndCommitTime = startEndCommitTimes.getValue1(); + try { + Pair startEndCommitTimes = + featureGroupCommitController.getStartEndCommitTimesByWindowTime(featuregroup, windowStartCommitTime, + windowEndCommitTime); + + windowStartCommitTime = startEndCommitTimes.getValue0(); + windowEndCommitTime = startEndCommitTimes.getValue1(); + } catch (FeaturestoreException e) { + if ((!e.getErrorCode().equals(RESTCodes.FeaturestoreErrorCode.FEATURE_GROUP_COMMIT_NOT_FOUND)) + || (descriptiveStatistics.iterator().next().getCount() > 0)) { + throw e; + } + } registerExtendedStatistics(project, user, featuregroup.getName(), featuregroup.getVersion(), "FeatureGroups", windowStartCommitTime, windowEndCommitTime, false, null, descriptiveStatistics); @@ -137,7 +153,8 @@ public FeatureGroupStatistics registerFeatureGroupStatistics(Project project, Us Set filters = buildStatisticsQueryFilters(windowStartCommitTime, windowEndCommitTime, rowPercentage, null); AbstractFacade.CollectionInfo fgs = - getStatisticsByFeatureGroup(0, 1, null, filters, featuregroup); + featureGroupStatisticsFacade.findByFeaturegroup(0, 1, null, filters, featuregroup); + if (fgs.getCount() > 0) { // if feature group statistics exist, append descriptive statistics statistics = registerFeatureGroupDescriptiveStatistics(filters, descriptiveStatistics, fgs.getItems().get(0), @@ -159,6 +176,90 @@ public void deleteFeatureGroupStatistics(Project project, Users user, Featuregro deleteExtendedStatistics(project, user, featuregroup.getName(), featuregroup.getVersion(), "FeatureGroups"); } + // Feature View Statistics (for feature monitoring) + // These are statistics without training dataset version and computed before transformation + // e.g., fv.get_batch or fv.query.as_of() + + public AbstractFacade.CollectionInfo getStatisticsByFeatureView(Integer offset, + Integer limit, Set sorts, Set filters, + FeatureView featureView) throws FeaturestoreException { + // return one or more Feature Group Statistics with all the Feature Descriptive Statistics. + overwriteFiltersIfNeeded(featureView, (Set) filters); + return featureViewStatisticsFacade.findByFeatureView(offset, limit, sorts, filters, featureView); + } + + public AbstractFacade.CollectionInfo getStatisticsByFeatureViewAndFeatureNames( + Integer offset, Integer limit, Set sorts, + Set filters, Set featureNames, FeatureView featureView) + throws FeaturestoreException { + // return one or more Feature Group Statistics with the specified Feature Descriptive Statistics. + overwriteFiltersIfNeeded(featureView, (Set) filters); + return featureViewStatisticsFacade.findByFeatureViewWithFeatureNames(offset, limit, sorts, filters, + featureNames, featureView); + } + + public FeatureViewStatistics registerFeatureViewStatistics(Project project, Users user, + Long computationTime, Long windowStartCommitTime, Long windowEndCommitTime, Float rowPercentage, + Collection descriptiveStatistics, FeatureView featureView) + throws FeaturestoreException, IOException, DatasetException, HopsSecurityException { + // Register statistics computed on a specific commit, commit window or the whole feature group (no time-travel + // enabled). In case of a specific commit, the commit window provided has the same start and end commit. + Timestamp computationTimestamp = new Timestamp(computationTime); + FeatureViewStatistics statistics; + // check if the left feature group is time-travel enabled + Featuregroup featuregroup = FeatureViewController.getLeftFeatureGroup(featureView); + if (!FeaturegroupController.isTimeTravelEnabled(featuregroup)) { // time-travel not enabled + // register extended statistics (hdfs files) + registerExtendedStatistics(project, user, featureView.getName(), featureView.getVersion(), "FeatureViews", + null, computationTime, false, null, descriptiveStatistics); + statistics = featureViewStatisticsFacade.update( + new FeatureViewStatistics(computationTimestamp, rowPercentage, descriptiveStatistics, featureView)); + } else { + // otherwise, time-travel enabled + try { + Pair startEndCommitTimes = + featureGroupCommitController.getStartEndCommitTimesByWindowTime(featuregroup, windowStartCommitTime, + windowEndCommitTime); + + windowStartCommitTime = startEndCommitTimes.getValue0(); + windowEndCommitTime = startEndCommitTimes.getValue1(); + } catch (FeaturestoreException e) { + if ((!e.getErrorCode().equals(RESTCodes.FeaturestoreErrorCode.FEATURE_GROUP_COMMIT_NOT_FOUND)) + || (descriptiveStatistics.iterator().next().getCount() > 0)) { + throw e; + } + } + + registerExtendedStatistics(project, user, featureView.getName(), featureView.getVersion(), "FeatureViews", + windowStartCommitTime, windowEndCommitTime, false, null, descriptiveStatistics); + + // check if feature view statistics already exist by querying feature view statistics without the feature names. + Set filters = + buildStatisticsQueryFilters(windowStartCommitTime, windowEndCommitTime, rowPercentage, null); + AbstractFacade.CollectionInfo fvs = + featureViewStatisticsFacade.findByFeatureView(0, 1, null, filters, featureView); + + if (fvs.getCount() > 0) { + // if feature view statistics exist, append descriptive statistics + statistics = registerFeatureViewDescriptiveStatistics(filters, descriptiveStatistics, fvs.getItems().get(0), + featureView); + } else { + // otherwise, create new feature view statistics + statistics = featureViewStatisticsFacade.update( + new FeatureViewStatistics(computationTimestamp, windowStartCommitTime, windowEndCommitTime, rowPercentage, + descriptiveStatistics, featureView)); + } + } + // Log statistics activity + fsActivityFacade.logStatisticsActivity(user, featureView, new Date(computationTimestamp.getTime()), statistics); + return statistics; + } + + public void deleteFeatureViewStatistics(Project project, Users user, FeatureView featureView) + throws FeaturestoreException { + deleteExtendedStatistics(project, user, featureView.getName(), featureView.getVersion(), "FeatureViews"); + } + // Training Dataset Statistics public AbstractFacade.CollectionInfo getStatisticsByTrainingDataset( @@ -287,6 +388,44 @@ private FeatureGroupStatistics registerFeatureGroupDescriptiveStatistics(Set filters, + Collection descriptiveStatistics, FeatureViewStatistics fvStatistics, + FeatureView featureView) throws FeaturestoreException { + // register descriptive statistics in an existing fg statistics. Descriptive statistics for a specific feature + // might or might not be already computed and stored in the DB. If the feature descriptive statistics already + // exists, it is updated. + Set featureNames = + descriptiveStatistics.stream().map(FeatureDescriptiveStatistics::getFeatureName).collect(Collectors.toSet()); + + // TODO: Optimize this query by using the Feature View Statistics ID obtained in the previous step, instead of + // the filters and limit + AbstractFacade.CollectionInfo fvsWithFeatures = + getStatisticsByFeatureViewAndFeatureNames(0, 1, null, filters, featureNames, featureView); + + HashMap existingFds = null; + if (fvsWithFeatures.getCount() > 0) { + // filter existing feature descriptive statistics + existingFds = fvsWithFeatures.getItems().get(0).getFeatureDescriptiveStatistics().stream() + .filter(ds -> featureNames.contains(ds.getFeatureName())) + .collect(Collectors.toMap(FeatureDescriptiveStatistics::getFeatureName, + Function.identity(), (prev, next) -> next, HashMap::new)); + } + + // append / update feature descriptive statistics using the intermediate table FeatureGroupDescriptiveStatistics + for (FeatureDescriptiveStatistics fds : descriptiveStatistics) { + if (existingFds != null && existingFds.containsKey(fds.getFeatureName())) { + // if feature descriptive statistics exists, set the ID so the statistics get updated + fds.setId(existingFds.get(fds.getFeatureName()).getId()); + } + // persist feature descriptive statistics + fds = featureDescriptiveStatisticsFacade.update(fds); + // persist feature group descriptive statistics to intermediate table + FeatureViewDescriptiveStatistics fgds = new FeatureViewDescriptiveStatistics(fvStatistics, fds); + featureViewDescriptiveStatisticsFacade.update(fgds); + } + return fvStatistics; + } + public Set buildStatisticsQueryFilters(Long windowStartCommitTime, Long windowEndCommitTime, Float rowPercentage, Boolean beforeTransformation) { Set filters = new HashSet<>(); @@ -457,6 +596,13 @@ private void overwriteFiltersIfNeeded(Featuregroup featuregroup, Set filters) + throws FeaturestoreException { + // check if the left feature group is time-travel enabled + Featuregroup featuregroup = FeatureViewController.getLeftFeatureGroup(featureView); + overwriteFiltersIfNeeded(featuregroup, filters); + } + public void overwriteTimeWindowFilters(Set filters, Long windowStartTime, Long windowEndTime, StatisticsFilters.Filters windowStartFilter, StatisticsFilters.Filters windowEndFilter) { diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsInputValidation.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsInputValidation.java index dff23c81b4..32242c7612 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsInputValidation.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/featurestore/statistics/StatisticsInputValidation.java @@ -18,8 +18,10 @@ import io.hops.hopsworks.common.dao.AbstractFacade; import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.common.featurestore.featureview.FeatureViewController; import io.hops.hopsworks.exceptions.FeaturestoreException; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDataset; import io.hops.hopsworks.restutils.RESTCodes; import org.jboss.weld.exceptions.IllegalArgumentException; @@ -117,6 +119,14 @@ public void validateRegisterForFeatureGroup(Featuregroup featuregroup, Statistic statisticsDTO.getWindowStartCommitTime(), statisticsDTO.getWindowEndCommitTime()); } + public void validateRegisterForFeatureView(FeatureView featureView, StatisticsDTO statisticsDTO) + throws FeaturestoreException { + this.validateRegisterStatistics(statisticsDTO); + // validate inputs based on left feature group in the feature view query + Featuregroup featuregroup = FeatureViewController.getLeftFeatureGroup(featureView); + this.validateRegisterForFeatureGroup(featuregroup, statisticsDTO); + } + public void validateGetForFeatureGroup(Featuregroup featuregroup, StatisticsFilters fgsFilters) throws FeaturestoreException { if (!FeaturegroupController.isTimeTravelEnabled(featuregroup)) { // time-travel not enabled @@ -136,6 +146,13 @@ public void validateGetForFeatureGroup(Featuregroup featuregroup, StatisticsFilt fgsFilters.getWindowEndCommitTime()); } + public void validateGetForFeatureView(FeatureView featureView, StatisticsFilters fvsFilters) + throws FeaturestoreException { + // validate inputs based on left feature group in the feature view query + Featuregroup featuregroup = FeatureViewController.getLeftFeatureGroup(featureView); + validateGetForFeatureGroup(featuregroup, fvsFilters); + } + public void validateRegisterForTrainingDataset(TrainingDataset trainingDataset, StatisticsDTO statisticsDTO) throws FeaturestoreException { this.validateRegisterStatistics(statisticsDTO); @@ -149,6 +166,10 @@ public void validateStatisticsFiltersForFeatureGroup(Set filters) { + validateStatisticsFilters(filters, "Feature View", true, true, false); + } + public void validateStatisticsFiltersForTrainingDataset(Set filters) { validateStatisticsFilters(filters, "Training Dataset", false, false, true); } diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/JobController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/JobController.java index a4dff2f066..cce707201a 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/JobController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/JobController.java @@ -43,6 +43,7 @@ import io.hops.hopsworks.common.dao.jobhistory.ExecutionFacade; import io.hops.hopsworks.common.dao.jobs.description.JobFacade; import io.hops.hopsworks.common.dao.user.activity.ActivityFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationFacade; import io.hops.hopsworks.common.hdfs.DistributedFileSystemOps; import io.hops.hopsworks.common.hdfs.DistributedFsService; import io.hops.hopsworks.common.hdfs.HdfsUsersController; @@ -55,6 +56,7 @@ import io.hops.hopsworks.common.util.SparkConfigurationUtil; import io.hops.hopsworks.exceptions.DatasetException; import io.hops.hopsworks.exceptions.JobException; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; import io.hops.hopsworks.persistence.entity.jobs.configuration.JobConfiguration; import io.hops.hopsworks.persistence.entity.jobs.configuration.JobType; import io.hops.hopsworks.persistence.entity.jobs.configuration.ScheduleDTO; @@ -108,6 +110,9 @@ public class JobController { private Settings settings; @EJB private ProjectUtils projectUtils; + @EJB + private FeatureMonitoringConfigurationFacade featureMonitoringConfigurationFacade; + private static final Logger LOGGER = Logger.getLogger(JobController.class.getName()); @@ -179,6 +184,14 @@ public boolean unscheduleJob(Jobs job) { @TransactionAttribute(TransactionAttributeType.NEVER) public void deleteJob(Jobs job, Users user) throws JobException { + // Check if the job is attached to a Feature monitoring config + Optional optConfig = featureMonitoringConfigurationFacade.findByJobId(job.getId()); + if (optConfig.isPresent()) { + throw new JobException(RESTCodes.JobErrorCode.JOB_DELETION_ERROR, Level.FINE, + "Job attached to Feature Monitoring configuration '" + optConfig.get().getName() + "'. Please," + + " delete the Feature Monitoring configuration first."); + } + // Delete schedule V1 if (job.getJobConfig().getSchedule() != null) { unscheduleJob(job); diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/scheduler/JobScheduleV2Controller.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/scheduler/JobScheduleV2Controller.java index ef7de48ec7..82601449d2 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/scheduler/JobScheduleV2Controller.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/jobs/scheduler/JobScheduleV2Controller.java @@ -104,13 +104,13 @@ public void deleteSchedule(Integer jobId) { jobScheduleFacade.removeByJobId(jobId); } - public JobScheduleV2 updateSchedule(JobScheduleV2DTO jobScheduleV2DTO) throws JobException { - JobScheduleV2 jobSchedule = jobScheduleFacade.getById(jobScheduleV2DTO.getId()) + public JobScheduleV2 updateSchedule(JobScheduleV2 jobScheduleV2) throws JobException { + JobScheduleV2 jobSchedule = jobScheduleFacade.getById(jobScheduleV2.getId()) .orElseThrow(() -> new JobException(RESTCodes.JobErrorCode.JOB_SCHEDULE_NOT_FOUND, Level.FINE)); - jobSchedule.setEnabled(jobScheduleV2DTO.getEnabled()); - jobSchedule.setCronExpression(jobScheduleV2DTO.getCronExpression()); - jobSchedule.setStartDateTime(jobScheduleV2DTO.getStartDateTime()); - jobSchedule.setEndDateTime(jobScheduleV2DTO.getEndDateTime()); + jobSchedule.setEnabled(jobScheduleV2.getEnabled()); + jobSchedule.setCronExpression(jobScheduleV2.getCronExpression()); + jobSchedule.setStartDateTime(jobScheduleV2.getStartDateTime()); + jobSchedule.setEndDateTime(jobScheduleV2.getEndDateTime()); if (jobSchedule.getStartDateTime() != null && jobSchedule.getNextExecutionDateTime() != null && diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java index d547a27995..3adc82fd65 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java @@ -875,6 +875,10 @@ private void populateCache() { STATISTICS_CLEANER_INTERVAL_MS = setIntVar(VARIABLE_STATISTICS_CLEANER_INTERVAL_MS, STATISTICS_CLEANER_INTERVAL_MS); + // Feature monitoring + ENABLE_FEATURE_MONITORING = setBoolVar(VARIABLE_ENABLE_FEATURE_MONITORING, + ENABLE_FEATURE_MONITORING); + TESTCONNECTOR_IMAGE_VERSION = setStrVar(VARIABLE_CONNECTOR_IMAGE_VERSION, "0.1"); YARN_RUNTIME = setStrVar(VARIABLE_YARN_RUNTIME, YARN_RUNTIME); DOCKER_MOUNTS = setStrVar(VARIABLE_DOCKER_MOUNTS, DOCKER_MOUNTS); @@ -3397,6 +3401,13 @@ public String getTEST_CONNECTOR_LAUNCHER() { return TEST_CONNECTOR_LAUNCHER; } // End - Storage connectors + + private boolean ENABLE_FEATURE_MONITORING = false; + public synchronized boolean isFeatureMonitoringEnabled() { + checkCache(); + return ENABLE_FEATURE_MONITORING; + } + private Boolean LOCALHOST = false; public synchronized Boolean isLocalHost() { diff --git a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringConfigurationInputValidation.java b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringConfigurationInputValidation.java new file mode 100644 index 0000000000..ed6cc14da9 --- /dev/null +++ b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringConfigurationInputValidation.java @@ -0,0 +1,286 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featuremonitoring; + +import io.hops.hopsworks.common.featurestore.featuregroup.FeaturegroupController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.DescriptiveStatisticsComparisonConfigurationDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationFacade; +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationInputValidation; +import io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration.MonitoringWindowConfigurationInputValidation; +import io.hops.hopsworks.common.jobs.scheduler.JobScheduleV2DTO; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.Featurestore; +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringType; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.WindowConfigurationType; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics.MetricDescriptiveStatistics; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.persistence.entity.user.Users; +import org.apache.commons.lang.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Optional; + +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_DESCRIPTION; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_FEATURE_NAME; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class TestFeatureMonitoringConfigurationInputValidation { + @InjectMocks + private final TestMonitoringWindowConfigurationInputValidation testMonitoringWindowConfigurationInputValidation = + new TestMonitoringWindowConfigurationInputValidation(); + @InjectMocks + private FeatureMonitoringConfigurationInputValidation featureMonitoringConfigurationInputValidation = + new FeatureMonitoringConfigurationInputValidation(); + + @Mock + private FeaturegroupController featuregroupController; + @Mock + private FeatureMonitoringConfigurationFacade featureMonitoringConfigurationFacade; + // + @Mock + private MonitoringWindowConfigurationInputValidation monitoringWindowConfigurationInputValidation; + + private final Project project = new Project(); + private final Users user = new Users(123); + private final Featurestore featurestore = new Featurestore(); + private final Featuregroup featureGroup = new Featuregroup(); + + @Before + public void setup() throws FeaturestoreException { + MockitoAnnotations.openMocks(this); + + featurestore.setProject(project); + featureGroup.setFeaturestore(featurestore); + featureGroup.setId(12); + featureGroup.setName("fgtest"); + + Mockito.when( + featuregroupController.getFeatureNames(Mockito.any(), Mockito.any(), Mockito.any()) + ).thenReturn( + Arrays.asList("testFeatureName") + ); + + Mockito.when( + featuregroupController.getFeaturegroupById(Mockito.any(), Mockito.any()) + ).thenReturn( + featureGroup + ); + } + + private FeatureMonitoringConfigurationDTO makeValidFeatureMonitoringConfigDTO(boolean withFeatureName, + WindowConfigurationType referenceWindowType) { + FeatureMonitoringConfigurationDTO dto = new FeatureMonitoringConfigurationDTO(); + dto.setName("testName"); + dto.setDescription("testDescription"); + + if (withFeatureName) { + dto.setFeatureName("testFeatureName"); + } + + JobScheduleV2DTO jobSchedule = new JobScheduleV2DTO(); + jobSchedule.setEnabled(true); + dto.setJobSchedule(jobSchedule); + + dto.setDetectionWindowConfig( + testMonitoringWindowConfigurationInputValidation.makeValidMonitoringWindowConfiguration( + WindowConfigurationType.ROLLING_TIME + ) + ); + + if (referenceWindowType != null) { + dto.setFeatureMonitoringType(FeatureMonitoringType.STATISTICS_COMPARISON); + dto.setReferenceWindowConfig( + testMonitoringWindowConfigurationInputValidation.makeValidMonitoringWindowConfiguration( + referenceWindowType + ) + ); + } else { + dto.setFeatureMonitoringType(FeatureMonitoringType.STATISTICS_COMPUTATION); + } + + if (dto.getFeatureMonitoringType() == FeatureMonitoringType.STATISTICS_COMPARISON) { + dto.setStatisticsComparisonConfig(makeValidStatisticsComparisonConfigDTO()); + } + + return dto; + } + + private DescriptiveStatisticsComparisonConfigurationDTO makeValidStatisticsComparisonConfigDTO() { + DescriptiveStatisticsComparisonConfigurationDTO statisticsComparisonConfigDTO = + new DescriptiveStatisticsComparisonConfigurationDTO(); + statisticsComparisonConfigDTO.setMetric(MetricDescriptiveStatistics.MEAN); + statisticsComparisonConfigDTO.setRelative(false); + statisticsComparisonConfigDTO.setStrict(false); + statisticsComparisonConfigDTO.setThreshold(0.1); + + return statisticsComparisonConfigDTO; + } + + @Test + public void testValidConfigIsValidated() throws FeaturestoreException { + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(false, null); + assertTrue( + featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + + dto = makeValidFeatureMonitoringConfigDTO(true, null); + assertTrue( + featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + + dto = makeValidFeatureMonitoringConfigDTO(true, WindowConfigurationType.SPECIFIC_VALUE); + assertTrue( + featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + + dto = makeValidFeatureMonitoringConfigDTO(true, WindowConfigurationType.TRAINING_DATASET); + assertTrue( + featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + + dto = makeValidFeatureMonitoringConfigDTO(true, WindowConfigurationType.ROLLING_TIME); + assertTrue( + featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + + dto = makeValidFeatureMonitoringConfigDTO(true, WindowConfigurationType.ALL_TIME); + assertTrue( + featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + } + + @Test + public void testValidateConfigFeatureNameFieldLength() throws FeaturestoreException { + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(true, null); + String tooLongFeatureName = StringUtils.repeat("a", MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_FEATURE_NAME + 1); + dto.setFeatureName(tooLongFeatureName); + Mockito.when( + featuregroupController.getFeatureNames(Mockito.any(), Mockito.any(), Mockito.any()) + ).thenReturn( + Arrays.asList(tooLongFeatureName) + ); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null)); + + assertTrue(illegalArgumentException.getMessage().contains("Feature name")); + assertTrue(illegalArgumentException.getMessage() + .contains(Integer.toString(MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_FEATURE_NAME))); + assertTrue(illegalArgumentException.getMessage().contains(dto.getName())); + } + + @Test + public void testValidateConfigNameFieldLength() { + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(true, null); + dto.setName(StringUtils.repeat("a", MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME + 1)); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null)); + + assertTrue(illegalArgumentException.getMessage().contains("Name")); + assertTrue(illegalArgumentException.getMessage() + .contains(Integer.toString(MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME))); + assertTrue(illegalArgumentException.getMessage().contains(dto.getName())); + } + + @Test + public void testValidateConfigFieldLength() { + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(true, null); + dto.setName(StringUtils.repeat("a", MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME + 1)); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null)); + + assertTrue(illegalArgumentException.getMessage().contains("Name")); + assertTrue(illegalArgumentException.getMessage() + .contains(Integer.toString(MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_NAME))); + assertTrue(illegalArgumentException.getMessage().contains(dto.getName())); + } + + @Test + public void testValidateConfigDescriptionFieldLength() { + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(true, null); + dto.setDescription(StringUtils.repeat("a", MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_DESCRIPTION + 1)); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null)); + + assertTrue(illegalArgumentException.getMessage().contains("Description")); + assertTrue(illegalArgumentException.getMessage() + .contains(Integer.toString(MAX_CHARACTERS_IN_FEATURE_MONITORING_CONFIG_DESCRIPTION))); + assertTrue(illegalArgumentException.getMessage().contains(dto.getName())); + } + + @Test + public void testValidateFeatureNameExist() { + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(true, WindowConfigurationType.ALL_TIME); + dto.setFeatureName("testInvalidFeatureName"); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + + assertTrue(illegalArgumentException.getMessage().contains(dto.getFeatureName())); + } + + @Test + public void testFailIfConfigNameAlreadyExistOnCreate() { + Mockito.when(featureMonitoringConfigurationFacade.findByFeatureGroupAndName(Mockito.any(), Mockito.any())) + .thenReturn(Optional.of(new FeatureMonitoringConfiguration())); + + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(false, null); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> featureMonitoringConfigurationInputValidation.validateConfigOnCreate(user, dto, featureGroup, null) + ); + + assertTrue(illegalArgumentException.getMessage().contains(dto.getName())); + assertTrue(illegalArgumentException.getMessage().contains("already exists")); + } + + @Test + public void testFailIfConfigNameNotExistOnUpdate() { + Mockito.when(featureMonitoringConfigurationFacade.findByFeatureGroupAndName(Mockito.any(), Mockito.any())) + .thenReturn(Optional.empty()); + + FeatureMonitoringConfigurationDTO dto = makeValidFeatureMonitoringConfigDTO(false, null); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> featureMonitoringConfigurationInputValidation.validateConfigOnUpdate(dto, featureGroup, null) + ); + + assertTrue(illegalArgumentException.getMessage().contains(dto.getName())); + assertTrue(illegalArgumentException.getMessage().contains("does not exist")); + } +} \ No newline at end of file diff --git a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringResultInputValidation.java b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringResultInputValidation.java new file mode 100644 index 0000000000..ae1389d4a1 --- /dev/null +++ b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestFeatureMonitoringResultInputValidation.java @@ -0,0 +1,278 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featuremonitoring; + +import io.hops.hopsworks.common.featurestore.featuremonitoring.config.FeatureMonitoringConfigurationController; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.result.FeatureMonitoringResultInputValidation; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringType; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.MonitoringWindowConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.WindowConfigurationType; + +import java.util.Date; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class TestFeatureMonitoringResultInputValidation { + @InjectMocks + private FeatureMonitoringResultInputValidation featureMonitoringResultInputValidation = new FeatureMonitoringResultInputValidation(); + + @Mock + private FeatureMonitoringConfigurationController featureMonitoringConfigurationController; + + private Integer featureStoreId = 33; + private Integer configIdStatsOnly = 22; + private Integer configIdComparison = 23; + private Integer configIdSpecificValue = 24; + private Integer executionId = 16; + private String featureName = "test_feature"; + private Integer detectionStatsId = 25; + private Integer referenceStatsId = 66; + + @Before + public void setup() throws FeaturestoreException { + MockitoAnnotations.openMocks(this); + + FeatureMonitoringConfiguration config = new FeatureMonitoringConfiguration(); + config.setName("test_fm_config"); + config.setFeatureMonitoringType(FeatureMonitoringType.STATISTICS_COMPUTATION); + + Mockito.when( + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByConfigId(configIdStatsOnly) + ).thenReturn( + config + ); + + FeatureMonitoringConfiguration configWithComparison = new FeatureMonitoringConfiguration(); + MonitoringWindowConfiguration windowConfig = new MonitoringWindowConfiguration(); + configWithComparison.setName("test_fm_config"); + configWithComparison.setFeatureMonitoringType(FeatureMonitoringType.STATISTICS_COMPARISON); + windowConfig.setWindowConfigType(WindowConfigurationType.ALL_TIME); + configWithComparison.setReferenceWindowConfig(windowConfig); + + Mockito.when( + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByConfigId(configIdComparison) + ).thenReturn( + configWithComparison + ); + + FeatureMonitoringConfiguration configWithSpecificValue = new FeatureMonitoringConfiguration(); + MonitoringWindowConfiguration windowConfigSpecificValue = new MonitoringWindowConfiguration(); + configWithSpecificValue.setName("test_fm_config"); + configWithSpecificValue.setFeatureMonitoringType(FeatureMonitoringType.STATISTICS_COMPARISON); + windowConfigSpecificValue.setWindowConfigType(WindowConfigurationType.SPECIFIC_VALUE); + windowConfigSpecificValue.setSpecificValue(0.5); + configWithSpecificValue.setReferenceWindowConfig(windowConfigSpecificValue); + + Mockito.when( + featureMonitoringConfigurationController.getFeatureMonitoringConfigurationByConfigId(configIdSpecificValue) + ).thenReturn( + configWithSpecificValue + ); + } + + private FeatureMonitoringResultDTO makeValidResultDTO(Integer referenceStatsId, Double specificValue) { + FeatureMonitoringResultDTO resultDto = new FeatureMonitoringResultDTO(); + + resultDto.setId(null); + resultDto.setConfigId(configIdStatsOnly); + resultDto.setFeatureStoreId(featureStoreId); + resultDto.setExecutionId(executionId); + resultDto.setFeatureName(featureName); + resultDto.setDetectionStatisticsId(detectionStatsId); + resultDto.setShiftDetected(false); + resultDto.setRaisedException(false); + resultDto.setEmptyDetectionWindow(false); + resultDto.setEmptyReferenceWindow(false); + resultDto.setMonitoringTime(new Date().getTime()); + + if (referenceStatsId != null) { + resultDto.setReferenceStatisticsId(referenceStatsId); + resultDto.setDifference(0.); + } + if (specificValue != null) { + resultDto.setSpecificValue(specificValue); + resultDto.setEmptyReferenceWindow(true); + resultDto.setDifference(0.); + } + + return resultDto; + } + + @Test + public void testValidateOnCreateForRaisedException() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = new FeatureMonitoringResultDTO(); + + resultDto.setConfigId(configIdStatsOnly); + resultDto.setFeatureName(""); + resultDto.setRaisedException(true); + + assertTrue(featureMonitoringResultInputValidation.validateOnCreate(resultDto)); + } + + @Test + public void testValidateOnCreateForMissingConfigId() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = new FeatureMonitoringResultDTO(); + + resultDto.setConfigId(null); + resultDto.setFeatureName(""); + resultDto.setRaisedException(false); + + assertThrows(IllegalArgumentException.class, () -> { + featureMonitoringResultInputValidation.validateOnCreate(resultDto); + }); + } + + @Test + public void testValidateOnCreateDetectionStatsOnlyResult() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdStatsOnly); + + assertTrue(featureMonitoringResultInputValidation.validateOnCreate(resultDto)); + } + + @Test + public void testValidateOnCreateWithReference() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(referenceStatsId, null); + resultDto.setConfigId(configIdComparison); + + assertTrue(featureMonitoringResultInputValidation.validateOnCreate(resultDto)); + } + + @Test + public void testValidateOnCreateWithSpecificValue() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, 0.); + resultDto.setConfigId(configIdSpecificValue); + + assertTrue(featureMonitoringResultInputValidation.validateOnCreate(resultDto)); + } + + @Test + public void testValidateOnCreateWithComparisonAndSpecificValue() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, 0.); + resultDto.setConfigId(configIdComparison); + + assertThrows(IllegalArgumentException.class, () -> { + featureMonitoringResultInputValidation.validateOnCreate(resultDto); + }); + } + + @Test + public void testValidateOnCreateWithEmptyReference() { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdComparison); + resultDto.setEmptyReferenceWindow(true); + resultDto.setDifference(0.); + + assertThrows(IllegalArgumentException.class, () -> { + featureMonitoringResultInputValidation.validateOnCreate(resultDto); + }); + } + + @Test + public void testValidateOnCreateWithEmptyDetection() { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdComparison); + resultDto.setDetectionStatisticsId(null); + resultDto.setEmptyDetectionWindow(true); + + assertThrows(IllegalArgumentException.class, () -> { + featureMonitoringResultInputValidation.validateOnCreate(resultDto); + }); + } + + @Test + public void testValidateOnCreateWithEmptyDetectionAndEmptyReference() { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdComparison); + resultDto.setDetectionStatisticsId(null); + resultDto.setReferenceStatisticsId(null); + resultDto.setEmptyDetectionWindow(true); + resultDto.setEmptyReferenceWindow(true); + + assertThrows(IllegalArgumentException.class, () -> { + featureMonitoringResultInputValidation.validateOnCreate(resultDto); + }); + } + + @Test + public void testValidateOnCreateWithEmptyDetectionAndStatsId() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdStatsOnly); + resultDto.setDetectionStatisticsId(detectionStatsId); + resultDto.setEmptyDetectionWindow(true); + + assertTrue(featureMonitoringResultInputValidation.validateOnCreate(resultDto)); + } + + @Test + public void testValidateOnCreateWithEmptyReferenceAndStatsId() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdComparison); + resultDto.setReferenceStatisticsId(referenceStatsId); + resultDto.setEmptyReferenceWindow(true); + resultDto.setDifference(null); + + assertTrue(featureMonitoringResultInputValidation.validateOnCreate(resultDto)); + } + + @Test + public void testValidateOnCreateBothEmptyWithStatsId() throws FeaturestoreException { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdComparison); + resultDto.setReferenceStatisticsId(referenceStatsId); + resultDto.setDetectionStatisticsId(detectionStatsId); + resultDto.setEmptyReferenceWindow(true); + resultDto.setEmptyDetectionWindow(true); + resultDto.setDifference(null); + + assertTrue(featureMonitoringResultInputValidation.validateOnCreate(resultDto)); + } + + @Test + public void testValidateOnCreateEmptyWithNotNullDifference() { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdComparison); + resultDto.setReferenceStatisticsId(referenceStatsId); + resultDto.setEmptyReferenceWindow(true); + resultDto.setDifference(0.); + + assertThrows(IllegalArgumentException.class, () -> { + featureMonitoringResultInputValidation.validateOnCreate(resultDto); + }); + } + + @Test + public void testValidateOnCreateStatsOnlyWithNotNullDifference() { + FeatureMonitoringResultDTO resultDto = makeValidResultDTO(null, null); + resultDto.setConfigId(configIdStatsOnly); + resultDto.setDetectionStatisticsId(detectionStatsId); + resultDto.setDifference(0.); + + assertThrows(IllegalArgumentException.class, () -> { + featureMonitoringResultInputValidation.validateOnCreate(resultDto); + }); + } +} \ No newline at end of file diff --git a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringAlertValidation.java b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringAlertValidation.java new file mode 100644 index 0000000000..747c074010 --- /dev/null +++ b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringAlertValidation.java @@ -0,0 +1,58 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featuremonitoring; + +import io.hops.hopsworks.common.featurestore.featuremonitoring.alert.FeatureMonitoringAlertController; +import io.hops.hopsworks.exceptions.FeaturestoreException; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult; +import io.hops.hopsworks.persistence.entity.jobs.scheduler.JobScheduleV2; + +import org.junit.Assert; +import org.junit.Test; + +public class TestMonitoringAlertValidation { + + @Test + public void testValidateInput() { + FeatureMonitoringConfiguration config = new FeatureMonitoringConfiguration(); + FeatureMonitoringResult result = new FeatureMonitoringResult(); + FeatureMonitoringAlertController controller = new FeatureMonitoringAlertController(); + + // assertThrows exception if config is null + FeaturestoreException exp = Assert.assertThrows(FeaturestoreException.class, () -> { + controller.triggerAlertsByStatus(null, result); + }); + Assert.assertEquals("Feature Monitoring Config should not be null.", exp.getUsrMsg()); + + // assertThrows exception if result is null + exp = Assert.assertThrows(FeaturestoreException.class, () -> { + controller.triggerAlertsByStatus(config, null); + }); + Assert.assertEquals("Feature Monitoring Result should not be null.", exp.getUsrMsg()); + + // assert throws exception if config is enabled but result is null + exp = Assert.assertThrows(FeaturestoreException.class, () -> { + JobScheduleV2 jobScheduleV2 = new JobScheduleV2(); + jobScheduleV2.setEnabled(false); + config.setJobSchedule(jobScheduleV2); + controller.triggerAlertsByStatus(config, result); + }); + Assert.assertEquals("Feature Monitoring Config is disabled, skipping triggering alert.", exp.getUsrMsg()); + + } + +} \ No newline at end of file diff --git a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringWindowConfigurationInputValidation.java b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringWindowConfigurationInputValidation.java new file mode 100644 index 0000000000..ba00ead6e3 --- /dev/null +++ b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/featuremonitoring/TestMonitoringWindowConfigurationInputValidation.java @@ -0,0 +1,442 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.common.featurestore.featuremonitoring; + +import io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration.MonitoringWindowConfigurationDTO; +import io.hops.hopsworks.common.featurestore.featuremonitoring.monitoringwindowconfiguration.MonitoringWindowConfigurationInputValidation; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.WindowConfigurationType; +import org.apache.commons.lang.StringUtils; +import org.junit.Test; +import org.apache.commons.lang.NotImplementedException; + +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_TIME_OFFSET; +import static io.hops.hopsworks.common.featurestore.FeaturestoreConstants.MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_WINDOW_LENGTH; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class TestMonitoringWindowConfigurationInputValidation { + private final MonitoringWindowConfigurationInputValidation inputValidation = + new MonitoringWindowConfigurationInputValidation(); + private final String configName = "testConfigName"; + + public MonitoringWindowConfigurationDTO makeValidMonitoringWindowConfiguration( + WindowConfigurationType windowConfigType) { + MonitoringWindowConfigurationDTO dto = new MonitoringWindowConfigurationDTO(); + dto.setWindowConfigType(windowConfigType); + + switch (windowConfigType) { + case ROLLING_TIME: + return populateValidRollingWindowConfiguration(dto); + case ALL_TIME: + return populateValidAllTimeWindowConfiguration(dto); + case TRAINING_DATASET: + return populateValidTrainingDatasetWindowConfiguration(dto); + case SPECIFIC_VALUE: + return populateValidSpecificValueWindowConfiguration(dto); + default: + throw new NotImplementedException("Implement unit test for this monitoring window configuration type"); + } + } + + private MonitoringWindowConfigurationDTO populateValidRollingWindowConfiguration( + MonitoringWindowConfigurationDTO windowConfigDTO) { + windowConfigDTO.setWindowLength("2w"); + windowConfigDTO.setTimeOffset("1w"); + windowConfigDTO.setRowPercentage(0.1f); + + return windowConfigDTO; + } + + private MonitoringWindowConfigurationDTO populateValidAllTimeWindowConfiguration( + MonitoringWindowConfigurationDTO windowConfigDTO) { + windowConfigDTO.setRowPercentage(0.1f); + + return windowConfigDTO; + } + + private MonitoringWindowConfigurationDTO populateValidTrainingDatasetWindowConfiguration( + MonitoringWindowConfigurationDTO windowConfigDTO) { + windowConfigDTO.setTrainingDatasetVersion(12); + + return windowConfigDTO; + } + + private MonitoringWindowConfigurationDTO populateValidSpecificValueWindowConfiguration( + MonitoringWindowConfigurationDTO windowConfigDTO) { + windowConfigDTO.setSpecificValue(1.2); + + return windowConfigDTO; + } + + @Test + public void testAcceptAllValidMonitoringWindowConfiguration() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ROLLING_TIME); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + windowConfigDTO = makeValidMonitoringWindowConfiguration(WindowConfigurationType.ALL_TIME); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + windowConfigDTO = makeValidMonitoringWindowConfiguration(WindowConfigurationType.TRAINING_DATASET); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + windowConfigDTO = makeValidMonitoringWindowConfiguration(WindowConfigurationType.SPECIFIC_VALUE); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + } + + @Test + public void testEnforceNullFieldsForSpecificValue() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.SPECIFIC_VALUE); + + windowConfigDTO.setSpecificValue(null); + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("SPECIFIC_VALUE")); + assertTrue(illegalArgumentException.getMessage().contains("specificValue")); + assertTrue(illegalArgumentException.getMessage().contains("cannot be null")); + + windowConfigDTO.setSpecificValue(1.); + windowConfigDTO.setWindowLength("2w"); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("SPECIFIC_VALUE")); + assertTrue(illegalArgumentException.getMessage().contains("windowLength")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setWindowLength(null); + windowConfigDTO.setTimeOffset("1w"); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("SPECIFIC_VALUE")); + assertTrue(illegalArgumentException.getMessage().contains("timeOffset")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setTimeOffset(null); + windowConfigDTO.setRowPercentage(0.1f); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("SPECIFIC_VALUE")); + assertTrue(illegalArgumentException.getMessage().contains("rowPercentage")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setRowPercentage(null); + windowConfigDTO.setTrainingDatasetVersion(12); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("SPECIFIC_VALUE")); + assertTrue(illegalArgumentException.getMessage().contains("trainingDatasetVersion")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + } + + @Test + public void testEnforceNullFieldsForTrainingDataset() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.TRAINING_DATASET); + + windowConfigDTO.setTrainingDatasetVersion(null); + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("TRAINING_DATASET")); + assertTrue(illegalArgumentException.getMessage().contains("trainingDatasetVersion")); + assertTrue(illegalArgumentException.getMessage().contains("cannot be null")); + + windowConfigDTO.setTrainingDatasetVersion(12); + windowConfigDTO.setWindowLength("2w"); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("TRAINING_DATASET")); + assertTrue(illegalArgumentException.getMessage().contains("windowLength")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setWindowLength(null); + windowConfigDTO.setTimeOffset("1w"); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("TRAINING_DATASET")); + assertTrue(illegalArgumentException.getMessage().contains("timeOffset")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setTimeOffset(null); + windowConfigDTO.setRowPercentage(0.1f); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("TRAINING_DATASET")); + assertTrue(illegalArgumentException.getMessage().contains("rowPercentage")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setRowPercentage(null); + windowConfigDTO.setSpecificValue(1.2); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("TRAINING_DATASET")); + assertTrue(illegalArgumentException.getMessage().contains("specificValue")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + } + + @Test + public void testEnforceNullFieldsForAllTime() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ALL_TIME); + + windowConfigDTO.setTrainingDatasetVersion(12); + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ALL_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("trainingDatasetVersion")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setTrainingDatasetVersion(null); + windowConfigDTO.setWindowLength("2w"); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ALL_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("windowLength")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setWindowLength(null); + windowConfigDTO.setTimeOffset("1w"); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ALL_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("timeOffset")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setTimeOffset(null); + windowConfigDTO.setRowPercentage(null); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ALL_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("rowPercentage")); + assertTrue(illegalArgumentException.getMessage().contains("cannot be null")); + + windowConfigDTO.setRowPercentage(0.1f); + windowConfigDTO.setSpecificValue(1.2); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ALL_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("specificValue")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + } + + @Test + public void testEnforceNullFieldsForRollingTime() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ROLLING_TIME); + + windowConfigDTO.setTimeOffset(null); + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ROLLING_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("timeOffset")); + assertTrue(illegalArgumentException.getMessage().contains("cannot be null")); + + windowConfigDTO.setTimeOffset("1w"); + windowConfigDTO.setRowPercentage(null); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ROLLING_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("rowPercentage")); + assertTrue(illegalArgumentException.getMessage().contains("cannot be null")); + + windowConfigDTO.setRowPercentage(0.1f); + windowConfigDTO.setSpecificValue(1.2); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ROLLING_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("specificValue")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + + windowConfigDTO.setSpecificValue(null); + windowConfigDTO.setTrainingDatasetVersion(12); + + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("ROLLING_TIME")); + assertTrue(illegalArgumentException.getMessage().contains("trainingDatasetVersion")); + assertTrue(illegalArgumentException.getMessage().contains("must be null")); + } + + @Test + public void testValidateMonitoringWindowConfigWindowFieldLength() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ROLLING_TIME); + windowConfigDTO.setWindowLength( + StringUtils.repeat("d", MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_WINDOW_LENGTH + 1)); + + IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDtoFieldMaximalLength(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Window length")); + assertTrue(illegalArgumentException.getMessage() + .contains(Integer.toString(MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_WINDOW_LENGTH))); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + } + + @Test + public void testValidateMonitoringWindowConfigTimeOffsetLength() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ROLLING_TIME); + windowConfigDTO.setTimeOffset(StringUtils.repeat("d", MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_TIME_OFFSET + 1)); + + IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDtoFieldMaximalLength(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Time offset")); + assertTrue(illegalArgumentException.getMessage() + .contains(Integer.toString(MAX_CHARACTERS_IN_MONITORING_WINDOW_CONFIG_TIME_OFFSET))); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + } + + @Test + public void testMonitoringWindowConfigurationAllTimeRowPercentage() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ALL_TIME); + windowConfigDTO.setRowPercentage(1.0001f); + + IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Row percentage")); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + + windowConfigDTO.setRowPercentage(-0.0001f); + illegalArgumentException = assertThrows(IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Row percentage")); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + } + + @Test + public void testMonitoringWindowConfigurationRollingTimeRowPercentage() { + MonitoringWindowConfigurationDTO windowConfigDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ROLLING_TIME); + windowConfigDTO.setRowPercentage(1.0001f); + + IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Row percentage")); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + + windowConfigDTO.setRowPercentage(-0.0001f); + illegalArgumentException = assertThrows(IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Row percentage")); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + } + + @Test + public void testWindowLengthOptionalForRollingTime() { + MonitoringWindowConfigurationDTO windowConfigurationDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ROLLING_TIME); + windowConfigurationDTO.setWindowLength(null); + + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigurationDTO)); + } + + @Test + public void testRegexForTimeOffsetAndWindowLength() { + MonitoringWindowConfigurationDTO windowConfigurationDTO = + makeValidMonitoringWindowConfiguration(WindowConfigurationType.ROLLING_TIME); + // minus signs not allowed + windowConfigurationDTO.setTimeOffset("-1w"); + + IllegalArgumentException illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigurationDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Time offset")); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + assertTrue(illegalArgumentException.getMessage().contains("1w2d3h")); + + // additional erroneous characters not allowed + windowConfigurationDTO.setTimeOffset("1w2d3h4m"); + illegalArgumentException = assertThrows( + IllegalArgumentException.class, + () -> inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigurationDTO)); + + assertTrue(illegalArgumentException.getMessage().contains("Time offset")); + assertTrue(illegalArgumentException.getMessage().contains(configName)); + assertTrue(illegalArgumentException.getMessage().contains("1w2d3h")); + + // two out of three allowed + windowConfigurationDTO.setTimeOffset("12h1d"); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigurationDTO)); + + // one out of three allowed + windowConfigurationDTO.setTimeOffset("1w"); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigurationDTO)); + windowConfigurationDTO.setTimeOffset("2d"); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigurationDTO)); + windowConfigurationDTO.setTimeOffset("3h"); + assertTrue(inputValidation.validateMonitoringWindowConfigDto(configName, windowConfigurationDTO)); + } +} \ No newline at end of file diff --git a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsController.java b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsController.java index 690f319d69..600c4bb750 100644 --- a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsController.java +++ b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsController.java @@ -17,8 +17,6 @@ package io.hops.hopsworks.common.featurestore.statistics; import io.hops.hopsworks.common.dao.AbstractFacade; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.cached.FeatureGroupCommit; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.cached.FeatureGroupCommitPK; import org.javatuples.Pair; import org.junit.Assert; import org.junit.Before; diff --git a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsInputValidation.java b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsInputValidation.java index 0da31816e9..52fadaed27 100644 --- a/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsInputValidation.java +++ b/hopsworks-common/src/test/io/hops/hopsworks/common/featurestore/statistics/TestStatisticsInputValidation.java @@ -22,6 +22,7 @@ import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.FeaturegroupType; import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDataset; +import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDatasetJoin; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -30,6 +31,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -43,7 +45,9 @@ public class TestStatisticsInputValidation { private Featuregroup ondemandFG, streamFG; @Mock - private FeatureView featureView; + private TrainingDatasetJoin ondemandFgTrainingDatasetJoin, streamFgTrainingDatasetJoin; + @Mock + private FeatureView onDemandFgFV, streamFgFV; @Mock private TrainingDataset trainingDataset; @@ -53,6 +57,11 @@ public void setup() throws Exception { MockitoAnnotations.openMocks(this); Mockito.when(ondemandFG.getFeaturegroupType()).thenReturn(FeaturegroupType.ON_DEMAND_FEATURE_GROUP); Mockito.when(streamFG.getFeaturegroupType()).thenReturn(FeaturegroupType.STREAM_FEATURE_GROUP); + + Mockito.when(ondemandFgTrainingDatasetJoin.getFeatureGroup()).thenReturn(ondemandFG); + Mockito.when(streamFgTrainingDatasetJoin.getFeatureGroup()).thenReturn(streamFG); + Mockito.when(onDemandFgFV.getJoins()).thenReturn(Collections.singleton(ondemandFgTrainingDatasetJoin)); + Mockito.when(streamFgFV.getJoins()).thenReturn(Collections.singleton(streamFgTrainingDatasetJoin)); } @Test @@ -165,6 +174,63 @@ public void testValidateGetForFeatureGroup() { }); } + @Test + public void testValidateRegisterForFeatureView() { + // statistics computation time (commit time) cannot be lower than window times + Assert.assertThrows(IllegalArgumentException.class, () -> { + StatisticsDTO stats = buildStatisticsDTO(0L, 1L, null); + target.validateRegisterForFeatureView(onDemandFgFV, stats); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + StatisticsDTO stats = buildStatisticsDTO(1L, 0L, 2L); + target.validateRegisterForFeatureView(onDemandFgFV, stats); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + StatisticsDTO stats = buildStatisticsDTO(1L, null, 2L); + target.validateRegisterForFeatureView(onDemandFgFV, stats); + }); + // window times not supported in non-time-travel enabled FGs + Assert.assertThrows(IllegalArgumentException.class, () -> { + StatisticsDTO stats = buildStatisticsDTO(null, 1L, null); + target.validateRegisterForFeatureView(onDemandFgFV, stats); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + StatisticsDTO stats = buildStatisticsDTO(null, null, 1L); + target.validateRegisterForFeatureView(onDemandFgFV, stats); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + StatisticsDTO stats = buildStatisticsDTO(null, 1L, 2L); + target.validateRegisterForFeatureView(onDemandFgFV, stats); + }); + // window start commit time can't be provided without end time + Assert.assertThrows(IllegalArgumentException.class, () -> { + StatisticsDTO stats = buildStatisticsDTO(null, 1L, null); + target.validateRegisterForFeatureView(streamFgFV, stats); + }); + } + + @Test + public void testValidateGetForFeatureView() { + // window times not supported in non-time-travel enabled left FGs + Assert.assertThrows(FeaturestoreException.class, () -> { + StatisticsFilters filters = buildStatisticsFilters(null, 1L, null); + target.validateGetForFeatureView(onDemandFgFV, filters); + }); + Assert.assertThrows(FeaturestoreException.class, () -> { + StatisticsFilters filters = buildStatisticsFilters(null, null, 2L); + target.validateGetForFeatureView(onDemandFgFV, filters); + }); + Assert.assertThrows(FeaturestoreException.class, () -> { + StatisticsFilters filters = buildStatisticsFilters(null, 1L, 2L); + target.validateGetForFeatureView(onDemandFgFV, filters); + }); + // window start commit time can't be provided without end time + Assert.assertThrows(FeaturestoreException.class, () -> { + StatisticsFilters filters = buildStatisticsFilters(null, 1L, null); + target.validateGetForFeatureView(streamFgFV, filters); + }); + } + @Test public void testValidateRegisterForTrainingDataset() { // window times are not supported diff --git a/hopsworks-common/src/test/io/hops/hopsworks/common/jobs/TestJobSchedulerV2Controller.java b/hopsworks-common/src/test/io/hops/hopsworks/common/jobs/TestJobSchedulerV2Controller.java index 00b22b8dd6..43529e833e 100644 --- a/hopsworks-common/src/test/io/hops/hopsworks/common/jobs/TestJobSchedulerV2Controller.java +++ b/hopsworks-common/src/test/io/hops/hopsworks/common/jobs/TestJobSchedulerV2Controller.java @@ -230,10 +230,10 @@ public void testEnableSchedulerWithPreviousExecutions() throws JobException { Mockito.when(jobScheduleV2Facade.update(any(JobScheduleV2.class))).thenReturn(scheduler); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(JobScheduleV2.class); - JobScheduleV2DTO updatedJobScheduleDTO = new JobScheduleV2DTO(); - updatedJobScheduleDTO.setEnabled(true); - updatedJobScheduleDTO.setCronExpression("0 */10 * ? * * *"); - target.updateSchedule(updatedJobScheduleDTO); + JobScheduleV2 updatedJobSchedule = new JobScheduleV2(); + updatedJobSchedule.setEnabled(true); + updatedJobSchedule.setCronExpression("0 */10 * ? * * *"); + target.updateSchedule(updatedJobSchedule); verify(jobScheduleV2Facade, times(1)).update(argumentCaptor.capture()); JobScheduleV2 capturedSchedule = argumentCaptor.getValue(); @@ -263,11 +263,11 @@ public void testEnableSchedulerWithPreviousExecutionPriorToNewStartDateTime() th newStartDateTime.setTimeZone(TimeZone.getTimeZone("UTC")); newStartDateTime.set(2021, 1, 1, 0, 30, 0); - JobScheduleV2DTO updatedJobScheduleDTO = new JobScheduleV2DTO(); - updatedJobScheduleDTO.setEnabled(true); - updatedJobScheduleDTO.setStartDateTime(newStartDateTime.toInstant()); - updatedJobScheduleDTO.setCronExpression("0 */10 * ? * * *"); - target.updateSchedule(updatedJobScheduleDTO); + JobScheduleV2 updatedJobSchedule = new JobScheduleV2(); + updatedJobSchedule.setEnabled(true); + updatedJobSchedule.setStartDateTime(newStartDateTime.toInstant()); + updatedJobSchedule.setCronExpression("0 */10 * ? * * *"); + target.updateSchedule(updatedJobSchedule); verify(jobScheduleV2Facade, times(1)).update(argumentCaptor.capture()); @@ -295,11 +295,11 @@ public void testEnableSchedulerWithNoPreviousExecutions() throws JobException { Mockito.when(jobScheduleV2Facade.update(any(JobScheduleV2.class))).thenReturn(scheduler); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(JobScheduleV2.class); - JobScheduleV2DTO updatedJobScheduleDTO = new JobScheduleV2DTO(); - updatedJobScheduleDTO.setEnabled(true); - updatedJobScheduleDTO.setStartDateTime(startDateTime.toInstant()); - updatedJobScheduleDTO.setCronExpression("0 */10 * ? * * *"); - target.updateSchedule(updatedJobScheduleDTO); + JobScheduleV2 updatedJobSchedule = new JobScheduleV2(); + updatedJobSchedule.setEnabled(true); + updatedJobSchedule.setStartDateTime(startDateTime.toInstant()); + updatedJobSchedule.setCronExpression("0 */10 * ? * * *"); + target.updateSchedule(updatedJobSchedule); verify(jobScheduleV2Facade, times(1)).update(argumentCaptor.capture()); @@ -324,10 +324,10 @@ public void testDisableScheduler() throws JobException { Mockito.when(jobScheduleV2Facade.update(any(JobScheduleV2.class))).thenReturn(scheduler); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(JobScheduleV2.class); - JobScheduleV2DTO updatedJobScheduleDTO = new JobScheduleV2DTO(); - updatedJobScheduleDTO.setEnabled(false); - updatedJobScheduleDTO.setCronExpression("0 0 0 ? * 1 *"); - target.updateSchedule(updatedJobScheduleDTO); + JobScheduleV2 updatedJobSchedule = new JobScheduleV2(); + updatedJobSchedule.setEnabled(false); + updatedJobSchedule.setCronExpression("0 0 0 ? * 1 *"); + target.updateSchedule(updatedJobSchedule); verify(jobScheduleV2Facade, times(1)).update(argumentCaptor.capture()); @@ -350,10 +350,10 @@ public void testUpdateCronExpression() throws JobException { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(JobScheduleV2.class); String updatedCronExpression = "0 */5 */5 ? * 1 *"; - JobScheduleV2DTO updatedJobScheduleDTO = new JobScheduleV2DTO(); - updatedJobScheduleDTO.setCronExpression(updatedCronExpression); - updatedJobScheduleDTO.setEnabled(true); - target.updateSchedule(updatedJobScheduleDTO); + JobScheduleV2 updatedJobSchedule = new JobScheduleV2(); + updatedJobSchedule.setCronExpression(updatedCronExpression); + updatedJobSchedule.setEnabled(true); + target.updateSchedule(updatedJobSchedule); verify(jobScheduleV2Facade, times(1)).update(argumentCaptor.capture()); diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/alertmanager/AlertReceiver.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/alertmanager/AlertReceiver.java index 69ce2c3f48..7974df522f 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/alertmanager/AlertReceiver.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/alertmanager/AlertReceiver.java @@ -36,6 +36,7 @@ import javax.xml.bind.annotation.XmlTransient; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert; import io.hops.hopsworks.persistence.entity.jobs.description.JobAlert; import io.hops.hopsworks.persistence.entity.project.alert.ProjectServiceAlert; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -83,6 +84,9 @@ public class AlertReceiver implements Serializable { @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "receiver") private Collection featureGroupAlertCollection; + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, + mappedBy = "receiver") + private Collection featureViewAlertCollection; public AlertReceiver() { } @@ -149,7 +153,18 @@ public Collection getFeatureGroupAlertCollection() { public void setFeatureGroupAlertCollection(Collection featureGroupAlertCollection) { this.featureGroupAlertCollection = featureGroupAlertCollection; } - + + @XmlTransient + @JsonIgnore + public Collection getFeatureViewAlertCollection() { + return featureViewAlertCollection; + } + + public void setFeatureViewAlertCollection( + Collection featureViewAlertCollection) { + this.featureViewAlertCollection = featureViewAlertCollection; + } + @Override public int hashCode() { int hash = 0; diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/activity/FeaturestoreActivity.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/activity/FeaturestoreActivity.java index e732b48906..5e806219b0 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/activity/FeaturestoreActivity.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/activity/FeaturestoreActivity.java @@ -22,6 +22,7 @@ import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.ValidationReport; import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupStatistics; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewStatistics; import io.hops.hopsworks.persistence.entity.featurestore.statistics.TrainingDatasetStatistics; import io.hops.hopsworks.persistence.entity.featurestore.trainingdataset.TrainingDataset; import io.hops.hopsworks.persistence.entity.jobs.history.Execution; @@ -104,7 +105,10 @@ public class FeaturestoreActivity implements Serializable { @JoinColumn(name = "feature_view_id", referencedColumnName = "id") private FeatureView featureView; - + + @JoinColumn(name = "feature_view_statistics_id", referencedColumnName = "id") + private FeatureViewStatistics featureViewStatistics; + @JoinColumn(name = "expectation_suite_id", referencedColumnName = "id") private ExpectationSuite expectationSuite; @@ -216,7 +220,15 @@ public FeatureView getFeatureView() { public void setFeatureView(FeatureView featureView) { this.featureView = featureView; } - + + public FeatureViewStatistics getFeatureViewStatistics() { + return featureViewStatistics; + } + + public void setFeatureViewStatistics(FeatureViewStatistics featureViewStatistics) { + this.featureViewStatistics = featureViewStatistics; + } + public ExpectationSuite getExpectationSuite() { return expectationSuite; } public void setExpectationSuite(ExpectationSuite expectationSuite) { this.expectationSuite = expectationSuite; } diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/alert/FeatureStoreAlert.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/alert/FeatureStoreAlert.java new file mode 100644 index 0000000000..7acd6167b4 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/alert/FeatureStoreAlert.java @@ -0,0 +1,174 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.persistence.entity.featurestore.alert; + +import io.hops.hopsworks.persistence.entity.alertmanager.AlertReceiver; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.Date; +import java.util.Objects; + +@MappedSuperclass +public class FeatureStoreAlert { + + public FeatureStoreAlert(Integer id, FeatureStoreAlertStatus status, AlertType alertType, AlertSeverity severity, + AlertReceiver receiver, Date created) { + this.id = id; + this.status = status; + this.alertType = alertType; + this.severity = severity; + this.receiver = receiver; + this.created = created; + } + + public FeatureStoreAlert() { + } + + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(name = "id") + private Integer id; + + @Basic(optional = false) + @NotNull + @Size(min = 1, + max = 45) + @Column(name = "status") + @Enumerated(EnumType.STRING) + private FeatureStoreAlertStatus status; + + @Basic(optional = false) + @NotNull + @Size(min = 1, + max = 45) + @Column(name = "type") + @Enumerated(EnumType.STRING) + private AlertType alertType; + @Basic(optional = false) + @NotNull + @Size(min = 1, + max = 45) + @Column(name = "severity") + @Enumerated(EnumType.STRING) + private AlertSeverity severity; + + @JoinColumn(name = "receiver", + referencedColumnName = "id") + @ManyToOne(optional = false) + private AlertReceiver receiver; + @Basic(optional = false) + @NotNull + @Column(name = "created") + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public FeatureStoreAlertStatus getStatus() { + return status; + } + + public void setStatus( + FeatureStoreAlertStatus status) { + this.status = status; + } + + public AlertType getAlertType() { + return alertType; + } + + public void setAlertType(AlertType alertType) { + this.alertType = alertType; + } + + public AlertSeverity getSeverity() { + return severity; + } + + public void setSeverity(AlertSeverity severity) { + this.severity = severity; + } + + public AlertReceiver getReceiver() { + return receiver; + } + + public void setReceiver(AlertReceiver receiver) { + this.receiver = receiver; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FeatureStoreAlert)) { + return false; + } + FeatureStoreAlert that = (FeatureStoreAlert) o; + return Objects.equals(id, that.id) && status == that.status && alertType == that.alertType && + severity == that.severity && Objects.equals(receiver, that.receiver) && + Objects.equals(created, that.created); + } + + @Override + public int hashCode() { + return Objects.hash(id, status, alertType, severity, receiver, created); + } + + @Override + public String toString() { + return "FeatureStoreAlert{" + + "id=" + id + + ", status=" + status + + ", alertType=" + alertType + + ", severity=" + severity + + ", receiver=" + receiver + + ", created=" + created + + '}'; + } +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/alert/ValidationRuleAlertStatus.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/alert/FeatureStoreAlertStatus.java similarity index 54% rename from hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/alert/ValidationRuleAlertStatus.java rename to hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/alert/FeatureStoreAlertStatus.java index 87f9c53bb1..fb8cdcff6e 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/alert/ValidationRuleAlertStatus.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/alert/FeatureStoreAlertStatus.java @@ -1,6 +1,6 @@ /* * This file is part of Hopsworks - * Copyright (C) 2021, Logical Clocks AB. All rights reserved + * Copyright (C) 2024, Hopsworks AB. All rights reserved * * Hopsworks is free software: you can redistribute it and/or modify it under the terms of * the GNU Affero General Public License as published by the Free Software Foundation, @@ -13,42 +13,46 @@ * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see . */ -package io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert; - -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.FeatureGroupValidationStatus; +package io.hops.hopsworks.persistence.entity.featurestore.alert; import javax.xml.bind.annotation.XmlEnum; @XmlEnum -public enum ValidationRuleAlertStatus { +public enum FeatureStoreAlertStatus { SUCCESS("Success"), WARNING("Warning"), - FAILURE("Failure"); - + FAILURE("Failure"), + FEATURE_MONITOR_SHIFT_UNDETECTED("FEATURE_MONITOR_SHIFT_UNDETECTED"), + FEATURE_MONITOR_SHIFT_DETECTED("FEATURE_MONITOR_SHIFT_DETECTED"); + private final String name; - - ValidationRuleAlertStatus(String name) { + + FeatureStoreAlertStatus(String name) { this.name = name; } - - public static ValidationRuleAlertStatus fromString(String name) { + + public static FeatureStoreAlertStatus fromString(String name) { return valueOf(name.toUpperCase()); } public String getName() { return name; } - - public static ValidationRuleAlertStatus getStatus(FeatureGroupValidationStatus status) { + + public static FeatureStoreAlertStatus fromBooleanFeatureMonitorResultStatus(Boolean status) { + if (status) + return FEATURE_MONITOR_SHIFT_DETECTED; + else + return FEATURE_MONITOR_SHIFT_UNDETECTED; + } + + public static boolean isFeatureMonitoringStatus(FeatureStoreAlertStatus status) { switch (status) { - case FAILURE: - return ValidationRuleAlertStatus.FAILURE; - case SUCCESS: - return ValidationRuleAlertStatus.SUCCESS; - case WARNING: - return ValidationRuleAlertStatus.WARNING; + case FEATURE_MONITOR_SHIFT_DETECTED: + case FEATURE_MONITOR_SHIFT_UNDETECTED: + return Boolean.TRUE; default: - throw new IllegalArgumentException("Invalid enum constant");//will happen if status is none + return Boolean.FALSE; } } diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/Featuregroup.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/Featuregroup.java index 52a2771179..c8b22e7d41 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/Featuregroup.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/Featuregroup.java @@ -24,6 +24,7 @@ import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.ValidationReport; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.ondemand.OnDemandFeaturegroup; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.stream.StreamFeatureGroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; import io.hops.hopsworks.persistence.entity.featurestore.statistics.StatisticsConfig; import io.hops.hopsworks.persistence.entity.user.Users; @@ -158,7 +159,9 @@ public class Featuregroup implements Serializable { private ExpectationSuite expectationSuite; @OneToMany(cascade = CascadeType.ALL, mappedBy = "featuregroup") private Collection validationReports; - + @OneToMany(cascade = CascadeType.ALL, mappedBy = "featureGroup") + private Collection featureMonitoringConfigurations; + public Featuregroup() { } public Featuregroup(Integer id) { @@ -347,6 +350,15 @@ public void setEmbedding(Embedding embedding) { this.embedding = embedding; } + public Collection getFeatureMonitoringConfigurations() { + return featureMonitoringConfigurations; + } + + public void setFeatureMonitoringConfigurations( + Collection featureMonitoringConfigurations) { + this.featureMonitoringConfigurations = featureMonitoringConfigurations; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/alert/FeatureGroupAlert.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/alert/FeatureGroupAlert.java index 2c7a96306f..b6915f1cdc 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/alert/FeatureGroupAlert.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/alert/FeatureGroupAlert.java @@ -18,170 +18,52 @@ import io.hops.hopsworks.persistence.entity.alertmanager.AlertReceiver; import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlert; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; -import javax.persistence.Basic; -import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; import java.util.Date; +import java.util.Objects; @Entity @Table(name = "feature_group_alert", - catalog = "hopsworks", - schema = "") + catalog = "hopsworks", + schema = "") @XmlRootElement @NamedQueries({ - @NamedQuery(name = "FeatureGroupAlert.findAll", - query - = "SELECT f FROM FeatureGroupAlert f") + @NamedQuery(name = "FeatureGroupAlert.findByFeatureGroupAndId", + query + = "SELECT f FROM FeatureGroupAlert f WHERE f.featureGroup = :featureGroup AND f.id = :id") , - @NamedQuery(name = "FeatureGroupAlert.findById", - query - = "SELECT f FROM FeatureGroupAlert f WHERE f.id = :id") - , - @NamedQuery(name = "FeatureGroupAlert.findByFeatureGroupAndId", - query - = "SELECT f FROM FeatureGroupAlert f WHERE f.featureGroup = :featureGroup AND f.id = :id") - , - @NamedQuery(name = "FeatureGroupAlert.findByStatus", - query - = "SELECT f FROM FeatureGroupAlert f WHERE f.status = :status") - , - @NamedQuery(name = "FeatureGroupAlert.findByFeatureGroupAndStatus", - query + @NamedQuery(name = "FeatureGroupAlert.findByFeatureGroupAndStatus", + query = "SELECT f FROM FeatureGroupAlert f WHERE f.featureGroup = :featureGroup AND f.status = :status") - , - @NamedQuery(name = "FeatureGroupAlert.findByType", - query - = "SELECT f FROM FeatureGroupAlert f WHERE f.alertType = :alertType") - , - @NamedQuery(name = "FeatureGroupAlert.findBySeverity", - query - = "SELECT f FROM FeatureGroupAlert f WHERE f.severity = :severity") - , - @NamedQuery(name = "FeatureGroupAlert.findByCreated", - query - = "SELECT f FROM FeatureGroupAlert f WHERE f.created = :created")}) -public class FeatureGroupAlert implements Serializable { - - private static final long serialVersionUID = 1L; - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Basic(optional = false) - @Column(name = "id") - private Integer id; - @Basic(optional = false) - @NotNull - @Size(min = 1, - max = 45) - @Column(name = "status") - @Enumerated(EnumType.STRING) - private ValidationRuleAlertStatus status; - @Basic(optional = false) - @NotNull - @Size(min = 1, - max = 45) - @Column(name = "type") - @Enumerated(EnumType.STRING) - private AlertType alertType; - @Basic(optional = false) - @NotNull - @Size(min = 1, - max = 45) - @Column(name = "severity") - @Enumerated(EnumType.STRING) - private AlertSeverity severity; - @Basic(optional = false) - @NotNull - @Column(name = "created") - @Temporal(TemporalType.TIMESTAMP) - private Date created; - @JoinColumn(name = "feature_group_id", - referencedColumnName = "id") - @ManyToOne(optional = false) - private Featuregroup featureGroup; - @JoinColumn(name = "receiver", - referencedColumnName = "id") - @ManyToOne(optional = false) - private AlertReceiver receiver; - - public FeatureGroupAlert() { - } - - public FeatureGroupAlert(Integer id, - ValidationRuleAlertStatus status, AlertType alertType, - AlertSeverity severity, Date created, - Featuregroup featureGroup, AlertReceiver receiver) { - this.id = id; - this.status = status; - this.alertType = alertType; - this.severity = severity; - this.created = created; + }) +public class FeatureGroupAlert extends FeatureStoreAlert implements Serializable { + + public FeatureGroupAlert(Integer id, FeatureStoreAlertStatus status, AlertType alertType, AlertSeverity severity, + Date created, Featuregroup featureGroup, AlertReceiver receiver) { + super(id, status, alertType, severity, receiver, created); this.featureGroup = featureGroup; - this.receiver = receiver; - } - - public FeatureGroupAlert(Integer id) { - this.id = id; - } - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; } - - public ValidationRuleAlertStatus getStatus() { - return status; - } - - public void setStatus(ValidationRuleAlertStatus status) { - this.status = status; - } - - public AlertType getAlertType() { - return alertType; - } - - public void setAlertType(AlertType alertType) { - this.alertType = alertType; - } - - public AlertSeverity getSeverity() { - return severity; - } - - public void setSeverity(AlertSeverity severity) { - this.severity = severity; - } - - public Date getCreated() { - return created; - } - - public void setCreated(Date created) { - this.created = created; + + public FeatureGroupAlert() { + } - + @JoinColumn(name = "feature_group_id", + referencedColumnName = "id") + @ManyToOne(optional = false) + private Featuregroup featureGroup; + public Featuregroup getFeatureGroup() { return featureGroup; } @@ -189,38 +71,31 @@ public Featuregroup getFeatureGroup() { public void setFeatureGroup(Featuregroup featureGroup) { this.featureGroup = featureGroup; } - - public AlertReceiver getReceiver() { - return receiver; - } - - public void setReceiver(AlertReceiver receiver) { - this.receiver = receiver; - } - + @Override - public int hashCode() { - int hash = 0; - hash += (id != null ? id.hashCode() : 0); - return hash; - } - - @Override - public boolean equals(Object object) { - // TODO: Warning - this method won't work in the case the id fields are not set - if (!(object instanceof FeatureGroupAlert)) { + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FeatureGroupAlert)) { return false; } - FeatureGroupAlert other = (FeatureGroupAlert) object; - if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) { + if (!super.equals(o)) { return false; } - return true; + FeatureGroupAlert that = (FeatureGroupAlert) o; + return Objects.equals(featureGroup, that.featureGroup); } + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), featureGroup); + } + @Override public String toString() { - return "FeatureGroupExpectationAlert[ id=" + id + " ]"; + return "FeatureGroupAlert{" + + "featureGroup=" + featureGroup + + '}'; } - -} +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/FeatureMonitoringConfiguration.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/FeatureMonitoringConfiguration.java new file mode 100644 index 0000000000..8d9aaedd2d --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/FeatureMonitoringConfiguration.java @@ -0,0 +1,293 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config; + +import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.Featuregroup; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics.DescriptiveStatisticsComparisonConfig; +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; +import io.hops.hopsworks.persistence.entity.jobs.description.Jobs; +import io.hops.hopsworks.persistence.entity.jobs.scheduler.JobScheduleV2; + +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Collection; +import java.util.Objects; + +@Entity +@Table(name = "feature_monitoring_config", catalog = "hopsworks") +@NamedQueries({ + @NamedQuery(name = "FeatureMonitoringConfiguration.findByFeatureGroup", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + + " WHERE config.featureGroup=:featureGroup ORDER BY config.name DESC"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByFeatureGroupAndFeatureName", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + + " WHERE config.featureGroup=:featureGroup AND config.featureName=:featureName ORDER BY config.name DESC"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByFeatureGroupAndConfigId", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + + " WHERE config.featureGroup=:featureGroup AND config.id=:configId"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByFeatureView", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + + " WHERE config.featureView=:featureView ORDER BY config.name DESC"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByFeatureViewAndFeatureName", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + + " WHERE config.featureView=:featureView AND config.featureName=:featureName ORDER BY config.name DESC"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findById", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + " WHERE config.id=:configId"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByJobId", + query = "SELECT config FROM FeatureMonitoringConfiguration config WHERE config.job.id=:jobId"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByName", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + " WHERE config.name=:name"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByFeatureGroupAndName", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + + " WHERE config.name=:name AND config.featureGroup=:featureGroup"), + @NamedQuery(name = "FeatureMonitoringConfiguration.findByFeatureViewAndName", + query = "SELECT config FROM FeatureMonitoringConfiguration config" + + " WHERE config.name=:name AND config.featureView=:featureView"),}) +@XmlRootElement +public class FeatureMonitoringConfiguration implements Serializable { + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(name = "id") + private Integer id; + + @JoinColumn(name = "feature_group_id", referencedColumnName = "id") + private Featuregroup featureGroup; + + @JoinColumn(name = "feature_view_id", referencedColumnName = "id") + private FeatureView featureView; + + @NotNull + @Basic(optional = false) + @Column(name = "feature_name") + @Size(max = 63) + private String featureName; + + @NotNull + @Basic + @Column(name = "name") + @Size(max = 63) + private String name; + + @Basic + @Column(name = "description") + @Size(max = 2000) + private String description; + + @NotNull + @Enumerated(EnumType.ORDINAL) + @Column(name = "feature_monitoring_type") + private FeatureMonitoringType featureMonitoringType; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "job_id", referencedColumnName = "id") + private Jobs job; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "job_schedule_id", referencedColumnName = "id") + private JobScheduleV2 jobSchedule; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "detection_window_config_id", referencedColumnName = "id") + private MonitoringWindowConfiguration detectionWindowConfig; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "reference_window_config_id", referencedColumnName = "id") + private MonitoringWindowConfiguration referenceWindowConfig; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "statistics_comparison_config_id", referencedColumnName = "id") + private DescriptiveStatisticsComparisonConfig dsComparisonConfig; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "featureMonitoringConfig") + private Collection results; + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Featuregroup getFeatureGroup() { + return this.featureGroup; + } + + public void setFeatureGroup(Featuregroup featureGroup) { + this.featureGroup = featureGroup; + } + + public FeatureView getFeatureView() { + return this.featureView; + } + + public void setFeatureView(FeatureView featureView) { + this.featureView = featureView; + } + + public Jobs getJob() { + return job; + } + + public void setJob(Jobs job) { + this.job = job; + } + + public String getFeatureName() { + return this.featureName; + } + + public void setFeatureName(String featureName) { + this.featureName = featureName; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public JobScheduleV2 getJobSchedule() { + return this.jobSchedule; + } + + public void setJobSchedule(JobScheduleV2 jobSchedule) { + this.jobSchedule = jobSchedule; + } + + public FeatureMonitoringType getFeatureMonitoringType() { + return this.featureMonitoringType; + } + + public void setFeatureMonitoringType(FeatureMonitoringType featureMonitoringType) { + this.featureMonitoringType = featureMonitoringType; + } + + public MonitoringWindowConfiguration getDetectionWindowConfig() { + return this.detectionWindowConfig; + } + + public void setDetectionWindowConfig(MonitoringWindowConfiguration detectionWindowConfig) { + this.detectionWindowConfig = detectionWindowConfig; + } + + public MonitoringWindowConfiguration getReferenceWindowConfig() { + return this.referenceWindowConfig; + } + + public void setReferenceWindowConfig(MonitoringWindowConfiguration referenceWindowConfig) { + this.referenceWindowConfig = referenceWindowConfig; + } + + public DescriptiveStatisticsComparisonConfig getDsComparisonConfig() { + return this.dsComparisonConfig; + } + + public void setDsComparisonConfig(DescriptiveStatisticsComparisonConfig dsComparisonConfig) { + this.dsComparisonConfig = dsComparisonConfig; + } + + public Collection getResults() { + return this.results; + } + + public void setResults(Collection results) { + this.results = results; + } + + + // Convenience + public String getEntityName() { + if (featureGroup != null) { + return featureGroup.getName(); + } else { + return featureView.getName(); + } + } + + public Integer getEntityVersion() { + if (featureGroup != null) { + return featureGroup.getVersion(); + } else { + return featureView.getVersion(); + } + } + + public Integer getEntityId() { + if (featureGroup != null) { + return featureGroup.getId(); + } else { + return featureView.getId(); + } + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof FeatureMonitoringConfiguration)) { + return false; + } + FeatureMonitoringConfiguration featureMonitoringConfiguration = (FeatureMonitoringConfiguration) o; + Integer entityId = getEntityId(); + Integer otherEntityId = featureMonitoringConfiguration.getEntityId(); + return Objects.equals(id, featureMonitoringConfiguration.id) && + Objects.equals(name, featureMonitoringConfiguration.name) && + Objects.equals(entityId, otherEntityId) && + Objects.equals(featureName, featureMonitoringConfiguration.featureName) && + Objects.equals(description, featureMonitoringConfiguration.description) && + Objects.equals(featureMonitoringType, featureMonitoringConfiguration.featureMonitoringType); + } + + @Override + public int hashCode() { + Integer entityId = getEntityId(); + return Objects.hash(id, name, description, featureMonitoringType, featureName, entityId, job.getId()); + } + +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/FeatureGroupValidationStatus.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/FeatureMonitoringType.java similarity index 52% rename from hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/FeatureGroupValidationStatus.java rename to hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/FeatureMonitoringType.java index 8f0e26ab07..9167437655 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuregroup/datavalidation/FeatureGroupValidationStatus.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/FeatureMonitoringType.java @@ -1,6 +1,6 @@ /* * This file is part of Hopsworks - * Copyright (C) 2022, Logical Clocks AB. All rights reserved + * Copyright (C) 2024, Hopsworks AB. All rights reserved * * Hopsworks is free software: you can redistribute it and/or modify it under the terms of * the GNU Affero General Public License as published by the Free Software Foundation, @@ -14,36 +14,10 @@ * If not, see . */ -package io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation; +package io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config; -public enum FeatureGroupValidationStatus { - NONE("None", 0), - SUCCESS("Success", 1), - WARNING("Warning", 2), - FAILURE("Failure", 3); - - private final String name; - private final int severity; - - FeatureGroupValidationStatus(String name, int severity) { - this.name = name; - this.severity = severity; - } - - public int getSeverity() { - return severity; - } - - public static FeatureGroupValidationStatus fromString(String name) { - return valueOf(name.toUpperCase()); - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return name; - } +public enum FeatureMonitoringType { + STATISTICS_COMPUTATION, // only statistics computation for visual analysis + STATISTICS_COMPARISON, // statistics computation and comparison against a reference + PROBABILITY_DENSITY_FUNCTION, // data distributions } \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/MonitoringWindowConfiguration.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/MonitoringWindowConfiguration.java new file mode 100644 index 0000000000..c38e845a98 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/MonitoringWindowConfiguration.java @@ -0,0 +1,152 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.Size; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Objects; + + +@Entity +@Table(name = "monitoring_window_config", catalog = "hopsworks") +@XmlRootElement +public class MonitoringWindowConfiguration implements Serializable { + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(name = "id") + private Integer id; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "window_config_type") + private WindowConfigurationType windowConfigType; + + // Could think of a way to use these fields with string like "1ingestion" or "2upserts" + @Basic + @Column(name = "time_offset") + @Size(max = 63) + private String timeOffset; // Duration e.g "1w" + + @Basic + @Column(name = "window_length") + @Size(max = 63) + private String windowLength; // Duration e.g "1w" or "1h + + @Basic + @Column(name = "training_dataset_version") + private Integer trainingDatasetVersion; + + @Basic + @Column(name = "row_percentage") + private Float rowPercentage; + + @Basic + @Column(name = "specific_value") + private Double specificValue; + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public WindowConfigurationType getWindowConfigType() { + return this.windowConfigType; + } + + public void setWindowConfigType(WindowConfigurationType windowConfigType) { + this.windowConfigType = windowConfigType; + } + + public String getTimeOffset() { + return this.timeOffset; + } + + public void setTimeOffset(String timeOffset) { + this.timeOffset = timeOffset; + } + + public String getWindowLength() { + return this.windowLength; + } + + public void setWindowLength(String windowLength) { + this.windowLength = windowLength; + } + + public Integer getTrainingDatasetVersion() { + return this.trainingDatasetVersion; + } + + public void setTrainingDatasetVersion(Integer trainingDatasetVersion) { + this.trainingDatasetVersion = trainingDatasetVersion; + } + + public Double getSpecificValue() { + return this.specificValue; + } + + public void setSpecificValue(Double specificValue) { + this.specificValue = specificValue; + } + + public Float getRowPercentage() { + return this.rowPercentage; + } + + public void setRowPercentage(Float rowPercentage) { + this.rowPercentage = rowPercentage; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof MonitoringWindowConfiguration)) { + return false; + } + MonitoringWindowConfiguration monitoringWindowConfiguration = (MonitoringWindowConfiguration) o; + return Objects.equals(id, monitoringWindowConfiguration.id) && + Objects.equals(windowConfigType, monitoringWindowConfiguration.windowConfigType) && + Objects.equals(timeOffset, monitoringWindowConfiguration.timeOffset) && + Objects.equals(windowLength, monitoringWindowConfiguration.windowLength) && + Objects.equals(trainingDatasetVersion, monitoringWindowConfiguration.trainingDatasetVersion) && + Objects.equals(specificValue, monitoringWindowConfiguration.specificValue) && + Objects.equals(rowPercentage, monitoringWindowConfiguration.rowPercentage); + } + + @Override + public int hashCode() { + return Objects.hash(id, windowConfigType, timeOffset, windowLength, trainingDatasetVersion, rowPercentage, + specificValue); + } + +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/WindowConfigurationType.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/WindowConfigurationType.java new file mode 100644 index 0000000000..2e64ac5842 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/config/WindowConfigurationType.java @@ -0,0 +1,25 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config; + +public enum WindowConfigurationType { + ALL_TIME, + ROLLING_TIME, + // REFERENCE ONLY + TRAINING_DATASET, // on feature views only + SPECIFIC_VALUE, +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/DescriptiveStatisticsComparisonConfig.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/DescriptiveStatisticsComparisonConfig.java new file mode 100644 index 0000000000..17823e49c0 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/DescriptiveStatisticsComparisonConfig.java @@ -0,0 +1,126 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics; + +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Objects; + +@Entity +@Table(name = "statistics_comparison_config", catalog = "hopsworks") +@XmlRootElement +public class DescriptiveStatisticsComparisonConfig implements Serializable { + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(name = "id") + private Integer id; + + @OneToOne(mappedBy = "dsComparisonConfig") + private FeatureMonitoringConfiguration featureMonitoringConfig; + + @Basic + @Column(name = "threshold") + private Double threshold; + + @Basic + @Column(name = "relative") // apply treshold to relative or absolute difference + private Boolean relative; + + @Basic + @Column(name = "strict") + private Boolean strict; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "metric") + private MetricDescriptiveStatistics metric; + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Double getThreshold() { + return this.threshold; + } + + public void setThreshold(Double threshold) { + this.threshold = threshold; + } + + public Boolean getRelative() { + return this.relative; + } + + public void setRelative(Boolean relative) { + this.relative = relative; + } + + public Boolean getStrict() { + return this.strict; + } + + public void setStrict(Boolean strict) { + this.strict = strict; + } + + public MetricDescriptiveStatistics getMetric() { + return this.metric; + } + + public void setMetric(MetricDescriptiveStatistics metric) { + this.metric = metric; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof DescriptiveStatisticsComparisonConfig)) { + return false; + } + DescriptiveStatisticsComparisonConfig descriptiveStatisticsComparisonConfig = + (DescriptiveStatisticsComparisonConfig) o; + return Objects.equals(id, descriptiveStatisticsComparisonConfig.id) && + Objects.equals(threshold, descriptiveStatisticsComparisonConfig.threshold) && + Objects.equals(relative, descriptiveStatisticsComparisonConfig.relative) && + Objects.equals(strict, descriptiveStatisticsComparisonConfig.strict) && + Objects.equals(metric, descriptiveStatisticsComparisonConfig.metric); + } + + @Override + public int hashCode() { + return Objects.hash(id, threshold, strict, relative, metric); + } +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/MetricDescriptiveStatistics.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/MetricDescriptiveStatistics.java new file mode 100644 index 0000000000..887609b4a2 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/descriptivestatistics/MetricDescriptiveStatistics.java @@ -0,0 +1,34 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics; + +public enum MetricDescriptiveStatistics { + MEAN, + STDDEV, + MIN, + MAX, + SUM, + COUNT, + COMPLETENESS, + NUM_RECORDS_NON_NULL, + NUM_RECORDS_NULL, + DISTINCTNESS, + ENTROPY, + UNIQUENESS, + APPROXIMATE_NUM_DISTINCT_VALUES, + EXACT_NUM_DISTINCT_VALUES, +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/result/FeatureMonitoringResult.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/result/FeatureMonitoringResult.java new file mode 100644 index 0000000000..491b348725 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featuremonitoring/result/FeatureMonitoringResult.java @@ -0,0 +1,246 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result; + +import io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration; +import io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureDescriptiveStatistics; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; + +@Entity +@Table(name = "feature_monitoring_result", catalog = "hopsworks") +@NamedQueries({@NamedQuery(name = "FeatureMonitoringResult.findByResultId", + query = "SELECT result FROM FeatureMonitoringResult result WHERE result.id=:resultId")}) +@XmlRootElement +public class FeatureMonitoringResult implements Serializable { + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(name = "id") + private Integer id; + + @JoinColumn(name = "feature_monitoring_config_id", nullable = false, referencedColumnName = "id") + @ManyToOne(optional = false, fetch = FetchType.LAZY) + private FeatureMonitoringConfiguration featureMonitoringConfig; + + @Basic + @Column(name = "execution_id") + @NotNull + private Integer executionId; + + @Basic(optional = false) + @Column(name = "detection_stats_id", updatable = false) + @NotNull + private Integer detectionStatsId; + + @Basic + @Column(name = "reference_stats_id", updatable = false) + private Integer referenceStatsId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "detection_stats_id", referencedColumnName = "id", updatable = false, insertable = false) + private FeatureDescriptiveStatistics detectionStatistics; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reference_stats_id", referencedColumnName = "id", updatable = false, insertable = false) + private FeatureDescriptiveStatistics referenceStatistics; + + @Basic + @Column(name = "difference") + private Double difference; + + @Basic + @Column(name = "specific_value") + private Double specificValue; + + @Basic + @Column(name = "monitoring_time") + @Temporal(TemporalType.TIMESTAMP) + @NotNull + private Date monitoringTime; + + @Basic + @Column(name = "shift_detected") + private Boolean shiftDetected; + + @Basic + @Column(name = "feature_name") + @Size(max = 63) + private String featureName; + + @Basic + @Column(name = "raised_exception") + private Boolean raisedException; + + @Basic + @Column(name = "empty_detection_window") + private Boolean emptyDetectionWindow; + + @Basic + @Column(name = "empty_reference_window") + private Boolean emptyReferenceWindow; + + // additional config field that are editable could be added, e.g threshold + public Integer getId() { return id; } + + public void setId(Integer id) { this.id = id; } + + public String getFeatureName() { return featureName; } + + public void setFeatureName(String featureName) { this.featureName = featureName; } + + public FeatureMonitoringConfiguration getFeatureMonitoringConfig() { return featureMonitoringConfig; } + + public void setFeatureMonitoringConfig(FeatureMonitoringConfiguration featureMonitoringConfig) { + this.featureMonitoringConfig = featureMonitoringConfig; + } + + public Integer getExecutionId() { + return executionId; + } + + public void setExecutionId(Integer executionId) { + this.executionId = executionId; + } + + public Integer getDetectionStatsId() { + return detectionStatsId; + } + + public void setDetectionStatsId(Integer detectionStatsId) { + this.detectionStatsId = detectionStatsId; + } + + public Integer getReferenceStatsId() { + return referenceStatsId; + } + + public void setReferenceStatsId(Integer referenceStatsId) { + this.referenceStatsId = referenceStatsId; + } + + public FeatureDescriptiveStatistics getDetectionStatistics() { return detectionStatistics; } + + public FeatureDescriptiveStatistics getReferenceStatistics() { return referenceStatistics; } + + public Double getDifference() { + return difference; + } + + public void setDifference(Double difference) { + this.difference = difference; + } + + public Double getSpecificValue() { + return specificValue; + } + + public void setSpecificValue(Double specificValue) { + this.specificValue = specificValue; + } + + public Date getMonitoringTime() { + return monitoringTime; + } + + public void setMonitoringTime(Date monitoringTime) { + this.monitoringTime = monitoringTime; + } + + public Boolean getShiftDetected() { + return shiftDetected; + } + + public void setShiftDetected(Boolean shiftDetected) { + this.shiftDetected = shiftDetected; + } + + public Boolean getRaisedException() { + return raisedException; + } + + public void setRaisedException(Boolean raisedException) { + this.raisedException = raisedException; + } + + public Boolean getEmptyDetectionWindow() { + return emptyDetectionWindow; + } + + public void setEmptyDetectionWindow(Boolean emptyDetectionWindow) { + this.emptyDetectionWindow = emptyDetectionWindow; + } + + public Boolean getEmptyReferenceWindow() { + return emptyReferenceWindow; + } + + public void setEmptyReferenceWindow(Boolean emptyReferenceWindow) { + this.emptyReferenceWindow = emptyReferenceWindow; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof FeatureMonitoringResult)) { + return false; + } + FeatureMonitoringResult featureMonitoringResult = (FeatureMonitoringResult) o; + return Objects.equals(id, featureMonitoringResult.id) && + Objects.equals(featureMonitoringConfig.getId(), featureMonitoringResult.featureMonitoringConfig.getId()) && + Objects.equals(detectionStatsId, featureMonitoringResult.detectionStatsId) && + Objects.equals(referenceStatsId, featureMonitoringResult.referenceStatsId) && + Objects.equals(difference, featureMonitoringResult.difference) && + Objects.equals(specificValue, featureMonitoringResult.specificValue) && + Objects.equals(shiftDetected, featureMonitoringResult.shiftDetected) && + Objects.equals(featureName, featureMonitoringResult.featureName) && + Objects.equals(monitoringTime, featureMonitoringResult.monitoringTime) && + Objects.equals(raisedException, featureMonitoringResult.raisedException) && + Objects.equals(emptyDetectionWindow, featureMonitoringResult.emptyDetectionWindow) && + Objects.equals(emptyReferenceWindow, featureMonitoringResult.emptyReferenceWindow); + } + + @Override + public int hashCode() { + return Objects.hash(id, difference, specificValue, monitoringTime, shiftDetected, raisedException, + emptyDetectionWindow, emptyReferenceWindow); + } + +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featureview/alert/FeatureViewAlert.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featureview/alert/FeatureViewAlert.java new file mode 100644 index 0000000000..e7b4ba2273 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/featureview/alert/FeatureViewAlert.java @@ -0,0 +1,93 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.persistence.entity.featurestore.featureview.alert; + +import io.hops.hopsworks.persistence.entity.alertmanager.AlertReceiver; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertSeverity; +import io.hops.hopsworks.persistence.entity.alertmanager.AlertType; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlert; +import io.hops.hopsworks.persistence.entity.featurestore.alert.FeatureStoreAlertStatus; +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; + +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; + +@Entity +@Table(name="feature_view_alert", catalog="hopsworks") +@NamedQueries({ + @NamedQuery(name = "FeatureViewAlert.findByFeatureViewAndId", + query = "select f from FeatureViewAlert f where f.id = :id and f.featureView = :featureView"), + @NamedQuery(name = "FeatureViewAlert.findByFeatureViewAndStatus", + query = "select f from FeatureViewAlert f where f.featureView = :featureView and f.status = :status") + }) +@XmlRootElement +public class FeatureViewAlert extends FeatureStoreAlert implements Serializable { + + public FeatureViewAlert(Integer id, FeatureStoreAlertStatus status, AlertType alertType, AlertSeverity severity, + Date created, AlertReceiver receiver, FeatureView featureView) { + super(id, status, alertType, severity, receiver, created); + this.featureView = featureView; + } + public FeatureViewAlert() { + } + + @ManyToOne + @JoinColumn(name = "feature_view_id") + private FeatureView featureView; + + public FeatureView getFeatureView() { + return featureView; + } + + public void setFeatureView(FeatureView featureView) { + this.featureView = featureView; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FeatureViewAlert)) { + return false; + } + if (!super.equals(o)) { + return false; + } + FeatureViewAlert that = (FeatureViewAlert) o; + return Objects.equals(featureView, that.featureView); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), featureView); + } + + @Override + public String toString() { + return "FeatureViewAlert{" + + "featureView=" + featureView + + '}'; + } +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureGroupStatistics.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureGroupStatistics.java index f9c33d3355..d8c0673ce0 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureGroupStatistics.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureGroupStatistics.java @@ -70,33 +70,24 @@ public FeatureGroupStatistics() { // statistics of feature groups with time-travel disabled. - public FeatureGroupStatistics(Date commitTime, Float rowPercentage, + public FeatureGroupStatistics(Date computationTime, Float rowPercentage, Collection featureDescriptiveStatistics, Featuregroup featureGroup) { // statistics computed on the whole feature group data - super(commitTime, rowPercentage); + super(computationTime, rowPercentage); this.featureGroup = featureGroup; this.featureDescriptiveStatistics = featureDescriptiveStatistics; this.windowStartCommitTime = 0L; - this.windowEndCommitTime = commitTime.getTime(); + this.windowEndCommitTime = computationTime.getTime(); } // statistics of feature group with time-travel enabled - public FeatureGroupStatistics(Date commitTime, Long windowEndCommitTime, Float rowPercentage, - Collection featureDescriptiveStatistics, Featuregroup featuregroup) { - // statistics computed on feature group data for a specific commit - super(commitTime, rowPercentage); - this.featureGroup = featuregroup; - this.featureDescriptiveStatistics = featureDescriptiveStatistics; - this.windowStartCommitTime = 0L; - this.windowEndCommitTime = windowEndCommitTime; - } - - public FeatureGroupStatistics(Date commitTime, Long windowStartCommitTime, Long windowEndCommitTime, + public FeatureGroupStatistics(Date computationTime, Long windowStartCommitTime, Long windowEndCommitTime, Float rowPercentage, Collection featureDescriptiveStatistics, Featuregroup featuregroup) { - // statistics computed on feature group data for a specific commit window - super(commitTime, rowPercentage); + // statistics computed on feature group data for a specific commit window or a specific commit (i.e., start time = + // null) + super(computationTime, rowPercentage); this.featureGroup = featuregroup; this.featureDescriptiveStatistics = featureDescriptiveStatistics; this.windowStartCommitTime = windowStartCommitTime; diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatistics.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatistics.java new file mode 100644 index 0000000000..0766fe0fc4 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatistics.java @@ -0,0 +1,83 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.persistence.entity.featurestore.statistics; + +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Objects; + +/** + * Entity class representing the feature_view_descriptive_statistics table in Hopsworks database. + * An instance of this class represents a row in the database. + */ +@Entity +@Table(name = "feature_view_descriptive_statistics", catalog = "hopsworks") +@XmlRootElement +public class FeatureViewDescriptiveStatistics { + + @EmbeddedId + protected FeatureViewDescriptiveStatisticsPK featureViewDescriptiveStatisticsPK; + + public FeatureViewDescriptiveStatistics() { + } + + public FeatureViewDescriptiveStatistics(FeatureViewStatistics featureViewStatistics, + FeatureDescriptiveStatistics featureDescriptiveStatistics) { + this.featureViewDescriptiveStatisticsPK = + new FeatureViewDescriptiveStatisticsPK(featureViewStatistics.getId(), featureDescriptiveStatistics.getId()); + } + + public Integer getFeatureViewStatisticsId() { + return featureViewDescriptiveStatisticsPK.getFeatureViewStatisticsId(); + } + + public void setFeatureViewStatisticsId(Integer featureViewStatisticsId) { + this.featureViewDescriptiveStatisticsPK.setFeatureViewStatisticsId(featureViewStatisticsId); + } + + public Integer getFeatureDescriptiveStatisticsId() { + return this.featureViewDescriptiveStatisticsPK.getFeatureDescriptiveStatisticsId(); + } + + public void setFeatureDescriptiveStatisticsId(Integer featureDescriptiveStatisticsId) { + this.featureViewDescriptiveStatisticsPK.setFeatureDescriptiveStatisticsId(featureDescriptiveStatisticsId); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FeatureViewDescriptiveStatistics that = (FeatureViewDescriptiveStatistics) o; + if (!Objects.equals(featureViewDescriptiveStatisticsPK, that.featureViewDescriptiveStatisticsPK)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + featureViewDescriptiveStatisticsPK.hashCode(); + return result; + } +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatisticsPK.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatisticsPK.java new file mode 100644 index 0000000000..870c802452 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewDescriptiveStatisticsPK.java @@ -0,0 +1,83 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.persistence.entity.featurestore.statistics; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Embeddable +public class FeatureViewDescriptiveStatisticsPK implements Serializable { + + @Basic(optional = false) + @NotNull + @Column(name = "feature_view_statistics_id") + private Integer featureViewStatisticsId; + + @Basic(optional = false) + @NotNull + @Column(name = "feature_descriptive_statistics_id") + private Integer featureDescriptiveStatisticsId; + + public FeatureViewDescriptiveStatisticsPK() { + } + + public FeatureViewDescriptiveStatisticsPK(Integer featureViewStatisticsId, Integer featureDescriptiveStatisticsId) { + this.featureViewStatisticsId = featureViewStatisticsId; + this.featureDescriptiveStatisticsId = featureDescriptiveStatisticsId; + } + + public Integer getFeatureViewStatisticsId() { + return featureViewStatisticsId; + } + + public void setFeatureViewStatisticsId(Integer featureViewStatisticsId) { + this.featureViewStatisticsId = featureViewStatisticsId; + } + + public Integer getFeatureDescriptiveStatisticsId() { + return featureDescriptiveStatisticsId; + } + + public void setFeatureDescriptiveStatisticsId(Integer featureDescriptiveStatisticsId) { + this.featureDescriptiveStatisticsId = featureDescriptiveStatisticsId; + } + + @Override + public int hashCode() { + int hash = 0; + hash += featureViewStatisticsId.hashCode(); + hash += featureDescriptiveStatisticsId.hashCode(); + return hash; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof FeatureViewDescriptiveStatisticsPK)) { + return false; + } + FeatureViewDescriptiveStatisticsPK other = (FeatureViewDescriptiveStatisticsPK) object; + if (!this.featureViewStatisticsId.equals(other.featureViewStatisticsId)) { + return false; + } + if (!this.featureDescriptiveStatisticsId.equals(other.featureDescriptiveStatisticsId)) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewStatistics.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewStatistics.java new file mode 100644 index 0000000000..7febf687c4 --- /dev/null +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/featurestore/statistics/FeatureViewStatistics.java @@ -0,0 +1,163 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2024, Hopsworks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.persistence.entity.featurestore.statistics; + +import io.hops.hopsworks.persistence.entity.featurestore.featureview.FeatureView; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; +import java.util.Date; +import java.util.Objects; + +/** + * Entity class representing the feature_view_statistics table in Hopsworks database. + * An instance of this class represents a row in the database. + */ +@Entity +@Table(name = "feature_view_statistics", catalog = "hopsworks") +@XmlRootElement +@NamedQueries( + {@NamedQuery(name = "FeatureViewStatistics.findAll", query = "SELECT fgs FROM FeatureViewStatistics fgs"), + @NamedQuery(name = "FeatureViewStatistics.findById", + query = "SELECT s FROM FeatureViewStatistics s WHERE s.id = :id")}) +public class FeatureViewStatistics extends EntityStatistics { + + @JoinTable(name = "hopsworks.feature_view_descriptive_statistics", + joinColumns = {@JoinColumn(name = "feature_view_statistics_id", referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "feature_descriptive_statistics_id", referencedColumnName = "id")}) + @ManyToMany(fetch = FetchType.LAZY) + private Collection featureDescriptiveStatistics; + + @JoinColumn(name = "feature_view_id", referencedColumnName = "id") + private FeatureView featureView; + + @Basic(optional = false) + @Column(name = "window_start_commit_time") + @NotNull + private Long windowStartCommitTime; + + @Basic(optional = false) + @Column(name = "window_end_commit_time") + @NotNull + private Long windowEndCommitTime; + + public FeatureViewStatistics() { + } + + // statistics of joined feature groups with time-travel disabled. + + public FeatureViewStatistics(Date computationTime, Float rowPercentage, + Collection featureDescriptiveStatistics, FeatureView featureView) { + // statistics computed on the whole joined feature group data + super(computationTime, rowPercentage); + this.featureView = featureView; + this.featureDescriptiveStatistics = featureDescriptiveStatistics; + this.windowStartCommitTime = 0L; + this.windowEndCommitTime = computationTime.getTime(); + } + + // statistics of joined feature group with time-travel enabled + + public FeatureViewStatistics(Date computationTime, Long windowStartCommitTime, Long windowEndCommitTime, + Float rowPercentage, Collection featureDescriptiveStatistics, + FeatureView featureView) { + // statistics computed on joined feature group data for a specific commit window or a specific commit (i.e., start + // time = null) + super(computationTime, rowPercentage); + this.featureView = featureView; + this.featureDescriptiveStatistics = featureDescriptiveStatistics; + this.windowStartCommitTime = windowStartCommitTime; + this.windowEndCommitTime = windowEndCommitTime; + } + + public Collection getFeatureDescriptiveStatistics() { + return featureDescriptiveStatistics; + } + + public void setFeatureDescriptiveStatistics(Collection featureDescriptiveStatistics) { + this.featureDescriptiveStatistics = featureDescriptiveStatistics; + } + + public FeatureView getFeatureView() { + return featureView; + } + + public void setFeatureView(FeatureView featureView) { + this.featureView = featureView; + } + + public Long getWindowStartCommitTime() { + return windowStartCommitTime; + } + + public void setWindowStartCommitTime(Long windowStartCommitTime) { + this.windowStartCommitTime = windowStartCommitTime; + } + + public Long getWindowEndCommitTime() { + return windowEndCommitTime; + } + + public void setWindowEndCommitTime(Long windowEndCommitTime) { + this.windowEndCommitTime = windowEndCommitTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FeatureViewStatistics that = (FeatureViewStatistics) o; + + if (!super.equals(that)) { + return false; + } + if (!Objects.equals(featureView, that.featureView)) { + return false; + } + if (!Objects.equals(windowStartCommitTime, that.windowStartCommitTime)) { + return false; + } + if (!Objects.equals(windowEndCommitTime, that.windowEndCommitTime)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (featureView != null ? featureView.hashCode() : 0); + result = 31 * result + (windowStartCommitTime != null ? windowStartCommitTime.hashCode() : 0); + result = 31 * result + (windowEndCommitTime != null ? windowEndCommitTime.hashCode() : 0); + return result; + } +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/project/alert/ProjectServiceAlertStatus.java b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/project/alert/ProjectServiceAlertStatus.java index 55dc60c86b..e9b89ca138 100644 --- a/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/project/alert/ProjectServiceAlertStatus.java +++ b/hopsworks-persistence/src/main/java/io/hops/hopsworks/persistence/entity/project/alert/ProjectServiceAlertStatus.java @@ -15,7 +15,6 @@ */ package io.hops.hopsworks.persistence.entity.project.alert; -import io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.FeatureGroupValidationStatus; import io.hops.hopsworks.persistence.entity.jobs.configuration.history.JobFinalStatus; import io.hops.hopsworks.persistence.entity.jobs.configuration.history.JobState; @@ -27,8 +26,11 @@ public enum ProjectServiceAlertStatus { //Job JOB_FINISHED("Finished"), JOB_FAILED("Failed"), - JOB_KILLED("Killed"); - + JOB_KILLED("Killed"), + // feature monitoring status + FEATURE_MONITOR_SHIFT_UNDETECTED("FEATURE_MONITOR_SHIFT_UNDETECTED"), + FEATURE_MONITOR_SHIFT_DETECTED("FEATURE_MONITOR_SHIFT_DETECTED"); + private final String name; ProjectServiceAlertStatus(String name) { @@ -42,20 +44,7 @@ public static ProjectServiceAlertStatus fromString(String name) { public String getName() { return name; } - - public static ProjectServiceAlertStatus getStatus(FeatureGroupValidationStatus status) { - switch (status) { - case FAILURE: - return ProjectServiceAlertStatus.VALIDATION_FAILURE; - case SUCCESS: - return ProjectServiceAlertStatus.VALIDATION_SUCCESS; - case WARNING: - return ProjectServiceAlertStatus.VALIDATION_WARNING; - default: - throw new IllegalArgumentException("Invalid enum constant");//will happen if status is none - } - } - + public static ProjectServiceAlertStatus getJobAlertStatus(JobState jobState) { switch (jobState) { case FINISHED: @@ -101,14 +90,24 @@ public boolean isFeatureGroupStatus() { case VALIDATION_FAILURE: case VALIDATION_SUCCESS: case VALIDATION_WARNING: + case FEATURE_MONITOR_SHIFT_UNDETECTED: + case FEATURE_MONITOR_SHIFT_DETECTED: return true; default: return false; } } - + + public static ProjectServiceAlertStatus fromBooleanFeatureMonitorResultStatus(Boolean status) { + if (status) + return FEATURE_MONITOR_SHIFT_DETECTED; + else + return FEATURE_MONITOR_SHIFT_UNDETECTED; + } + + @Override public String toString() { return name; } -} +} \ No newline at end of file diff --git a/hopsworks-persistence/src/main/resources/META-INF/persistence.xml b/hopsworks-persistence/src/main/resources/META-INF/persistence.xml index 4e381848e6..3a670c3236 100644 --- a/hopsworks-persistence/src/main/resources/META-INF/persistence.xml +++ b/hopsworks-persistence/src/main/resources/META-INF/persistence.xml @@ -84,8 +84,10 @@ io.hops.hopsworks.persistence.entity.featurestore.statistics.StatisticsConfig io.hops.hopsworks.persistence.entity.featurestore.statistics.StatisticColumn io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupStatistics + io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewStatistics io.hops.hopsworks.persistence.entity.featurestore.statistics.TrainingDatasetStatistics io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureGroupDescriptiveStatistics + io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureViewDescriptiveStatistics io.hops.hopsworks.persistence.entity.featurestore.statistics.FeatureDescriptiveStatistics io.hops.hopsworks.persistence.entity.featurestore.statistics.PercentilesConverter io.hops.hopsworks.persistence.entity.featurestore.code.FeaturestoreCode @@ -94,6 +96,10 @@ io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.Expectation io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.ValidationReport io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidationv2.ValidationResult + io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.FeatureMonitoringConfiguration + io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.config.MonitoringWindowConfiguration + io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.descriptivestatistics.DescriptiveStatisticsComparisonConfig + io.hops.hopsworks.persistence.entity.featurestore.featuremonitoring.result.FeatureMonitoringResult io.hops.hopsworks.persistence.entity.featurestore.transformationFunction.TransformationFunction io.hops.hopsworks.persistence.entity.hdfs.HdfsDirectoryWithQuotaFeature io.hops.hopsworks.persistence.entity.hdfs.HdfsLeDescriptors @@ -161,6 +167,7 @@ io.hops.hopsworks.persistence.entity.alertmanager.AlertManagerConfigEntity io.hops.hopsworks.persistence.entity.jobs.description.JobAlert io.hops.hopsworks.persistence.entity.featurestore.featuregroup.datavalidation.alert.FeatureGroupAlert + io.hops.hopsworks.persistence.entity.featurestore.featureview.alert.FeatureViewAlert io.hops.hopsworks.persistence.entity.project.alert.ProjectServiceAlert io.hops.hopsworks.persistence.entity.git.GitOpExecution diff --git a/hopsworks-rest-utils/src/main/java/io/hops/hopsworks/restutils/RESTCodes.java b/hopsworks-rest-utils/src/main/java/io/hops/hopsworks/restutils/RESTCodes.java index bad67f9353..9fba42196f 100644 --- a/hopsworks-rest-utils/src/main/java/io/hops/hopsworks/restutils/RESTCodes.java +++ b/hopsworks-rest-utils/src/main/java/io/hops/hopsworks/restutils/RESTCodes.java @@ -455,7 +455,10 @@ public enum JobErrorCode implements RESTErrorCode { EXECUTIONS_LIMIT_REACHED(40, "Job reached the maximum number of executions.", Response.Status.BAD_REQUEST), JOB_ALREADY_EXISTS(41, "Job with this name already exists.", Response.Status.BAD_REQUEST), - JOB_SCHEDULE_NOT_FOUND(42, "Cannot find the job schedule.", Response.Status.NOT_FOUND); + JOB_SCHEDULE_NOT_FOUND(42, "Cannot find the job schedule.", Response.Status.NOT_FOUND), + UNMATCHED_JOB_NAME(43, "Provided job names do not match.", Response.Status.BAD_REQUEST), + UNMATCHED_JOB_SCHEDULE_AND_JOB_NAME(44, "Requested job schedule id does not match the job name.", + Response.Status.BAD_REQUEST); private Integer code; private String message; @@ -1692,7 +1695,10 @@ public enum FeaturestoreErrorCode implements RESTErrorCode { COULD_NOT_INITIATE_ARROW_FLIGHT_CONNECTION(231, "Could not initiate connection to Arrow Flight server", Response.Status.INTERNAL_SERVER_ERROR), ARROW_FLIGHT_READ_QUERY_ERROR(232, "Arrow Flight server Read Query failed", - Response.Status.INTERNAL_SERVER_ERROR); + Response.Status.INTERNAL_SERVER_ERROR), + FEATURE_MONITORING_ENTITY_NOT_FOUND(233, "Feature Monitoring entity not found.", + Response.Status.NOT_FOUND), + FEATURE_MONITORING_NOT_ENABLED(234, "Feature monitoring is not enabled.", Response.Status.BAD_REQUEST); private int code; private String message;