Add metrics/dimensions/names/values api for grafana templating optimization

This endpoint will return all the dimension values for a given
dimension name and optional metric name (for the scoped project).
This will allow grafana templating to use this (much faster call)
instead of making a full metric-list call and then parsing out
dimensions.

Change-Id: Ia1e2487fe0f96dee03d97d865c58a3728b43f448
Implements: blueprint dimensions-api
This commit is contained in:
Brad Klein 2016-07-01 09:13:18 -06:00
parent 1bea52fb02
commit a317508031
24 changed files with 1164 additions and 86 deletions

View File

@ -1,7 +1,9 @@
Andreas Jaeger <aj@suse.com>
Angelo Mendonca <angelomendonca@gmail.com>
Ben Motz <bmotz@cray.com>
Bertrand Lallau <bertrand.lallau@thalesgroup.com>
Brad Klein <bradley.klein@twcable.com>
Clenimar Filemon <clenimar.filemon@gmail.com>
Craig Bryant <craig.bryant@hp.com>
David C Kennedy <david.c.kennedy@hp.com>
Deklan Dieterly <deklan.dieterly@hp.com>

View File

@ -21,6 +21,7 @@ alarms = monasca_api.v2.reference.alarms:Alarms
alarms_count = monasca_api.v2.reference.alarms:AlarmsCount
alarms_state_history = monasca_api.v2.reference.alarms:AlarmsStateHistory
notification_methods = monasca_api.v2.reference.notifications:Notifications
dimension_values = monasca_api.v2.reference.metrics:DimensionValues
[security]
# The roles that are allowed full access to the API.

View File

@ -16,11 +16,14 @@ Document Version: v2.0
- [Measurement](#measurement)
- [Value Meta](#value-meta)
- [Alarm Definitions and Alarms](#alarm-definitions-and-alarms)
- [Deterministic or non-deterministic alarms](#deterministic-or-non-deterministic-alarms)
- [Alarm Definition Expressions](#alarm-definition-expressions)
- [Syntax](#syntax)
- [Simple Example](#simple-example)
- [More Complex Example](#more-complex-example)
- [Compound alarm example](#compound-alarm-example)
- [Deterministic alarm example](#deterministic-alarm-example)
- [Non-deterministic alarm with deterministic sub expressions](#non-deterministic-alarm-with-deterministic-sub-expressions)
- [Changing Alarm Definitions](#changing-alarm-definitions)
- [Notification Methods](#notification-methods)
- [Common Request Headers](#common-request-headers)
@ -79,9 +82,8 @@ Document Version: v2.0
- [Status Code](#status-code-1)
- [Response Body](#response-body-3)
- [Response Examples](#response-examples-2)
- [Measurements](#measurements)
- [List measurements](#list-measurements)
- [GET /v2.0/metrics/measurements](#get-v20metricsmeasurements)
- [List dimension values](#list-dimension-values)
- [GET /v2.0/metrics/dimensions/names/values](#get-v20metricsdimensionsnamesvalues)
- [Headers](#headers-4)
- [Path Parameters](#path-parameters-4)
- [Query Parameters](#query-parameters-4)
@ -91,9 +93,9 @@ Document Version: v2.0
- [Status Code](#status-code-2)
- [Response Body](#response-body-4)
- [Response Examples](#response-examples-3)
- [Metric Names](#metric-names)
- [List names](#list-names)
- [GET /v2.0/metrics/names](#get-v20metricsnames)
- [Measurements](#measurements)
- [List measurements](#list-measurements)
- [GET /v2.0/metrics/measurements](#get-v20metricsmeasurements)
- [Headers](#headers-5)
- [Path Parameters](#path-parameters-5)
- [Query Parameters](#query-parameters-5)
@ -103,9 +105,9 @@ Document Version: v2.0
- [Status Code](#status-code-3)
- [Response Body](#response-body-5)
- [Response Examples](#response-examples-4)
- [Statistics](#statistics)
- [List statistics](#list-statistics)
- [GET /v2.0/metrics/statistics](#get-v20metricsstatistics)
- [Metric Names](#metric-names)
- [List names](#list-names)
- [GET /v2.0/metrics/names](#get-v20metricsnames)
- [Headers](#headers-6)
- [Path Parameters](#path-parameters-6)
- [Query Parameters](#query-parameters-6)
@ -115,9 +117,9 @@ Document Version: v2.0
- [Status Code](#status-code-4)
- [Response Body](#response-body-6)
- [Response Examples](#response-examples-5)
- [Notification Methods](#notification-methods-1)
- [Create Notification Method](#create-notification-method)
- [POST /v2.0/notification-methods](#post-v20notification-methods)
- [Statistics](#statistics)
- [List statistics](#list-statistics)
- [GET /v2.0/metrics/statistics](#get-v20metricsstatistics)
- [Headers](#headers-7)
- [Path Parameters](#path-parameters-7)
- [Query Parameters](#query-parameters-7)
@ -127,8 +129,9 @@ Document Version: v2.0
- [Status Code](#status-code-5)
- [Response Body](#response-body-7)
- [Response Examples](#response-examples-6)
- [List Notification Methods](#list-notification-methods)
- [GET /v2.0/notification-methods](#get-v20notification-methods)
- [Notification Methods](#notification-methods-1)
- [Create Notification Method](#create-notification-method)
- [POST /v2.0/notification-methods](#post-v20notification-methods)
- [Headers](#headers-8)
- [Path Parameters](#path-parameters-8)
- [Query Parameters](#query-parameters-8)
@ -138,8 +141,8 @@ Document Version: v2.0
- [Status Code](#status-code-6)
- [Response Body](#response-body-8)
- [Response Examples](#response-examples-7)
- [Get Notification Method](#get-notification-method)
- [GET /v2.0/notification-methods/{notification_method_id}](#get-v20notification-methodsnotification_method_id)
- [List Notification Methods](#list-notification-methods)
- [GET /v2.0/notification-methods](#get-v20notification-methods)
- [Headers](#headers-9)
- [Path Parameters](#path-parameters-9)
- [Query Parameters](#query-parameters-9)
@ -149,8 +152,8 @@ Document Version: v2.0
- [Status Code](#status-code-7)
- [Response Body](#response-body-9)
- [Response Examples](#response-examples-8)
- [Update Notification Method](#update-notification-method)
- [PUT /v2.0/notification-methods/{notification_method_id}](#put-v20notification-methodsnotification_method_id)
- [Get Notification Method](#get-notification-method)
- [GET /v2.0/notification-methods/{notification_method_id}](#get-v20notification-methodsnotification_method_id)
- [Headers](#headers-10)
- [Path Parameters](#path-parameters-10)
- [Query Parameters](#query-parameters-10)
@ -171,8 +174,8 @@ Document Version: v2.0
- [Status Code](#status-code-8)
- [Response Body](#response-body-10)
- [Response Examples](#response-examples-9)
- [Delete Notification Method](#delete-notification-method)
- [DELETE /v2.0/notification-methods/{notification_method_id}](#delete-v20notification-methodsnotification_method_id)
- [Update Notification Method](#update-notification-method)
- [PUT /v2.0/notification-methods/{notification_method_id}](#put-v20notification-methodsnotification_method_id)
- [Headers](#headers-11)
- [Path Parameters](#path-parameters-11)
- [Query Parameters](#query-parameters-11)
@ -181,9 +184,9 @@ Document Version: v2.0
- [Response](#response-11)
- [Status Code](#status-code-9)
- [Response Body](#response-body-11)
- [Alarm Definitions](#alarm-definitions)
- [Create Alarm Definition](#create-alarm-definition)
- [POST /v2.0/alarm-definitions](#post-v20alarm-definitions)
- [Response Examples](#response-examples-10)
- [Patch Notification Method](#patch-notification-method)
- [PATCH /v2.0/notification-methods/{notification_method_id}](#patch-v20notification-methodsnotification_method_id)
- [Headers](#headers-12)
- [Path Parameters](#path-parameters-12)
- [Query Parameters](#query-parameters-12)
@ -192,9 +195,9 @@ Document Version: v2.0
- [Response](#response-12)
- [Status Code](#status-code-10)
- [Response Body](#response-body-12)
- [Response Examples](#response-examples-10)
- [List Alarm Definitions](#list-alarm-definitions)
- [GET /v2.0/alarm-definitions](#get-v20alarm-definitions)
- [Response Examples](#response-examples-11)
- [Delete Notification Method](#delete-notification-method)
- [DELETE /v2.0/notification-methods/{notification_method_id}](#delete-v20notification-methodsnotification_method_id)
- [Headers](#headers-13)
- [Path Parameters](#path-parameters-13)
- [Query Parameters](#query-parameters-13)
@ -203,41 +206,41 @@ Document Version: v2.0
- [Response](#response-13)
- [Status Code](#status-code-11)
- [Response Body](#response-body-13)
- [Response Examples](#response-examples-11)
- [Get Alarm Definition](#get-alarm-definition)
- [GET /v2.0/alarm-definitions/{alarm_definition_id}](#get-v20alarm-definitionsalarm_definition_id)
- [Alarm Definitions](#alarm-definitions)
- [Create Alarm Definition](#create-alarm-definition)
- [POST /v2.0/alarm-definitions](#post-v20alarm-definitions)
- [Headers](#headers-14)
- [Path Parameters](#path-parameters-14)
- [Query Parameters](#query-parameters-14)
- [Request Body](#request-body-14)
- [Request Examples](#request-examples-14)
- [Response](#response-14)
- [Status Code](#status-code-12)
- [Response Body](#response-body-14)
- [Response Examples](#response-examples-12)
- [Update Alarm Definition](#update-alarm-definition)
- [PUT /v2.0/alarm-definitions/{alarm_definition_id}](#put-v20alarm-definitionsalarm_definition_id)
- [List Alarm Definitions](#list-alarm-definitions)
- [GET /v2.0/alarm-definitions](#get-v20alarm-definitions)
- [Headers](#headers-15)
- [Path Parameters](#path-parameters-15)
- [Query Parameters](#query-parameters-15)
- [Request Body](#request-body-15)
- [Request Examples](#request-examples-14)
- [Request Examples](#request-examples-15)
- [Response](#response-15)
- [Status Code](#status-code-13)
- [Response Body](#response-body-15)
- [Response Examples](#response-examples-13)
- [Patch Alarm Definition](#patch-alarm-definition)
- [PATCH /v2.0/alarm-definitions/{alarm_definition_id}](#patch-v20alarm-definitionsalarm_definition_id)
- [Get Alarm Definition](#get-alarm-definition)
- [GET /v2.0/alarm-definitions/{alarm_definition_id}](#get-v20alarm-definitionsalarm_definition_id)
- [Headers](#headers-16)
- [Path Parameters](#path-parameters-16)
- [Query Parameters](#query-parameters-16)
- [Request Body](#request-body-16)
- [Request Examples](#request-examples-15)
- [Response](#response-16)
- [Status Code](#status-code-14)
- [Response Body](#response-body-16)
- [Response Examples](#response-examples-14)
- [Delete Alarm Definition](#delete-alarm-definition)
- [DELETE /v2.0/alarm-definitions/{alarm_definition_id}](#delete-v20alarm-definitionsalarm_definition_id)
- [Update Alarm Definition](#update-alarm-definition)
- [PUT /v2.0/alarm-definitions/{alarm_definition_id}](#put-v20alarm-definitionsalarm_definition_id)
- [Headers](#headers-17)
- [Path Parameters](#path-parameters-17)
- [Query Parameters](#query-parameters-17)
@ -246,9 +249,9 @@ Document Version: v2.0
- [Response](#response-17)
- [Status Code](#status-code-15)
- [Response Body](#response-body-17)
- [Alarms](#alarms)
- [List Alarms](#list-alarms)
- [GET /v2.0/alarms](#get-v20alarms)
- [Response Examples](#response-examples-15)
- [Patch Alarm Definition](#patch-alarm-definition)
- [PATCH /v2.0/alarm-definitions/{alarm_definition_id}](#patch-v20alarm-definitionsalarm_definition_id)
- [Headers](#headers-18)
- [Path Parameters](#path-parameters-18)
- [Query Parameters](#query-parameters-18)
@ -257,9 +260,9 @@ Document Version: v2.0
- [Response](#response-18)
- [Status Code](#status-code-16)
- [Response Body](#response-body-18)
- [Response Examples](#response-examples-15)
- [Get Alarm Counts](#get-alarm-counts)
- [GET /v2.0/alarms/count](#get-v20alarmscount)
- [Response Examples](#response-examples-16)
- [Delete Alarm Definition](#delete-alarm-definition)
- [DELETE /v2.0/alarm-definitions/{alarm_definition_id}](#delete-v20alarm-definitionsalarm_definition_id)
- [Headers](#headers-19)
- [Path Parameters](#path-parameters-19)
- [Query Parameters](#query-parameters-19)
@ -268,19 +271,20 @@ Document Version: v2.0
- [Response](#response-19)
- [Status Code](#status-code-17)
- [Response Body](#response-body-19)
- [Response Example](#response-example)
- [List Alarms State History](#list-alarms-state-history)
- [GET /v2.0/alarms/state-history](#get-v20alarmsstate-history)
- [Alarms](#alarms)
- [List Alarms](#list-alarms)
- [GET /v2.0/alarms](#get-v20alarms)
- [Headers](#headers-20)
- [Path Parameters](#path-parameters-20)
- [Query Parameters](#query-parameters-20)
- [Request Body](#request-body-20)
- [Request Examples](#request-examples-19)
- [Response](#response-20)
- [Status Code](#status-code-18)
- [Response Body](#response-body-20)
- [Response Examples](#response-examples-16)
- [Get Alarm](#get-alarm)
- [GET /v2.0/alarms/{alarm_id}](#get-v20alarmsalarm_id)
- [Response Examples](#response-examples-17)
- [List Alarms State History](#list-alarms-state-history)
- [GET /v2.0/alarms/state-history](#get-v20alarmsstate-history)
- [Headers](#headers-21)
- [Path Parameters](#path-parameters-21)
- [Query Parameters](#query-parameters-21)
@ -288,20 +292,19 @@ Document Version: v2.0
- [Response](#response-21)
- [Status Code](#status-code-19)
- [Response Body](#response-body-21)
- [Response Examples](#response-examples-17)
- [Update Alarm](#update-alarm)
- [PUT /v2.0/alarms/{alarm_id}](#put-v20alarmsalarm_id)
- [Response Examples](#response-examples-18)
- [Get Alarm](#get-alarm)
- [GET /v2.0/alarms/{alarm_id}](#get-v20alarmsalarm_id)
- [Headers](#headers-22)
- [Path Parameters](#path-parameters-22)
- [Query Parameters](#query-parameters-22)
- [Request Body](#request-body-22)
- [Request Examples](#request-examples-19)
- [Response](#response-22)
- [Status Code](#status-code-20)
- [Response Body](#response-body-22)
- [Response Examples](#response-examples-18)
- [Patch Alarm](#patch-alarm)
- [PATCH /v2.0/alarms/{alarm_id}](#patch-v20alarmsalarm_id)
- [Response Examples](#response-examples-19)
- [Update Alarm](#update-alarm)
- [PUT /v2.0/alarms/{alarm_id}](#put-v20alarmsalarm_id)
- [Headers](#headers-23)
- [Path Parameters](#path-parameters-23)
- [Query Parameters](#query-parameters-23)
@ -310,9 +313,9 @@ Document Version: v2.0
- [Response](#response-23)
- [Status Code](#status-code-21)
- [Response Body](#response-body-23)
- [Response Examples](#response-examples-19)
- [Delete Alarm](#delete-alarm)
- [DELETE /v2.0/alarms/{alarm_id}](#delete-v20alarmsalarm_id)
- [Response Examples](#response-examples-20)
- [Patch Alarm](#patch-alarm)
- [PATCH /v2.0/alarms/{alarm_id}](#patch-v20alarmsalarm_id)
- [Headers](#headers-24)
- [Path Parameters](#path-parameters-24)
- [Query Parameters](#query-parameters-24)
@ -321,17 +324,28 @@ Document Version: v2.0
- [Response](#response-24)
- [Status Code](#status-code-22)
- [Response Body](#response-body-24)
- [List Alarm State History](#list-alarm-state-history)
- [GET /v2.0/alarms/{alarm_id}/state-history](#get-v20alarmsalarm_idstate-history)
- [Response Examples](#response-examples-21)
- [Delete Alarm](#delete-alarm)
- [DELETE /v2.0/alarms/{alarm_id}](#delete-v20alarmsalarm_id)
- [Headers](#headers-25)
- [Path Parameters](#path-parameters-25)
- [Query Parameters](#query-parameters-25)
- [Request Body](#request-body-25)
- [Request Data](#request-data)
- [Request Examples](#request-examples-22)
- [Response](#response-25)
- [Status Code](#status-code-23)
- [Response Body](#response-body-25)
- [Response Examples](#response-examples-20)
- [List Alarm State History](#list-alarm-state-history)
- [GET /v2.0/alarms/{alarm_id}/state-history](#get-v20alarmsalarm_idstate-history)
- [Headers](#headers-26)
- [Path Parameters](#path-parameters-26)
- [Query Parameters](#query-parameters-26)
- [Request Body](#request-body-26)
- [Request Data](#request-data)
- [Response](#response-26)
- [Status Code](#status-code-24)
- [Response Body](#response-body-26)
- [Response Examples](#response-examples-22)
- [License](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@ -774,6 +788,13 @@ offset=2104-01-01T00:00:01Z
```
A dimension value offset would look as follows:
```
offset=dimensionValue2
```
Different resources use different offset types because of the internal implementation of different resources depends on different types of mechanisms for indexing and identifying resources. The type and form of the offsets for each resource can be determined by referring to the examples in each resource section below.
The offset is determined by the ID of the last element in the result list. Users wishing to manually create a query URL can use the ID of the last element in the previously returned result set as the offset. The proceeding result set will return all elements with an ID greater than the offset up to the limit. The automatically generated offset in the next link does exactly this; it uses the ID in the last element.
@ -1137,6 +1158,76 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of m
````
___
## List dimension values
Get dimension values
#### GET /v2.0/metrics/dimensions/names/values
#### Headers
* X-Auth-Token (string, required) - Keystone auth token
* Accept (string) - application/json
#### Path Parameters
None.
#### Query Parameters
* tenant_id (string, optional, restricted) - Tenant ID to from which to get dimension values. This parameter can be used to get dimension values from a tenant other than the tenant the request auth token is scoped to. Usage of this query parameter is restricted to users with the the monasca admin role, as defined in the monasca api configuration file, which defaults to `monasca-admin`.
* metric_name (string(255), optional) - A metric name to filter dimension values by.
* dimension_name (string(255), required) - A dimension name to filter dimension values by.
* offset (string(255), optional) - The dimension values are returned in alphabetic order, and the offset is the dimension name after which to return in the next pagination request.
* limit (integer, optional)
#### Request Body
None.
#### Request Examples
```
GET /v2.0/metrics/dimensions/names/values?dimension_name=dimension_name HTTP/1.1
Host: 192.168.10.4:8080
Content-Type: application/json
X-Auth-Token: 27feed73a0ce4138934e30d619b415b0
Cache-Control: no-cache
```
### Response
#### Status Code
* 200 - OK
#### Response Body
Returns a JSON object with a 'links' array of links and an 'elements' array of metric definition objects with the following fields:
* dimension_name (string)
* metric_name (string)
* values (list of strings)
#### Response Examples
````
{
"links": [
{
"rel": "self",
"href": "http://192.168.10.4:8080/v2.0/metrics/dimensions/names/values?dimension_name=dimension_name"
},
{
"rel": "next",
"href": "http://192.168.10.4:8080/v2.0/metrics/dimensions/names/values?dimension_name=dimension_name&offset=dimensionValue2"
}
],
"elements": [
{
"id": "b63d9bc1e16582d0ca039616ce3c870556b3095b",
"metric_name": "metric_name",
"dimension_name": "dimension_name",
"values": [
"dimensonValue1",
"dimensonValue2"
]
}
]
}
````
___
# Measurements
Operations for accessing measurements of metrics.

View File

@ -21,6 +21,7 @@ alarms = monasca_api.v2.reference.alarms:Alarms
alarms_count = monasca_api.v2.reference.alarms:AlarmsCount
alarms_state_history = monasca_api.v2.reference.alarms:AlarmsStateHistory
notification_methods = monasca_api.v2.reference.notifications:Notifications
dimension_values = monasca_api.v2.reference.metrics:DimensionValues
[security]
# The roles that are allowed full access to the API.

View File

@ -38,6 +38,7 @@ import monasca.api.infrastructure.servlet.PreAuthenticationFilter;
import monasca.api.infrastructure.servlet.RoleAuthorizationFilter;
import monasca.api.resource.AlarmDefinitionResource;
import monasca.api.resource.AlarmResource;
import monasca.api.resource.DimensionResource;
import monasca.api.resource.MeasurementResource;
import monasca.api.resource.MetricResource;
import monasca.api.resource.NotificationMethodResource;
@ -107,6 +108,7 @@ public class MonApiApplication extends Application<ApiConfig> {
environment.jersey().register(Injector.getInstance(VersionResource.class));
environment.jersey().register(Injector.getInstance(AlarmDefinitionResource.class));
environment.jersey().register(Injector.getInstance(AlarmResource.class));
environment.jersey().register(Injector.getInstance(DimensionResource.class));
environment.jersey().register(Injector.getInstance(MetricResource.class));
environment.jersey().register(Injector.getInstance(MeasurementResource.class));
environment.jersey().register(Injector.getInstance(StatisticResource.class));

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2016 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.api.domain.model.dimension;
import java.util.List;
import javax.annotation.Nullable;
/**
* Repository for dimensions.
*/
public interface DimensionRepo {
/**
* Finds dimension values given a dimension name and
* optional metric name.
*/
DimensionValues find(String metricName,
String tenantId,
String dimensionName,
@Nullable String offset,
int limit)
throws Exception;
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) 2016 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.api.domain.model.dimension;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import monasca.common.model.domain.common.AbstractEntity;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.binary.Hex;
/**
* Encapsulates the list of dimension values for a given dimension name
* (and optional metric-name).
*/
public class DimensionValues extends AbstractEntity {
protected String id = null;
protected String dimensionName;
@JsonInclude(JsonInclude.Include.NON_NULL)
protected String metricName;
protected List<String> values;
protected Map<String, List<String>> dimensionValues;
public DimensionValues() {
this.values = new ArrayList<String>();
this.dimensionValues = new HashMap<String, List<String>>();
}
public DimensionValues(String metricName, String dimensionName, List<String> values) {
this.metricName = metricName;
this.dimensionName = dimensionName;
this.values = values;
this.dimensionValues = new HashMap<String, List<String>>();
this.dimensionValues.put(dimensionName, values);
this.id = generateId();
}
public List<String> getValues() {
return values;
}
public String getDimensionName() {
return dimensionName;
}
public String getMetricName() {
return metricName;
}
public String getId() {
if (null == this.id) {
this.id = generateId();
}
return this.id;
}
private String generateId() {
String hashstr = "metricName=" + metricName + "dimensionName=" + dimensionName;
byte[] sha1Hash = DigestUtils.sha(hashstr);
return Hex.encodeHexString(sha1Hash);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DimensionValues other = (DimensionValues) obj;
if (dimensionName == null) {
if (other.dimensionName != null)
return false;
} else if (!dimensionName.equals(other.dimensionName))
return false;
if (metricName == null) {
if (other.metricName != null)
return false;
} else if (!metricName.equals(other.metricName))
return false;
if (values == null) {
if (other.values != null)
return false;
} else if (!values.equals(other.values))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((dimensionName == null) ? 0 : dimensionName.hashCode());
result = prime * result + ((metricName == null) ? 0 : metricName.hashCode());
result = prime * result + ((values == null) ? 0 : values.hashCode());
return result;
}
@Override
public String toString() {
return String.format("MetricName=%s DimensionValues [name=%s, values=%s]",
metricName, dimensionName, values);
}
}

View File

@ -22,6 +22,7 @@ import monasca.api.ApiConfig;
import monasca.api.domain.model.alarm.AlarmRepo;
import monasca.api.domain.model.alarmdefinition.AlarmDefinitionRepo;
import monasca.api.domain.model.alarmstatehistory.AlarmStateHistoryRepo;
import monasca.api.domain.model.dimension.DimensionRepo;
import monasca.api.domain.model.measurement.MeasurementRepo;
import monasca.api.domain.model.metric.MetricDefinitionRepo;
import monasca.api.domain.model.notificationmethod.NotificationMethodRepo;
@ -29,6 +30,7 @@ import monasca.api.domain.model.statistic.StatisticRepo;
import monasca.api.infrastructure.persistence.PersistUtils;
import monasca.api.infrastructure.persistence.Utils;
import monasca.api.infrastructure.persistence.influxdb.InfluxV9AlarmStateHistoryRepo;
import monasca.api.infrastructure.persistence.influxdb.InfluxV9DimensionRepo;
import monasca.api.infrastructure.persistence.influxdb.InfluxV9MeasurementRepo;
import monasca.api.infrastructure.persistence.influxdb.InfluxV9MetricDefinitionRepo;
import monasca.api.infrastructure.persistence.influxdb.InfluxV9RepoReader;
@ -43,6 +45,7 @@ import monasca.api.infrastructure.persistence.hibernate.AlarmSqlRepoImpl;
import monasca.api.infrastructure.persistence.hibernate.NotificationMethodSqlRepoImpl;
import monasca.api.infrastructure.persistence.hibernate.AlarmHibernateUtils;
import monasca.api.infrastructure.persistence.vertica.AlarmStateHistoryVerticaRepoImpl;
import monasca.api.infrastructure.persistence.vertica.DimensionVerticaRepoImpl;
import monasca.api.infrastructure.persistence.vertica.MeasurementVerticaRepoImpl;
import monasca.api.infrastructure.persistence.vertica.MetricDefinitionVerticaRepoImpl;
import monasca.api.infrastructure.persistence.vertica.StatisticVerticaRepoImpl;
@ -84,6 +87,7 @@ public class InfrastructureModule extends AbstractModule {
if (config.databaseConfiguration.getDatabaseType().trim().equalsIgnoreCase(VERTICA)) {
bind(AlarmStateHistoryRepo.class).to(AlarmStateHistoryVerticaRepoImpl.class).in(Singleton.class);
bind(DimensionRepo.class).to(DimensionVerticaRepoImpl.class).in(Singleton.class);
bind(MetricDefinitionRepo.class).to(MetricDefinitionVerticaRepoImpl.class).in(Singleton.class);
bind(MeasurementRepo.class).to(MeasurementVerticaRepoImpl.class).in(Singleton.class);
bind(StatisticRepo.class).to(StatisticVerticaRepoImpl.class).in(Singleton.class);
@ -103,6 +107,7 @@ public class InfrastructureModule extends AbstractModule {
bind(InfluxV9Utils.class).in(Singleton.class);
bind(InfluxV9RepoReader.class).in(Singleton.class);
bind(AlarmStateHistoryRepo.class).to(InfluxV9AlarmStateHistoryRepo.class).in(Singleton.class);
bind(DimensionRepo.class).to(InfluxV9DimensionRepo.class).in(Singleton.class);
bind(MetricDefinitionRepo.class).to(InfluxV9MetricDefinitionRepo.class).in(Singleton.class);
bind(MeasurementRepo.class).to(InfluxV9MeasurementRepo.class).in(Singleton.class);
bind(StatisticRepo.class).to(InfluxV9StatisticRepo.class).in(Singleton.class);

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2016 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.api.infrastructure.persistence.influxdb;
import com.google.inject.Inject;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.Set;
import monasca.api.ApiConfig;
import monasca.api.domain.model.dimension.DimensionValues;
import monasca.api.domain.model.dimension.DimensionRepo;
public class InfluxV9DimensionRepo implements DimensionRepo {
private static final Logger logger = LoggerFactory.getLogger(InfluxV9DimensionRepo.class);
private final ApiConfig config;
private final InfluxV9RepoReader influxV9RepoReader;
private final InfluxV9Utils influxV9Utils;
private final String region;
private final ObjectMapper objectMapper = new ObjectMapper();
@Inject
public InfluxV9DimensionRepo(ApiConfig config,
InfluxV9RepoReader influxV9RepoReader,
InfluxV9Utils influxV9Utils) {
this.config = config;
this.region = config.region;
this.influxV9RepoReader = influxV9RepoReader;
this.influxV9Utils = influxV9Utils;
}
@Override
public DimensionValues find(
String metricName,
String tenantId,
String dimensionName,
String offset,
int limit) throws Exception
{
//
// Use treeset to keep list in alphabetic/predictable order
// for string based offset.
//
Set<String> matchingValues = new TreeSet<String>();
String dimNamePart = "and \""
+ this.influxV9Utils.sanitize(dimensionName)
+ "\" =~ /.*/";
String q = String.format("show series %1$s where %2$s %3$s",
this.influxV9Utils.namePart(metricName, false),
this.influxV9Utils.privateTenantIdPart(tenantId),
dimNamePart);
logger.debug("Dimension values query: {}", q);
String r = this.influxV9RepoReader.read(q);
Series series = this.objectMapper.readValue(r, Series.class);
if (!series.isEmpty()) {
for (Serie serie : series.getSeries()) {
for (String[] values : serie.getValues()) {
Map<String, String> dimensions = this.influxV9Utils.getDimensions(values, serie.getColumns());
for (Map.Entry<String, String> entry : dimensions.entrySet()) {
if (dimensionName.equals(entry.getKey())) {
matchingValues.add(entry.getValue());
}
}
}
}
}
List<String> filteredValues = filterDimensionValues(matchingValues,
dimensionName,
limit,
offset);
return new DimensionValues(metricName, dimensionName, filteredValues);
}
private List<String> filterDimensionValues(Set<String> matchingValues,
String dimensionName,
int limit,
String offset)
{
Boolean haveOffset = (null != offset && !"".equals(offset));
List<String> filteredValues = new ArrayList<String>();
int remaining_limit = limit + 1;
for (String dimVal : matchingValues) {
if (remaining_limit <= 0) {
break;
}
if (haveOffset && dimVal.compareTo(offset) <= 0) {
continue;
}
filteredValues.add(dimVal);
remaining_limit--;
}
return filteredValues;
}
}

View File

@ -166,7 +166,8 @@ public class InfluxV9MetricDefinitionRepo implements MetricDefinitionRepo {
for (String[] values : serie.getValues()) {
MetricDefinition m = new MetricDefinition(serie.getName(), dims(values, serie.getColumns()));
MetricDefinition m = new MetricDefinition(serie.getName(),
this.influxV9Utils.getDimensions(values, serie.getColumns()));
//
// If start/end time are specified, ensure we've got measurements
// for this definition before we add to the return list
@ -202,27 +203,6 @@ public class InfluxV9MetricDefinitionRepo implements MetricDefinitionRepo {
return metricNameList;
}
private Map<String, String> dims(String[] vals, String[] cols) {
Map<String, String> dims = new HashMap<>();
for (int i = 0; i < cols.length; ++i) {
// Dimension names that start with underscore are reserved. I.e., _key, _region, _tenant_id.
// Influxdb inserts _key.
// Monasca Persister inserts _region and _tenant_id.
if (!cols[i].startsWith("_")) {
if (!vals[i].equalsIgnoreCase("null")) {
dims.put(cols[i], vals[i]);
}
}
}
return dims;
}
private boolean hasMeasurements(MetricDefinition m,
String tenantId,
DateTime startTime,

View File

@ -291,4 +291,20 @@ public class InfluxV9Utils {
public List<String> parseMultiOffset(String offsetStr) {
return offsetSplitter.splitToList(offsetStr);
}
public Map<String, String> getDimensions(String[] vals, String[] cols) {
Map<String, String> dims = new HashMap<>();
for (int i = 0; i < cols.length; ++i) {
// Dimension names that start with underscore are reserved. I.e., _key, _region, _tenant_id.
// Influxdb inserts _key.
// Monasca Persister inserts _region and _tenant_id.
if (!cols[i].startsWith("_")) {
if (!vals[i].equalsIgnoreCase("null")) {
dims.put(cols[i], vals[i]);
}
}
}
return dims;
}
}

View File

@ -0,0 +1,109 @@
/* Copyright (c) 2016 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.api.infrastructure.persistence.vertica;
import monasca.api.ApiConfig;
import monasca.api.domain.model.dimension.DimensionRepo;
import monasca.api.domain.model.dimension.DimensionValues;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.annotation.Nullable;
import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DimensionVerticaRepoImpl implements DimensionRepo {
private static final Logger logger = LoggerFactory
.getLogger(DimensionVerticaRepoImpl.class);
private static final String FIND_DIMENSION_VALUES_SQL =
"SELECT %s" // dbHint goes here
+ " DISTINCT dims.value as dValue "
+ "FROM "
+ " MonMetrics.Definitions def,"
+ " MonMetrics.DefinitionDimensions defdims "
+ "LEFT OUTER JOIN"
+ " MonMetrics.Dimensions dims"
+ " ON dims.dimension_set_id = defdims.dimension_set_id "
+ "WHERE"
+ " def.id = defdims.definition_id"
+ " %s " // optional offset goes here
+ " %s " // optional metric name goes here
+ " and def.tenant_id = '%s'" // tenant_id goes here
+ " and dims.name = '%s' " // dimension name goes here
+ "ORDER BY dims.value ASC "
+ "%s "; // limit goes here
private final DBI db;
private final String dbHint;
@Inject
public DimensionVerticaRepoImpl(
@Named("vertica") DBI db, ApiConfig config)
{
this.db = db;
this.dbHint = config.vertica.dbHint;
}
@Override
public DimensionValues find(
String metricName,
String tenantId,
String dimensionName,
String offset,
int limit) throws Exception
{
List<String> values = new ArrayList<String>();
String offsetPart = "";
String metricNamePart = "";
try (Handle h = db.open()) {
if (offset != null && !offset.isEmpty()) {
offsetPart = " and dims.value > '" + offset + "' ";
}
if (metricName != null && !metricName.isEmpty()) {
metricNamePart = " and def.name = '" + metricName + "' ";
}
String limitPart = " limit " + Integer.toString(limit + 1);
String sql = String.format(FIND_DIMENSION_VALUES_SQL,
this.dbHint,
offsetPart,
metricNamePart,
tenantId,
dimensionName,
limitPart);
Query<Map<String, Object>> query = h.createQuery(sql);
List<Map<String, Object>> rows = query.list();
for (Map<String, Object> row : rows) {
String dimValue = (String) row.get("dValue");
values.add(dimValue);
}
}
return new DimensionValues(metricName, dimensionName, values);
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2016 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.api.resource;
import static monasca.api.app.validation.Validation.DEFAULT_ADMIN_ROLE;
import com.google.common.base.Strings;
import com.codahale.metrics.annotation.Timed;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
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.UriInfo;
import monasca.api.app.validation.MetricNameValidation;
import monasca.api.ApiConfig;
import monasca.api.app.validation.Validation;
import monasca.api.domain.model.dimension.DimensionRepo;
import monasca.api.domain.model.dimension.DimensionValues;
import monasca.api.infrastructure.persistence.PersistUtils;
/**
* Dimension resource implementation.
*/
@Path("/v2.0/metrics/dimensions/names/values")
public class DimensionResource {
private final DimensionRepo repo;
private final PersistUtils persistUtils;
private final String admin_role;
@Inject
public DimensionResource(ApiConfig config, DimensionRepo repo, PersistUtils persistUtils) {
this.admin_role = (config.middleware == null || config.middleware.adminRole == null)
? DEFAULT_ADMIN_ROLE : config.middleware.adminRole;
this.repo = repo;
this.persistUtils = persistUtils;
}
@GET
@Timed
@Produces(MediaType.APPLICATION_JSON)
public Object get(
@Context UriInfo uriInfo,
@HeaderParam("X-Tenant-Id") String tenantId,
@HeaderParam("X-Roles") String roles,
@QueryParam("limit") String limit,
@QueryParam("dimension_name") String dimensionName,
@QueryParam("metric_name") String metricName,
@QueryParam("offset") String offset,
@QueryParam("tenant_id") String crossTenantId) throws Exception
{
Validation.validateNotNullOrEmpty(dimensionName, "dimension_name");
final int paging_limit = this.persistUtils.getLimit(limit);
String queryTenantId = Validation.getQueryProject(roles, crossTenantId, tenantId, admin_role);
DimensionValues dimVals = repo.find(metricName, queryTenantId, dimensionName, offset, paging_limit);
return Links.paginateDimensionValues(dimVals, paging_limit, uriInfo);
}
}

View File

@ -26,6 +26,7 @@ import com.google.common.base.Preconditions;
import monasca.api.ApiConfig;
import monasca.api.domain.model.alarm.AlarmCount;
import monasca.api.domain.model.common.Paged;
import monasca.api.domain.model.dimension.DimensionValues;
import monasca.api.domain.model.measurement.Measurements;
import monasca.common.model.domain.common.AbstractEntity;
import monasca.api.domain.model.common.Link;
@ -359,4 +360,21 @@ public final class Links {
alarmCount.setLinks(links);
}
public static Paged paginateDimensionValues(DimensionValues dimVals, int limit, UriInfo uriInfo)
throws UnsupportedEncodingException {
Paged paged = new Paged();
List<DimensionValues> elements = new ArrayList<DimensionValues>();
paged.links.add(getSelfLink(uriInfo));
if ((null != dimVals) && (dimVals.getValues().size() > limit)) {
dimVals.getValues().remove(dimVals.getValues().size()-1);
String offset = dimVals.getValues().get(dimVals.getValues().size()-1);
paged.links.add(getNextLink(offset, uriInfo));
}
elements.add(dimVals);
paged.elements = elements;
return paged;
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2016 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.api.resource;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertEquals;
import java.util.List;
import java.util.Map;
import org.joda.time.DateTime;
import org.testng.annotations.Test;
import monasca.api.ApiConfig;
import monasca.api.domain.model.dimension.DimensionRepo;
import monasca.api.infrastructure.persistence.PersistUtils;
import com.sun.jersey.api.client.ClientResponse;
@Test
public class DimensionResourceTest extends AbstractMonApiResourceTest {
private DimensionRepo dimensionRepo;
private ApiConfig apiConfig;
@Override
protected void setupResources() throws Exception {
super.setupResources();
dimensionRepo = mock(DimensionRepo.class);
apiConfig = mock(ApiConfig.class);
addResources(new DimensionResource(apiConfig, dimensionRepo, new PersistUtils()));
}
@SuppressWarnings("unchecked")
public void shouldQueryWithDefaultParams() throws Exception {
client()
.resource(
"/v2.0/metrics/dimensions/names/values?dimension_name=hpcs.compute")
.header("X-Tenant-Id", "abc").get(ClientResponse.class);
verify(dimensionRepo).find(anyString(), anyString(), anyString(), anyString(),
anyInt());
}
public void shouldQueryWithOptionalMetricName() throws Exception {
client()
.resource(
"/v2.0/metrics/dimensions/names/values?dimension_name=hpcs.compute&metric_name=cpu_utilization")
.header("X-Tenant-Id", "abc").get(ClientResponse.class);
verify(dimensionRepo).find(anyString(), anyString(), anyString(), anyString(),
anyInt());
}
}

View File

@ -18,6 +18,7 @@ import monasca.api.domain.model.alarm.Alarm;
import monasca.api.domain.model.common.Link;
import monasca.api.domain.model.common.Paged;
import monasca.common.model.alarm.AlarmState;
import monasca.api.domain.model.dimension.DimensionValues;
import static org.testng.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -133,4 +134,50 @@ public class LinksTest {
+ AlarmDefinitionResource.ALARM_DEFINITIONS_PATH.substring(1) + "/"
+ ALARM_DEF_ID));
}
public void verifyPaginateDimensionValues() throws UnsupportedEncodingException, URISyntaxException{
final String base = "http://TheVip:8070/v2.0/metrics/dimensions/names/values";
final String limitParam = "limit=1";
final String url = base + "?" + limitParam;
final UriInfo uriInfo = mock(UriInfo.class);
when(uriInfo.getRequestUri()).thenReturn(new URI(url));
when(uriInfo.getAbsolutePath()).thenReturn(new URI(base));
final Map<String, String> params = new HashMap<>();
params.put("limit", "1");
@SuppressWarnings("unchecked")
final MultivaluedMap<String, String> mockParams = mock(MultivaluedMap.class);
when(uriInfo.getQueryParameters()).thenReturn(mockParams);
when(mockParams.keySet()).thenReturn(params.keySet());
when(mockParams.get("limit")).thenReturn(Arrays.asList("1"));
List<String> values = new ArrayList<String>();
values.add("value1");
values.add("value2");
List<String> oneValue = Arrays.asList("value1");
DimensionValues dimVals = new DimensionValues("custom_metric",
"dimension_name",
values);
DimensionValues expectedDimVal = new DimensionValues("custom_metric",
"dimension_name",
oneValue);
final int limit = 1;
final Paged expected = new Paged();
final List<Link> links = new ArrayList<>();
String expectedSelf = url;
String expectedNext = base + "?offset=value1&" + limitParam;
links.add(new Link("self", expectedSelf));
links.add(new Link("next", expectedNext));
expected.links = links;
final ArrayList<DimensionValues> expectedElements = new ArrayList<DimensionValues>();
// Since limit is one, only the first element is returned
expectedElements.add(expectedDimVal);
expected.elements = expectedElements;
final Paged actual = Links.paginateDimensionValues(dimVals, 1, uriInfo);
assertEquals(actual, expected);
}
}

View File

@ -55,3 +55,12 @@ class MetricsNamesV2API(object):
def on_get(self, req, res):
res.status = '501 Not Implemented'
class DimensionValuesV2API(object):
def __init__(self):
super(DimensionValuesV2API, self).__init__()
LOG.info('Initializing DimensionValuesV2API!')
def on_get(self, req, res):
res.status = '501 Not Implemented'

View File

@ -43,7 +43,9 @@ dispatcher_opts = [cfg.StrOpt('versions', default=None,
cfg.StrOpt('alarms_state_history', default=None,
help='Alarms state history'),
cfg.StrOpt('notification_methods', default=None,
help='Notification methods')]
help='Notification methods'),
cfg.StrOpt('dimension_values', default=None,
help='Dimension values')]
dispatcher_group = cfg.OptGroup(name='dispatcher', title='dispatcher')
cfg.CONF.register_group(dispatcher_group)
@ -108,6 +110,9 @@ def launch(conf, config_file="/etc/monasca/api-config.conf"):
app.add_route("/v2.0/notification-methods/{notification_method_id}",
notification_methods)
dimension_values = simport.load(cfg.CONF.dispatcher.dimension_values)()
app.add_route("/v2.0/metrics/dimensions/names/values", dimension_values)
LOG.debug('Dispatcher drivers have been added to the routes!')
return app

View File

@ -15,6 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from datetime import datetime
import hashlib
import json
from influxdb import client
@ -205,6 +207,46 @@ class MetricsRepository(metrics_repository.MetricsRepository):
LOG.exception(ex)
raise exceptions.RepositoryException(ex)
def _generate_dimension_values_id(self, metric_name, dimension_name):
sha1 = hashlib.sha1()
hashstr = "metricName=" + (metric_name or "") + "dimensionName=" + dimension_name
sha1.update(hashstr)
return sha1.hexdigest()
def _build_serie_dimension_values(self, series_names, metric_name, dimension_name,
tenant_id, region, offset):
dim_vals = []
sha1_id = self._generate_dimension_values_id(metric_name, dimension_name)
json_dim_vals = {u'id': sha1_id,
u'dimension_name': dimension_name,
u'values': dim_vals}
#
# Only return metric name if one was provided
#
if metric_name:
json_dim_vals[u'metric_name'] = metric_name
if not series_names:
return json_dim_vals
if 'series' in series_names.raw:
for series in series_names.raw['series']:
for tag_values in series[u'values']:
dims = {
name: value
for name, value in zip(series[u'columns'], tag_values)
if value and not name.startswith(u'_')
}
if dimension_name in dims and dims[dimension_name] not in dim_vals:
dim_vals.append(dims[dimension_name])
dim_vals = sorted(dim_vals)
json_dim_vals[u'values'] = dim_vals
return json_dim_vals
def _build_serie_metric_list(self, series_names, tenant_id, region,
start_timestamp, end_timestamp,
offset):
@ -585,3 +627,23 @@ class MetricsRepository(metrics_repository.MetricsRepository):
def _get_millis_from_timestamp(self, dt):
dt = dt.replace(tzinfo=None)
return int((dt - datetime(1970, 1, 1)).total_seconds() * 1000)
def list_dimension_values(self, tenant_id, region, metric_name,
dimension_name, offset, limit):
try:
query = self._build_show_series_query(None, metric_name, tenant_id, region)
result = self.influxdb_client.query(query)
json_dim_vals = self._build_serie_dimension_values(result,
metric_name,
dimension_name,
tenant_id,
region,
offset)
return json_dim_vals
except Exception as ex:
LOG.exception(ex)
raise exceptions.RepositoryException(ex)

View File

@ -120,3 +120,40 @@ class TestRepoMetricsInfluxDB(unittest.TestCase):
u'hosttype': u'native'
},
}])
@patch("monasca_api.common.repositories.influxdb.metrics_repository.client.InfluxDBClient")
def test_list_dimension_values(self, influxdb_client_mock):
mock_client = influxdb_client_mock.return_value
mock_client.query.return_value.raw = {
u'series': [{
u'values': [[
u'custom_metric,_region=useast,_tenant_id=38dc2a2549f94d2e9a4fa1cc45a4970c,'
u'hostname=custom_host,service=custom_service',
u'useast',
u'38dc2a2549f94d2e9a4fa1cc45a4970c',
u'custom_host',
u'custom_service'
]],
u'name': u'custom_metric',
u'columns': [u'_key', u'_region', u'_tenant_id', u'hostname', u'service']
}]
}
repo = influxdb_repo.MetricsRepository()
result = repo.list_dimension_values(
"38dc2a2549f94d2e9a4fa1cc45a4970c",
"useast",
"custom_metric",
"hostname",
offset=None,
limit=1)
self.assertEqual(result, {
u'dimension_name': 'hostname',
u'values': [
u'custom_host'
],
u'id': 'bea9565d854a16a3366164de213694c190f27675',
u'metric_name': 'custom_metric'
})

View File

@ -398,6 +398,61 @@ def paginate_alarming(resource, uri, limit):
return resource
def paginate_dimension_values(dimvals, uri, offset, limit):
parsed_uri = urlparse.urlparse(uri)
self_link = build_base_uri(parsed_uri)
old_query_params = _get_old_query_params(parsed_uri)
if old_query_params:
self_link += '?' + '&'.join(old_query_params)
if (dimvals and dimvals[u'values']):
have_more, truncated_values = _truncate_dimension_values(dimvals[u'values'],
limit,
offset)
links = [{u'rel': u'self', u'href': self_link.decode('utf8')}]
if have_more:
new_offset = truncated_values[limit - 1]
next_link = build_base_uri(parsed_uri)
new_query_params = [u'offset' + '=' + urlparse.quote(
new_offset.encode('utf8'), safe='')]
_get_old_query_params_except_offset(new_query_params, parsed_uri)
if new_query_params:
next_link += '?' + '&'.join(new_query_params)
links.append({u'rel': u'next', u'href': next_link.decode('utf8')})
truncated_dimvals = {u'id': dimvals[u'id'],
u'dimension_name': dimvals[u'dimension_name'],
u'values': truncated_values}
#
# Only return metric name if one was provided
#
if u'metric_name' in dimvals:
truncated_dimvals[u'metric_name'] = dimvals[u'metric_name']
resource = {u'links': links,
u'elements': [truncated_dimvals]}
else:
resource = {u'links': ([{u'rel': u'self',
u'href': self_link.decode('utf8')}]),
u'elements': [dimvals]}
return resource
def _truncate_dimension_values(values, limit, offset):
if offset and offset in values:
next_value_pos = values.index(offset) + 1
values = values[next_value_pos:]
have_more = len(values) > limit
return have_more, values[:limit]
def paginate_measurement(measurement, uri, limit):
parsed_uri = urlparse.urlparse(uri)

View File

@ -302,3 +302,44 @@ class MetricsNames(metrics_api_v2.MetricsNamesV2API):
offset, limit)
return helpers.paginate(result, req_uri, limit)
class DimensionValues(metrics_api_v2.DimensionValuesV2API):
def __init__(self):
try:
super(DimensionValues, self).__init__()
self._region = cfg.CONF.region
self._default_authorized_roles = (
cfg.CONF.security.default_authorized_roles)
self._metrics_repo = simport.load(
cfg.CONF.repositories.metrics_driver)()
except Exception as ex:
LOG.exception(ex)
raise falcon.HTTPInternalServerError('Service unavailable',
ex.message)
def on_get(self, req, res):
helpers.validate_authorization(req, self._default_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
metric_name = helpers.get_query_param(req, 'metric_name')
dimension_name = helpers.get_query_param(req, 'dimension_name', required=True)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._dimension_values(tenant_id, req.uri, metric_name,
dimension_name, offset, limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
@resource.resource_try_catch_block
def _dimension_values(self, tenant_id, req_uri, metric_name,
dimension_name, offset, limit):
result = self._metrics_repo.list_dimension_values(tenant_id,
self._region,
metric_name,
dimension_name,
offset,
limit)
return helpers.paginate_dimension_values(result, req_uri, offset, limit)

View File

@ -52,6 +52,13 @@ class MonascaClient(rest_client.RestClient):
resp, response_body = self.get(uri)
return resp, json.loads(response_body)
def list_dimension_values(self, query_params=None):
uri = 'metrics/dimensions/names/values'
if query_params is not None:
uri = uri + query_params
resp, response_body = self.get(uri)
return resp, json.loads(response_body)
def list_measurements(self, query_params=None):
uri = 'metrics/measurements'
if query_params is not None:

View File

@ -0,0 +1,159 @@
# (C) Copyright 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 the License.
import time
from oslo_utils import timeutils
from monasca_tempest_tests.tests.api import base
from monasca_tempest_tests.tests.api import constants
from monasca_tempest_tests.tests.api import helpers
from tempest.common.utils import data_utils
from tempest import test
from tempest.lib import exceptions
class TestDimensions(base.BaseMonascaTest):
@classmethod
def resource_setup(cls):
super(TestDimensions, cls).resource_setup()
name = data_utils.rand_name()
key = data_utils.rand_name()
value1 = "value_1"
value2 = "value_2"
cls._param = key + ':' + value1
cls._dimension_name = key
cls._dim_val_1 = value1
cls._dim_val_2 = value2
metric = helpers.create_metric(name=name,
dimensions={key: value1})
cls.monasca_client.create_metrics(metric)
metric = helpers.create_metric(name=name,
dimensions={key: value2})
cls.monasca_client.create_metrics(metric)
cls._test_metric = metric
start_time = str(timeutils.iso8601_from_timestamp(
metric['timestamp'] / 1000.0))
parms = '?name=' + str(cls._test_metric['name']) + \
'&start_time=' + start_time
for i in xrange(constants.MAX_RETRIES):
resp, response_body = cls.monasca_client.list_metrics(
parms)
elements = response_body['elements']
for element in elements:
if str(element['name']) == cls._test_metric['name']:
return
time.sleep(constants.RETRY_WAIT_SECS)
assert False, 'Unable to initialize metrics'
@classmethod
def resource_cleanup(cls):
super(TestDimensions, cls).resource_cleanup()
@test.attr(type='gate')
def test_list_dimension_values_without_metric_name(self):
parms = '?dimension_name=' + self._dimension_name
resp, response_body = self.monasca_client.list_dimension_values(parms)
self.assertEqual(200, resp.status)
self.assertTrue(set(['links', 'elements']) == set(response_body))
if not self._is_dimension_name_in_list(response_body):
self.fail('Dimension name not found in response')
if self._is_metric_name_in_list(response_body):
self.fail('Metric name was in response and should not be')
if not self._are_dim_vals_in_list(response_body):
self.fail('Dimension value not found in response')
@test.attr(type='gate')
def test_list_dimension_values_with_metric_name(self):
parms = '?metric_name=' + self._test_metric['name']
parms += '&dimension_name=' + self._dimension_name
resp, response_body = self.monasca_client.list_dimension_values(parms)
self.assertEqual(200, resp.status)
self.assertTrue(set(['links', 'elements']) == set(response_body))
if not self._is_metric_name_in_list(response_body):
self.fail('Metric name not found in response')
if not self._is_dimension_name_in_list(response_body):
self.fail('Dimension name not found in response')
if not self._are_dim_vals_in_list(response_body):
self.fail('Dimension value not found in response')
@test.attr(type='gate')
def test_list_dimension_values_limit_and_offset(self):
parms = '?dimension_name=' + self._dimension_name
parms += '&limit=1'
resp, response_body = self.monasca_client.list_dimension_values(parms)
self.assertEqual(200, resp.status)
self.assertTrue(set(['links', 'elements']) == set(response_body))
if not self._is_dimension_name_in_list(response_body):
self.fail('Dimension name not found in response')
if not self._is_dim_val_in_list(response_body, self._dim_val_1):
self.fail('First dimension value not found in response')
if not self._is_offset_in_links(response_body, self._dim_val_1):
self.fail('Offset not found in response')
if not self._is_limit_in_links(response_body):
self.fail('Limit not found in response')
@test.attr(type='gate')
@test.attr(type=['negative'])
def test_list_dimension_values_no_dimension_name(self):
self.assertRaises(exceptions.UnprocessableEntity,
self.monasca_client.list_dimension_values)
def _is_metric_name_in_list(self, response_body):
elements = response_body['elements'][0]
if 'metric_name' not in elements:
return False
if str(elements['metric_name']) == self._test_metric['name']:
return True
return False
def _is_dimension_name_in_list(self, response_body):
elements = response_body['elements'][0]
if str(elements['dimension_name']) == self._dimension_name:
return True
return False
def _are_dim_vals_in_list(self, response_body):
elements = response_body['elements'][0]
have_dim_1 = self._is_dim_val_in_list(response_body, self._dim_val_1)
have_dim_2 = self._is_dim_val_in_list(response_body, self._dim_val_2)
if have_dim_1 and have_dim_1:
return True
return False
def _is_dim_val_in_list(self, response_body, dim_val):
elements = response_body['elements'][0]
if dim_val in elements['values']:
return True
return False
def _is_offset_in_links(self, response_body, dim_val):
links = response_body['links']
offset = "offset=" + dim_val
for link in links:
if offset in link['href']:
return True
return False
def _is_limit_in_links(self, response_body):
links = response_body['links']
limit = "limit=1"
for link in links:
if limit in link['href']:
return True
return False