Skip to content

Commit 8b795bd

Browse files
Merge pull request #80 from Dynatrace-James-Kitson/fixes-and-improvements
Additional settings implementation and update SLO endpoint
2 parents 3d16f88 + d9a7481 commit 8b795bd

10 files changed

+314
-112
lines changed

README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ from dynatrace import Dynatrace
1818
from dynatrace import TOO_MANY_REQUESTS_WAIT
1919
from dynatrace.environment_v2.tokens_api import SCOPE_METRICS_READ, SCOPE_METRICS_INGEST
2020
from dynatrace.configuration_v1.credential_vault import PublicCertificateCredentials
21+
from dynatrace.environment_v2.settings import SettingsObject, SettingsObjectCreate
2122

2223
from datetime import datetime, timedelta
2324

@@ -95,6 +96,42 @@ my_cred = PublicCertificateCredentials(
9596

9697
r = dt.credentials.post(my_cred)
9798
print(r.id)
99+
100+
# Create a new settings 2.0 object
101+
settings_value = {
102+
"enabled": True,
103+
"summary": "DT API TEST 1",
104+
"queryDefinition": {
105+
"type": "METRIC_KEY",
106+
"metricKey": "netapp.ontap.node.fru.state",
107+
"aggregation": "AVG",
108+
"entityFilter": {
109+
"dimensionKey": "dt.entity.netapp_ontap:fru",
110+
"conditions": [],
111+
},
112+
"dimensionFilter": [],
113+
},
114+
"modelProperties": {
115+
"type": "STATIC_THRESHOLD",
116+
"threshold": 100.0,
117+
"alertOnNoData": False,
118+
"alertCondition": "BELOW",
119+
"violatingSamples": 3,
120+
"samples": 5,
121+
"dealertingSamples": 5,
122+
},
123+
"eventTemplate": {
124+
"title": "OnTap {dims:type} {dims:fru_id} is in Error State",
125+
"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",
126+
"eventType": "RESOURCE",
127+
"davisMerge": True,
128+
"metadata": [],
129+
},
130+
"eventEntityDimensionKey": "dt.entity.netapp_ontap:fru",
131+
}
132+
133+
settings_object = SettingsObjectCreate(schema_id="builtin:anomaly-detection.metric-events", value=settings_value, scope="environment")
134+
dt.settings.create_object(validate_only=False, body=settings_object)
98135
```
99136

100137
## Implementation Progress
@@ -118,7 +155,8 @@ print(r.id)
118155
Network zones | :warning: | `dt.network_zones` |
119156
Problems | :heavy_check_mark: | `dt.problems` |
120157
Security problems | :x: | |
121-
Service-level objectives | :heavy_check_mark: | `dt.slos` |
158+
Service-level objectives | :heavy_check_mark: | `dt.slos` |
159+
Settings | :warning: | `dt.settings` |
122160

123161
### Environment API V1
124162

dynatrace/environment_v2/service_level_objectives.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ def list(
4141
time_frame: Optional[str] = "CURRENT",
4242
page_idx: Optional[int] = 1,
4343
demo: Optional[bool] = False,
44-
evaluate: Optional[bool] = False,
44+
evaluate: Optional[str] = "false",
45+
enabled_slos: Optional[str] = "all"
4546
) -> PaginatedList["Slo"]:
4647
"""Lists all available SLOs along with calculated values
4748
@@ -53,7 +54,8 @@ def list(
5354
:param time_frame: The timeframe to calculate the SLO values. CURRENT: SLO's own timeframe. GTF: timeframe specified by from and to parameters.
5455
:param page_idx: Only SLOs on the given page are included in the response. The first page has the index '1'.
5556
:param demo: Get your SLOs (false) or a set of demo SLOs (true)
56-
:param evaluate: Get your SLOs without them being evaluated (false) or with evaluations (true).
57+
:param evaluate: Get your SLOs without them being evaluated ("false") or with evaluations ("true"). This value must be a lowercase string.
58+
: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.
5759
5860
:returns PaginatedList[Slo]: the list of SLOs matching criteria
5961
"""
@@ -67,6 +69,7 @@ def list(
6769
"pageIdx": page_idx,
6870
"demo": demo,
6971
"evaluate": evaluate,
72+
"enabledSlos": enabled_slos
7073
}
7174
return PaginatedList(target_class=Slo, http_client=self.__http_client, target_params=params, target_url=f"{self.ENDPOINT}", list_item="slo")
7275

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

97100
def post(self, slo: "Slo") -> "Response":
@@ -127,11 +130,13 @@ def create(
127130
target: float,
128131
warning: float,
129132
timeframe: str,
130-
use_rate_metric: bool,
133+
use_rate_metric: Optional[bool] = None,
131134
metric_rate: Optional[str] = None,
132135
metric_numerator: Optional[str] = None,
133136
metric_denominator: Optional[str] = None,
134-
filter_: Optional[str] = None,
137+
metric_expression: Optional[str] = None,
138+
metric_name: Optional[str] = None,
139+
filter: Optional[str] = None,
135140
evaluation_type: Optional[str] = "AGGREGATE",
136141
custom_description: Optional[str] = None,
137142
enabled: Optional[bool] = False,
@@ -142,10 +147,11 @@ def create(
142147
:param target: The target value of the SLO.
143148
:param warning: The warning value of the SLO. At warning state the SLO is still fulfilled but is getting close to failure.
144149
:param timeframe: The timeframe for the SLO evaluation. Use the syntax of the global timeframe selector.
145-
: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)
146-
:param metric_rate: The percentage-based metric for the calculation of the SLO. Required when the useRateMetric is set to true.
147-
:param metric_numerator: The metric for the count of successes (the numerator in rate calculation).Required when the useRateMetric is set to false.
148-
:param metric_denominator: The total count metric (the denominator in rate calculation). Required when the useRateMetric is set to false.
150+
: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)
151+
:param metric_rate: [DEPRECATED] The percentage-based metric for the calculation of the SLO. Required when the useRateMetric is set to true.
152+
:param metric_numerator: [DEPRECATED] The metric for the count of successes (the numerator in rate calculation).Required when the useRateMetric is set to false.
153+
:param metric_denominator: [DEPRECATED] The total count metric (the denominator in rate calculation). Required when the useRateMetric is set to false.
154+
:param metric_expression: The percentage-based metric expression for the calculation of the SLO.
149155
:param evaluation_type: The evaluation type of the SLO.
150156
:param filter_: The entity filter for the SLO evaluation. Use the syntax of entity selector.
151157
:param custom_description: The custom description of the SLO.
@@ -162,7 +168,9 @@ def create(
162168
"metricRate": metric_rate if use_rate_metric else "",
163169
"metricNumerator": metric_numerator if not use_rate_metric else "",
164170
"metricDenominator": metric_denominator if not use_rate_metric else "",
165-
"filter": filter_,
171+
"metricExpression": metric_expression,
172+
"metricName": metric_name,
173+
"filter": filter,
166174
"evaluationType": evaluation_type,
167175
"description": custom_description,
168176
"enabled": enabled,
@@ -179,22 +187,24 @@ def _create_from_raw_data(self, raw_element: Dict[str, Any]):
179187
self.warning: float = raw_element.get("warning")
180188
self.timeframe: str = raw_element.get("timeframe")
181189
self.evaluation_type: SloEvaluationType = SloEvaluationType(raw_element.get("evaluationType"))
182-
self.use_rate_metric: bool = raw_element.get("useRateMetric")
183190

184191
# optional
185192
self.status: Optional[SloStatus] = SloStatus(raw_element.get("status")) if raw_element.get("status") else None
186193
self.metric_rate: Optional[str] = raw_element.get("metricRate")
187194
self.metric_numerator: Optional[str] = raw_element.get("metricNumerator")
188195
self.metric_denominator: Optional[str] = raw_element.get("metricDenominator")
196+
self.metric_expression: Optional[str] = raw_element.get("metricExpression")
197+
self.metric_name: Optional[str] = raw_element.get("metricName")
189198
self.error_budget: Optional[float] = raw_element.get("errorBudget", 0)
190199
self.numerator_value: Optional[float] = raw_element.get("numeratorValue", 0)
191200
self.denominator_value: Optional[float] = raw_element.get("denominatorValue", 0)
192201
self.related_open_problems: Optional[int] = raw_element.get("relatedOpenProblems", 0)
193202
self.evaluated_percentage: Optional[float] = raw_element.get("evaluatedPercentage", 0)
194203
self.filter: Optional[str] = raw_element.get("filter")
195204
self.enabled: Optional[bool] = raw_element.get("enabled", False)
196-
self.custom_description: Optional[str] = raw_element.get("description", "")
205+
self.custom_description: Optional[str] = raw_element.get("description")
197206
self.error: Optional[SloError] = SloError(raw_element.get("error", SloError.NONE))
207+
self.use_rate_metric: Optional[bool] = raw_element.get("useRateMetric")
198208

199209
def to_json(self) -> Dict[str, Any]:
200210
"""Translates an Slo to a JSON dict."""
@@ -209,6 +219,8 @@ def to_json(self) -> Dict[str, Any]:
209219
"metricRate": self.metric_rate,
210220
"metricNumerator": self.metric_numerator,
211221
"metricDenominator": self.metric_denominator,
222+
"metricExpression": self.metric_expression,
223+
"metricName": self.metric_name,
212224
"filter": self.filter,
213225
"customDescription": self.custom_description,
214226
}

dynatrace/environment_v2/settings.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,23 @@
88

99

1010
class SettingService:
11-
ENDPOINT = "/api/v2/settings/objects"
11+
OBJECTS_ENDPOINT = "/api/v2/settings/objects"
12+
SCHEMAS_ENDPOINT = "/api/v2/settings/schemas"
1213

1314
def __init__(self, http_client: HttpClient):
1415
self.__http_client = http_client
16+
17+
18+
def list_schemas(self) -> PaginatedList["SchemaStub"]:
19+
"""Lists all settings schemas available in your environment"""
20+
21+
return PaginatedList(
22+
SchemaStub,
23+
self.__http_client,
24+
target_url=self.SCHEMAS_ENDPOINT,
25+
list_item="items"
26+
)
27+
1528

1629
def list_objects(
1730
self,
@@ -39,7 +52,7 @@ def list_objects(
3952
return PaginatedList(
4053
SettingsObject,
4154
self.__http_client,
42-
target_url=self.ENDPOINT,
55+
target_url=self.OBJECTS_ENDPOINT,
4356
list_item="items",
4457
target_params=params,
4558
)
@@ -65,7 +78,7 @@ def create_object(
6578
body = [o.json() for o in body]
6679

6780
response = self.__http_client.make_request(
68-
self.ENDPOINT, params=body, method="POST", query_params=query_params
81+
self.OBJECTS_ENDPOINT, params=body, method="POST", query_params=query_params
6982
).json()
7083
return response
7184

@@ -76,20 +89,20 @@ def get_object(self, object_id: str):
7689
:return: a Settings object
7790
"""
7891
response = self.__http_client.make_request(
79-
f"{self.ENDPOINT}/{object_id}"
92+
f"{self.OBJECTS_ENDPOINT}/{object_id}"
8093
).json()
8194
return SettingsObject(raw_element=response)
8295

8396
def update_object(
84-
self, object_id: str, value: Optional["SettingsObjectCreate"] = None
97+
self, object_id: str, body: Optional["SettingsObjectUpdate"] = None
8598
):
8699
"""Updates an existing settings object
87100
88101
:param object_id: the ID of the object
89102
:param value: the JSON body of the request. Contains updated parameters of the settings object.
90103
"""
91104
return self.__http_client.make_request(
92-
f"{self.ENDPOINT}/{object_id}", params=value.json(), method="PUT"
105+
f"{self.OBJECTS_ENDPOINT}/{object_id}", params=body.json(), method="PUT"
93106
)
94107

95108
def delete_object(self, object_id: str, update_token: Optional[str] = None):
@@ -101,7 +114,7 @@ def delete_object(self, object_id: str, update_token: Optional[str] = None):
101114
"""
102115
query_params = {"updateToken": update_token}
103116
return self.__http_client.make_request(
104-
f"{self.ENDPOINT}/{object_id}",
117+
f"{self.OBJECTS_ENDPOINT}/{object_id}",
105118
method="DELETE",
106119
query_params=query_params,
107120
).json()
@@ -120,7 +133,7 @@ def _create_from_raw_data(self, raw_element: Dict[str, Any]):
120133
class SettingsObject(DynatraceObject):
121134
def _create_from_raw_data(self, raw_element: Dict[str, Any]):
122135
# Mandatory
123-
self.objectId: str = raw_element["objectId"]
136+
self.object_id: str = raw_element["objectId"]
124137
self.value: dict = raw_element["value"]
125138
# Optional
126139
self.author: str = raw_element.get("author")
@@ -173,7 +186,6 @@ def __init__(
173186

174187
def json(self) -> dict:
175188
body = {"schemaId": self.schema_id, "value": self.value, "scope": self.scope}
176-
177189
if self.external_id:
178190
body["externalId"] = self.external_id
179191
if self.insert_after:
@@ -182,5 +194,39 @@ def json(self) -> dict:
182194
body["objectId"] = self.object_id
183195
if self.schema_version:
184196
body["schemaVersion"] = self.schema_version
197+
return body
198+
185199

200+
class SettingsObjectUpdate:
201+
def __init__(
202+
self,
203+
value: dict,
204+
insert_after: Optional[str] = None,
205+
insert_before: Optional[str] = None,
206+
schema_version: Optional[str] = None,
207+
update_token: Optional[str] = None,
208+
):
209+
self.value = value
210+
self.insert_after = insert_after
211+
self.insert_before = insert_before
212+
self.schema_version = schema_version
213+
self.update_token = update_token
214+
215+
def json(self) -> dict:
216+
body = {"value": self.value}
217+
if self.insert_after:
218+
body["insertAfter"] = self.insert_after
219+
if self.insert_before:
220+
body["insertBefore"] = self.insert_before
221+
if self.schema_version:
222+
body["schemaVersion"] = self.schema_version
223+
if self.update_token:
224+
body["updateToken"] = self.update_token
186225
return body
226+
227+
228+
class SchemaStub(DynatraceObject):
229+
def _create_from_raw_data(self, raw_element: Dict[str, Any]):
230+
self.display_name = raw_element["displayName"]
231+
self.latest_schema_version = raw_element["latestSchemaVersion"]
232+
self.schema_id = raw_element["schemaId"]

test/environment_v2/test_service_level_objectives.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
from dynatrace.environment_v2.service_level_objectives import Slo, SloStatus, SloError, SloEvaluationType
33
from dynatrace.pagination import PaginatedList
44

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

77

88
def test_list(dt: Dynatrace):
9-
slos = dt.slos.list(page_size=20, evaluate=True)
9+
slos = dt.slos.list(enabled_slos="all")
1010

1111
assert isinstance(slos, PaginatedList)
12-
assert len(list(slos)) == 2
12+
assert len(list(slos)) == 4
1313
assert all(isinstance(s, Slo) for s in slos)
1414

1515

@@ -41,21 +41,20 @@ def test_get(dt: Dynatrace):
4141
# value checks
4242
assert slo.id == SLO_ID
4343
assert slo.enabled == True
44-
assert slo.name == "MySLOService"
45-
assert slo.custom_description == "Service Errors Fivexx SuccessCount / Service RequestCount Total"
46-
assert slo.evaluated_percentage == 99.92798959015639
47-
assert slo.error_budget == -0.022010409843616685
48-
assert slo.status == SloStatus.FAILURE
44+
assert slo.name == "test123"
45+
assert slo.custom_description == "test"
46+
assert slo.evaluated_percentage == 100.0
47+
assert slo.error_budget == 2.0
48+
assert slo.status == SloStatus.SUCCESS
4949
assert slo.error == SloError.NONE
50-
assert slo.use_rate_metric == False
5150
assert slo.metric_rate == ""
52-
assert slo.metric_numerator == "builtin:service.errors.fivexx.successCount:splitBy()"
53-
assert slo.metric_denominator == "builtin:service.requestCount.total:splitBy()"
54-
assert slo.numerator_value == 1704081
55-
assert slo.denominator_value == 1705309
56-
assert slo.target == 99.95
57-
assert slo.warning == 99.97
51+
assert slo.metric_numerator == ""
52+
assert slo.metric_denominator == ""
53+
assert slo.numerator_value == 0.0
54+
assert slo.denominator_value == 0.0
55+
assert slo.target == 98.0
56+
assert slo.warning == 99.0
5857
assert slo.evaluation_type == SloEvaluationType.AGGREGATE
59-
assert slo.timeframe == "-2h"
60-
assert slo.filter == "type(SERVICE),entityId(SERVICE-D89AF859A68D9072)"
58+
assert slo.timeframe == "now-1h"
59+
assert slo.filter == ""
6160
assert slo.related_open_problems == 0

test/environment_v2/test_settings.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime
22

3-
from dynatrace.environment_v2.settings import SettingsObject, SettingsObjectCreate
3+
from dynatrace.environment_v2.settings import SettingsObject, SettingsObjectCreate, SchemaStub
44
from dynatrace import Dynatrace
55
from dynatrace.pagination import PaginatedList
66

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

41+
def test_list_schemas(dt: Dynatrace):
42+
schemas = dt.settings.list_schemas()
43+
assert isinstance(schemas, PaginatedList)
44+
assert len(list(schemas)) == 3
45+
assert all(isinstance(s, SchemaStub) for s in schemas)
46+
4147
def test_list_objects(dt: Dynatrace):
4248
settings = dt.settings.list_objects(schema_id="builtin:anomaly-detection.metric-events")
4349
assert isinstance(settings, PaginatedList)

0 commit comments

Comments
 (0)