dt-extensions-sdk 1.6.2__py3-none-any.whl → 1.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dt_extensions_sdk-1.6.2.dist-info → dt_extensions_sdk-1.7.1.dist-info}/METADATA +1 -1
- {dt_extensions_sdk-1.6.2.dist-info → dt_extensions_sdk-1.7.1.dist-info}/RECORD +11 -10
- dynatrace_extension/__about__.py +1 -1
- dynatrace_extension/__init__.py +1 -1
- dynatrace_extension/sdk/callback.py +1 -1
- dynatrace_extension/sdk/communication.py +24 -170
- dynatrace_extension/sdk/extension.py +46 -23
- dynatrace_extension/sdk/status.py +259 -0
- {dt_extensions_sdk-1.6.2.dist-info → dt_extensions_sdk-1.7.1.dist-info}/WHEEL +0 -0
- {dt_extensions_sdk-1.6.2.dist-info → dt_extensions_sdk-1.7.1.dist-info}/entry_points.txt +0 -0
- {dt_extensions_sdk-1.6.2.dist-info → dt_extensions_sdk-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dt-extensions-sdk
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.7.1
|
4
4
|
Project-URL: Documentation, https://github.com/dynatrace-extensions/dt-extensions-python-sdk#readme
|
5
5
|
Project-URL: Issues, https://github.com/dynatrace-extensions/dt-extensions-python-sdk/issues
|
6
6
|
Project-URL: Source, https://github.com/dynatrace-extensions/dt-extensions-python-sdk
|
@@ -1,5 +1,5 @@
|
|
1
|
-
dynatrace_extension/__about__.py,sha256=
|
2
|
-
dynatrace_extension/__init__.py,sha256=
|
1
|
+
dynatrace_extension/__about__.py,sha256=EKqfngohTaiZ-afjIydbqpFds5q6Y6m0tK_D6Wyw2Dc,110
|
2
|
+
dynatrace_extension/__init__.py,sha256=MJNJYCFWLEwPmBLoETWFZddyUCMDgZfKkRycmmGM_w4,806
|
3
3
|
dynatrace_extension/cli/__init__.py,sha256=HCboY_eJPoqjFmoPDsBL8Jk6aNvank8K7JpkVrgwzUM,123
|
4
4
|
dynatrace_extension/cli/main.py,sha256=OTjJ4XHJvvYXj10a7WFFHVNnkyECPg1ClW6Os8piN8k,20168
|
5
5
|
dynatrace_extension/cli/schema.py,sha256=d8wKUodRiaU3hfSZDWVNpD15lBfhmif2oQ-k07IxcaA,3230
|
@@ -17,21 +17,22 @@ dynatrace_extension/cli/create/extension_template/extension_name/__init__.py.tem
|
|
17
17
|
dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template,sha256=cS79GVxJB-V-gocu4ZOjmZ54HXJNg89eXdLf89zDHJQ,1249
|
18
18
|
dynatrace_extension/sdk/__init__.py,sha256=RsqQ1heGyCmSK3fhuEKAcxQIRCg4gEK0-eSkIehL5Nc,86
|
19
19
|
dynatrace_extension/sdk/activation.py,sha256=KIoPWMZs3tKiMG8XhCfeNgRlz2vxDKcAASgSACcEfIQ,1456
|
20
|
-
dynatrace_extension/sdk/callback.py,sha256=
|
21
|
-
dynatrace_extension/sdk/communication.py,sha256
|
20
|
+
dynatrace_extension/sdk/callback.py,sha256=woumpcWID09QHGc_rSrukNslVG8Qo0UQEgv8VTB6m7c,6681
|
21
|
+
dynatrace_extension/sdk/communication.py,sha256=-ccvNz0NYLSDXbqcpnWEqrGKZ4AgTy9XMyu8xnd_qEU,18418
|
22
22
|
dynatrace_extension/sdk/event.py,sha256=J261imbFKpxfuAQ6Nfu3RRcsIQKKivy6fme1nww2g-8,388
|
23
|
-
dynatrace_extension/sdk/extension.py,sha256=
|
23
|
+
dynatrace_extension/sdk/extension.py,sha256=BAdvQbXl03RGSPw_hYm1y5_9aZZia8fFzIZ0dpoZedo,49681
|
24
24
|
dynatrace_extension/sdk/helper.py,sha256=m4gGHtIKYkfANC2MOGdxKUZlmH5tnZO6WTNqll27lyY,6476
|
25
25
|
dynatrace_extension/sdk/metric.py,sha256=-kq7JWpk7UGvcjqafTt-o6k4urwhsGVXmnuQg7Sf9PQ,3622
|
26
26
|
dynatrace_extension/sdk/runtime.py,sha256=7bC4gUJsVSHuL_E7r2EWrne95nm1BjZiMGkyNqA7ZCU,2796
|
27
27
|
dynatrace_extension/sdk/snapshot.py,sha256=LnWVCtCK4NIEV3_kX-ly_LGHpNBSeErtsxCI1PH3L28,7521
|
28
|
+
dynatrace_extension/sdk/status.py,sha256=TG00-Aenp6D5aFbNxJeQYEyasthAW_vi9-OsutpNC58,8425
|
28
29
|
dynatrace_extension/sdk/throttled_logger.py,sha256=JXDiHh8syl8R0gJ-wfxmmBqvGCBMQX4pxPkxscaCsXo,3292
|
29
30
|
dynatrace_extension/sdk/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
31
|
dynatrace_extension/sdk/vendor/mureq/LICENSE,sha256=8AVcgZgiT_mvK1fOofXtRRr2f1dRXS_K21NuxQgP4VM,671
|
31
32
|
dynatrace_extension/sdk/vendor/mureq/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
33
|
dynatrace_extension/sdk/vendor/mureq/mureq.py,sha256=znF4mvzk5L03CLNozRz8UpK-fMijmSkObDFwlbhwLUg,14656
|
33
|
-
dt_extensions_sdk-1.
|
34
|
-
dt_extensions_sdk-1.
|
35
|
-
dt_extensions_sdk-1.
|
36
|
-
dt_extensions_sdk-1.
|
37
|
-
dt_extensions_sdk-1.
|
34
|
+
dt_extensions_sdk-1.7.1.dist-info/METADATA,sha256=5Fq2vrh0GcZ4VTUgAnNnRxgU7-xuhaKb4gY4evc48bs,2721
|
35
|
+
dt_extensions_sdk-1.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
36
|
+
dt_extensions_sdk-1.7.1.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
|
37
|
+
dt_extensions_sdk-1.7.1.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
|
38
|
+
dt_extensions_sdk-1.7.1.dist-info/RECORD,,
|
dynatrace_extension/__about__.py
CHANGED
dynatrace_extension/__init__.py
CHANGED
@@ -6,7 +6,6 @@
|
|
6
6
|
# ruff: noqa: F401
|
7
7
|
|
8
8
|
from .sdk.activation import ActivationConfig, ActivationType
|
9
|
-
from .sdk.communication import EndpointStatus, EndpointStatuses, IgnoreStatus, MultiStatus, Status, StatusValue
|
10
9
|
from .sdk.event import Severity
|
11
10
|
from .sdk.extension import DtEventType, Extension
|
12
11
|
from .sdk.helper import (
|
@@ -25,3 +24,4 @@ from .sdk.helper import (
|
|
25
24
|
schedule_function,
|
26
25
|
)
|
27
26
|
from .sdk.metric import Metric, MetricType, SummaryStat
|
27
|
+
from .sdk.status import EndpointStatus, EndpointStatuses, IgnoreStatus, MultiStatus, Status, StatusValue
|
@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
|
|
9
9
|
from timeit import default_timer as timer
|
10
10
|
|
11
11
|
from .activation import ActivationType
|
12
|
-
from .
|
12
|
+
from .status import EndpointStatuses, IgnoreStatus, MultiStatus, Status, StatusValue
|
13
13
|
|
14
14
|
|
15
15
|
class WrappedCallback:
|
@@ -10,11 +10,10 @@ import sys
|
|
10
10
|
from abc import ABC, abstractmethod
|
11
11
|
from collections.abc import Generator, Sequence
|
12
12
|
from dataclasses import dataclass
|
13
|
-
from enum import Enum
|
14
13
|
from pathlib import Path
|
15
|
-
from threading import RLock
|
16
14
|
from typing import Any, TypeVar
|
17
15
|
|
16
|
+
from .status import Status
|
18
17
|
from .vendor.mureq.mureq import HTTPException, Response, request
|
19
18
|
|
20
19
|
CONTENT_TYPE_JSON = "application/json;charset=utf-8"
|
@@ -30,173 +29,6 @@ MAX_METRIC_REQUEST_SIZE = 1_000_000 # actually 1_048_576
|
|
30
29
|
HTTP_BAD_REQUEST = 400
|
31
30
|
|
32
31
|
|
33
|
-
class StatusValue(Enum):
|
34
|
-
EMPTY = ""
|
35
|
-
OK = "OK"
|
36
|
-
GENERIC_ERROR = "GENERIC_ERROR"
|
37
|
-
INVALID_ARGS_ERROR = "INVALID_ARGS_ERROR"
|
38
|
-
EEC_CONNECTION_ERROR = "EEC_CONNECTION_ERROR"
|
39
|
-
INVALID_CONFIG_ERROR = "INVALID_CONFIG_ERROR"
|
40
|
-
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
|
41
|
-
DEVICE_CONNECTION_ERROR = "DEVICE_CONNECTION_ERROR"
|
42
|
-
WARNING = "WARNING"
|
43
|
-
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
44
|
-
|
45
|
-
|
46
|
-
class IgnoreStatus:
|
47
|
-
pass
|
48
|
-
|
49
|
-
|
50
|
-
class Status:
|
51
|
-
def __init__(self, status: StatusValue = StatusValue.EMPTY, message: str = "", timestamp: int | None = None):
|
52
|
-
self.status = status
|
53
|
-
self.message = message
|
54
|
-
self.timestamp = timestamp
|
55
|
-
|
56
|
-
def to_json(self) -> dict:
|
57
|
-
status = {"status": self.status.value, "message": self.message}
|
58
|
-
if self.timestamp:
|
59
|
-
status["timestamp"] = self.timestamp # type: ignore
|
60
|
-
return status
|
61
|
-
|
62
|
-
def __repr__(self):
|
63
|
-
return json.dumps(self.to_json())
|
64
|
-
|
65
|
-
def is_error(self) -> bool:
|
66
|
-
# WARNING is treated as an error
|
67
|
-
return self.status not in (StatusValue.OK, StatusValue.EMPTY)
|
68
|
-
|
69
|
-
def is_warning(self) -> bool:
|
70
|
-
return self.status == StatusValue.WARNING
|
71
|
-
|
72
|
-
|
73
|
-
class MultiStatus:
|
74
|
-
def __init__(self):
|
75
|
-
self.statuses: list[Status] = []
|
76
|
-
|
77
|
-
def add_status(self, status: StatusValue, message):
|
78
|
-
self.statuses.append(Status(status, message))
|
79
|
-
|
80
|
-
def build(self) -> Status:
|
81
|
-
ret = Status(StatusValue.OK)
|
82
|
-
if len(self.statuses) == 0:
|
83
|
-
return ret
|
84
|
-
|
85
|
-
messages = []
|
86
|
-
all_ok = True
|
87
|
-
all_err = True
|
88
|
-
any_warning = False
|
89
|
-
|
90
|
-
for stored_status in self.statuses:
|
91
|
-
if stored_status.message != "":
|
92
|
-
messages.append(stored_status.message)
|
93
|
-
|
94
|
-
if stored_status.is_warning():
|
95
|
-
any_warning = True
|
96
|
-
|
97
|
-
if stored_status.is_error():
|
98
|
-
all_ok = False
|
99
|
-
else:
|
100
|
-
all_err = False
|
101
|
-
|
102
|
-
ret.message = ", ".join(messages)
|
103
|
-
|
104
|
-
if any_warning:
|
105
|
-
ret.status = StatusValue.WARNING
|
106
|
-
elif all_ok:
|
107
|
-
ret.status = StatusValue.OK
|
108
|
-
elif all_err:
|
109
|
-
ret.status = StatusValue.GENERIC_ERROR
|
110
|
-
else:
|
111
|
-
ret.status = StatusValue.WARNING
|
112
|
-
|
113
|
-
return ret
|
114
|
-
|
115
|
-
|
116
|
-
class EndpointStatus:
|
117
|
-
def __init__(self, endpoint_hint: str, short_status: StatusValue, message: str):
|
118
|
-
self.endpoint = endpoint_hint
|
119
|
-
self.status: StatusValue = short_status
|
120
|
-
self.message = message
|
121
|
-
|
122
|
-
def __str__(self):
|
123
|
-
return str(self.__dict__)
|
124
|
-
|
125
|
-
|
126
|
-
class EndpointStatuses:
|
127
|
-
class TooManyEndpointStatusesError(Exception):
|
128
|
-
pass
|
129
|
-
|
130
|
-
class MergeConflictError(Exception):
|
131
|
-
def __init__(self, first: EndpointStatus, second: EndpointStatus):
|
132
|
-
super().__init__(f"Endpoint Statuses conflict while merging - first: {first}; second: {second}")
|
133
|
-
|
134
|
-
def __init__(self, total_endpoints_number: int):
|
135
|
-
self._lock = RLock()
|
136
|
-
self._faulty_endpoints: dict[str, EndpointStatus] = {}
|
137
|
-
self._num_endpoints = total_endpoints_number
|
138
|
-
|
139
|
-
def add_endpoint_status(self, status: EndpointStatus):
|
140
|
-
with self._lock:
|
141
|
-
if status.status == StatusValue.OK:
|
142
|
-
self.clear_endpoint_error(status.endpoint)
|
143
|
-
else:
|
144
|
-
if len(self._faulty_endpoints) == self._num_endpoints:
|
145
|
-
message = "Cannot add another endpoint status. \
|
146
|
-
The number of reported statuses already has reached preconfigured maximum of {self._num_endpoints} endpoints."
|
147
|
-
raise EndpointStatuses.TooManyEndpointStatusesError(message)
|
148
|
-
|
149
|
-
self._faulty_endpoints[status.endpoint] = status
|
150
|
-
|
151
|
-
def clear_endpoint_error(self, endpoint_hint: str):
|
152
|
-
with self._lock:
|
153
|
-
try:
|
154
|
-
del self._faulty_endpoints[endpoint_hint]
|
155
|
-
except KeyError:
|
156
|
-
pass
|
157
|
-
|
158
|
-
def merge(self, other: EndpointStatuses):
|
159
|
-
with self._lock:
|
160
|
-
with other._lock:
|
161
|
-
self._num_endpoints += other._num_endpoints
|
162
|
-
|
163
|
-
for endpoint, status in other._faulty_endpoints.items():
|
164
|
-
if endpoint not in self._faulty_endpoints.keys():
|
165
|
-
self._faulty_endpoints[endpoint] = status
|
166
|
-
else:
|
167
|
-
self._num_endpoints -= 1
|
168
|
-
raise EndpointStatuses.MergeConflictError(
|
169
|
-
self._faulty_endpoints[endpoint], other._faulty_endpoints[endpoint]
|
170
|
-
)
|
171
|
-
|
172
|
-
def build_common_status(self) -> Status:
|
173
|
-
with self._lock:
|
174
|
-
ok_count = self._num_endpoints - len(self._faulty_endpoints)
|
175
|
-
nok_count = len(self._faulty_endpoints)
|
176
|
-
|
177
|
-
if nok_count == 0:
|
178
|
-
return Status(StatusValue.OK, f"Endpoints OK: {self._num_endpoints} NOK: 0")
|
179
|
-
|
180
|
-
error_messages = []
|
181
|
-
for ep_status in self._faulty_endpoints.values():
|
182
|
-
error_messages.append(f"{ep_status.endpoint} - {ep_status.status.value} {ep_status.message}")
|
183
|
-
common_msg = ", ".join(error_messages)
|
184
|
-
|
185
|
-
# Determine status value
|
186
|
-
all_endpoints_faulty = nok_count == self._num_endpoints
|
187
|
-
has_warning_status = StatusValue.WARNING in [
|
188
|
-
ep_status.status for ep_status in self._faulty_endpoints.values()
|
189
|
-
]
|
190
|
-
|
191
|
-
if all_endpoints_faulty and not has_warning_status:
|
192
|
-
status_value = StatusValue.GENERIC_ERROR
|
193
|
-
else:
|
194
|
-
status_value = StatusValue.WARNING
|
195
|
-
|
196
|
-
message = f"Endpoints OK: {ok_count} NOK: {nok_count} NOK_reported_errors: {common_msg}"
|
197
|
-
return Status(status=status_value, message=message)
|
198
|
-
|
199
|
-
|
200
32
|
class CommunicationClient(ABC):
|
201
33
|
"""
|
202
34
|
Abstract class for extension communication
|
@@ -250,6 +82,10 @@ class CommunicationClient(ABC):
|
|
250
82
|
def send_dt_event(self, event: dict) -> None:
|
251
83
|
pass
|
252
84
|
|
85
|
+
@abstractmethod
|
86
|
+
def send_sfm_logs(self, sfm_logs: dict | list[dict]) -> list[dict | None]:
|
87
|
+
pass
|
88
|
+
|
253
89
|
|
254
90
|
class HttpClient(CommunicationClient):
|
255
91
|
"""
|
@@ -261,6 +97,7 @@ class HttpClient(CommunicationClient):
|
|
261
97
|
self._extension_config_url = f"{base_url}/extconfig/{datasource_id}"
|
262
98
|
self._metric_url = f"{base_url}/mint/{datasource_id}"
|
263
99
|
self._sfm_url = f"{base_url}/sfm/{datasource_id}"
|
100
|
+
self._sfm_logs_url = f"{base_url}/sfmlogs/{datasource_id}"
|
264
101
|
self._keep_alive_url = f"{base_url}/alive/{datasource_id}"
|
265
102
|
self._timediff_url = f"{base_url}/timediffms"
|
266
103
|
self._events_url = f"{base_url}/logs/{datasource_id}"
|
@@ -417,7 +254,13 @@ class HttpClient(CommunicationClient):
|
|
417
254
|
|
418
255
|
def send_events(self, events: dict | list[dict], eec_enrichment: bool = True) -> list[dict | None]:
|
419
256
|
self.logger.debug(f"Sending log events: {events}")
|
257
|
+
return self._send_events(self._events_url, events, eec_enrichment)
|
258
|
+
|
259
|
+
def send_sfm_logs(self, sfm_logs: dict | list[dict]):
|
260
|
+
self.logger.debug(f"Sending SFM logs: {sfm_logs}")
|
261
|
+
return self._send_events(self._sfm_logs_url, sfm_logs)
|
420
262
|
|
263
|
+
def _send_events(self, url, events: dict | list[dict], eec_enrichment: bool = True) -> list[dict | None]:
|
421
264
|
responses = []
|
422
265
|
if isinstance(events, dict):
|
423
266
|
events = [events]
|
@@ -426,7 +269,7 @@ class HttpClient(CommunicationClient):
|
|
426
269
|
for batch in batches:
|
427
270
|
try:
|
428
271
|
eec_response = self._make_request(
|
429
|
-
|
272
|
+
url,
|
430
273
|
"POST",
|
431
274
|
batch,
|
432
275
|
extra_headers={"Content-Type": CONTENT_TYPE_JSON, "eec-enrichment": str(eec_enrichment).lower()},
|
@@ -583,6 +426,17 @@ class DebugClient(CommunicationClient):
|
|
583
426
|
|
584
427
|
return activation_config_string
|
585
428
|
|
429
|
+
def send_sfm_logs(self, sfm_logs: dict | list[dict]) -> list[dict | None]:
|
430
|
+
if isinstance(sfm_logs, dict):
|
431
|
+
sfm_logs = [sfm_logs]
|
432
|
+
|
433
|
+
self.logger.info(f"send_sfm_logs: {len(sfm_logs)} logs")
|
434
|
+
|
435
|
+
if self.print_metrics:
|
436
|
+
for log in sfm_logs:
|
437
|
+
self.logger.info(f"send_sfm_log: {log}")
|
438
|
+
return []
|
439
|
+
|
586
440
|
|
587
441
|
def divide_into_batches(
|
588
442
|
items: Sequence[dict | str], max_size_bytes: int, join_with: str | None = None
|
@@ -20,19 +20,12 @@ from typing import Any, ClassVar, NamedTuple
|
|
20
20
|
|
21
21
|
from .activation import ActivationConfig, ActivationType
|
22
22
|
from .callback import WrappedCallback
|
23
|
-
from .communication import
|
24
|
-
CommunicationClient,
|
25
|
-
DebugClient,
|
26
|
-
EndpointStatuses,
|
27
|
-
HttpClient,
|
28
|
-
IgnoreStatus,
|
29
|
-
Status,
|
30
|
-
StatusValue,
|
31
|
-
)
|
23
|
+
from .communication import CommunicationClient, DebugClient, HttpClient
|
32
24
|
from .event import Severity
|
33
25
|
from .metric import Metric, MetricType, SfmMetric, SummaryStat
|
34
26
|
from .runtime import RuntimeProperties
|
35
27
|
from .snapshot import Snapshot
|
28
|
+
from .status import EndpointStatuses, EndpointStatusesMap, IgnoreStatus, Status, StatusValue
|
36
29
|
from .throttled_logger import StrictThrottledHandler, ThrottledHandler
|
37
30
|
|
38
31
|
HEARTBEAT_INTERVAL = timedelta(seconds=50)
|
@@ -285,6 +278,13 @@ class Extension:
|
|
285
278
|
# Error message from caught exception in self.initialize()
|
286
279
|
self._initialization_error: str = ""
|
287
280
|
|
281
|
+
# Map of all Endpoint Statuses
|
282
|
+
self._sfm_logs_allowed = not self.extension_name.startswith("custom:")
|
283
|
+
if not self._sfm_logs_allowed:
|
284
|
+
self.logger.warning("SFM logs not allowed for custom extensions.")
|
285
|
+
|
286
|
+
self._ep_statuses = EndpointStatusesMap(send_sfm_logs_function=self._send_sfm_logs)
|
287
|
+
|
288
288
|
self._parse_args()
|
289
289
|
|
290
290
|
for function, interval, args, activation_type in Extension.schedule_decorators:
|
@@ -1037,8 +1037,7 @@ class Extension:
|
|
1037
1037
|
if internal_callback_error:
|
1038
1038
|
return Status(overall_status_value, "\n".join(messages))
|
1039
1039
|
|
1040
|
-
# Handle regular statuses,
|
1041
|
-
ep_status_merged = EndpointStatuses(0)
|
1040
|
+
# Handle regular statuses, report all EndpointStatuses
|
1042
1041
|
all_ok = True
|
1043
1042
|
all_err = True
|
1044
1043
|
any_warning = False
|
@@ -1048,10 +1047,7 @@ class Extension:
|
|
1048
1047
|
continue
|
1049
1048
|
|
1050
1049
|
if isinstance(callback.status, EndpointStatuses):
|
1051
|
-
|
1052
|
-
ep_status_merged.merge(callback.status)
|
1053
|
-
except EndpointStatuses.MergeConflictError as e:
|
1054
|
-
self.logger.exception(e)
|
1050
|
+
self._ep_statuses.update_ep_statuses(callback.status)
|
1055
1051
|
continue
|
1056
1052
|
|
1057
1053
|
if callback.status.is_warning():
|
@@ -1066,8 +1062,9 @@ class Extension:
|
|
1066
1062
|
messages.append(f"{callback.name()}: {callback.status.status.value} - {callback.status.message}")
|
1067
1063
|
|
1068
1064
|
# Handle merged EndpointStatuses
|
1069
|
-
if
|
1070
|
-
|
1065
|
+
if self._ep_statuses.contains_any_status():
|
1066
|
+
self._ep_statuses.send_ep_logs()
|
1067
|
+
ep_status_merged = self._ep_statuses.build_common_status()
|
1071
1068
|
messages.insert(0, ep_status_merged.message)
|
1072
1069
|
|
1073
1070
|
if ep_status_merged.is_warning():
|
@@ -1081,17 +1078,13 @@ class Extension:
|
|
1081
1078
|
# Build overall status
|
1082
1079
|
overall_status = Status(StatusValue.OK, "\n".join(messages))
|
1083
1080
|
if any_warning:
|
1084
|
-
|
1085
|
-
# overall_status.status = StatusValue.WARNING
|
1086
|
-
overall_status.status = StatusValue.GENERIC_ERROR
|
1081
|
+
overall_status.status = StatusValue.WARNING
|
1087
1082
|
elif all_ok:
|
1088
1083
|
overall_status.status = StatusValue.OK
|
1089
1084
|
elif all_err:
|
1090
1085
|
overall_status.status = StatusValue.GENERIC_ERROR
|
1091
1086
|
else:
|
1092
|
-
|
1093
|
-
# overall_status.status = StatusValue.WARNING
|
1094
|
-
overall_status.status = StatusValue.GENERIC_ERROR
|
1087
|
+
overall_status.status = StatusValue.WARNING
|
1095
1088
|
|
1096
1089
|
return overall_status
|
1097
1090
|
|
@@ -1225,3 +1218,33 @@ class Extension:
|
|
1225
1218
|
raise FileNotFoundError(msg)
|
1226
1219
|
|
1227
1220
|
return Snapshot.parse_from_file(snapshot_file)
|
1221
|
+
|
1222
|
+
def _send_sfm_logs_internal(self, logs: dict | list[dict]):
|
1223
|
+
try:
|
1224
|
+
responses = self._client.send_sfm_logs(logs)
|
1225
|
+
|
1226
|
+
for response in responses:
|
1227
|
+
with self._internal_callbacks_results_lock:
|
1228
|
+
self._internal_callbacks_results[self._send_sfm_logs_internal.__name__] = Status(StatusValue.OK)
|
1229
|
+
if not response or "error" not in response or "message" not in response["error"]:
|
1230
|
+
return
|
1231
|
+
self._internal_callbacks_results[self._send_sfm_logs_internal.__name__] = Status(
|
1232
|
+
StatusValue.GENERIC_ERROR, response["error"]["message"]
|
1233
|
+
)
|
1234
|
+
except Exception as e:
|
1235
|
+
api_logger.error(f"Error sending SFM logs: {e!r}", exc_info=True)
|
1236
|
+
with self._internal_callbacks_results_lock:
|
1237
|
+
self._internal_callbacks_results[self._send_sfm_logs_internal.__name__] = Status(
|
1238
|
+
StatusValue.GENERIC_ERROR, str(e)
|
1239
|
+
)
|
1240
|
+
|
1241
|
+
def _send_sfm_logs(self, logs: dict | list[dict]):
|
1242
|
+
if not self._sfm_logs_allowed or not logs:
|
1243
|
+
return
|
1244
|
+
|
1245
|
+
for log in logs:
|
1246
|
+
log.update(self._metadata)
|
1247
|
+
log["dt.extension.config.label"] = self.monitoring_config_name
|
1248
|
+
log.pop("monitoring.configuration", None)
|
1249
|
+
|
1250
|
+
self._internal_executor.submit(self._send_sfm_logs_internal, logs)
|
@@ -0,0 +1,259 @@
|
|
1
|
+
import json
|
2
|
+
from collections.abc import Callable
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
from enum import Enum
|
6
|
+
from threading import Lock
|
7
|
+
|
8
|
+
from .event import Severity
|
9
|
+
|
10
|
+
|
11
|
+
class DynatraceDeprecatedError(Exception):
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
15
|
+
class StatusValue(Enum):
|
16
|
+
EMPTY = ""
|
17
|
+
OK = "OK"
|
18
|
+
GENERIC_ERROR = "GENERIC_ERROR"
|
19
|
+
INVALID_ARGS_ERROR = "INVALID_ARGS_ERROR"
|
20
|
+
EEC_CONNECTION_ERROR = "EEC_CONNECTION_ERROR"
|
21
|
+
INVALID_CONFIG_ERROR = "INVALID_CONFIG_ERROR"
|
22
|
+
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
|
23
|
+
DEVICE_CONNECTION_ERROR = "DEVICE_CONNECTION_ERROR"
|
24
|
+
WARNING = "WARNING"
|
25
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
26
|
+
|
27
|
+
def is_error(self) -> bool:
|
28
|
+
# WARNING is treated as an error
|
29
|
+
return self not in (StatusValue.OK, StatusValue.EMPTY)
|
30
|
+
|
31
|
+
def is_warning(self) -> bool:
|
32
|
+
return self == StatusValue.WARNING
|
33
|
+
|
34
|
+
|
35
|
+
class IgnoreStatus:
|
36
|
+
pass
|
37
|
+
|
38
|
+
|
39
|
+
class Status:
|
40
|
+
def __init__(self, status: StatusValue = StatusValue.EMPTY, message: str = "", timestamp: int | None = None):
|
41
|
+
self.status = status
|
42
|
+
self.message = message
|
43
|
+
self.timestamp = timestamp
|
44
|
+
|
45
|
+
def to_json(self) -> dict:
|
46
|
+
status = {"status": self.status.value, "message": self.message}
|
47
|
+
if self.timestamp:
|
48
|
+
status["timestamp"] = self.timestamp # type: ignore
|
49
|
+
return status
|
50
|
+
|
51
|
+
def __repr__(self):
|
52
|
+
return json.dumps(self.to_json())
|
53
|
+
|
54
|
+
def is_error(self) -> bool:
|
55
|
+
# WARNING is treated as an error
|
56
|
+
return self.status.is_error()
|
57
|
+
|
58
|
+
def is_warning(self) -> bool:
|
59
|
+
return self.status.is_warning()
|
60
|
+
|
61
|
+
|
62
|
+
class MultiStatus:
|
63
|
+
def __init__(self) -> None:
|
64
|
+
self.statuses: list[Status] = []
|
65
|
+
|
66
|
+
def add_status(self, status: StatusValue, message):
|
67
|
+
self.statuses.append(Status(status, message))
|
68
|
+
|
69
|
+
def build(self) -> Status:
|
70
|
+
ret = Status(StatusValue.OK)
|
71
|
+
if len(self.statuses) == 0:
|
72
|
+
return ret
|
73
|
+
|
74
|
+
messages = []
|
75
|
+
all_ok = True
|
76
|
+
all_err = True
|
77
|
+
any_warning = False
|
78
|
+
|
79
|
+
for stored_status in self.statuses:
|
80
|
+
if stored_status.message != "":
|
81
|
+
messages.append(stored_status.message)
|
82
|
+
|
83
|
+
if stored_status.is_warning():
|
84
|
+
any_warning = True
|
85
|
+
|
86
|
+
if stored_status.is_error():
|
87
|
+
all_ok = False
|
88
|
+
else:
|
89
|
+
all_err = False
|
90
|
+
|
91
|
+
ret.message = ", ".join(messages)
|
92
|
+
|
93
|
+
if any_warning:
|
94
|
+
ret.status = StatusValue.WARNING
|
95
|
+
elif all_ok:
|
96
|
+
ret.status = StatusValue.OK
|
97
|
+
elif all_err:
|
98
|
+
ret.status = StatusValue.GENERIC_ERROR
|
99
|
+
else:
|
100
|
+
ret.status = StatusValue.WARNING
|
101
|
+
|
102
|
+
return ret
|
103
|
+
|
104
|
+
|
105
|
+
class EndpointStatus:
|
106
|
+
def __init__(self, endpoint_hint: str, short_status: StatusValue, message: str | None = None):
|
107
|
+
self.endpoint = endpoint_hint
|
108
|
+
self.status: StatusValue = short_status
|
109
|
+
self.message = message
|
110
|
+
|
111
|
+
def __repr__(self):
|
112
|
+
return str(self.__dict__)
|
113
|
+
|
114
|
+
def __eq__(self, other):
|
115
|
+
return isinstance(other, EndpointStatus) and self.__dict__ == other.__dict__
|
116
|
+
|
117
|
+
|
118
|
+
class EndpointStatuses:
|
119
|
+
def __init__(self, total_endpoints_number=None) -> None:
|
120
|
+
if total_endpoints_number is not None:
|
121
|
+
msg = (
|
122
|
+
"EndpointStatuses.__init__: usage of `total_endpoints_number` parameter is abandoned. "
|
123
|
+
"Use other class methods to explicitly report all status changes for any endpoint."
|
124
|
+
)
|
125
|
+
raise DynatraceDeprecatedError(msg)
|
126
|
+
|
127
|
+
self._lock = Lock()
|
128
|
+
self._endpoints_statuses: dict[str, EndpointStatus] = {}
|
129
|
+
|
130
|
+
def add_endpoint_status(self, status: EndpointStatus):
|
131
|
+
with self._lock:
|
132
|
+
self._endpoints_statuses[status.endpoint] = status
|
133
|
+
|
134
|
+
|
135
|
+
class StatusState(Enum):
|
136
|
+
INITIAL = "INITIAL"
|
137
|
+
NEW = "NEW"
|
138
|
+
ONGOING = "ONGOING"
|
139
|
+
|
140
|
+
|
141
|
+
@dataclass
|
142
|
+
class EndpointStatusRecord:
|
143
|
+
ep_status: EndpointStatus
|
144
|
+
last_sent: datetime | None
|
145
|
+
state: StatusState
|
146
|
+
|
147
|
+
def __repr__(self):
|
148
|
+
return str(self.__dict__)
|
149
|
+
|
150
|
+
|
151
|
+
class EndpointStatusesMap:
|
152
|
+
RESENDING_INTERVAL = timedelta(hours=2)
|
153
|
+
|
154
|
+
def __init__(self, send_sfm_logs_function: Callable) -> None:
|
155
|
+
self._lock = Lock()
|
156
|
+
self._ep_records: dict[str, EndpointStatusRecord] = {}
|
157
|
+
self._send_sfm_logs_function = send_sfm_logs_function
|
158
|
+
self._logs_to_send: list[str] = []
|
159
|
+
|
160
|
+
def contains_any_status(self) -> bool:
|
161
|
+
return len(self._ep_records) > 0
|
162
|
+
|
163
|
+
def update_ep_statuses(self, new_ep_statuses: EndpointStatuses):
|
164
|
+
with self._lock:
|
165
|
+
with new_ep_statuses._lock:
|
166
|
+
for endpoint, ep_status in new_ep_statuses._endpoints_statuses.items():
|
167
|
+
if endpoint not in self._ep_records.keys():
|
168
|
+
self._ep_records[endpoint] = EndpointStatusRecord(
|
169
|
+
ep_status=ep_status, last_sent=None, state=StatusState.INITIAL
|
170
|
+
)
|
171
|
+
elif ep_status != self._ep_records[endpoint].ep_status:
|
172
|
+
self._ep_records[endpoint] = EndpointStatusRecord(
|
173
|
+
ep_status=ep_status, last_sent=None, state=StatusState.NEW
|
174
|
+
)
|
175
|
+
|
176
|
+
def send_ep_logs(self):
|
177
|
+
logs_to_send = []
|
178
|
+
|
179
|
+
with self._lock:
|
180
|
+
for ep_record in self._ep_records.values():
|
181
|
+
if self._should_be_reported(ep_record):
|
182
|
+
logs_to_send.append(
|
183
|
+
self._prepare_ep_status_log(
|
184
|
+
ep_record.ep_status.endpoint,
|
185
|
+
ep_record.state,
|
186
|
+
ep_record.ep_status.status,
|
187
|
+
ep_record.ep_status.message,
|
188
|
+
)
|
189
|
+
)
|
190
|
+
ep_record.last_sent = datetime.now()
|
191
|
+
ep_record.state = StatusState.ONGOING
|
192
|
+
|
193
|
+
if logs_to_send:
|
194
|
+
self._send_sfm_logs_function(logs_to_send)
|
195
|
+
|
196
|
+
def _should_be_reported(self, ep_record: EndpointStatusRecord):
|
197
|
+
if ep_record.ep_status.status == StatusValue.OK:
|
198
|
+
return ep_record.state == StatusState.NEW
|
199
|
+
elif ep_record.state in (StatusState.INITIAL, StatusState.NEW):
|
200
|
+
return True
|
201
|
+
elif ep_record.state == StatusState.ONGOING and (
|
202
|
+
ep_record.last_sent is None or datetime.now() - ep_record.last_sent >= self.RESENDING_INTERVAL
|
203
|
+
):
|
204
|
+
return True
|
205
|
+
else:
|
206
|
+
return False
|
207
|
+
|
208
|
+
def _prepare_ep_status_log(
|
209
|
+
self, endpoint_name: str, prefix: StatusState, status_value: StatusValue, status_message: str
|
210
|
+
) -> dict:
|
211
|
+
level = Severity.ERROR.value
|
212
|
+
|
213
|
+
if status_value.is_error() is False:
|
214
|
+
level = Severity.INFO.value
|
215
|
+
elif status_value.is_warning():
|
216
|
+
level = Severity.WARN.value
|
217
|
+
|
218
|
+
ep_status_log = {
|
219
|
+
"device.address": endpoint_name,
|
220
|
+
"level": level,
|
221
|
+
"message": f"{endpoint_name}: [{prefix.value}] - {status_value.value} {status_message}",
|
222
|
+
}
|
223
|
+
|
224
|
+
return ep_status_log
|
225
|
+
|
226
|
+
def build_common_status(self) -> Status:
|
227
|
+
with self._lock:
|
228
|
+
# Summarize all statuses
|
229
|
+
ok_count = 0
|
230
|
+
nok_count = 0
|
231
|
+
error_messages = []
|
232
|
+
has_warning_status = False
|
233
|
+
|
234
|
+
for ep_record in self._ep_records.values():
|
235
|
+
ep_status = ep_record.ep_status
|
236
|
+
if ep_status.status.is_warning():
|
237
|
+
has_warning_status = True
|
238
|
+
|
239
|
+
if ep_status.status.is_error():
|
240
|
+
nok_count += 1
|
241
|
+
error_messages.append(f"{ep_status.endpoint} - {ep_status.status.value} {ep_status.message}")
|
242
|
+
else:
|
243
|
+
ok_count += 1
|
244
|
+
|
245
|
+
# Early return if all OK
|
246
|
+
if nok_count == 0:
|
247
|
+
return Status(StatusValue.OK, f"Endpoints OK: {ok_count} NOK: 0")
|
248
|
+
|
249
|
+
# Build final status if some errors present
|
250
|
+
common_msg = ", ".join(error_messages)
|
251
|
+
all_endpoints_faulty = ok_count == 0
|
252
|
+
|
253
|
+
if all_endpoints_faulty and not has_warning_status:
|
254
|
+
status_value = StatusValue.GENERIC_ERROR
|
255
|
+
else:
|
256
|
+
status_value = StatusValue.WARNING
|
257
|
+
|
258
|
+
message = f"Endpoints OK: {ok_count} NOK: {nok_count} NOK_reported_errors: {common_msg}"
|
259
|
+
return Status(status=status_value, message=message)
|
File without changes
|
File without changes
|
{dt_extensions_sdk-1.6.2.dist-info → dt_extensions_sdk-1.7.1.dist-info}/licenses/LICENSE.txt
RENAMED
File without changes
|