insightconnect-plugin-runtime 6.3.4__tar.gz → 6.3.6__tar.gz

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.
Files changed (94) hide show
  1. {insightconnect_plugin_runtime-6.3.4/insightconnect_plugin_runtime.egg-info → insightconnect_plugin_runtime-6.3.6}/PKG-INFO +13 -10
  2. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/README.md +2 -0
  3. insightconnect_plugin_runtime-6.3.6/insightconnect_plugin_runtime/telemetry.py +193 -0
  4. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/util.py +43 -0
  5. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6/insightconnect_plugin_runtime.egg-info}/PKG-INFO +13 -10
  6. insightconnect_plugin_runtime-6.3.6/insightconnect_plugin_runtime.egg-info/requires.txt +20 -0
  7. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/setup.py +11 -10
  8. insightconnect_plugin_runtime-6.3.4/insightconnect_plugin_runtime/telemetry.py +0 -94
  9. insightconnect_plugin_runtime-6.3.4/insightconnect_plugin_runtime.egg-info/requires.txt +0 -19
  10. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/MANIFEST.in +0 -0
  11. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect-plugin-swagger.json +0 -0
  12. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/__init__.py +0 -0
  13. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/action.py +0 -0
  14. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/api/__init__.py +0 -0
  15. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/api/endpoints.py +0 -0
  16. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/api/schemas.py +0 -0
  17. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/cli.py +0 -0
  18. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/clients/__init__.py +0 -0
  19. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/clients/aws_client.py +0 -0
  20. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/clients/oauth.py +0 -0
  21. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/connection.py +0 -0
  22. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/data/input_message_schema.json +0 -0
  23. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/data/output_message_schema.json +0 -0
  24. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/dispatcher.py +0 -0
  25. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/exceptions.py +0 -0
  26. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/helper.py +0 -0
  27. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/metrics.py +0 -0
  28. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/plugin.py +0 -0
  29. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/schema.py +0 -0
  30. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/server.py +0 -0
  31. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/step.py +0 -0
  32. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/task.py +0 -0
  33. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/trigger.py +0 -0
  34. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime/variables.py +0 -0
  35. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime.egg-info/SOURCES.txt +0 -0
  36. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime.egg-info/dependency_links.txt +0 -0
  37. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/insightconnect_plugin_runtime.egg-info/top_level.txt +0 -0
  38. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/setup.cfg +0 -0
  39. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/__init__.py +0 -0
  40. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/__init__.py +0 -0
  41. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/__init__.py +0 -0
  42. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/__init__.py +0 -0
  43. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/__init__.py +0 -0
  44. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/__init__.py +0 -0
  45. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/hello/__init__.py +0 -0
  46. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/hello/action.py +0 -0
  47. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/hello/schema.py +0 -0
  48. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/return_bad_json/__init__.py +0 -0
  49. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/return_bad_json/action.py +0 -0
  50. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/return_bad_json/schema.py +0 -0
  51. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/throw_exception/__init__.py +0 -0
  52. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/throw_exception/action.py +0 -0
  53. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/throw_exception/schema.py +0 -0
  54. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/connection/__init__.py +0 -0
  55. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/connection/connection.py +0 -0
  56. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/connection/schema.py +0 -0
  57. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/__init__.py +0 -0
  58. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/monitor_events/__init__.py +0 -0
  59. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/monitor_events/schema.py +0 -0
  60. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/monitor_events/task.py +0 -0
  61. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/__init__.py +0 -0
  62. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/__init__.py +0 -0
  63. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/schema.py +0 -0
  64. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/trigger.py +0 -0
  65. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/__init__.py +0 -0
  66. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/schema.py +0 -0
  67. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/trigger.py +0 -0
  68. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/__init__.py +0 -0
  69. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/schema.py +0 -0
  70. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/trigger.py +0 -0
  71. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/komand_hello_world/util/__init__.py +0 -0
  72. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/hello_world/setup.py +0 -0
  73. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/tests/__init__.py +0 -0
  74. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/tests/conftest.py +0 -0
  75. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/tests/test_cli.py +0 -0
  76. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/tests/test_hello_world.py +0 -0
  77. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/plugin/hello_world/tests/test_server.py +0 -0
  78. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/__init__.py +0 -0
  79. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_action.py +0 -0
  80. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_api.py +0 -0
  81. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_aws_action.py +0 -0
  82. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_custom_encoder.py +0 -0
  83. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_endpoints.py +0 -0
  84. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_exceptions.py +0 -0
  85. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_helpers.py +0 -0
  86. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_metrics.py +0 -0
  87. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_oauth.py +0 -0
  88. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_plugin.py +0 -0
  89. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_schema.py +0 -0
  90. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_server_cloud_plugins.py +0 -0
  91. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_server_spec.py +0 -0
  92. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_trigger.py +0 -0
  93. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/test_variables.py +0 -0
  94. {insightconnect_plugin_runtime-6.3.4 → insightconnect_plugin_runtime-6.3.6}/tests/unit/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: insightconnect-plugin-runtime
3
- Version: 6.3.4
3
+ Version: 6.3.6
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,26 @@ 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
18
+ Requires-Dist: certifi==2025.4.26
19
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: python-dateutil==2.9.0.post0
31
+ Requires-Dist: opentelemetry-sdk==1.34.0
32
+ Requires-Dist: opentelemetry-instrumentation-flask==0.55b0
33
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.34.0
34
+ Requires-Dist: opentelemetry-instrumentation-requests==0.55b0
34
35
  Dynamic: author
35
36
  Dynamic: author-email
36
37
  Dynamic: classifier
@@ -224,6 +225,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
224
225
  after cloning this repository.
225
226
 
226
227
  ## Changelog
228
+ * 6.3.6 - Added required dependency to plugin runtime
229
+ * 6.3.5 - Added `monitor_task_delay` decorator to detect processing delays
227
230
  * 6.3.4 - Addressed vulnerabilities within the slim and non-slim Python images (bumping packages)
228
231
  * 6.3.3 - Add helper func for tracing context to preserve parent span in threadpools
229
232
  * 6.3.2 - Raise `APIException` from within the `response_handler` to easily access the status code within the plugin
@@ -182,6 +182,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
182
182
  after cloning this repository.
183
183
 
184
184
  ## Changelog
185
+ * 6.3.6 - Added required dependency to plugin runtime
186
+ * 6.3.5 - Added `monitor_task_delay` decorator to detect processing delays
185
187
  * 6.3.4 - Addressed vulnerabilities within the slim and non-slim Python images (bumping packages)
186
188
  * 6.3.3 - Add helper func for tracing context to preserve parent span in threadpools
187
189
  * 6.3.2 - Raise `APIException` from within the `response_handler` to easily access the status code within the plugin
@@ -0,0 +1,193 @@
1
+ import contextvars
2
+ from datetime import UTC, datetime
3
+ from functools import partial, wraps
4
+ from typing import Any, Callable, List, Union
5
+
6
+ from dateutil.parser import parse
7
+ from flask.app import Flask
8
+ from opentelemetry import trace
9
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
10
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
11
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
12
+ from opentelemetry.sdk.resources import Resource
13
+ from opentelemetry.sdk.trace import TracerProvider
14
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
15
+ from opentelemetry.trace import Status, StatusCode
16
+
17
+ from insightconnect_plugin_runtime.plugin import Plugin
18
+ from insightconnect_plugin_runtime.util import (
19
+ OTEL_ENDPOINT,
20
+ is_running_in_cloud,
21
+ parse_from_string,
22
+ )
23
+
24
+
25
+ def init_tracing(app: Flask, plugin: Plugin, endpoint: str) -> None:
26
+ """
27
+ Initialize OpenTelemetry Tracing
28
+
29
+ The function sets up the tracer provider, span processor and exporter with auto-instrumentation
30
+
31
+ :param app: The Flask Application
32
+ :param plugin: The plugin to derive the service name from
33
+ :param endpoint: The Otel Endpoint to emit traces to
34
+ """
35
+
36
+ if not is_running_in_cloud():
37
+ return
38
+
39
+ resource = Resource(attributes={"service.name": f'{plugin.name.lower().replace(" ", "_")}-{plugin.version}'})
40
+
41
+ trace_provider = TracerProvider(resource=resource)
42
+ exporter = OTLPSpanExporter(endpoint=endpoint)
43
+ trace_provider.add_span_processor(BatchSpanProcessor(exporter))
44
+ trace.set_tracer_provider(trace_provider)
45
+
46
+ FlaskInstrumentor().instrument_app(app)
47
+
48
+ def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
49
+ if hasattr(response, "status_code"):
50
+ span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
51
+
52
+ RequestsInstrumentor().instrument(trace_provider=trace_provider, response_hook=requests_callback)
53
+
54
+
55
+ def auto_instrument(func: Callable) -> Callable:
56
+ """
57
+ Decorator that auto-instruments a function with a trace
58
+
59
+ :param func: function to instrument
60
+ :return:
61
+ """
62
+
63
+ @wraps(func)
64
+ def wrapper(*args, **kwargs):
65
+ tracer = trace.get_tracer(__name__)
66
+ with tracer.start_as_current_span(func.__name__):
67
+ return func(*args, **kwargs)
68
+
69
+ return wrapper
70
+
71
+
72
+ def create_post_fork(app_getter: Callable, plugin_getter: Callable, config_getter: Callable) -> Callable:
73
+ def post_fork(server, worker):
74
+ app = app_getter()
75
+ plugin = plugin_getter()
76
+ endpoint = config_getter().get(OTEL_ENDPOINT, None)
77
+ if endpoint:
78
+ init_tracing(app, plugin, endpoint)
79
+
80
+ return post_fork
81
+
82
+
83
+ def with_context(context: contextvars.Context, function: Callable) -> Callable:
84
+ """
85
+ Creates a wrapper function that executes the target function with the specified context.
86
+
87
+ :param context: The Context object to apply when executing the function
88
+ :type context: contextvars.Context
89
+
90
+ :param function: The function to wrap with the specified context
91
+ :type function: Callable
92
+
93
+ :return: A wrapper function that applies the context when called
94
+ :rtype: Callable
95
+ """
96
+
97
+ def _wrapper(context_: contextvars.Context, function_: Callable, *args, **kwargs):
98
+ return context_.copy().run(function_, *args, **kwargs)
99
+
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.4
3
+ Version: 6.3.6
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,26 @@ 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
18
+ Requires-Dist: certifi==2025.4.26
19
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: python-dateutil==2.9.0.post0
31
+ Requires-Dist: opentelemetry-sdk==1.34.0
32
+ Requires-Dist: opentelemetry-instrumentation-flask==0.55b0
33
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.34.0
34
+ Requires-Dist: opentelemetry-instrumentation-requests==0.55b0
34
35
  Dynamic: author
35
36
  Dynamic: author-email
36
37
  Dynamic: classifier
@@ -224,6 +225,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
224
225
  after cloning this repository.
225
226
 
226
227
  ## Changelog
228
+ * 6.3.6 - Added required dependency to plugin runtime
229
+ * 6.3.5 - Added `monitor_task_delay` decorator to detect processing delays
227
230
  * 6.3.4 - Addressed vulnerabilities within the slim and non-slim Python images (bumping packages)
228
231
  * 6.3.3 - Add helper func for tracing context to preserve parent span in threadpools
229
232
  * 6.3.2 - Raise `APIException` from within the `response_handler` to easily access the status code within the plugin
@@ -0,0 +1,20 @@
1
+ requests==2.32.4
2
+ python_jsonschema_objects==0.5.2
3
+ jsonschema==4.22.0
4
+ certifi==2025.4.26
5
+ Flask==3.1.1
6
+ gunicorn==23.0.0
7
+ greenlet==3.2.3
8
+ gevent==25.5.1
9
+ marshmallow==3.21.0
10
+ apispec==6.5.0
11
+ apispec-webframeworks==1.0.0
12
+ blinker==1.9.0
13
+ structlog==25.4.0
14
+ python-json-logger==2.0.7
15
+ Jinja2==3.1.6
16
+ python-dateutil==2.9.0.post0
17
+ opentelemetry-sdk==1.34.0
18
+ opentelemetry-instrumentation-flask==0.55b0
19
+ opentelemetry-exporter-otlp-proto-http==1.34.0
20
+ opentelemetry-instrumentation-requests==0.55b0
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name="insightconnect-plugin-runtime",
8
- version="6.3.4",
8
+ version="6.3.6",
9
9
  description="InsightConnect Plugin Runtime",
10
10
  long_description=long_description,
11
11
  long_description_content_type="text/markdown",
@@ -14,25 +14,26 @@ setup(
14
14
  url="https://github.com/rapid7/komand-plugin-sdk-python",
15
15
  packages=find_packages(),
16
16
  install_requires=[
17
- "requests==2.32.3",
17
+ "requests==2.32.4",
18
18
  "python_jsonschema_objects==0.5.2",
19
19
  "jsonschema==4.22.0",
20
- "certifi==2024.12.14",
20
+ "certifi==2025.4.26",
21
21
  "Flask==3.1.1",
22
22
  "gunicorn==23.0.0",
23
- "greenlet==3.1.1",
24
- "gevent==24.11.1",
23
+ "greenlet==3.2.3",
24
+ "gevent==25.5.1",
25
25
  "marshmallow==3.21.0",
26
26
  "apispec==6.5.0",
27
27
  "apispec-webframeworks==1.0.0",
28
28
  "blinker==1.9.0",
29
- "structlog==24.4.0",
29
+ "structlog==25.4.0",
30
30
  "python-json-logger==2.0.7",
31
31
  "Jinja2==3.1.6",
32
- "opentelemetry-sdk==1.31.1",
33
- "opentelemetry-instrumentation-flask==0.52b1",
34
- "opentelemetry-exporter-otlp-proto-http==1.31.1",
35
- "opentelemetry-instrumentation-requests==0.52b1",
32
+ "python-dateutil==2.9.0.post0",
33
+ "opentelemetry-sdk==1.34.0",
34
+ "opentelemetry-instrumentation-flask==0.55b0",
35
+ "opentelemetry-exporter-otlp-proto-http==1.34.0",
36
+ "opentelemetry-instrumentation-requests==0.55b0",
36
37
  ],
37
38
  tests_require=[
38
39
  "pytest",
@@ -1,94 +0,0 @@
1
- import contextvars
2
- from functools import wraps, partial
3
- from typing import Any, Callable
4
-
5
- from flask.app import Flask
6
- from opentelemetry import trace
7
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
8
- from opentelemetry.instrumentation.flask import FlaskInstrumentor
9
- from opentelemetry.instrumentation.requests import RequestsInstrumentor
10
- from opentelemetry.sdk.resources import Resource
11
- from opentelemetry.sdk.trace import TracerProvider
12
- from opentelemetry.sdk.trace.export import BatchSpanProcessor
13
- from opentelemetry.trace import Status, StatusCode
14
-
15
- from insightconnect_plugin_runtime.plugin import Plugin
16
- from insightconnect_plugin_runtime.util import is_running_in_cloud, OTEL_ENDPOINT
17
-
18
-
19
- def init_tracing(app: Flask, plugin: Plugin, endpoint: str) -> None:
20
- """
21
- Initialize OpenTelemetry Tracing
22
-
23
- The function sets up the tracer provider, span processor and exporter with auto-instrumentation
24
-
25
- :param app: The Flask Application
26
- :param plugin: The plugin to derive the service name from
27
- :param endpoint: The Otel Endpoint to emit traces to
28
- """
29
-
30
- if not is_running_in_cloud():
31
- return
32
-
33
- resource = Resource(attributes={"service.name": f'{plugin.name.lower().replace(" ", "_")}-{plugin.version}'})
34
-
35
- trace_provider = TracerProvider(resource=resource)
36
- exporter = OTLPSpanExporter(endpoint=endpoint)
37
- trace_provider.add_span_processor(BatchSpanProcessor(exporter))
38
- trace.set_tracer_provider(trace_provider)
39
-
40
- FlaskInstrumentor().instrument_app(app)
41
-
42
- def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
43
- if hasattr(response, "status_code"):
44
- span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
45
-
46
- RequestsInstrumentor().instrument(trace_provider=trace_provider, response_hook=requests_callback)
47
-
48
-
49
- def auto_instrument(func: Callable) -> Callable:
50
- """
51
- Decorator that auto-instruments a function with a trace
52
-
53
- :param func: function to instrument
54
- :return:
55
- """
56
-
57
- @wraps(func)
58
- def wrapper(*args, **kwargs):
59
- tracer = trace.get_tracer(__name__)
60
- with tracer.start_as_current_span(func.__name__):
61
- return func(*args, **kwargs)
62
-
63
- return wrapper
64
-
65
-
66
- def create_post_fork(app_getter: Callable, plugin_getter: Callable, config_getter: Callable) -> Callable:
67
- def post_fork(server, worker):
68
- app = app_getter()
69
- plugin = plugin_getter()
70
- endpoint = config_getter().get(OTEL_ENDPOINT, None)
71
- if endpoint:
72
- init_tracing(app, plugin, endpoint)
73
-
74
- return post_fork
75
-
76
-
77
- def with_context(context: contextvars.Context, function: Callable) -> Callable:
78
- """
79
- Creates a wrapper function that executes the target function with the specified context.
80
-
81
- :param context: The Context object to apply when executing the function
82
- :type context: contextvars.Context
83
-
84
- :param function: The function to wrap with the specified context
85
- :type function: Callable
86
-
87
- :return: A wrapper function that applies the context when called
88
- :rtype: Callable
89
- """
90
-
91
- def _wrapper(context_: contextvars.Context, function_: Callable, *args, **kwargs):
92
- return context_.copy().run(function_, *args, **kwargs)
93
-
94
- return partial(_wrapper, context, function)
@@ -1,19 +0,0 @@
1
- requests==2.32.3
2
- python_jsonschema_objects==0.5.2
3
- jsonschema==4.22.0
4
- certifi==2024.12.14
5
- Flask==3.1.1
6
- gunicorn==23.0.0
7
- greenlet==3.1.1
8
- gevent==24.11.1
9
- marshmallow==3.21.0
10
- apispec==6.5.0
11
- apispec-webframeworks==1.0.0
12
- blinker==1.9.0
13
- structlog==24.4.0
14
- python-json-logger==2.0.7
15
- Jinja2==3.1.6
16
- opentelemetry-sdk==1.31.1
17
- opentelemetry-instrumentation-flask==0.52b1
18
- opentelemetry-exporter-otlp-proto-http==1.31.1
19
- opentelemetry-instrumentation-requests==0.52b1