def deleteMetricByName(self, metricName): """ Delete both metric and corresponding model (if any) NOTE: this method is specific to HTM Metrics :param metricName: name of the HTM metric :type metricName: string :raises htmengine.exceptions.ObjectNotFoundError: if the requested metric doesn't exist """ with self.connectionFactory() as conn: metricObj = repository.retryOnTransientErrors( repository.getCustomMetricByName)(conn, metricName, fields=[schema.metric.c.uid, schema.metric.c.status]) if metricObj.status != MetricStatus.UNMONITORED: self.unmonitorMetric(metricObj.uid) # Delete the metric from the database with self.connectionFactory() as conn: repository.retryOnTransientErrors(repository.deleteMetric)( conn, metricObj.uid) self._log.info("HTM Metric deleted: metric=%s; name=%s", metricObj.uid, metricName)
def deleteMetricByName(self, metricName): """ Delete both metric and corresponding model (if any) NOTE: this method is specific to HTM Metrics :param metricName: name of the HTM metric :type metricName: string :raises htmengine.exceptions.ObjectNotFoundError: if the requested metric doesn't exist """ with self.connectionFactory() as conn: metricObj = repository.retryOnTransientErrors( repository.getCustomMetricByName)( conn, metricName, fields=[schema.metric.c.uid, schema.metric.c.status]) if metricObj.status != MetricStatus.UNMONITORED: self.unmonitorMetric(metricObj.uid) # Delete the metric from the database with self.connectionFactory() as conn: repository.retryOnTransientErrors(repository.deleteMetric)( conn, metricObj.uid) self._log.info("HTM Metric deleted: metric=%s; name=%s", metricObj.uid, metricName)
def importModel(self, spec): """ Import a model :param spec: datasource-specific value created by `exportModel` :type spec: dict :: { "datasource": "custom", "metricSpec": { "metric": "prod.web.14.memory", "unit": "Count" # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional }, "data": [[value, "2014-07-17 01:36:48"], ...] # optional } :returns: datasource-specific unique metric identifier """ try: metricId = self.createMetric(spec["metricSpec"]["metric"]) except app_exceptions.MetricAlreadyExists as e: metricId = e.uid with self.connectionFactory() as conn: metricObj = repository.retryOnTransientErrors(repository.getMetric)( conn, metricId) if metricObj.status != MetricStatus.UNMONITORED: self._log.info("importModel: metric is already monitored: %r", metricObj) return metricId # Add data data = spec.get("data") if data: if repository.getMetricDataCount(conn, metricId) == 0: repository.retryOnTransientErrors(repository.addMetricData)(conn, metricId, data) # Start monitoring modelSpec = spec.copy() modelSpec.pop("data", None) return self.monitorMetric(modelSpec)
def importModel(self, spec): """ Import a model :param spec: datasource-specific value created by `exportModel` :type spec: dict :: { "datasource": "custom", "metricSpec": { "metric": "prod.web.14.memory", "unit": "Count" # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional }, "data": [[value, "2014-07-17 01:36:48"], ...] # optional } :returns: datasource-specific unique metric identifier """ try: metricId = self.createMetric(spec["metricSpec"]["metric"]) except app_exceptions.MetricAlreadyExists as e: metricId = e.uid with self.connectionFactory() as conn: metricObj = repository.retryOnTransientErrors( repository.getMetric)(conn, metricId) if metricObj.status != MetricStatus.UNMONITORED: self._log.info("importModel: metric is already monitored: %r", metricObj) return metricId # Add data data = spec.get("data") if data: if repository.getMetricDataCount(conn, metricId) == 0: repository.retryOnTransientErrors( repository.addMetricData)(conn, metricId, data) # Start monitoring modelSpec = spec.copy() modelSpec.pop("data", None) return self.monitorMetric(modelSpec)
def exportModel(self, metricId): """ Export the given model :param metricId: datasource-specific unique metric identifier :returns: model-export specification for HTM model :rtype: dict :: { "datasource": "custom", "metricSpec": { "metric": "prod.web.14.memory", "unit": "Count" # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional }, "data": [[value, datetime.datetime], ...] # optional } :raises htmengine.exceptions.ObjectNotFoundError: if metric with the referenced metric uid doesn't exist """ with self.connectionFactory() as conn: metricObj = repository.retryOnTransientErrors( repository.getMetric)(conn, metricId) if metricObj.datasource != self._DATASOURCE: raise TypeError("exportModel: not an HTM metric=%r" % (metricObj, )) data = repository.getMetricData( conn, metricId, fields=[ schema.metric_data.c.metric_value, schema.metric_data.c.timestamp ], fromTimestamp=datetime.datetime.utcnow() - datetime.timedelta(days=14)) modelSpec = htmengine.utils.jsonDecode(metricObj.parameters) modelSpec["data"] = list(data) return modelSpec
def exportModel(self, metricId): """ Export the given model :param metricId: datasource-specific unique metric identifier :returns: model-export specification for HTM model :rtype: dict :: { "datasource": "custom", "metricSpec": { "metric": "prod.web.14.memory", "unit": "Count" # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional }, "data": [[value, datetime.datetime], ...] # optional } :raises htmengine.exceptions.ObjectNotFoundError: if metric with the referenced metric uid doesn't exist """ with self.connectionFactory() as conn: metricObj = repository.retryOnTransientErrors(repository.getMetric)( conn, metricId) if metricObj.datasource != self._DATASOURCE: raise TypeError("exportModel: not an HTM metric=%r" % (metricObj,)) data = repository.getMetricData( conn, metricId, fields=[schema.metric_data.c.metric_value, schema.metric_data.c.timestamp], fromTimestamp=datetime.datetime.utcnow() - datetime.timedelta(days=14)) modelSpec = htmengine.utils.jsonDecode(metricObj.parameters) modelSpec["data"] = list(data) return modelSpec
def _getMetricStatistics(self, metricId): """ Get metric data statistics :param metricId: unique identifier of the metric row :returns: a dictionary with the metric's statistics :rtype: dict; {"min": <min-value>, "max": <max-value>} :raises htmengine.exceptions.ObjectNotFoundError: if metric with the referenced metric uid doesn't exist :raises htmengine.exceptions.MetricStatisticsNotReadyError: """ with self.connectionFactory() as conn: stats = repository.retryOnTransientErrors( repository.getMetricStats)(conn, metricId) minVal = stats["min"] maxVal = stats["max"] # Handle an edge case which should almost never happen # (borrowed from legacy custom adapter logic) if maxVal <= minVal: self._log.warn( "Max encoder value (%g) is not greater than min (%g).", maxVal if maxVal is not None else float("nan"), minVal if minVal is not None else float("nan")) maxVal = minVal + 1 # Add a 20% buffer on both ends of the range # (borrowed from legacy custom adapter) buff = (maxVal - minVal) * 0.2 minVal -= buff maxVal += buff self._log.debug( "getMetricStatistics for metric=%s: minVal=%g, " "maxVal=%g.", metricId, minVal, maxVal) return {"max": maxVal, "min": minVal}
def _getMetricStatistics(self, metricId): """ Get metric data statistics :param metricId: unique identifier of the metric row :returns: a dictionary with the metric's statistics :rtype: dict; {"min": <min-value>, "max": <max-value>} :raises htmengine.exceptions.ObjectNotFoundError: if metric with the referenced metric uid doesn't exist :raises htmengine.exceptions.MetricStatisticsNotReadyError: """ with self.connectionFactory() as conn: stats = repository.retryOnTransientErrors(repository.getMetricStats)( conn, metricId) minVal = stats["min"] maxVal = stats["max"] # Handle an edge case which should almost never happen # (borrowed from legacy custom adapter logic) if maxVal <= minVal: self._log.warn("Max encoder value (%g) is not greater than min (%g).", maxVal if maxVal is not None else float("nan"), minVal if minVal is not None else float("nan")) maxVal = minVal + 1 # Add a 20% buffer on both ends of the range # (borrowed from legacy custom adapter) buff = (maxVal - minVal) * 0.2 minVal -= buff maxVal += buff self._log.debug("getMetricStatistics for metric=%s: minVal=%g, " "maxVal=%g.", metricId, minVal, maxVal) return {"max": maxVal, "min": minVal}
def monitorMetric(self, modelSpec): """ Start monitoring a metric; perform model creation logic specific to custom metrics. Start the model if possible: this will happen if modelParams includes both "min" and "max" or there is enough data to estimate them. Alternatively, users may pass in a full set of model parameters. :param modelSpec: model specification for HTM model; per ``model_spec_schema.json`` with the ``metricSpec`` property per ``custom_metric_spec_schema.json`` :type modelSpec: dict :: 1st variant: `uid` is the unique id of an existing metric; # TODO: it would be preferable to punt on this variant, and refer # to custom metric by name in modelSpec for consistency with # import/export. Web GUI uses metric name; some tests use this variant, # though. { "datasource": "custom", "metricSpec": { "uid": "4a833e2294494b4fbc5004e03bad45b6", "unit": "Count", # optional "resource": "prod.web1", # optional "userInfo": {"symbol": "<TICKER_SYMBOL>"} # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional } } :: 2nd variant: `metric` is the unique name of the metric; a new custom metric row will be created with this name, if it doesn't exit { "datasource": "custom", "metricSpec": { "metric": "prod.web.14.memory", "unit": "Count", # optional "resource": "prod.web1", # optional "userInfo": {"symbol": "<TICKER_SYMBOL>"} # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional } } :returns: datasource-specific unique model identifier :raises ValueError: if finds something invalid in arg :raises TypeError: if metric with the referenced uid is not a custom metric :raises htmengine.exceptions.ObjectNotFoundError: if referenced metric doesn't exist :raises htmengine.exceptions.MetricNotSupportedError: if requested metric isn't supported :raises htmengine.exceptions.MetricAlreadyMonitored: if the metric is already being monitored """ metricSpec = modelSpec["metricSpec"] with self.connectionFactory() as conn: if "uid" in metricSpec: # Via metric ID metricId = metricSpec["uid"] # Convert modelSpec to canonical form modelSpec = copy.deepcopy(modelSpec) modelSpec["metricSpec"].pop("uid") modelSpec["metricSpec"]["metric"] = ( repository.retryOnTransientErrors(repository.getMetric)( conn, metricId).name) elif "metric" in metricSpec: # Via metric name try: # Create the metric, if needed metricId = repository.retryOnTransientErrors(self._createMetric)( conn, metricSpec["metric"]) except app_exceptions.MetricAlreadyExists as e: # It already existed metricId = e.uid else: raise ValueError( "Neither uid nor metric name present in metricSpec; modelSpec=%r" % (modelSpec,)) if "completeModelParams" in modelSpec: # Attempt to build swarm params from complete model params if present swarmParams = ( scalar_metric_utils.generateSwarmParamsFromCompleteModelParams( modelSpec)) else: # Generate swarm params from specified metric min and max or estimate them modelParams = modelSpec.get("modelParams", dict()) minVal = modelParams.get("min") maxVal = modelParams.get("max") minResolution = modelParams.get("minResolution") enableClassifier = modelParams.get("enableClassifier", False) if (minVal is None) != (maxVal is None): raise ValueError( "min and max params must both be None or non-None; metric=%s; " "modelSpec=%r" % (metricId, modelSpec,)) # Start monitoring if minVal is None or maxVal is None: minVal = maxVal = None with self.connectionFactory() as conn: numDataRows = repository.retryOnTransientErrors( repository.getMetricDataCount)(conn, metricId) if numDataRows >= scalar_metric_utils.MODEL_CREATION_RECORD_THRESHOLD: try: stats = self._getMetricStatistics(metricId) self._log.info("monitorMetric: trigger numDataRows=%d, stats=%s", numDataRows, stats) minVal = stats["min"] maxVal = stats["max"] except app_exceptions.MetricStatisticsNotReadyError: pass stats = {"min": minVal, "max": maxVal, "minResolution": minResolution} self._log.debug("monitorMetric: metric=%s, stats=%r", metricId, stats) swarmParams = scalar_metric_utils.generateSwarmParams(stats, enableClassifier) self._startMonitoringWithRetries(metricId, modelSpec, swarmParams) return metricId
def monitorMetric(self, modelSpec): """ Start monitoring a metric; perform model creation logic specific to custom metrics. Start the model if possible: this will happen if modelParams includes both "min" and "max" or there is enough data to estimate them. :param modelSpec: model specification for HTM model; per ``model_spec_schema.json`` with the ``metricSpec`` property per ``custom_metric_spec_schema.json`` :type modelSpec: dict :: 1st variant: `uid` is the unique id of an existing metric; # TODO: it would be preferable to punt on this variant, and refer # to custom metric by name in modelSpec for consistency with # import/export. Web GUI uses metric name; some tests use this variant, # though. { "datasource": "custom", "metricSpec": { "uid": "4a833e2294494b4fbc5004e03bad45b6", "unit": "Count", # optional "resource": "prod.web1", # optional "userInfo": {"symbol": "<TICKER_SYMBOL>"} # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional } } :: 2nd variant: `metric` is the unique name of the metric; a new custom metric row will be created with this name, if it doesn't exit { "datasource": "custom", "metricSpec": { "metric": "prod.web.14.memory", "unit": "Count", # optional "resource": "prod.web1", # optional "userInfo": {"symbol": "<TICKER_SYMBOL>"} # optional }, # Optional model params "modelParams": { "min": min-value, # optional "max": max-value # optional } } :returns: datasource-specific unique model identifier :raises ValueError: if finds something invalid in arg :raises TypeError: if metric with the referenced uid is not a custom metric :raises htmengine.exceptions.ObjectNotFoundError: if referenced metric doesn't exist :raises htmengine.exceptions.MetricNotSupportedError: if requested metric isn't supported :raises htmengine.exceptions.MetricAlreadyMonitored: if the metric is already being monitored """ metricSpec = modelSpec["metricSpec"] with self.connectionFactory() as conn: if "uid" in metricSpec: # Via metric ID metricId = metricSpec["uid"] # Convert modelSpec to canonical form modelSpec = copy.deepcopy(modelSpec) modelSpec["metricSpec"].pop("uid") modelSpec["metricSpec"]["metric"] = ( repository.retryOnTransientErrors(repository.getMetric)( conn, metricId).name) elif "metric" in metricSpec: # Via metric name try: # Crete the metric, if needed metricId = repository.retryOnTransientErrors( self._createMetric)(conn, metricSpec["metric"]) except app_exceptions.MetricAlreadyExists as e: # It already existed metricId = e.uid else: raise ValueError( "Neither uid nor metric name present in metricSpec; modelSpec=%r" % (modelSpec, )) modelParams = modelSpec.get("modelParams", dict()) minVal = modelParams.get("min") maxVal = modelParams.get("max") minResolution = modelParams.get("minResolution") if (minVal is None) != (maxVal is None): raise ValueError( "min and max params must both be None or non-None; metric=%s; " "modelSpec=%r" % ( metricId, modelSpec, )) # Start monitoring if minVal is None or maxVal is None: minVal = maxVal = None with self.connectionFactory() as conn: numDataRows = repository.retryOnTransientErrors( repository.getMetricDataCount)(conn, metricId) if numDataRows >= scalar_metric_utils.MODEL_CREATION_RECORD_THRESHOLD: try: stats = self._getMetricStatistics(metricId) self._log.info( "monitorMetric: trigger numDataRows=%d, stats=%s", numDataRows, stats) minVal = stats["min"] maxVal = stats["max"] except app_exceptions.MetricStatisticsNotReadyError: pass stats = {"min": minVal, "max": maxVal, "minResolution": minResolution} self._log.debug("monitorMetric: metric=%s, stats=%r", metricId, stats) swarmParams = scalar_metric_utils.generateSwarmParams(stats) self._startMonitoringWithRetries(metricId, modelSpec, swarmParams) return metricId