insightconnect-plugin-runtime 6.3.3__py3-none-any.whl → 6.3.5__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.
@@ -1,7 +1,9 @@
1
1
  import contextvars
2
- from functools import wraps, partial
3
- from typing import Any, Callable
2
+ from datetime import UTC, datetime
3
+ from functools import partial, wraps
4
+ from typing import Any, Callable, List, Union
4
5
 
6
+ from dateutil.parser import parse
5
7
  from flask.app import Flask
6
8
  from opentelemetry import trace
7
9
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
@@ -13,7 +15,11 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor
13
15
  from opentelemetry.trace import Status, StatusCode
14
16
 
15
17
  from insightconnect_plugin_runtime.plugin import Plugin
16
- from insightconnect_plugin_runtime.util import is_running_in_cloud, OTEL_ENDPOINT
18
+ from insightconnect_plugin_runtime.util import (
19
+ OTEL_ENDPOINT,
20
+ is_running_in_cloud,
21
+ parse_from_string,
22
+ )
17
23
 
18
24
 
19
25
  def init_tracing(app: Flask, plugin: Plugin, endpoint: str) -> None:
@@ -92,3 +98,96 @@ def with_context(context: contextvars.Context, function: Callable) -> Callable:
92
98
  return context_.copy().run(function_, *args, **kwargs)
93
99
 
94
100
  return partial(_wrapper, context, function)
101
+
102
+
103
+ def monitor_task_delay(
104
+ timestamp_keys: Union[str, List[str]], default_delay_threshold: str = "2d"
105
+ ) -> Callable:
106
+ """Monitor timestamp fields in task state to detect processing delays.
107
+
108
+ This decorator checks if specified timestamp fields in a task's state have fallen
109
+ behind a configurable threshold, indicating the task is processing data with a lag.
110
+ When timestamps fall behind the threshold, an error is logged.
111
+
112
+ The threshold can be overridden at runtime by setting the "task_delay_threshold" key
113
+ in the task's custom_config.
114
+
115
+ :param timestamp_keys: One or more state keys containing timestamps to monitor
116
+ :type timestamp_keys: Union[str, List[str]]
117
+
118
+ :param default_delay_threshold: Time duration string representing maximum acceptable lag (e.g. "2d" for 2 days).
119
+ :type default_delay_threshold: str
120
+
121
+ :return: Decorator function that wraps the original task function
122
+ :rtype: Callable
123
+ """
124
+
125
+ # Check if time_fields is a string and convert it to a list
126
+ if isinstance(timestamp_keys, str):
127
+ timestamp_keys = [timestamp_keys]
128
+
129
+ def _decorator(function_: Callable):
130
+ @wraps(function_)
131
+ def _wrapper(self, *args, **kwargs):
132
+ # Unpack response tuple from task `def run()` method
133
+ output, state, has_more_pages, status_code, error_object = function_(
134
+ self, *args, **kwargs
135
+ )
136
+
137
+ # Try-except with pass to make sure any exception won't stop the task from running
138
+ try:
139
+ # Check if any time fields are in the past
140
+ threshold = kwargs.get("custom_config", {}).get(
141
+ "task_delay_threshold", default_delay_threshold
142
+ )
143
+
144
+ # Calculate the delayed time based on the threshold
145
+ delay_threshold_time = (
146
+ datetime.now(UTC) - parse_from_string(threshold)
147
+ ).replace(tzinfo=None)
148
+
149
+ # Loop over the state time fields and check if they are below the set threshold
150
+ for state_time in timestamp_keys:
151
+ # Check if the state time exists in the state dictionary
152
+ if not (current_state_time := state.get(state_time)):
153
+ continue
154
+
155
+ # Parse and normalize the state time value
156
+ try:
157
+ # First, try to parse the epoch timestamp
158
+ normalized_state_time = datetime.fromtimestamp(
159
+ float(current_state_time)
160
+ )
161
+ except (ValueError, TypeError):
162
+ # If parsing fails, parse as a string
163
+ normalized_state_time = parse(str(current_state_time))
164
+
165
+ # Normalize the state time to UTC and remove timezone info
166
+ normalized_state_time = normalized_state_time.astimezone(
167
+ UTC
168
+ ).replace(tzinfo=None)
169
+
170
+ # If the normalized state time is below the threshold, log an error message
171
+ if normalized_state_time < delay_threshold_time:
172
+ # Log an error message that indicates the integration state time is below the threshold
173
+ self.logger.error(
174
+ f"ERROR: THE INTEGRATION IS FALLING BEHIND",
175
+ field=state_time,
176
+ current_value=current_state_time,
177
+ normalized_time=normalized_state_time,
178
+ threshold_time=delay_threshold_time,
179
+ configured_threshold=threshold,
180
+ has_more_pages=has_more_pages,
181
+ )
182
+ break
183
+ except Exception as error:
184
+ self.logger.info(
185
+ f"An error occurred while checking task delay. Error: {error}"
186
+ )
187
+
188
+ # Return task output
189
+ return output, state, has_more_pages, status_code, error_object
190
+
191
+ return _wrapper
192
+
193
+ return _decorator
@@ -2,7 +2,9 @@
2
2
  import copy
3
3
  import logging
4
4
  import os
5
+ import re
5
6
  import sys
7
+ from datetime import timedelta
6
8
  from typing import Any, Dict, List, Tuple, Union
7
9
 
8
10
  import python_jsonschema_objects as pjs
@@ -261,3 +263,44 @@ def trace():
261
263
 
262
264
  def is_running_in_cloud():
263
265
  return os.environ.get("PLUGIN_RUNTIME_ENVIRONMENT") == "cloud"
266
+
267
+
268
+ def parse_from_string(
269
+ duration: str,
270
+ default: timedelta = timedelta(days=2),
271
+ unit_map: Dict[str, str] = None,
272
+ ) -> timedelta:
273
+ """
274
+ Parse a string duration (i.e. '5d', '12h', '30m', '45s') into a timedelta object.
275
+
276
+ :param duration: String in format like '5d', '12h', '30m', '45s'
277
+ :type duration: str
278
+
279
+ :param default: Default timedelta to return if parsing fails
280
+ :type default: timedelta
281
+
282
+ :param unit_map: Mapping of unit letters to timedelta parameter names
283
+ :type unit_map: Dict[str, str]
284
+
285
+ :return: Parsed timedelta or default if parsing fails
286
+ :rtype: timedelta
287
+ """
288
+
289
+ # If unit_map is not provided, use the default mapping
290
+ if unit_map is None:
291
+ unit_map = {"s": "seconds", "m": "minutes", "h": "hours", "d": "days"}
292
+
293
+ # Check if duration is not empty and is a string
294
+ if not duration or not isinstance(duration, str):
295
+ return default
296
+
297
+ # Use regex to match the duration format
298
+ match = re.match(r"(\d+)([smhd])", duration.lower())
299
+ if not match:
300
+ return default
301
+
302
+ # If match is found, extract the value and unit
303
+ value, unit = match.groups()
304
+
305
+ # Return a timedelta object based on the matched value and unit from the unit_map
306
+ return timedelta(**{unit_map[unit]: int(value)})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: insightconnect-plugin-runtime
3
- Version: 6.3.3
3
+ Version: 6.3.5
4
4
  Summary: InsightConnect Plugin Runtime
5
5
  Home-page: https://github.com/rapid7/komand-plugin-sdk-python
6
6
  Author: Rapid7 Integrations Alliance
@@ -12,25 +12,25 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Natural Language :: English
13
13
  Classifier: Topic :: Software Development :: Build Tools
14
14
  Description-Content-Type: text/markdown
15
- Requires-Dist: requests==2.32.3
15
+ Requires-Dist: requests==2.32.4
16
16
  Requires-Dist: python_jsonschema_objects==0.5.2
17
17
  Requires-Dist: jsonschema==4.22.0
18
- Requires-Dist: certifi==2024.12.14
19
- Requires-Dist: Flask==3.1.0
18
+ Requires-Dist: certifi==2025.4.26
19
+ Requires-Dist: Flask==3.1.1
20
20
  Requires-Dist: gunicorn==23.0.0
21
- Requires-Dist: greenlet==3.1.1
22
- Requires-Dist: gevent==24.11.1
21
+ Requires-Dist: greenlet==3.2.3
22
+ Requires-Dist: gevent==25.5.1
23
23
  Requires-Dist: marshmallow==3.21.0
24
24
  Requires-Dist: apispec==6.5.0
25
25
  Requires-Dist: apispec-webframeworks==1.0.0
26
26
  Requires-Dist: blinker==1.9.0
27
- Requires-Dist: structlog==24.4.0
27
+ Requires-Dist: structlog==25.4.0
28
28
  Requires-Dist: python-json-logger==2.0.7
29
29
  Requires-Dist: Jinja2==3.1.6
30
- Requires-Dist: opentelemetry-sdk==1.31.1
31
- Requires-Dist: opentelemetry-instrumentation-flask==0.52b1
32
- Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.31.1
33
- Requires-Dist: opentelemetry-instrumentation-requests==0.52b1
30
+ Requires-Dist: opentelemetry-sdk==1.34.0
31
+ Requires-Dist: opentelemetry-instrumentation-flask==0.55b0
32
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.34.0
33
+ Requires-Dist: opentelemetry-instrumentation-requests==0.55b0
34
34
  Dynamic: author
35
35
  Dynamic: author-email
36
36
  Dynamic: classifier
@@ -224,6 +224,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
224
224
  after cloning this repository.
225
225
 
226
226
  ## Changelog
227
+ * 6.3.5 - Added `monitor_task_delay` decorator to detect processing delays
228
+ * 6.3.4 - Addressed vulnerabilities within the slim and non-slim Python images (bumping packages)
227
229
  * 6.3.3 - Add helper func for tracing context to preserve parent span in threadpools
228
230
  * 6.3.2 - Raise `APIException` from within the `response_handler` to easily access the status code within the plugin
229
231
  * 6.3.1 - Improved filtering for `custom_config` parameters for plugin tasks
@@ -11,9 +11,9 @@ insightconnect_plugin_runtime/schema.py,sha256=6MVw5hqGATU1VLgwfOWfPsP3hy1OnsugC
11
11
  insightconnect_plugin_runtime/server.py,sha256=DHooHBQa1M2z7aETTWK6u9B2jChi8vONzqK4n_c94f4,13138
12
12
  insightconnect_plugin_runtime/step.py,sha256=KdERg-789-s99IEKN61DR08naz-YPxyinPT0C_T81C4,855
13
13
  insightconnect_plugin_runtime/task.py,sha256=d-H1EAzVnmSdDEJtXyIK5JySprxpF9cetVoFGtWlHrg,123
14
- insightconnect_plugin_runtime/telemetry.py,sha256=qJJ6N7hWhOQ-I40AJIAAZjdOvRmXMdJo0e3jwXQLOfs,3312
14
+ insightconnect_plugin_runtime/telemetry.py,sha256=9ibZCX2sbLl9GaWnyOuJfk7lJ4xTFWO5lxh0jlGEbxs,7580
15
15
  insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2sdy_qbedUY8,874
16
- insightconnect_plugin_runtime/util.py,sha256=8cle29INhnshEcL2LWpaC0ZGqevjq8pW8TE0MFEiYYw,8475
16
+ insightconnect_plugin_runtime/util.py,sha256=DMspk1DQ5oOUnKi2foX2N-Qo-cxhJWETYQCaVjHX8Qo,9804
17
17
  insightconnect_plugin_runtime/variables.py,sha256=7FjJGnU7KUR7m9o-_tRq7Q3KiaB1Pp0Apj1NGgOwrJk,3056
18
18
  insightconnect_plugin_runtime/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  insightconnect_plugin_runtime/api/endpoints.py,sha256=ddqqYM7s1huvXFD0nCjpV_J2XULDn8F5sRakdjq-AKU,38893
@@ -79,7 +79,7 @@ tests/unit/test_server_spec.py,sha256=je97BaktgK0Fiz3AwFPkcmHzYtOJJNqJV_Fw5hrvqX
79
79
  tests/unit/test_trigger.py,sha256=E53mAUoVyponWu_4IQZ0IC1gQ9lakBnTn_9vKN2IZfg,1692
80
80
  tests/unit/test_variables.py,sha256=OUEOqGYZA3Nd5oKk5GVY3hcrWKHpZpxysBJcO_v5gzs,291
81
81
  tests/unit/utils.py,sha256=hcY0A2H_DMgCDXUTvDtCXMdMvRjLQgTaGcTpATb8YG0,2236
82
- insightconnect_plugin_runtime-6.3.3.dist-info/METADATA,sha256=eYx7EgwH9i2PNL-uTXqctPyJIVztYzFOPibMThW04EE,16504
83
- insightconnect_plugin_runtime-6.3.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
84
- insightconnect_plugin_runtime-6.3.3.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
85
- insightconnect_plugin_runtime-6.3.3.dist-info/RECORD,,
82
+ insightconnect_plugin_runtime-6.3.5.dist-info/METADATA,sha256=1dVj_hHOHaNHQ6KROnbmuubyBSeXo1IzwhnZkgKrdSM,16675
83
+ insightconnect_plugin_runtime-6.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
84
+ insightconnect_plugin_runtime-6.3.5.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
85
+ insightconnect_plugin_runtime-6.3.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5