insightconnect-plugin-runtime 6.2.6__py3-none-any.whl → 6.3.0__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.
@@ -787,7 +787,7 @@ class Endpoints:
787
787
  return version
788
788
 
789
789
  def add_plugin_custom_config(
790
- self, input_data: Dict[str, Any], org_id: str
790
+ self, input_data: Dict[str, Any], org_id: str
791
791
  ) -> Dict[str, Any]:
792
792
  """
793
793
  Using the retrieved configs pulled from komand-props, pass the configuration that matches the requesting
@@ -815,7 +815,7 @@ class Endpoints:
815
815
  # This means we still need to manually delete the state for plugins on a per org basis.
816
816
  # This also means first time customers for their 'initial' lookup would get the lookback value passed in.
817
817
  if input_data.get("body", {}).get("state") and additional_config.get(
818
- "lookback"
818
+ "lookback"
819
819
  ):
820
820
  self.logger.info(
821
821
  "Found an existing plugin state, not passing lookback value..."
@@ -842,8 +842,8 @@ class Endpoints:
842
842
  if isinstance(wrapped_exception, ClientException):
843
843
  status_code = 400
844
844
  elif (
845
- isinstance(wrapped_exception, PluginException)
846
- and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
845
+ isinstance(wrapped_exception, PluginException)
846
+ and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
847
847
  ):
848
848
  status_code = 400
849
849
  elif isinstance(wrapped_exception, (ConnectionTestException, ClientException)):
@@ -868,8 +868,8 @@ class Endpoints:
868
868
  if isinstance(wrapped_exception, (ConnectionTestException, ClientException)):
869
869
  return 400
870
870
  elif (
871
- isinstance(wrapped_exception, PluginException)
872
- and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
871
+ isinstance(wrapped_exception, PluginException)
872
+ and wrapped_exception.preset is PluginException.Preset.BAD_REQUEST
873
873
  ):
874
874
  return 400
875
875
  elif isinstance(wrapped_exception, ServerException):
@@ -1,17 +1,16 @@
1
- import os
2
- import sys
3
1
  import json
4
2
  import logging
3
+ import os
4
+ import sys
5
+ from time import sleep
5
6
 
7
+ import gunicorn.app.base
8
+ import structlog
6
9
  from apispec import APISpec
7
10
  from apispec.ext.marshmallow import MarshmallowPlugin
8
11
  from apispec_webframeworks.flask import FlaskPlugin
9
-
10
12
  from flask import Flask, request_started, request
11
- import gunicorn.app.base
12
13
  from gunicorn.arbiter import Arbiter
13
-
14
- import structlog
15
14
  from pythonjsonlogger.jsonlogger import JsonFormatter
16
15
  from requests import get as request_get
17
16
  from requests.exceptions import (
@@ -21,9 +20,9 @@ from requests.exceptions import (
21
20
  JSONDecodeError,
22
21
  ConnectionError,
23
22
  )
24
- from time import sleep
25
23
  from werkzeug.utils import secure_filename
26
24
 
25
+ from insightconnect_plugin_runtime.api.endpoints import Endpoints, handle_errors
27
26
  from insightconnect_plugin_runtime.api.schemas import (
28
27
  PluginInfoSchema,
29
28
  ActionTriggerOutputBodySchema,
@@ -39,9 +38,9 @@ from insightconnect_plugin_runtime.api.schemas import (
39
38
  ConnectionDetailsSchema,
40
39
  ConnectionTestSchema,
41
40
  )
42
- from insightconnect_plugin_runtime.api.endpoints import Endpoints, handle_errors
43
- from insightconnect_plugin_runtime.util import is_running_in_cloud
44
41
  from insightconnect_plugin_runtime.helper import clean_dict
42
+ from insightconnect_plugin_runtime.telemetry import create_post_fork
43
+ from insightconnect_plugin_runtime.util import is_running_in_cloud, OTEL_ENDPOINT
45
44
 
46
45
  API_TITLE = "InsightConnect Plugin Runtime API"
47
46
  API_VERSION = "1.0"
@@ -72,14 +71,14 @@ class PluginServer(gunicorn.app.base.BaseApplication):
72
71
  """
73
72
 
74
73
  def __init__(
75
- self,
76
- plugin,
77
- port=10001,
78
- workers=1,
79
- threads=4,
80
- debug=False,
81
- worker_class="sync",
82
- worker_connections=200,
74
+ self,
75
+ plugin,
76
+ port=10001,
77
+ workers=1,
78
+ threads=4,
79
+ debug=False,
80
+ worker_class="sync",
81
+ worker_connections=200,
83
82
  ):
84
83
 
85
84
  gunicorn_file = os.environ.get("GUNICORN_CONFIG_FILE")
@@ -120,6 +119,9 @@ class PluginServer(gunicorn.app.base.BaseApplication):
120
119
  self.get_plugin_properties_from_cps()
121
120
  self.app, self.blueprints = self.create_flask_app()
122
121
 
122
+ if os.environ.get(OTEL_ENDPOINT):
123
+ self.config_options[OTEL_ENDPOINT] = os.environ[OTEL_ENDPOINT]
124
+
123
125
  @staticmethod
124
126
  def configure_structlog_instance(is_debug: bool) -> None:
125
127
  structlog.configure(
@@ -166,6 +168,9 @@ class PluginServer(gunicorn.app.base.BaseApplication):
166
168
  for key, value in config.items():
167
169
  self.cfg.set(key.lower(), value)
168
170
 
171
+ post_fork = create_post_fork(lambda: self.app, lambda: self.plugin, lambda: self.config_options)
172
+ self.cfg.set("post_fork", post_fork)
173
+
169
174
  def create_flask_app(self):
170
175
  app = Flask(__name__)
171
176
 
@@ -193,7 +198,7 @@ class PluginServer(gunicorn.app.base.BaseApplication):
193
198
 
194
199
  def get_plugin_properties_from_cps(self):
195
200
  # Call out to komand-props to get configurations related to only the plugin pod running.
196
- if is_running_in_cloud() and self.plugin.tasks:
201
+ if is_running_in_cloud():
197
202
  for attempt in range(1, CPS_RETRY + 1):
198
203
  self.logger.info(
199
204
  f"Getting plugin configuration information... (attempt {attempt}/{CPS_RETRY})"
@@ -206,7 +211,12 @@ class PluginServer(gunicorn.app.base.BaseApplication):
206
211
  ) # match how we name our images
207
212
  plugin_config = resp_json.get("plugins", {}).get(plugin, {})
208
213
 
209
- self.config_options = plugin_config
214
+ if self.plugin.tasks:
215
+ self.config_options = plugin_config
216
+
217
+ if resp_json.get(OTEL_ENDPOINT, {}):
218
+ self.config_options[OTEL_ENDPOINT] = resp_json.get(OTEL_ENDPOINT, {})
219
+
210
220
  self.logger.info("Plugin configuration successfully retrieved...")
211
221
  return
212
222
  except MissingSchema as missing_schema:
@@ -0,0 +1,73 @@
1
+ import functools
2
+ from typing import Any, Callable
3
+ from flask.app import Flask
4
+
5
+ from opentelemetry import trace
6
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
7
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
8
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
9
+ from opentelemetry.sdk.resources import Resource
10
+ from opentelemetry.sdk.trace import TracerProvider
11
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
12
+ from opentelemetry.trace import Status, StatusCode
13
+
14
+ from insightconnect_plugin_runtime.plugin import Plugin
15
+ from insightconnect_plugin_runtime.util import is_running_in_cloud, OTEL_ENDPOINT
16
+
17
+
18
+ def init_tracing(app: Flask, plugin: Plugin, endpoint: str) -> None:
19
+ """
20
+ Initialize OpenTelemetry Tracing
21
+
22
+ The function sets up the tracer provider, span processor and exporter with auto-instrumentation
23
+
24
+ :param app: The Flask Application
25
+ :param plugin: The plugin to derive the service name from
26
+ :param endpoint: The Otel Endpoint to emit traces to
27
+ """
28
+
29
+ if not is_running_in_cloud():
30
+ return
31
+
32
+ resource = Resource(attributes={"service.name": f'{plugin.name.lower().replace(" ", "_")}-{plugin.version}'})
33
+
34
+ trace_provider = TracerProvider(resource=resource)
35
+ exporter = OTLPSpanExporter(endpoint=endpoint)
36
+ trace_provider.add_span_processor(BatchSpanProcessor(exporter))
37
+ trace.set_tracer_provider(trace_provider)
38
+
39
+ FlaskInstrumentor().instrument_app(app)
40
+
41
+ def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
42
+ if hasattr(response, "status_code"):
43
+ span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
44
+
45
+ RequestsInstrumentor().instrument(trace_provider=trace_provider, response_hook=requests_callback)
46
+
47
+
48
+ def auto_instrument(func: Callable) -> Callable:
49
+ """
50
+ Decorator that auto-instruments a function with a trace
51
+
52
+ :param func: function to instrument
53
+ :return:
54
+ """
55
+
56
+ @functools.wraps(func)
57
+ def wrapper(*args, **kwargs):
58
+ tracer = trace.get_tracer(__name__)
59
+ with tracer.start_as_current_span(func.__name__):
60
+ return func(*args, **kwargs)
61
+
62
+ return wrapper
63
+
64
+
65
+ def create_post_fork(app_getter: Callable, plugin_getter: Callable, config_getter: Callable) -> Callable:
66
+ def post_fork(server, worker):
67
+ app = app_getter()
68
+ plugin = plugin_getter()
69
+ endpoint = config_getter().get(OTEL_ENDPOINT, None)
70
+ if endpoint:
71
+ init_tracing(app, plugin, endpoint)
72
+
73
+ return post_fork
@@ -10,6 +10,7 @@ import python_jsonschema_objects as pjs
10
10
  KEYS_TO_CHECK_SECRETS = ("secretKey", "password", "key", "token")
11
11
  DEFAULT_REPLACE_THRESHOLD = 0.8
12
12
  DEFAULT_NUMBER_OF_ITERATIONS = 50
13
+ OTEL_ENDPOINT = "otel_tracing"
13
14
 
14
15
 
15
16
  class OutputMasker:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: insightconnect-plugin-runtime
3
- Version: 6.2.6
3
+ Version: 6.3.0
4
4
  Summary: InsightConnect Plugin Runtime
5
5
  Home-page: https://github.com/rapid7/komand-plugin-sdk-python
6
6
  Author: Rapid7 Integrations Alliance
@@ -26,6 +26,11 @@ Requires-Dist: apispec-webframeworks==1.0.0
26
26
  Requires-Dist: blinker==1.9.0
27
27
  Requires-Dist: structlog==24.4.0
28
28
  Requires-Dist: python-json-logger==2.0.7
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
29
34
  Dynamic: author
30
35
  Dynamic: author-email
31
36
  Dynamic: classifier
@@ -219,6 +224,7 @@ contributed. Black is installed as a test dependency and the hook can be initial
219
224
  after cloning this repository.
220
225
 
221
226
  ## Changelog
227
+ * 6.3.0 - Add Tracing Instrumentation
222
228
  * 6.2.6 - Remove setuptools after installation
223
229
  * 6.2.5 - Fixed bug related to failure to set default region to assume role method in `aws_client` for newer versions of boto3 | Updated alpine image packages on build
224
230
  * 6.2.4 - Update `make_request` helper to support extra parameter of `max_response_size` to cap the response
@@ -8,14 +8,15 @@ insightconnect_plugin_runtime/helper.py,sha256=B0XqAXmn8CT1KQ6i5IoWLQrQ_HVOvuKrI
8
8
  insightconnect_plugin_runtime/metrics.py,sha256=hf_Aoufip_s4k4o8Gtzz90ymZthkaT2e5sXh5B4LcF0,3186
9
9
  insightconnect_plugin_runtime/plugin.py,sha256=Yf4LNczykDVc31F9G8uuJ9gxEsgmxmAr0n4pcZzichM,26393
10
10
  insightconnect_plugin_runtime/schema.py,sha256=6MVw5hqGATU1VLgwfOWfPsP3hy1OnsugCTsgX8sknes,521
11
- insightconnect_plugin_runtime/server.py,sha256=09fxsbKf2ZZvSqRP2Bv9e9-fspDyEFR8_YgIFeMnXqQ,12578
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=6JgZOyhB0qcDYKPlQZ47M2ZPfDpfAE_zuAhBtw4GMvo,2586
14
15
  insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2sdy_qbedUY8,874
15
- insightconnect_plugin_runtime/util.py,sha256=qPkZ3LA55nYuNYdansEbnCnBccQkpzIpp9NA1B64Kvw,8444
16
+ insightconnect_plugin_runtime/util.py,sha256=8cle29INhnshEcL2LWpaC0ZGqevjq8pW8TE0MFEiYYw,8475
16
17
  insightconnect_plugin_runtime/variables.py,sha256=7FjJGnU7KUR7m9o-_tRq7Q3KiaB1Pp0Apj1NGgOwrJk,3056
17
18
  insightconnect_plugin_runtime/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- insightconnect_plugin_runtime/api/endpoints.py,sha256=8QQrxzW8jmQIkalud8fqYwB05uUw8sTiDNgO5ZekOCA,33353
19
+ insightconnect_plugin_runtime/api/endpoints.py,sha256=Kn9VfnpvcVO9oY5W07laJkXFNp0PfZQrY2AecYFbvNw,33377
19
20
  insightconnect_plugin_runtime/api/schemas.py,sha256=jRmDrwLJTBl-iQOnyZkSwyJlCWg4eNjAnKfD9Eko4z0,2754
20
21
  insightconnect_plugin_runtime/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  insightconnect_plugin_runtime/clients/aws_client.py,sha256=ViJF3klbD1YM5GVxme3BFYfpuADUlVGP8sdbX2Z6_Cw,23061
@@ -73,12 +74,12 @@ tests/unit/test_metrics.py,sha256=PjjTrB9w7uQ2Q5UN-893-SsH3EGJuBseOMHSD1I004s,79
73
74
  tests/unit/test_oauth.py,sha256=nbFG0JH1x04ExXqSe-b5BGdt_hJs7DP17eUa6bQzcYI,2093
74
75
  tests/unit/test_plugin.py,sha256=ZTNAZWwZhDIAbxkVuWhnz9FzmojbijgMmsLWM2mXQI0,4160
75
76
  tests/unit/test_schema.py,sha256=swWZPRo_Q4M6VHte-srmxcV2wH-XS7pgmNRxpaL0Qrg,642
76
- tests/unit/test_server_cloud_plugins.py,sha256=PuMDHTz3af6lR9QK1BtPScr7_cRbWhetowADieVlXdo,5096
77
+ tests/unit/test_server_cloud_plugins.py,sha256=3hEdNrJmnDXqhxhd4uSdTJ0ksn2S11RsNdZCmnethCw,5340
77
78
  tests/unit/test_server_spec.py,sha256=je97BaktgK0Fiz3AwFPkcmHzYtOJJNqJV_Fw5hrvqX4,644
78
79
  tests/unit/test_trigger.py,sha256=E53mAUoVyponWu_4IQZ0IC1gQ9lakBnTn_9vKN2IZfg,1692
79
80
  tests/unit/test_variables.py,sha256=OUEOqGYZA3Nd5oKk5GVY3hcrWKHpZpxysBJcO_v5gzs,291
80
81
  tests/unit/utils.py,sha256=hcY0A2H_DMgCDXUTvDtCXMdMvRjLQgTaGcTpATb8YG0,2236
81
- insightconnect_plugin_runtime-6.2.6.dist-info/METADATA,sha256=EAB_1cINrkNbkoHu5n6xbhtqrT6HwPbBonogpA_eqsE,15934
82
- insightconnect_plugin_runtime-6.2.6.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
83
- insightconnect_plugin_runtime-6.2.6.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
84
- insightconnect_plugin_runtime-6.2.6.dist-info/RECORD,,
82
+ insightconnect_plugin_runtime-6.3.0.dist-info/METADATA,sha256=GCmcMcuLriTNb5PMLZc83UqlpxByddurw6G-WAwui-4,16225
83
+ insightconnect_plugin_runtime-6.3.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
84
+ insightconnect_plugin_runtime-6.3.0.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
85
+ insightconnect_plugin_runtime-6.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -4,6 +4,7 @@ from unittest import TestCase, skip
4
4
  from unittest.mock import patch, MagicMock
5
5
 
6
6
  from insightconnect_plugin_runtime.server import PluginServer
7
+ from insightconnect_plugin_runtime.util import OTEL_ENDPOINT
7
8
  from tests.plugin.hello_world import KomandHelloWorld
8
9
  from .utils import MockResponse, Logger
9
10
 
@@ -20,15 +21,19 @@ class TestServerCloudPlugins(TestCase):
20
21
  @parameterized.expand([["Set cloud to false", False], ["Set cloud to true", True]])
21
22
  @patch("insightconnect_plugin_runtime.server.request_get")
22
23
  def test_cloud_plugin_no_tasks_ignore_cps(self, _test_name, cloud, mocked_req, mock_cloud, _run):
24
+ fake_endpoint = "http://fake.endpoint.com"
25
+ mocked_req.return_value = MockResponse({OTEL_ENDPOINT: fake_endpoint}) if cloud else MockResponse({})
23
26
  mock_cloud.return_value = cloud # Mock plugin running in cloud vs not
24
27
  self.plugin.tasks = None # ensure still no tasks as other tests edit this and could fail before reverting
25
- plugin_server = PluginServer(self.plugin) # this plugin has no tasks by default
26
28
 
29
+ plugin_server = PluginServer(self.plugin) # this plugin has no tasks by default
27
30
  plugin_server.start()
28
- self.assertEqual(plugin_server.config_options, {})
29
31
 
30
- # Plugin server never calls out to CPS as either we are not running in cloud mode or have no tasks.
31
- self.assertFalse(mocked_req.called)
32
+ self.assertEqual(plugin_server.config_options, {OTEL_ENDPOINT: fake_endpoint} if cloud else {})
33
+
34
+ # Plugin server calls out to CPS when cloud to get tracing endpoint
35
+ self.assertEqual(mocked_req.called, cloud)
36
+
32
37
 
33
38
  @patch("insightconnect_plugin_runtime.server.request_get")
34
39
  def test_cloud_plugin_calls_cps(self, mocked_req, _mock_cloud, _run):