Skip to content

Commit

Permalink
Merge pull request #80 from Dynatrace-James-Kitson/fixes-and-improvem…
Browse files Browse the repository at this point in the history
…ents

Additional settings implementation and update SLO endpoint
  • Loading branch information
Dynatrace-James-Kitson authored Jan 18, 2024
2 parents 3d16f88 + d9a7481 commit 8b795bd
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 112 deletions.
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ from dynatrace import Dynatrace
from dynatrace import TOO_MANY_REQUESTS_WAIT
from dynatrace.environment_v2.tokens_api import SCOPE_METRICS_READ, SCOPE_METRICS_INGEST
from dynatrace.configuration_v1.credential_vault import PublicCertificateCredentials
from dynatrace.environment_v2.settings import SettingsObject, SettingsObjectCreate

from datetime import datetime, timedelta

Expand Down Expand Up @@ -95,6 +96,42 @@ my_cred = PublicCertificateCredentials(

r = dt.credentials.post(my_cred)
print(r.id)

# Create a new settings 2.0 object
settings_value = {
"enabled": True,
"summary": "DT API TEST 1",
"queryDefinition": {
"type": "METRIC_KEY",
"metricKey": "netapp.ontap.node.fru.state",
"aggregation": "AVG",
"entityFilter": {
"dimensionKey": "dt.entity.netapp_ontap:fru",
"conditions": [],
},
"dimensionFilter": [],
},
"modelProperties": {
"type": "STATIC_THRESHOLD",
"threshold": 100.0,
"alertOnNoData": False,
"alertCondition": "BELOW",
"violatingSamples": 3,
"samples": 5,
"dealertingSamples": 5,
},
"eventTemplate": {
"title": "OnTap {dims:type} {dims:fru_id} is in Error State",
"description": "OnTap field replaceable unit (FRU) {dims:type} with id {dims:fru_id} on node {dims:node} in cluster {dims:cluster} is in an error state.\n",
"eventType": "RESOURCE",
"davisMerge": True,
"metadata": [],
},
"eventEntityDimensionKey": "dt.entity.netapp_ontap:fru",
}

settings_object = SettingsObjectCreate(schema_id="builtin:anomaly-detection.metric-events", value=settings_value, scope="environment")
dt.settings.create_object(validate_only=False, body=settings_object)
```

## Implementation Progress
Expand All @@ -118,7 +155,8 @@ print(r.id)
Network zones | :warning: | `dt.network_zones` |
Problems | :heavy_check_mark: | `dt.problems` |
Security problems | :x: | |
Service-level objectives | :heavy_check_mark: | `dt.slos` |
Service-level objectives | :heavy_check_mark: | `dt.slos` |
Settings | :warning: | `dt.settings` |

### Environment API V1

Expand Down
36 changes: 24 additions & 12 deletions dynatrace/environment_v2/service_level_objectives.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def list(
time_frame: Optional[str] = "CURRENT",
page_idx: Optional[int] = 1,
demo: Optional[bool] = False,
evaluate: Optional[bool] = False,
evaluate: Optional[str] = "false",
enabled_slos: Optional[str] = "all"
) -> PaginatedList["Slo"]:
"""Lists all available SLOs along with calculated values
Expand All @@ -53,7 +54,8 @@ def list(
:param time_frame: The timeframe to calculate the SLO values. CURRENT: SLO's own timeframe. GTF: timeframe specified by from and to parameters.
:param page_idx: Only SLOs on the given page are included in the response. The first page has the index '1'.
:param demo: Get your SLOs (false) or a set of demo SLOs (true)
:param evaluate: Get your SLOs without them being evaluated (false) or with evaluations (true).
:param evaluate: Get your SLOs without them being evaluated ("false") or with evaluations ("true"). This value must be a lowercase string.
:param enabled_slos: Get your enabled SLOs ("true"), disabled ones ("false") or both enabled and disabled ones ("all"). This value must be a lowercase string.
:returns PaginatedList[Slo]: the list of SLOs matching criteria
"""
Expand All @@ -67,6 +69,7 @@ def list(
"pageIdx": page_idx,
"demo": demo,
"evaluate": evaluate,
"enabledSlos": enabled_slos
}
return PaginatedList(target_class=Slo, http_client=self.__http_client, target_params=params, target_url=f"{self.ENDPOINT}", list_item="slo")

Expand All @@ -91,7 +94,7 @@ def get(
"to": timestamp_to_string(time_to),
"timeFrame": time_frame,
}
response = self.__http_client.make_request(path=f"{self.ENDPOINT}/{slo_id}", params=params).json()
response = self.__http_client.make_request(f"{self.ENDPOINT}/{slo_id}", params=params).json()
return Slo(raw_element=response)

def post(self, slo: "Slo") -> "Response":
Expand Down Expand Up @@ -127,11 +130,13 @@ def create(
target: float,
warning: float,
timeframe: str,
use_rate_metric: bool,
use_rate_metric: Optional[bool] = None,
metric_rate: Optional[str] = None,
metric_numerator: Optional[str] = None,
metric_denominator: Optional[str] = None,
filter_: Optional[str] = None,
metric_expression: Optional[str] = None,
metric_name: Optional[str] = None,
filter: Optional[str] = None,
evaluation_type: Optional[str] = "AGGREGATE",
custom_description: Optional[str] = None,
enabled: Optional[bool] = False,
Expand All @@ -142,10 +147,11 @@ def create(
:param target: The target value of the SLO.
:param warning: The warning value of the SLO. At warning state the SLO is still fulfilled but is getting close to failure.
:param timeframe: The timeframe for the SLO evaluation. Use the syntax of the global timeframe selector.
:param use_rate_metric: The type of the metric to use for SLO calculation - an existing percentage-based metric (true) or a ratio of two metrics (false)
:param metric_rate: The percentage-based metric for the calculation of the SLO. Required when the useRateMetric is set to true.
:param metric_numerator: The metric for the count of successes (the numerator in rate calculation).Required when the useRateMetric is set to false.
:param metric_denominator: The total count metric (the denominator in rate calculation). Required when the useRateMetric is set to false.
:param use_rate_metric: [DEPRECATED] The type of the metric to use for SLO calculation - an existing percentage-based metric (true) or a ratio of two metrics (false)
:param metric_rate: [DEPRECATED] The percentage-based metric for the calculation of the SLO. Required when the useRateMetric is set to true.
:param metric_numerator: [DEPRECATED] The metric for the count of successes (the numerator in rate calculation).Required when the useRateMetric is set to false.
:param metric_denominator: [DEPRECATED] The total count metric (the denominator in rate calculation). Required when the useRateMetric is set to false.
:param metric_expression: The percentage-based metric expression for the calculation of the SLO.
:param evaluation_type: The evaluation type of the SLO.
:param filter_: The entity filter for the SLO evaluation. Use the syntax of entity selector.
:param custom_description: The custom description of the SLO.
Expand All @@ -162,7 +168,9 @@ def create(
"metricRate": metric_rate if use_rate_metric else "",
"metricNumerator": metric_numerator if not use_rate_metric else "",
"metricDenominator": metric_denominator if not use_rate_metric else "",
"filter": filter_,
"metricExpression": metric_expression,
"metricName": metric_name,
"filter": filter,
"evaluationType": evaluation_type,
"description": custom_description,
"enabled": enabled,
Expand All @@ -179,22 +187,24 @@ def _create_from_raw_data(self, raw_element: Dict[str, Any]):
self.warning: float = raw_element.get("warning")
self.timeframe: str = raw_element.get("timeframe")
self.evaluation_type: SloEvaluationType = SloEvaluationType(raw_element.get("evaluationType"))
self.use_rate_metric: bool = raw_element.get("useRateMetric")

# optional
self.status: Optional[SloStatus] = SloStatus(raw_element.get("status")) if raw_element.get("status") else None
self.metric_rate: Optional[str] = raw_element.get("metricRate")
self.metric_numerator: Optional[str] = raw_element.get("metricNumerator")
self.metric_denominator: Optional[str] = raw_element.get("metricDenominator")
self.metric_expression: Optional[str] = raw_element.get("metricExpression")
self.metric_name: Optional[str] = raw_element.get("metricName")
self.error_budget: Optional[float] = raw_element.get("errorBudget", 0)
self.numerator_value: Optional[float] = raw_element.get("numeratorValue", 0)
self.denominator_value: Optional[float] = raw_element.get("denominatorValue", 0)
self.related_open_problems: Optional[int] = raw_element.get("relatedOpenProblems", 0)
self.evaluated_percentage: Optional[float] = raw_element.get("evaluatedPercentage", 0)
self.filter: Optional[str] = raw_element.get("filter")
self.enabled: Optional[bool] = raw_element.get("enabled", False)
self.custom_description: Optional[str] = raw_element.get("description", "")
self.custom_description: Optional[str] = raw_element.get("description")
self.error: Optional[SloError] = SloError(raw_element.get("error", SloError.NONE))
self.use_rate_metric: Optional[bool] = raw_element.get("useRateMetric")

def to_json(self) -> Dict[str, Any]:
"""Translates an Slo to a JSON dict."""
Expand All @@ -209,6 +219,8 @@ def to_json(self) -> Dict[str, Any]:
"metricRate": self.metric_rate,
"metricNumerator": self.metric_numerator,
"metricDenominator": self.metric_denominator,
"metricExpression": self.metric_expression,
"metricName": self.metric_name,
"filter": self.filter,
"customDescription": self.custom_description,
}
Expand Down
64 changes: 55 additions & 9 deletions dynatrace/environment_v2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,23 @@


class SettingService:
ENDPOINT = "/api/v2/settings/objects"
OBJECTS_ENDPOINT = "/api/v2/settings/objects"
SCHEMAS_ENDPOINT = "/api/v2/settings/schemas"

def __init__(self, http_client: HttpClient):
self.__http_client = http_client


def list_schemas(self) -> PaginatedList["SchemaStub"]:
"""Lists all settings schemas available in your environment"""

return PaginatedList(
SchemaStub,
self.__http_client,
target_url=self.SCHEMAS_ENDPOINT,
list_item="items"
)


def list_objects(
self,
Expand Down Expand Up @@ -39,7 +52,7 @@ def list_objects(
return PaginatedList(
SettingsObject,
self.__http_client,
target_url=self.ENDPOINT,
target_url=self.OBJECTS_ENDPOINT,
list_item="items",
target_params=params,
)
Expand All @@ -65,7 +78,7 @@ def create_object(
body = [o.json() for o in body]

response = self.__http_client.make_request(
self.ENDPOINT, params=body, method="POST", query_params=query_params
self.OBJECTS_ENDPOINT, params=body, method="POST", query_params=query_params
).json()
return response

Expand All @@ -76,20 +89,20 @@ def get_object(self, object_id: str):
:return: a Settings object
"""
response = self.__http_client.make_request(
f"{self.ENDPOINT}/{object_id}"
f"{self.OBJECTS_ENDPOINT}/{object_id}"
).json()
return SettingsObject(raw_element=response)

def update_object(
self, object_id: str, value: Optional["SettingsObjectCreate"] = None
self, object_id: str, body: Optional["SettingsObjectUpdate"] = None
):
"""Updates an existing settings object
:param object_id: the ID of the object
:param value: the JSON body of the request. Contains updated parameters of the settings object.
"""
return self.__http_client.make_request(
f"{self.ENDPOINT}/{object_id}", params=value.json(), method="PUT"
f"{self.OBJECTS_ENDPOINT}/{object_id}", params=body.json(), method="PUT"
)

def delete_object(self, object_id: str, update_token: Optional[str] = None):
Expand All @@ -101,7 +114,7 @@ def delete_object(self, object_id: str, update_token: Optional[str] = None):
"""
query_params = {"updateToken": update_token}
return self.__http_client.make_request(
f"{self.ENDPOINT}/{object_id}",
f"{self.OBJECTS_ENDPOINT}/{object_id}",
method="DELETE",
query_params=query_params,
).json()
Expand All @@ -120,7 +133,7 @@ def _create_from_raw_data(self, raw_element: Dict[str, Any]):
class SettingsObject(DynatraceObject):
def _create_from_raw_data(self, raw_element: Dict[str, Any]):
# Mandatory
self.objectId: str = raw_element["objectId"]
self.object_id: str = raw_element["objectId"]
self.value: dict = raw_element["value"]
# Optional
self.author: str = raw_element.get("author")
Expand Down Expand Up @@ -173,7 +186,6 @@ def __init__(

def json(self) -> dict:
body = {"schemaId": self.schema_id, "value": self.value, "scope": self.scope}

if self.external_id:
body["externalId"] = self.external_id
if self.insert_after:
Expand All @@ -182,5 +194,39 @@ def json(self) -> dict:
body["objectId"] = self.object_id
if self.schema_version:
body["schemaVersion"] = self.schema_version
return body


class SettingsObjectUpdate:
def __init__(
self,
value: dict,
insert_after: Optional[str] = None,
insert_before: Optional[str] = None,
schema_version: Optional[str] = None,
update_token: Optional[str] = None,
):
self.value = value
self.insert_after = insert_after
self.insert_before = insert_before
self.schema_version = schema_version
self.update_token = update_token

def json(self) -> dict:
body = {"value": self.value}
if self.insert_after:
body["insertAfter"] = self.insert_after
if self.insert_before:
body["insertBefore"] = self.insert_before
if self.schema_version:
body["schemaVersion"] = self.schema_version
if self.update_token:
body["updateToken"] = self.update_token
return body


class SchemaStub(DynatraceObject):
def _create_from_raw_data(self, raw_element: Dict[str, Any]):
self.display_name = raw_element["displayName"]
self.latest_schema_version = raw_element["latestSchemaVersion"]
self.schema_id = raw_element["schemaId"]
33 changes: 16 additions & 17 deletions test/environment_v2/test_service_level_objectives.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
from dynatrace.environment_v2.service_level_objectives import Slo, SloStatus, SloError, SloEvaluationType
from dynatrace.pagination import PaginatedList

SLO_ID = "d4adc421-245e-3bc5-a683-df2ed030997c"
SLO_ID = "88991da4-be17-3d57-aada-cfb3977767f4"


def test_list(dt: Dynatrace):
slos = dt.slos.list(page_size=20, evaluate=True)
slos = dt.slos.list(enabled_slos="all")

assert isinstance(slos, PaginatedList)
assert len(list(slos)) == 2
assert len(list(slos)) == 4
assert all(isinstance(s, Slo) for s in slos)


Expand Down Expand Up @@ -41,21 +41,20 @@ def test_get(dt: Dynatrace):
# value checks
assert slo.id == SLO_ID
assert slo.enabled == True
assert slo.name == "MySLOService"
assert slo.custom_description == "Service Errors Fivexx SuccessCount / Service RequestCount Total"
assert slo.evaluated_percentage == 99.92798959015639
assert slo.error_budget == -0.022010409843616685
assert slo.status == SloStatus.FAILURE
assert slo.name == "test123"
assert slo.custom_description == "test"
assert slo.evaluated_percentage == 100.0
assert slo.error_budget == 2.0
assert slo.status == SloStatus.SUCCESS
assert slo.error == SloError.NONE
assert slo.use_rate_metric == False
assert slo.metric_rate == ""
assert slo.metric_numerator == "builtin:service.errors.fivexx.successCount:splitBy()"
assert slo.metric_denominator == "builtin:service.requestCount.total:splitBy()"
assert slo.numerator_value == 1704081
assert slo.denominator_value == 1705309
assert slo.target == 99.95
assert slo.warning == 99.97
assert slo.metric_numerator == ""
assert slo.metric_denominator == ""
assert slo.numerator_value == 0.0
assert slo.denominator_value == 0.0
assert slo.target == 98.0
assert slo.warning == 99.0
assert slo.evaluation_type == SloEvaluationType.AGGREGATE
assert slo.timeframe == "-2h"
assert slo.filter == "type(SERVICE),entityId(SERVICE-D89AF859A68D9072)"
assert slo.timeframe == "now-1h"
assert slo.filter == ""
assert slo.related_open_problems == 0
8 changes: 7 additions & 1 deletion test/environment_v2/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime

from dynatrace.environment_v2.settings import SettingsObject, SettingsObjectCreate
from dynatrace.environment_v2.settings import SettingsObject, SettingsObjectCreate, SchemaStub
from dynatrace import Dynatrace
from dynatrace.pagination import PaginatedList

Expand Down Expand Up @@ -38,6 +38,12 @@
settings_object = SettingsObjectCreate("builtin:anomaly-detection.metric-events", settings_dict, "environment")
test_object_id = "vu9U3hXa3q0AAAABACdidWlsdGluOmFub21hbHktZGV0ZWN0aW9uLm1ldHJpYy1ldmVudHMABnRlbmFudAAGdGVuYW50ACRiYmYzZWNhNy0zMmZmLTM2ZTEtOTFiOS05Y2QxZjE3OTc0YjC-71TeFdrerQ"

def test_list_schemas(dt: Dynatrace):
schemas = dt.settings.list_schemas()
assert isinstance(schemas, PaginatedList)
assert len(list(schemas)) == 3
assert all(isinstance(s, SchemaStub) for s in schemas)

def test_list_objects(dt: Dynatrace):
settings = dt.settings.list_objects(schema_id="builtin:anomaly-detection.metric-events")
assert isinstance(settings, PaginatedList)
Expand Down
Loading

0 comments on commit 8b795bd

Please sign in to comment.