diff --git a/AUTHORS b/AUTHORS index 5af9aebdf..b3eb5f7c0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,7 @@ Angelo Mendonca Ben Motz Brad Klein Craig Bryant +David C Kennedy Deklan Dieterly Deklan Dieterly Deklan Dieterly @@ -38,6 +39,7 @@ Tomasz Trębski Tong Li Victor Ion Munteanu Witold Bedyk +ZhiQiang Fan alpineriveredge bklei cindy oneill diff --git a/devstack/files/schema/mon_mysql.sql b/devstack/files/schema/mon_mysql.sql index 325a5fc6c..3ca50daa2 100644 --- a/devstack/files/schema/mon_mysql.sql +++ b/devstack/files/schema/mon_mysql.sql @@ -1,5 +1,6 @@ /* * (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +* Copyright 2016 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,6 +157,7 @@ CREATE TABLE `sub_alarm_definition` ( `threshold` double NOT NULL, `period` int(11) NOT NULL, `periods` int(11) NOT NULL, + `is_deterministic` tinyint(1) NOT NULL DEFAULT '0', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`), diff --git a/docs/monasca-api-spec.md b/docs/monasca-api-spec.md index 11098c3d5..cece63b0f 100644 --- a/docs/monasca-api-spec.md +++ b/docs/monasca-api-spec.md @@ -481,6 +481,58 @@ The Alarms are evaluated and their state is set once per minute. Alarms contain three fields that may be edited via the API. These are the alarm state, lifecycle state, and the link. The alarm state is updated by Monasca as measurements are evaluated, and can be changed manually as necessary. The lifecycle state and link fields are not maintained or updated by Monasca, instead these are provided for storing information related to external tools. +### Deterministic or non-deterministic alarms + +By default all alarm definitions are assumed to be **non-deterministic**. +There are 3 possible states such alarms can transition to: *OK*, *ALARM*, +*UNDETERMINED*. On the other hand, alarm definitions can be also +**deterministic**. In that case alarm is allowed to transition only: *OK* +and *ALARM* state. + +Following expression ```avg(cpu.user_perc{hostname=compute_node_1}) > 10``` means that potential +alarm and transition to *ALARM* state is restricted to specific machine. If for some reason that +host would crash and stay offline long enough, there would be no measurements received from it. +In this case alarm will transition to *UNDETERMINED* state. + +On the other hand, some metrics are irregular and look more like events. One case is +metric created only if something critical happens in the system. +For example an error in log file or deadlock in database. +If non-deterministic alarm definition would be created using expression ```count(log.error{component=mysql}) >= 1)```, +that alarm could stay in *UNDETERMINED* state for most of its lifetime. +However, from operator point of view, if there are no errors related to MySQL, everything works correctly. +Answer to that situation is creating *deterministic* alarm definition +using expression ```count(log.error{component=mysql}, deterministic) >= 1```. + +The deterministic alarm's main trait is preventing from transition to *UNDETERMINED* state. +The alarm should be *OK* if no data is received. Also such alarms transition to *OK* immediately when created, +rather than to *UNDETERMINED* state. + +Finally, it must be mentioned that alarm definition can be composed of multiple expressions and +that *deterministic* is actually part of it. The entire alarm definition is considered *deterministic* +only if all of its expressions are such. Otherwise the alarm is *non-deterministic*. + +For example: +``` +avg(disk.space_used_perc{hostname=compute_node_1}) >= 99 + and +count(log.error{hostname=compute_node_1,component=kafka},deterministic) >= 1 +``` +potential alarm will transition to *ALARM* state if there is no usable disk space left and kafka starts to report errors regarding +inability to save data to it. Second expression is *deterministic*, however entire alarm will be kept in *UNDETERMINED* state +until such situation happens. + +On the other hand, expression like this: +``` +avg(disk.space_used_perc{hostname=compute_node_1},deterministic) >= 99 + and +count(log.error{hostname=compute_node_1,component=kafka},deterministic) >= 1 +``` +makes entire alarm *deterministic*. In other words - *all parts of alarm's expression +must be marked as deterministic in order for entire alarm to be considered such*. +Having definition like one above, potential alarm will stay in *OK* state as long as there is enough +disk space left at *compute_node_1* and there are no errors reported from *kafka* running +at the same host. + ## Alarm Definition Expressions The alarm definition expression syntax allows the creation of simple or complex alarm definitions to handle a wide variety of needs. Alarm expressions are evaluated every 60 seconds. @@ -511,11 +563,16 @@ Each subexpression is made up of several parts with a couple of options: ```` - ::= '(' [',' period] ')' threshold_value ['times' periods] + ::= '(' [',' deterministic] [',' period] ')' threshold_value ['times' periods] | '(' expression ')' ```` -Period must be an integer multiple of 60. The default period is 60 seconds. +Period must be an integer multiple of 60. The default period is 60 seconds. + +Expression is by default **non-deterministic** (i.e. when expression does +not contain *deterministic* keyword). If however **deterministic** +option would be desired, it is enough to have *deterministic* keyword +inside expression. The logical_operators are: `and` (also `&&`), `or` (also `||`). @@ -603,6 +660,22 @@ In this example a compound alarm expression is evaluated involving two threshold avg(cpu.system_perc{hostname=hostname.domain.com}) > 90 or avg(disk_read_ops{hostname=hostname.domain.com, device=vda}, 120) > 1000 ``` +#### Deterministic alarm example +In this example alarm is created with one expression which is deterministic + +``` +count(log.error{}, deterministic) > 1 +``` + +#### Non-deterministic alarm with deterministic sub expressions +In this example alarm's expression is composed of 3 parts where two of them +are marked as **deterministic**. However entire expression is non-deterministic because +of the 3rd expression. + +``` +count(log.error{}, deterministic) > 1 or count(log.warning{}, deterministic) > 1 and avg(cpu.user_perc{}) > 10 +``` + ### Changing Alarm Definitions Once an Alarm Definition has been created, the value for match_by and any metrics in the expression cannot be changed. This is because those fields control the metrics used to create Alarms and Alarms may already have been created. The function, operator, period, periods and any boolean operators can change, but not the metrics in subexpressions or the number of subexpressions. All other fields in an Alarm Definition can be changed. @@ -1715,6 +1788,34 @@ Cache-Control: no-cache } ``` +To create deterministic definition following request should be sent: +``` +POST /v2.0/alarm-definitions HTTP/1.1 +Host: 192.168.10.4:8080 +Content-Type: application/json +X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7 +Cache-Control: no-cache + +{ + "name":"Average CPU percent greater than 10", + "description":"The average CPU percent is greater than 10", + "expression":"(avg(cpu.user_perc{hostname=devstack},deterministic) > 10)", + "match_by":[ + "hostname" + ], + "severity":"LOW", + "ok_actions":[ + "c60ec47e-5038-4bf1-9f95-4046c6e9a759" + ], + "alarm_actions":[ + "c60ec47e-5038-4bf1-9f95-4046c6e9a759" + ], + "undetermined_actions":[ + "c60ec47e-5038-4bf1-9f95-4046c6e9a759" + ] +} +``` + ### Response #### Status Code * 201 - Created @@ -1727,6 +1828,7 @@ Returns a JSON object of alarm definition objects with the following fields: * name (string) - Name of alarm definition. * description (string) - Description of alarm definition. * expression (string) - The alarm definition expression. +* deterministic (boolean) - Is the underlying expression deterministic ? **Read-only**, computed from *expression* * expression_data (JSON object) - The alarm definition expression as a JSON object. * match_by ([string]) - The metric dimensions to match to the alarm dimensions * severity (string) - The severity of an alarm definition. Either `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. @@ -1748,6 +1850,7 @@ Returns a JSON object of alarm definition objects with the following fields: "name":"Average CPU percent greater than 10", "description":"The average CPU percent is greater than 10", "expression":"(avg(cpu.user_perc{hostname=devstack}) > 10)", + "deterministic": false, "expression_data":{ "function":"AVG", "metric_name":"cpu.user_perc", @@ -1819,6 +1922,7 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a * name (string) - Name of alarm definition. * description (string) - Description of alarm definition. * expression (string) - The alarm definition expression. +* deterministic (boolean) - Is the underlying expression deterministic ? **Read-only**, computed from *expression* * expression_data (JSON object) - The alarm definition expression as a JSON object. * match_by ([string]) - The metric dimensions to use to create unique alarms * severity (string) - The severity of an alarm definition. Either `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. @@ -1852,6 +1956,7 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "name": "CPU percent greater than 10", "description": "Release the hounds", "expression": "(avg(cpu.user_perc{hostname=devstack}) > 10)", + "deterministic": false, "expression_data": { "function": "AVG", "metric_name": "cpu.user_perc", @@ -1877,6 +1982,38 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "undetermined_actions": [ "c60ec47e-5038-4bf1-9f95-4046c6e9a759" ] + }, + { + "id": "g9323232-6543-4cbf-1234-0993a947ea83", + "links": [ + { + "rel": "self", + "href": "http://192.168.10.4:8080/v2.0/alarm-definitions/g9323232-6543-4cbf-1234-0993a947ea83" + } + ], + "name": "Log error count exceeds 1000", + "description": "Release the cats", + "expression": "(count(log.error{hostname=devstack}, deterministic) > 1000)", + "deterministic": true, + "expression_data": { + "function": "AVG", + "metric_name": "log.error", + "dimensions": { + "hostname": "devstack" + }, + "operator": "GT", + "threshold": 1000, + "period": 60, + "periods": 1 + }, + "match_by": [ + "hostname" + ], + "severity": "CRITICAL", + "actions_enabled": true, + "alarm_actions": [], + "ok_actions": [], + "undetermined_actions": [] } ] } @@ -1913,6 +2050,7 @@ Returns a JSON alarm definition object with the following fields: * name (string) - Name of alarm definition. * description (string) - Description of alarm definition. * expression (string) - The alarm definition expression. +* deterministic (boolean) - Is the underlying expression deterministic ? **Read-only**, computed from *expression* * expression_data (JSON object) - The alarm definition expression as a JSON object. * match_by ([string]) - The metric dimensions to use to create unique alarms * severity (string) - The severity of an alarm definition. Either `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. @@ -1934,6 +2072,7 @@ Returns a JSON alarm definition object with the following fields: "name": "CPU percent greater than 10", "description": "Release the hounds", "expression": "(avg(cpu.user_perc{hostname=devstack}) > 10)", + "deterministic": false, "expression_data": { "function": "AVG", "metric_name": "cpu.user_perc", @@ -2035,6 +2174,7 @@ Returns a JSON alarm definition object with the following parameters: * name (string) - Name of alarm definition. * description (string) - Description of alarm definition. * expression (string) - The alarm definition expression. +* deterministic (boolean) - Is the underlying expression deterministic ? **Read-only**, computed from *expression* * expression_data (JSON object) - The alarm definition expression as a JSON object. * match_by ([string]) - The metric dimensions to use to create unique alarms * severity (string) - The severity of an alarm definition. Either `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. @@ -2056,6 +2196,7 @@ Returns a JSON alarm definition object with the following parameters: "name": "CPU percent greater than 15", "description": "Release the hounds", "expression": "(avg(cpu.user_perc{hostname=devstack}) > 15)", + "deterministic": false, "expression_data": { "function": "AVG", "metric_name": "cpu.user_perc", @@ -2086,7 +2227,7 @@ ___ ## Patch Alarm Definition ### PATCH /v2.0/alarm-definitions/{alarm_definition_id} -Update select parameters of the specified alarm definition, and enable/disable its actions. +Update selected parameters of the specified alarm definition, and enable/disable its actions. #### Headers * X-Auth-Token (string, required) - Keystone auth token @@ -2154,6 +2295,7 @@ Returns a JSON alarm definition object with the following fields: * name (string) - Name of alarm definition. * description (string) - Description of alarm definition. * expression (string) - The alarm definition expression. +* deterministic (boolean) - Is the underlying expression deterministic ? **Read-only**, computed from *expression* * expression_data (JSON object) - The alarm definition expression as a JSON object. * match_by ([string]) - The metric dimensions to use to create unique alarms * severity (string) - The severity of an alarm definition. Either `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. @@ -2175,6 +2317,7 @@ Returns a JSON alarm definition object with the following fields: "name": "CPU percent greater than 15", "description": "Release the hounds", "expression": "(avg(cpu.user_perc{hostname=devstack}) > 15)", + "deterministic": false, "expression_data": { "function": "AVG", "metric_name": "cpu.user_perc", @@ -2461,7 +2604,7 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a * reason (string) - The reason for the state transition. * reason_data (string) - The reason for the state transition as a JSON object. * timestamp (string) - The time in ISO 8601 combined date and time format in UTC when the state transition occurred. -* sub_alarms ({{string, string, string(255): string(255), string, string, string, string}, string, [string]) - The sub-alarms stated of when the alarm state transition occurred. +* sub_alarms ({{string, string, string(255): string(255), string, string, string, string, boolean}, string, [string]) - The sub-alarms stated of when the alarm state transition occurred. #### Response Examples ``` @@ -2505,7 +2648,8 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "operator": "GT", "threshold": 15, "period": 60, - "periods": 1 + "periods": 1, + "deterministic": false }, "sub_alarm_state": "OK", "current_values": [ @@ -2542,7 +2686,8 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "operator": "GT", "threshold": 10, "period": 60, - "periods": 3 + "periods": 3, + "deterministic": false }, "sub_alarm_state": "OK", "current_values": [ @@ -2581,7 +2726,8 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "operator": "GT", "threshold": 10, "period": 60, - "periods": 3 + "periods": 3, + "deterministic": false }, "sub_alarm_state": "ALARM", "current_values": [ @@ -2928,7 +3074,7 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a * reason (string) - The reason for the state transition. * reason_data (string) - The reason for the state transition as a JSON object. * timestamp (string) - The time in ISO 8601 combined date and time format in UTC when the state transition occurred. -* sub_alarms ({{string, string, string(255): string(255), string, string, string, string}, string, [string]) - The sub-alarms stated of when the alarm state transition occurred. +* sub_alarms ({{string, string, string(255): string(255), string, string, string, string, boolean}, string, [string]) - The sub-alarms stated of when the alarm state transition occurred. #### Response Examples ``` @@ -2970,7 +3116,8 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "operator": "LT", "threshold": 10, "period": 60, - "periods": 3 + "periods": 3, + "deterministic": false }, "sub_alarm_state": "ALARM", "current_values": [ @@ -3007,7 +3154,8 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "operator": "LT", "threshold": 10, "period": 60, - "periods": 3 + "periods": 3, + "deterministic": false }, "sub_alarm_state": "OK", "current_values": [ @@ -3044,7 +3192,8 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a "operator": "LT", "threshold": 10, "period": 60, - "periods": 3 + "periods": 3, + "deterministic": false }, "sub_alarm_state": "ALARM", "current_values": [ @@ -3095,4 +3244,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinition.java b/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinition.java index 9876f9ae3..634c73e22 100644 --- a/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinition.java +++ b/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinition.java @@ -32,6 +32,7 @@ public class AlarmDefinition extends AbstractEntity implements Linked { private String name; private String description = ""; private String expression; + private boolean deterministic; private Object expressionData; private List matchBy; private String severity; @@ -88,6 +89,9 @@ public class AlarmDefinition extends AbstractEntity implements Linked { return false; } else if (!expressionData.equals(other.expressionData)) return false; + if (this.deterministic != other.deterministic) { + return false; + } if (links == null) { if (other.links != null) return false; @@ -174,6 +178,7 @@ public class AlarmDefinition extends AbstractEntity implements Linked { result = prime * result + ((description == null) ? 0 : description.hashCode()); result = prime * result + ((expression == null) ? 0 : expression.hashCode()); result = prime * result + ((expressionData == null) ? 0 : expressionData.hashCode()); + result = prime * result + Boolean.valueOf(this.deterministic).hashCode(); result = prime * result + ((links == null) ? 0 : links.hashCode()); result = prime * result + ((matchBy == null) ? 0 : matchBy.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); @@ -201,7 +206,10 @@ public class AlarmDefinition extends AbstractEntity implements Linked { public void setExpression(String expression) { this.expression = expression; - setExpressionData(AlarmExpression.of(expression).getExpressionTree()); + + final AlarmExpression alarmExpression = AlarmExpression.of(expression); + this.setExpressionData(alarmExpression.getExpressionTree()); + this.deterministic = alarmExpression.isDeterministic(); } @JsonIgnore @@ -239,6 +247,10 @@ public class AlarmDefinition extends AbstractEntity implements Linked { this.undeterminedActions = undeterminedActions; } + public boolean isDeterministic() { + return this.deterministic; + } + @Override public String toString() { return String.format("AlarmDefinition [name=%s]", name); diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java b/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java index ceb653d19..9437966c8 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java @@ -451,13 +451,24 @@ public class AlarmDefinitionSqlRepoImpl double threshold = subAlarmDef.getThreshold(); int period = subAlarmDef.getPeriod(); int periods = subAlarmDef.getPeriods(); + boolean isDeterministic = subAlarmDef.isDeterministic(); Map dimensions = Collections.emptyMap(); if (subAlarmDefDimensionMapExpression.containsKey(id)) { dimensions = subAlarmDefDimensionMapExpression.get(id); } - subExpressions.put(id, new AlarmSubExpression(function, new MetricDefinition(metricName, dimensions), operator, threshold, period, periods)); + subExpressions.put(id, + new AlarmSubExpression( + function, + new MetricDefinition(metricName, dimensions), + operator, + threshold, + period, + periods, + isDeterministic + ) + ); } return subExpressions; @@ -545,6 +556,7 @@ public class AlarmDefinitionSqlRepoImpl subAlarmDefinitionDb.setOperator(sa.getOperator().name()); subAlarmDefinitionDb.setThreshold(sa.getThreshold()); subAlarmDefinitionDb.setUpdatedAt(this.getUTCNow()); + subAlarmDefinitionDb.setDeterministic(sa.isDeterministic()); session.saveOrUpdate(subAlarmDefinitionDb); } } @@ -580,6 +592,7 @@ public class AlarmDefinitionSqlRepoImpl alarmDefinitionDb.setMatchBy(matchBy == null || Iterables.isEmpty(matchBy) ? null : COMMA_JOINER.join(matchBy)); alarmDefinitionDb.setSeverity(AlarmSeverity.valueOf(severity)); alarmDefinitionDb.setActionsEnabled(actionsEnabled); + alarmDefinitionDb.setUpdatedAt(this.getUTCNow()); session.saveOrUpdate(alarmDefinitionDb); @@ -715,7 +728,7 @@ public class AlarmDefinitionSqlRepoImpl // Persist sub-alarm final DateTime now = this.getUTCNow(); - SubAlarmDefinitionDb subAlarmDefinitionDb = new SubAlarmDefinitionDb( + final SubAlarmDefinitionDb subAlarmDefinitionDb = new SubAlarmDefinitionDb( subAlarmId, alarmDefinition, subExpr.getFunction().name(), @@ -725,7 +738,8 @@ public class AlarmDefinitionSqlRepoImpl subExpr.getPeriod(), subExpr.getPeriods(), now, - now + now, + subExpr.isDeterministic() ); session.save(subAlarmDefinitionDb); diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java index 290712d15..e927629e4 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java @@ -1,11 +1,11 @@ /* * (C) Copyright 2014,2016 Hewlett Packard Enterprise Development Company LP - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under @@ -60,6 +60,13 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { "select sa.*, sad.dimensions from sub_alarm_definition as sa " + "left join (select sub_alarm_definition_id, group_concat(dimension_name, '=', value) as dimensions from sub_alarm_definition_dimension group by sub_alarm_definition_id ) as sad " + "on sad.sub_alarm_definition_id = sa.id where sa.alarm_definition_id = :alarmDefId"; + private static final String CREATE_SUB_EXPRESSION_SQL = "insert into sub_alarm_definition " + + "(id, alarm_definition_id, function, metric_name, " + + "operator, threshold, period, periods, is_deterministic, " + + "created_at, updated_at) " + + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())"; + private static final String UPDATE_SUB_ALARM_DEF_SQL = "update sub_alarm_definition set " + + "operator = ?, threshold = ?, is_deterministic = ?, updated_at = NOW() where id = ?"; private final DBI db; private final PersistUtils persistUtils; @@ -112,7 +119,7 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { "update alarm_definition set deleted_at = NOW() where tenant_id = ? and id = ? and deleted_at is NULL", tenantId, alarmDefId) == 0) throw new EntityNotFoundException("No alarm definition exists for %s", alarmDefId); - + // Cascade soft delete to alarms h.execute("delete from alarm where alarm_definition_id = :id", alarmDefId); } @@ -280,10 +287,23 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { // Need to convert the results appropriately based on type. Integer period = Conversions.variantToInteger(row.get("period")); Integer periods = Conversions.variantToInteger(row.get("periods")); + Boolean isDeterministic = (Boolean) row.get("is_deterministic"); Map dimensions = DimensionQueries.dimensionsFor((String) row.get("dimensions")); - subExpressions.put(id, new AlarmSubExpression(function, new MetricDefinition(metricName, - dimensions), operator, threshold, period, periods)); + + subExpressions.put( + id, + new AlarmSubExpression( + function, + new MetricDefinition(metricName, dimensions), + operator, + threshold, + period, + periods, + isDeterministic + ) + ); + } return subExpressions; @@ -315,8 +335,12 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { for (Map.Entry entry : changedSubAlarms.entrySet()) { AlarmSubExpression sa = entry.getValue(); h.execute( - "update sub_alarm_definition set operator = ?, threshold = ?, updated_at = NOW() where id = ?", - sa.getOperator().name(), sa.getThreshold(), entry.getKey()); + UPDATE_SUB_ALARM_DEF_SQL, + sa.getOperator().name(), + sa.getThreshold(), + sa.isDeterministic(), + entry.getKey() + ); } // Insert new sub-alarms @@ -365,12 +389,9 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { MetricDefinition metricDef = subExpr.getMetricDefinition(); // Persist sub-alarm - handle - .insert( - "insert into sub_alarm_definition (id, alarm_definition_id, function, metric_name, operator, threshold, period, periods, created_at, updated_at) " - + "values (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())", subAlarmId, id, subExpr - .getFunction().name(), metricDef.name, subExpr.getOperator().name(), subExpr - .getThreshold(), subExpr.getPeriod(), subExpr.getPeriods()); + handle.insert(CREATE_SUB_EXPRESSION_SQL, subAlarmId, id, subExpr.getFunction().name(), + metricDef.name, subExpr.getOperator().name(), subExpr.getThreshold(), + subExpr.getPeriod(), subExpr.getPeriods(), subExpr.isDeterministic()); // Persist sub-alarm dimensions if (metricDef.dimensions != null && !metricDef.dimensions.isEmpty()) diff --git a/java/src/test/java/monasca/api/app/validation/AlarmExpressionsTest.java b/java/src/test/java/monasca/api/app/validation/AlarmExpressionsTest.java index 62a5daad1..1775ae124 100644 --- a/java/src/test/java/monasca/api/app/validation/AlarmExpressionsTest.java +++ b/java/src/test/java/monasca/api/app/validation/AlarmExpressionsTest.java @@ -54,4 +54,9 @@ public class AlarmExpressionsTest { public void shouldThrowOnInvalidThreshold() throws Exception { AlarmValidation.validateNormalizeAndGet("avg(hpcs.compute.net_out_bytes) > abc"); } + + @Test(expectedExceptions = WebApplicationException.class) + public void shouldThrowOnMalformedDeterministicKeyword() throws Exception { + AlarmValidation.validateNormalizeAndGet("avg(hpcs.compute.net_out_bytes,determ) > 1"); + } } diff --git a/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java b/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java index f016e400a..7be374e29 100644 --- a/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java +++ b/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java @@ -57,7 +57,9 @@ import com.sun.jersey.api.client.ClientResponse; @Test public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { private String expression; + private String detExpression; private AlarmDefinition alarm; + private AlarmDefinition detAlarm; private AlarmDefinition alarmItem; private AlarmDefinitionService service; private AlarmDefinitionRepo repo; @@ -69,6 +71,7 @@ public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { super.setupResources(); expression = "avg(disk_read_ops{service=hpcs.compute, instance_id=937}) >= 90"; + detExpression = "count(log.error{service=test,instance_id=2},deterministic) >= 10 times 10"; List matchBy = Arrays.asList("service", "instance_id"); alarmItem = new AlarmDefinition("123", "Disk Exceeds 1k Operations", null, "LOW", expression, @@ -76,18 +79,28 @@ public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { alarmActions = new ArrayList(); alarmActions.add("29387234"); alarmActions.add("77778687"); + alarm = new AlarmDefinition("123", "Disk Exceeds 1k Operations", null, "LOW", expression, matchBy, true, alarmActions, null, null); + detAlarm = + new AlarmDefinition("456", "log.error", null, "LOW", detExpression, matchBy, + true, alarmActions, null, null); service = mock(AlarmDefinitionService.class); + when( service.create(eq("abc"), eq("Disk Exceeds 1k Operations"), any(String.class), eq("LOW"), eq(expression), eq(AlarmExpression.of(expression)), eq(matchBy), any(List.class), any(List.class), any(List.class))).thenReturn(alarm); + when( + service.create(eq("abc"), eq("log.error"), any(String.class), eq("LOW"), + eq(detExpression), eq(AlarmExpression.of(detExpression)), eq(matchBy), any(List.class), + any(List.class), any(List.class))).thenReturn(detAlarm); repo = mock(AlarmDefinitionRepo.class); when(repo.findById(eq("abc"), eq("123"))).thenReturn(alarm); + when(repo.findById(eq("abc"), eq("456"))).thenReturn(detAlarm); when(repo.find(anyString(), anyString(), (Map) anyMap(), AlarmSeverity.fromString(anyString()), (List) anyList(), anyString(), anyInt())).thenReturn( Arrays.asList(alarmItem)); @@ -112,6 +125,31 @@ public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { any(List.class), any(List.class)); } + public void shouldCreateDeterministic() { + final CreateAlarmDefinitionCommand request = new CreateAlarmDefinitionCommand( + "log.error", + null, + detExpression, + Arrays.asList("service", "instance_id"), + "LOW", + alarmActions, + null, + null + ); + final ClientResponse response = this.createResponseFor(request); + + assertEquals(response.getStatus(), 201); + AlarmDefinition newAlarm = response.getEntity(AlarmDefinition.class); + String location = response.getHeaders().get("Location").get(0); + assertEquals(location, "/v2.0/alarm-definitions/" + newAlarm.getId()); + assertEquals(newAlarm, detAlarm); + + verify(service).create(eq("abc"), eq("log.error"), any(String.class), + eq("LOW"), eq(detExpression), eq(AlarmExpression.of(detExpression)), + eq(Arrays.asList("service", "instance_id")), any(List.class), + any(List.class), any(List.class)); + } + public void shouldUpdate() { when( service.update(eq("abc"), eq("123"), any(AlarmExpression.class), diff --git a/java/src/test/resources/fixtures/alarm.json b/java/src/test/resources/fixtures/alarm.json index cb53ebf0c..04f287819 100644 --- a/java/src/test/resources/fixtures/alarm.json +++ b/java/src/test/resources/fixtures/alarm.json @@ -1 +1 @@ -{"id":"123","links":[{"rel":"self","href":"https://cloudsvc.example.com/v1.0"}],"name":"90% CPU","description":"","expression":"avg(hpcs.compute{instance_id=666, image_id=345}) >= 90","match_by":[],"severity":"LOW","actions_enabled":false,"alarm_actions":["123345345","23423"],"ok_actions":null,"undetermined_actions":null} +{"id":"123","links":[{"rel":"self","href":"https://cloudsvc.example.com/v1.0"}],"name":"90% CPU","description":"","expression":"avg(hpcs.compute{instance_id=666, image_id=345}) >= 90","deterministic":false,"match_by":[],"severity":"LOW","actions_enabled":false,"alarm_actions":["123345345","23423"],"ok_actions":null,"undetermined_actions":null} diff --git a/java/src/test/resources/monasca/api/infrastructure/persistence/mysql/alarm.sql b/java/src/test/resources/monasca/api/infrastructure/persistence/mysql/alarm.sql index 47d7fb32b..8c3d76177 100644 --- a/java/src/test/resources/monasca/api/infrastructure/persistence/mysql/alarm.sql +++ b/java/src/test/resources/monasca/api/infrastructure/persistence/mysql/alarm.sql @@ -59,6 +59,7 @@ CREATE TABLE `sub_alarm_definition` ( `threshold` double NOT NULL, `period` int(11) NOT NULL, `periods` int(11) NOT NULL, + `is_deterministic` tinyint(1) NOT NULL DEFAULT '0', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`) diff --git a/monasca_api/common/repositories/model/sub_alarm_definition.py b/monasca_api/common/repositories/model/sub_alarm_definition.py index b24af3227..a0d33ab28 100644 --- a/monasca_api/common/repositories/model/sub_alarm_definition.py +++ b/monasca_api/common/repositories/model/sub_alarm_definition.py @@ -48,6 +48,7 @@ class SubAlarmDefinition(object): self.periods = str(row['periods']).decode('utf8') # threshold comes from the DB as a float in 0.0 form. self.threshold = str(row['threshold']).decode('utf8') + self.deterministic = str(row['is_deterministic']) == '1' if sub_expr: # id is not used for compare or hash. @@ -62,6 +63,7 @@ class SubAlarmDefinition(object): self.period = sub_expr.period self.periods = sub_expr.periods self.threshold = sub_expr.threshold + self.deterministic = sub_expr.deterministic def _init_dimensions(self, dimensions_str): @@ -85,6 +87,9 @@ class SubAlarmDefinition(object): if self.dimensions_str: result += "{{{}}}".format(self.dimensions_str.encode('utf8')) + if self.deterministic: + result += ', deterministic' + if self.period: result += ", {}".format(self.period.encode('utf8')) @@ -111,6 +116,7 @@ class SubAlarmDefinition(object): hash(self.operator) ^ hash(self.period) ^ hash(self.periods) ^ + hash(self.deterministic) ^ # Convert to float to handle cases like 0.0 == 0 hash(float(self.threshold))) @@ -130,12 +136,13 @@ class SubAlarmDefinition(object): self.operator == other.operator and self.period == other.period and self.periods == other.periods and + self.deterministic == other.deterministic and # Convert to float to handle cases like 0.0 == 0 float(self.threshold) == float(other.threshold)) def same_key_fields(self, other): - # compare everything but operator and threshold + # compare everything but operator, threshold and deterministic return (self.metric_name == other.metric_name and self.dimensions == other.dimensions and self.function == other.function and diff --git a/monasca_api/common/repositories/mysql/alarm_definitions_repository.py b/monasca_api/common/repositories/mysql/alarm_definitions_repository.py index 55d936107..b090ab7d3 100644 --- a/monasca_api/common/repositories/mysql/alarm_definitions_repository.py +++ b/monasca_api/common/repositories/mysql/alarm_definitions_repository.py @@ -268,10 +268,11 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, threshold, period, periods, + is_deterministic, created_at, updated_at) values(%s,%s,%s,%s,%s,%s,%s,%s,%s, - %s)""", + %s, %s)""", ( sub_alarm_definition_id, alarm_definition_id, @@ -281,7 +282,10 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, sub_expr.normalized_operator.encode('utf8'), sub_expr.threshold.encode('utf8'), sub_expr.period.encode('utf8'), - sub_expr.periods.encode('utf8'), now, now)) + sub_expr.periods.encode('utf8'), + sub_expr.deterministic, + now, + now)) for dimension in sub_expr.dimensions_as_list: parsed_dimension = dimension.split('=') @@ -463,13 +467,17 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, update sub_alarm_definition set operator = %s, threshold = %s, - updated_at = %s + is_deterministic = %s, + updated_at = %s, where id = %s""" for sub_alarm_definition_id, sub_alarm_def in ( changed_sub_alarm_defs_by_id.iteritems()): - parms = [sub_alarm_def.operator, sub_alarm_def.threshold, - now, sub_alarm_definition_id] + parms = [sub_alarm_def.operator, + sub_alarm_def.threshold, + sub_alarm_def.deterministic, + now, + sub_alarm_definition_id] cursor.execute(query, parms) # Insert new sub alarm definitions @@ -483,9 +491,10 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, threshold, period, periods, + is_deterministic, created_at, updated_at) - values(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""" + values(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""" sub_query = """ insert into sub_alarm_definition_dimension( @@ -503,6 +512,7 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, str(sub_alarm_def.threshold).encode('utf8'), str(sub_alarm_def.period).encode('utf8'), str(sub_alarm_def.periods).encode('utf8'), + sub_alarm_def.deterministic, now, now] diff --git a/monasca_api/common/repositories/sqla/alarm_definitions_repository.py b/monasca_api/common/repositories/sqla/alarm_definitions_repository.py index e166cfea4..1f5757eee 100644 --- a/monasca_api/common/repositories/sqla/alarm_definitions_repository.py +++ b/monasca_api/common/repositories/sqla/alarm_definitions_repository.py @@ -187,6 +187,7 @@ class AlarmDefinitionsRepository(sql_repository.SQLRepository, threshold=bindparam('b_threshold'), period=bindparam('b_period'), periods=bindparam('b_periods'), + is_deterministic=bindparam('b_is_deterministic'), created_at=bindparam('b_created_at'), updated_at=bindparam('b_updated_at'))) @@ -209,6 +210,7 @@ class AlarmDefinitionsRepository(sql_repository.SQLRepository, .values( operator=bindparam('b_operator'), threshold=bindparam('b_threshold'), + is_deterministic=bindparam('b_is_deterministic'), updated_at=bindparam('b_updated_at'))) b_ad_id = bindparam('b_alarm_definition_id'), @@ -222,6 +224,7 @@ class AlarmDefinitionsRepository(sql_repository.SQLRepository, threshold=bindparam('b_threshold'), period=bindparam('b_period'), periods=bindparam('b_periods'), + is_deterministic=bindparam('b_is_deterministic'), created_at=bindparam('b_created_at'), updated_at=bindparam('b_updated_at'))) @@ -426,6 +429,7 @@ class AlarmDefinitionsRepository(sql_repository.SQLRepository, b_threshold=sub_expr.threshold.encode('utf8'), b_period=sub_expr.period.encode('utf8'), b_periods=sub_expr.periods.encode('utf8'), + b_is_deterministic=sub_expr.deterministic, b_created_at=now, b_updated_at=now) @@ -574,6 +578,7 @@ class AlarmDefinitionsRepository(sql_repository.SQLRepository, changed_sub_alarm_defs_by_id.iteritems()): parms.append({'b_operator': sub_alarm_def.operator, 'b_threshold': sub_alarm_def.threshold, + 'b_is_deterministic': sub_alarm_def.deterministic, 'b_updated_at': now, 'b_id': sub_alarm_definition_id}) if len(parms) > 0: @@ -590,6 +595,7 @@ class AlarmDefinitionsRepository(sql_repository.SQLRepository, threshold = str(sub_alarm_def.threshold).encode('utf8') period = str(sub_alarm_def.period).encode('utf8') periods = str(sub_alarm_def.periods).encode('utf8') + is_deterministic = sub_alarm_def.is_deterministic parms.append({'b_id': sub_alarm_def.id, 'b_alarm_definition_id': adi, 'b_function': function, @@ -598,6 +604,7 @@ class AlarmDefinitionsRepository(sql_repository.SQLRepository, 'b_threshold': threshold, 'b_period': period, 'b_periods': periods, + 'b_is_deterministic': is_deterministic, 'b_created_at': now, 'b_updated_at': now}) diff --git a/monasca_api/common/repositories/sqla/models.py b/monasca_api/common/repositories/sqla/models.py index 138ba65e7..5aa94e166 100644 --- a/monasca_api/common/repositories/sqla/models.py +++ b/monasca_api/common/repositories/sqla/models.py @@ -126,6 +126,7 @@ def create_sad_model(metadata=None): Column('threshold', Float), Column('period', Integer), Column('periods', Integer), + Column('is_deterministic', Boolean), Column('created_at', DateTime), Column('updated_at', DateTime)) diff --git a/monasca_api/expression_parser/alarm_expr_parser.py b/monasca_api/expression_parser/alarm_expr_parser.py index 01a75e2f7..7536bba48 100644 --- a/monasca_api/expression_parser/alarm_expr_parser.py +++ b/monasca_api/expression_parser/alarm_expr_parser.py @@ -17,6 +17,10 @@ import sys import pyparsing +_DETERMINISTIC_ASSIGNMENT_LEN = 3 +_DETERMINISTIC_ASSIGNMENT_SHORT_LEN = 1 +_DETERMINISTIC_ASSIGNMENT_VALUE_INDEX = 2 + class SubExpr(object): @@ -35,6 +39,7 @@ class SubExpr(object): self._threshold = tokens.threshold self._period = tokens.period self._periods = tokens.periods + self._deterministic = tokens.deterministic self._id = None @property @@ -128,6 +133,10 @@ class SubExpr(object): else: return u'1' + @property + def deterministic(self): + return True if self._deterministic else False + @property def normalized_operator(self): """Get the operator as one of LT, GT, LTE, or GTE.""" @@ -237,8 +246,16 @@ period = integer_number("period") threshold = decimal_number("threshold") periods = integer_number("periods") -function_and_metric = (func + LPAREN + metric + pyparsing.Optional( - COMMA + period) + RPAREN) +deterministic = ( + pyparsing.CaselessLiteral('deterministic') +)('deterministic') + +function_and_metric = ( + func + LPAREN + metric + + pyparsing.Optional(COMMA + deterministic) + + pyparsing.Optional(COMMA + period) + + RPAREN +) expression = pyparsing.Forward() @@ -291,6 +308,10 @@ def main(): "ntp.offset > 1 or ntp.offset < -5", "max(3test_metric5{it's this=that's it}) lt 5 times 3", + + "count(log.error{test=1}, deterministic) > 1.0", + + "count(log.error{test=1}, deterministic, 120) > 1.0" ] for expr in expr_list: @@ -306,6 +327,10 @@ def main(): sub_expr.fmtd_sub_expr_str.encode('utf8'))) print('sub_expr dimensions: {}'.format( sub_expr.dimensions_str.encode('utf8'))) + print('sub_expr deterministic: {}'.format( + sub_expr.deterministic)) + print('sub_expr period: {}'.format( + sub_expr.period)) print("") print("") diff --git a/monasca_api/tests/sqlite_alarm.sql b/monasca_api/tests/sqlite_alarm.sql index 0edd4140f..2f3291e27 100644 --- a/monasca_api/tests/sqlite_alarm.sql +++ b/monasca_api/tests/sqlite_alarm.sql @@ -86,6 +86,7 @@ CREATE TABLE `sub_alarm_definition` ( `threshold` double NOT NULL, `period` int(11) NOT NULL, `periods` int(11) NOT NULL, + `is_deterministic` tinyint(1) NOT NULL DEFAULT(0), `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`) diff --git a/monasca_api/tests/test_ad_repository.py b/monasca_api/tests/test_ad_repository.py index d5c2d8c04..7f58a5ec3 100644 --- a/monasca_api/tests/test_ad_repository.py +++ b/monasca_api/tests/test_ad_repository.py @@ -83,6 +83,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): threshold=bindparam('threshold'), period=bindparam('period'), periods=bindparam('periods'), + is_deterministic=bindparam('is_deterministic'), created_at=bindparam('created_at'), updated_at=bindparam('updated_at'))) @@ -152,6 +153,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'threshold': 10, 'period': 60, 'periods': 1, + 'is_deterministic': False, 'created_at': datetime.datetime.now(), 'updated_at': datetime.datetime.now()}, {'id': '222', @@ -162,6 +164,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'threshold': 20, 'period': 60, 'periods': 1, + 'is_deterministic': False, 'created_at': datetime.datetime.now(), 'updated_at': datetime.datetime.now()}, {'id': '223', @@ -172,6 +175,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'threshold': 100, 'period': 60, 'periods': 1, + 'is_deterministic': False, 'created_at': datetime.datetime.now(), 'updated_at': datetime.datetime.now()}, ] @@ -317,6 +321,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'operator': 'GT', 'period': 60, 'periods': 1, + 'is_deterministic': False, 'threshold': 20.0}, {'alarm_definition_id': '234', 'dimensions': None, @@ -326,6 +331,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'operator': 'LT', 'period': 60, 'periods': 1, + 'is_deterministic': False, 'threshold': 100.0}] self.assertEqual(len(sub_alarms), len(expected)) @@ -477,6 +483,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'operator': 'GT', 'period': 60, 'periods': 1, + 'is_deterministic': False, 'threshold': 10.0}] self.assertEqual(len(sub_alarms), len(expected)) @@ -497,6 +504,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'operator': 'GT', 'period': 60, 'periods': 1, + 'is_deterministic': False, 'threshold': 20.0}, {'alarm_definition_id': '234', 'dimensions': None, @@ -506,6 +514,7 @@ class TestAlarmDefinitionRepoDB(testtools.TestCase, fixtures.TestWithFixtures): 'operator': 'LT', 'period': 60, 'periods': 1, + 'is_deterministic': False, 'threshold': 100.0}] self.assertEqual(len(sub_alarms), len(expected)) diff --git a/monasca_api/tests/test_alarms.py b/monasca_api/tests/test_alarms.py index 4cb515e85..dece1c627 100644 --- a/monasca_api/tests/test_alarms.py +++ b/monasca_api/tests/test_alarms.py @@ -233,6 +233,7 @@ class TestAlarmDefinition(AlarmTestBase): u'actions_enabled': u'true', u'undetermined_actions': [], u'expression': u'test.metric > 10', + u'deterministic': False, u'id': u'00000001-0001-0001-0001-000000000001', u'severity': u'LOW', } @@ -285,6 +286,7 @@ class TestAlarmDefinition(AlarmTestBase): u'actions_enabled': u'true', u'undetermined_actions': [], u'expression': u'test.metric > 10', + u'deterministic': False, u'id': u'00000001-0001-0001-0001-000000000001', u'severity': u'LOW', } @@ -334,6 +336,7 @@ class TestAlarmDefinition(AlarmTestBase): u'name': u'Test Alarm', u'actions_enabled': True, u'undetermined_actions': [], + u'is_deterministic': False, u'expression': u'max(test.metric{hostname=host}) gte 1', u'id': u'00000001-0001-0001-0001-000000000001', u'severity': u'LOW'}, @@ -346,6 +349,7 @@ class TestAlarmDefinition(AlarmTestBase): 'operator': 'gte', 'threshold': 1, 'period': 60, + 'is_deterministic': False, 'periods': 1})}, 'changed': {}, 'new': {}, @@ -358,6 +362,7 @@ class TestAlarmDefinition(AlarmTestBase): 'operator': 'gte', 'threshold': 1, 'period': 60, + 'is_deterministic': False, 'periods': 1})} } ) @@ -374,6 +379,7 @@ class TestAlarmDefinition(AlarmTestBase): u'name': u'Test Alarm', u'actions_enabled': True, u'undetermined_actions': [], + u'deterministic': False, u'expression': u'max(test.metric{hostname=host}) gte 1', u'severity': u'LOW', } @@ -386,6 +392,7 @@ class TestAlarmDefinition(AlarmTestBase): u'name': u'Test Alarm', u'actions_enabled': True, u'undetermined_actions': [], + u'deterministic': False, u'expression': u'max(test.metric{hostname=host}) gte 1', u'severity': u'LOW', } @@ -411,6 +418,7 @@ class TestAlarmDefinition(AlarmTestBase): u'undetermined_actions': [], u'expression': u'max(test.metric{hostname=host}) gte 1', u'id': u'00000001-0001-0001-0001-000000000001', + u'is_deterministic': False, u'severity': u'LOW'}, {'old': {'11111': sub_alarm_definition.SubAlarmDefinition( row={'id': '11111', @@ -421,7 +429,8 @@ class TestAlarmDefinition(AlarmTestBase): 'operator': 'gte', 'threshold': 1, 'period': 60, - 'periods': 1})}, + 'periods': 1, + 'is_deterministic': False})}, 'changed': {}, 'new': {}, 'unchanged': {'11111': sub_alarm_definition.SubAlarmDefinition( @@ -433,7 +442,8 @@ class TestAlarmDefinition(AlarmTestBase): 'operator': 'gte', 'threshold': 1, 'period': 60, - 'periods': 1})} + 'periods': 1, + 'is_deterministic': False})} } ) @@ -451,6 +461,7 @@ class TestAlarmDefinition(AlarmTestBase): u'undetermined_actions': [], u'expression': u'max(test.metric{hostname=host}) gte 1', u'severity': u'LOW', + u'deterministic': False } alarm_def = { @@ -462,7 +473,7 @@ class TestAlarmDefinition(AlarmTestBase): u'actions_enabled': True, u'undetermined_actions': [], u'expression': u'max(test.metric{hostname=host}) gte 1', - u'severity': u'LOW', + u'severity': u'LOW' } result = self.simulate_request("/v2.0/alarm-definitions/%s" % expected_def[u'id'], @@ -495,6 +506,7 @@ class TestAlarmDefinition(AlarmTestBase): 'name': u'Test Alarm', 'actions_enabled': 1, 'undetermined_actions': None, + 'deterministic': False, 'expression': u'max(test.metric{hostname=host}) gte 1', 'id': u'00000001-0001-0001-0001-000000000001', 'severity': u'LOW' @@ -508,6 +520,7 @@ class TestAlarmDefinition(AlarmTestBase): u'name': u'Test Alarm', u'actions_enabled': True, u'undetermined_actions': [], + u'deterministic': False, u'expression': u'max(test.metric{hostname=host}) gte 1', u'id': u'00000001-0001-0001-0001-000000000001', u'severity': u'LOW', @@ -546,6 +559,7 @@ class TestAlarmDefinition(AlarmTestBase): u'name': u'Test Alarm', u'actions_enabled': True, u'undetermined_actions': [], + u'deterministic': False, u'expression': u'max(test.metric{hostname=host}) gte 1', u'id': u'00000001-0001-0001-0001-000000000001', u'severity': u'LOW', diff --git a/monasca_api/v2/reference/__init__.py b/monasca_api/v2/reference/__init__.py index 0f053ffec..7b03041c8 100644 --- a/monasca_api/v2/reference/__init__.py +++ b/monasca_api/v2/reference/__init__.py @@ -138,7 +138,7 @@ cfg.CONF.register_opts(mysql_opts, mysql_group) sql_opts = [cfg.StrOpt('url', default=None), cfg.StrOpt('host', default=None), cfg.StrOpt('username', default=None), cfg.StrOpt('password', default=None), - cfg.StrOpt('drivername', default=None), cfg.PortOpt('port', default=None), + cfg.StrOpt('drivername', default=None), cfg.IntOpt('port', default=None), cfg.StrOpt('database', default=None), cfg.StrOpt('query', default=None)] sql_group = cfg.OptGroup(name='database', title='sql') diff --git a/monasca_api/v2/reference/alarm_definitions.py b/monasca_api/v2/reference/alarm_definitions.py index ab6be9f68..5311fe5de 100644 --- a/monasca_api/v2/reference/alarm_definitions.py +++ b/monasca_api/v2/reference/alarm_definitions.py @@ -263,13 +263,17 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, description = (alarm_definition_row['description'].decode('utf8') if alarm_definition_row['description'] is not None else None) + expression = alarm_definition_row['expression'].decode('utf8') + is_deterministic = is_definition_deterministic(expression) + result = { u'actions_enabled': alarm_definition_row['actions_enabled'] == 1, u'alarm_actions': alarm_actions_list, u'undetermined_actions': undetermined_actions_list, u'ok_actions': ok_actions_list, u'description': description, - u'expression': alarm_definition_row['expression'].decode('utf8'), + u'expression': expression, + u'deterministic': is_deterministic, u'id': alarm_definition_row['id'].decode('utf8'), u'match_by': match_by, u'name': alarm_definition_row['name'].decode('utf8'), @@ -321,11 +325,14 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, undetermined_actions_list = get_comma_separated_str_as_list( alarm_definition_row['undetermined_actions']) + expression = alarm_definition_row['expression'] + is_deterministic = is_definition_deterministic(expression) ad = {u'id': alarm_definition_row['id'], u'name': alarm_definition_row['name'], u'description': alarm_definition_row['description'] if ( alarm_definition_row['description']) else u'', u'expression': alarm_definition_row['expression'], + u'deterministic': is_deterministic, u'match_by': match_by, u'severity': alarm_definition_row['severity'].upper(), u'actions_enabled': @@ -512,6 +519,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, u'severity': severity, u'actions_enabled': u'true', u'undetermined_actions': undetermined_actions, u'expression': expression, u'id': alarm_definition_id, + u'deterministic': is_definition_deterministic(expression), u'name': name}) return result @@ -707,3 +715,27 @@ def get_comma_separated_str_as_list(comma_separated_str): return [] else: return comma_separated_str.decode('utf8').split(',') + + +def is_definition_deterministic(expression): + """Evaluates if found expression is deterministic or not. + + In order to do that expression is parsed into sub expressions. + Each sub expression needs to be deterministic in order for + entity expression to be such. + + Otherwise expression is non-deterministic. + + :param str expression: expression to be evaluated + :return: true/false + :rtype: bool + """ + expr_parser = (monasca_api.expression_parser + .alarm_expr_parser.AlarmExprParser(expression)) + sub_expressions = expr_parser.sub_expr_list + + for sub_expr in sub_expressions: + if not sub_expr.deterministic: + return False + + return True diff --git a/monasca_tempest_tests/tests/api/test_alarm_definitions.py b/monasca_tempest_tests/tests/api/test_alarm_definitions.py index 54e812d71..40ab7839d 100644 --- a/monasca_tempest_tests/tests/api/test_alarm_definitions.py +++ b/monasca_tempest_tests/tests/api/test_alarm_definitions.py @@ -147,6 +147,69 @@ class TestAlarmDefinitions(base.BaseMonascaTest): alarm_definition) self._delete_notification(notification_id) + @test.attr(type='gate') + def test_create_deterministic_alarm_definition(self): + name = data_utils.rand_name('log.error') + expression = "count(log.error{},deterministic) > 0" + + alarm_definition = helpers.create_alarm_definition( + name=name, + description="description", + expression=expression, + match_by=['hostname'], + severity="MEDIUM" + ) + resp, response_body = self.monasca_client.create_alarm_definitions( + alarm_definition + ) + self._verify_create_alarm_definitions(resp, + response_body, + alarm_definition, + deterministic=True) + + @test.attr(type='gate') + def test_create_non_deterministic_alarm_definition_compound_mixed_expr(self): + name = data_utils.rand_name('log.error.and.disk.used_perc') + expression = ('max(disk.used_perc{hostname=node_1}) > 99.0 AND ' + 'count(log.error{hostname=node_1},deterministic) > 0') + + alarm_definition = helpers.create_alarm_definition( + name=name, + description="description", + expression=expression, + match_by=['hostname'], + severity="MEDIUM" + ) + resp, response_body = self.monasca_client.create_alarm_definitions( + alarm_definition + ) + self._verify_create_alarm_definitions(resp, + response_body, + alarm_definition, + deterministic=False) + + @test.attr(type='gate') + def test_create_deterministic_alarm_definition_compound_expr(self): + name = data_utils.rand_name('log.error.nodes_1_2') + expression = ('count(log.error{hostname=node_2},deterministic) > 0 ' + 'AND ' + 'count(log.error{hostname=node_1},deterministic) > 0') + + alarm_definition = helpers.create_alarm_definition( + name=name, + description="description", + expression=expression, + match_by=['hostname'], + severity="MEDIUM" + ) + resp, response_body = self.monasca_client.create_alarm_definitions( + alarm_definition + ) + self._verify_create_alarm_definitions(resp, + response_body, + alarm_definition, + deterministic=True) + @test.attr(type="gate") @test.attr(type=['negative']) def test_create_alarm_definition_with_special_chars_in_expression(self): @@ -815,6 +878,8 @@ class TestAlarmDefinitions(base.BaseMonascaTest): res_body_create_alarm_def['match_by']) self.assertEqual(response_body['severity'], res_body_create_alarm_def['severity']) + self.assertEqual(response_body['deterministic'], + res_body_create_alarm_def['deterministic']) def _verify_element_set(self, element): self.assertTrue(set(['id', @@ -822,6 +887,7 @@ class TestAlarmDefinitions(base.BaseMonascaTest): 'name', 'description', 'expression', + 'deterministic', 'match_by', 'severity', 'actions_enabled', @@ -836,13 +902,18 @@ class TestAlarmDefinitions(base.BaseMonascaTest): self.assertTrue(set(['rel', 'href']) == set(link)) self.assertEqual(link['rel'], u'self') - def _verify_create_alarm_definitions(self, resp, response_body, - alarm_definition): + def _verify_create_alarm_definitions(self, + resp, + response_body, + alarm_definition, + deterministic=False): self.assertEqual(201, resp.status) self.assertEqual(alarm_definition['name'], response_body['name']) self.assertEqual(alarm_definition['expression'], str(response_body['expression'])) + self.assertEqual(deterministic, bool(response_body['deterministic'])) + if 'description' in alarm_definition: self.assertEqual(alarm_definition['description'], str(response_body['description'])) diff --git a/monasca_tempest_tests/tests/api/test_alarms.py b/monasca_tempest_tests/tests/api/test_alarms.py index 4d5aaeec1..000f1a738 100644 --- a/monasca_tempest_tests/tests/api/test_alarms.py +++ b/monasca_tempest_tests/tests/api/test_alarms.py @@ -739,6 +739,64 @@ class TestAlarms(base.BaseMonascaTest): if i != j: self.assertNotEqual(dimensions[i], dimensions[j]) + @test.attr(type="gate") + def test_verify_deterministic_alarm(self): + metric_name = data_utils.rand_name('log.fancy') + metric_dimensions = {'service': 'monitoring', + 'hostname': 'mini-mon'} + + name = data_utils.rand_name('alarm_definition') + expression = ('count(%s{service=monitoring},deterministic) > 10' + % metric_name) + match_by = ['hostname', 'device'] + description = 'deterministic' + + alarm_definition = helpers.create_alarm_definition( + name=name, description=description, + expression=expression, match_by=match_by) + + resp, response_body = self.monasca_client.create_alarm_definitions( + alarm_definition) + + alarm_definition_id = response_body['id'] + query_param = '?alarm_definition_id=' + str(alarm_definition_id) + + # 1. ensure alarm was not created + resp, response_body = self.monasca_client.list_alarms(query_param) + self._verify_list_alarms_elements(resp, response_body, 0) + + # 2. put some metrics here to create it, should be in ok + metrics_count = 5 + for it in range(0, metrics_count): + metric = helpers.create_metric(name=metric_name, + value=1.0, + dimensions=metric_dimensions) + self.monasca_client.create_metrics(metric) + + self._wait_for_alarms(1, alarm_definition_id) + + resp, response_body = self.monasca_client.list_alarms(query_param) + self._verify_list_alarms_elements(resp, response_body, 1) + element = response_body['elements'][0] + + self.assertEqual('OK', element['state']) + + # 3. exceed threshold + metrics_count = 20 + for it in range(0, metrics_count): + metric = helpers.create_metric(name=metric_name, + value=1.0, + dimensions=metric_dimensions) + self.monasca_client.create_metrics(metric) + + self._wait_for_alarms(1, alarm_definition_id) + + resp, response_body = self.monasca_client.list_alarms(query_param) + self._verify_list_alarms_elements(resp, response_body, 1) + element = response_body['elements'][0] + + self.assertEqual('ALARM', element['state']) + def _verify_list_alarms_elements(self, resp, response_body, expect_num_elements): self.assertEqual(200, resp.status)