omnata-plugin-runtime 0.7.0a184__py3-none-any.whl → 0.8.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.
@@ -11,6 +11,7 @@ from enum import Enum
11
11
 
12
12
  from abc import ABC
13
13
  from pydantic import BaseModel, Field, PrivateAttr, SerializationInfo, TypeAdapter, field_validator, model_serializer, validator # pylint: disable=no-name-in-module
14
+ from .logging import logger, tracer
14
15
 
15
16
  if tuple(sys.version_info[:2]) >= (3, 9):
16
17
  # Python 3.9 and above
@@ -19,9 +20,6 @@ else:
19
20
  # Python 3.8 and below
20
21
  from typing_extensions import Annotated
21
22
 
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
23
  class MapperType(str, Enum):
26
24
  FIELD_MAPPING_SELECTOR = "field_mapping_selector"
27
25
  JINJA_TEMPLATE = "jinja_template"
@@ -872,7 +870,7 @@ InboundSyncStreamsConfiguration.model_rebuild()
872
870
  StoredFieldMappings.model_rebuild()
873
871
  OutboundSyncConfigurationParameters.model_rebuild()
874
872
 
875
-
873
+ @tracer.start_as_current_span("get_secrets")
876
874
  def get_secrets(oauth_secret_name: Optional[str], other_secrets_name: Optional[str]
877
875
  ) -> Dict[str, StoredConfigurationValue]:
878
876
  connection_secrets = {}
@@ -4,11 +4,39 @@ Custom logging functionality for Omnata
4
4
  import logging
5
5
  import logging.handlers
6
6
  import traceback
7
- from logging import Logger
7
+ from logging import Logger, getLogger
8
8
  from typing import Dict, List, Optional
9
9
  from snowflake.snowpark import Session
10
10
  from pydantic import ValidationError
11
+ from snowflake import telemetry
12
+ from opentelemetry import trace
11
13
 
14
+ tracer = trace.get_tracer('omnata_plugin_runtime')
15
+
16
+ class CustomLoggerAdapter(logging.LoggerAdapter):
17
+ """
18
+ A logger adapter which attaches current trace and span IDs to log messages, so that they can be correlated to traces.
19
+ Also offers the ability to add static extras.
20
+ """
21
+ def __init__(self, logger:logging.Logger, extra):
22
+ super(CustomLoggerAdapter, self).__init__(logger, extra)
23
+ self.extra_extras = {}
24
+
25
+ def add_extra(self, key, value):
26
+ self.extra_extras[key] = value
27
+
28
+ def process(self, msg, kwargs):
29
+ extra = kwargs.get("extra", {})
30
+ current_span = trace.get_current_span()
31
+ context = current_span.get_span_context() if current_span is not None else None
32
+ if context is not None:
33
+ extra["trace_id"] = format(context.trace_id, 'x') # format as hex string to be consistent with Snowflake's handler
34
+ extra["span_id"] = format(context.span_id, 'x')
35
+ extra.update(self.extra_extras)
36
+ kwargs["extra"] = extra
37
+ return msg, kwargs
38
+
39
+ logger = CustomLoggerAdapter(getLogger("omnata_plugin"), {})
12
40
 
13
41
  def log_exception(exception: Exception, logger_instance: Logger):
14
42
  """
@@ -47,7 +47,8 @@ from snowflake.snowpark import Session
47
47
  from snowflake.snowpark.functions import col
48
48
  from tenacity import Retrying, stop_after_attempt, wait_fixed, retry_if_exception_message
49
49
 
50
- from .logging import OmnataPluginLogHandler
50
+ from .logging import OmnataPluginLogHandler, logger, tracer
51
+ from opentelemetry import context
51
52
 
52
53
  from .api import (
53
54
  PluginMessage,
@@ -88,7 +89,6 @@ from .rate_limiting import (
88
89
  RateLimitedSession
89
90
  )
90
91
 
91
- logger = getLogger(__name__)
92
92
  SortDirectionType = Literal["asc", "desc"]
93
93
 
94
94
 
@@ -317,6 +317,8 @@ class SyncRequest(ABC):
317
317
  self._cancel_checking_task = None
318
318
  self._rate_limit_update_task = None
319
319
  self._last_stream_progress_update = None
320
+ # store the opentelemetry context so that it can be attached inside threads
321
+ self.opentelemetry_context = context.get_current()
320
322
 
321
323
  threading.excepthook = self.thread_exception_hook
322
324
  if self.development_mode is False:
@@ -388,6 +390,7 @@ class SyncRequest(ABC):
388
390
  """
389
391
  Designed to be run in a thread, this method polls the results every 20 seconds and sends them back to Snowflake.
390
392
  """
393
+ context.attach(self.opentelemetry_context)
391
394
  while not cancellation_token.is_set():
392
395
  logger.debug("apply results worker checking for results")
393
396
  self.apply_results_queue()
@@ -400,6 +403,7 @@ class SyncRequest(ABC):
400
403
  It also gives us the latest rate limit state from Snowflake, so that activity on other syncs/branches can
401
404
  impact rate limiting on this one.
402
405
  """
406
+ context.attach(self.opentelemetry_context)
403
407
  while not cancellation_token.is_set():
404
408
  try:
405
409
  self.apply_rate_limit_state()
@@ -409,6 +413,7 @@ class SyncRequest(ABC):
409
413
  logger.info("rate limit update worker exiting")
410
414
 
411
415
  def __cancel_checking_worker(self, cancellation_token:threading.Event):
416
+ context.attach(self.opentelemetry_context)
412
417
  """
413
418
  Designed to be run in a thread, this method checks to see if the sync run has been cancelled
414
419
  or if the deadline has been reached.
@@ -810,22 +815,23 @@ class OutboundSyncRequest(SyncRequest):
810
815
  logger.debug("applying results to table")
811
816
  # use a random table name with a random string to avoid collisions
812
817
  with self._snowflake_query_lock:
813
- for attempt in Retrying(stop=stop_after_attempt(30),wait=wait_fixed(2),reraise=True,retry=retry_if_exception_message(match=".*(is being|was) committed.*")):
814
- with attempt:
815
- success, nchunks, nrows, _ = write_pandas(
816
- conn=self._session._conn._cursor.connection, # pylint: disable=protected-access
817
- df=self._preprocess_results_dataframe(results_df),
818
- quote_identifiers=False,
819
- table_name=self._full_results_table_name,
820
- auto_create_table=False
821
- )
822
- if not success:
823
- raise ValueError(
824
- f"Failed to write results to table {self._full_results_table_name}"
818
+ with tracer.start_as_current_span("apply_results"):
819
+ for attempt in Retrying(stop=stop_after_attempt(30),wait=wait_fixed(2),reraise=True,retry=retry_if_exception_message(match=".*(is being|was) committed.*")):
820
+ with attempt:
821
+ success, nchunks, nrows, _ = write_pandas(
822
+ conn=self._session._conn._cursor.connection, # pylint: disable=protected-access
823
+ df=self._preprocess_results_dataframe(results_df),
824
+ quote_identifiers=False,
825
+ table_name=self._full_results_table_name,
826
+ auto_create_table=False
827
+ )
828
+ if not success:
829
+ raise ValueError(
830
+ f"Failed to write results to table {self._full_results_table_name}"
831
+ )
832
+ logger.debug(
833
+ f"Wrote {nrows} rows and {nchunks} chunks to table {self._full_results_table_name}"
825
834
  )
826
- logger.debug(
827
- f"Wrote {nrows} rows and {nchunks} chunks to table {self._full_results_table_name}"
828
- )
829
835
 
830
836
  def __dataframe_wrapper(
831
837
  self, data_frame: pandas.DataFrame, render_jinja: bool = True
@@ -1465,36 +1471,37 @@ class InboundSyncRequest(SyncRequest):
1465
1471
  """
1466
1472
  if len(results_df) > 0:
1467
1473
  with self._snowflake_query_lock:
1468
- for attempt in Retrying(stop=stop_after_attempt(30),wait=wait_fixed(2),reraise=True,retry=retry_if_exception_message(match=".*(is being|was) committed.*")):
1469
- with attempt:
1470
- logger.debug(
1471
- f"Applying {len(results_df)} results to {self._full_results_table_name}"
1472
- )
1473
- # try setting parquet engine here, since the engine parameter does not seem to make it through to the write_pandas function
1474
- success, nchunks, nrows, _ = write_pandas(
1475
- conn=self._session._conn._cursor.connection, # pylint: disable=protected-access
1476
- df=results_df,
1477
- table_name=self._full_results_table_name,
1478
- quote_identifiers=False, # already done in get_temp_table_name
1479
- # schema='INBOUND_RAW', # it seems to be ok to provide schema in the table name
1480
- table_type="transient"
1481
- )
1482
- if not success:
1483
- raise ValueError(
1484
- f"Failed to write results to table {self._full_results_table_name}"
1474
+ with tracer.start_as_current_span("apply_results"):
1475
+ for attempt in Retrying(stop=stop_after_attempt(30),wait=wait_fixed(2),reraise=True,retry=retry_if_exception_message(match=".*(is being|was) committed.*")):
1476
+ with attempt:
1477
+ logger.debug(
1478
+ f"Applying {len(results_df)} results to {self._full_results_table_name}"
1485
1479
  )
1486
- logger.debug(
1487
- f"Wrote {nrows} rows and {nchunks} chunks to table {self._full_results_table_name}"
1488
- )
1489
- # temp tables aren't allowed
1490
- # snowflake_df = self._session.create_dataframe(results_df)
1491
- # snowflake_df.write.save_as_table(table_name=temp_table,
1492
- # mode='append',
1493
- # column_order='index',
1494
- # #create_temp_table=True
1495
- # )
1496
- for stream_name in stream_names:
1497
- self._results_exist[stream_name] = True
1480
+ # try setting parquet engine here, since the engine parameter does not seem to make it through to the write_pandas function
1481
+ success, nchunks, nrows, _ = write_pandas(
1482
+ conn=self._session._conn._cursor.connection, # pylint: disable=protected-access
1483
+ df=results_df,
1484
+ table_name=self._full_results_table_name,
1485
+ quote_identifiers=False, # already done in get_temp_table_name
1486
+ # schema='INBOUND_RAW', # it seems to be ok to provide schema in the table name
1487
+ table_type="transient"
1488
+ )
1489
+ if not success:
1490
+ raise ValueError(
1491
+ f"Failed to write results to table {self._full_results_table_name}"
1492
+ )
1493
+ logger.debug(
1494
+ f"Wrote {nrows} rows and {nchunks} chunks to table {self._full_results_table_name}"
1495
+ )
1496
+ # temp tables aren't allowed
1497
+ # snowflake_df = self._session.create_dataframe(results_df)
1498
+ # snowflake_df.write.save_as_table(table_name=temp_table,
1499
+ # mode='append',
1500
+ # column_order='index',
1501
+ # #create_temp_table=True
1502
+ # )
1503
+ for stream_name in stream_names:
1504
+ self._results_exist[stream_name] = True
1498
1505
  else:
1499
1506
  logger.debug("Results dataframe is empty, not applying")
1500
1507
 
@@ -1636,6 +1643,8 @@ class OmnataPlugin(ABC):
1636
1643
  take care of various status updates and loading enqueued results.
1637
1644
  Only set this to True if you plan to do all of this yourself (e.g. in a Java stored proc)
1638
1645
  """
1646
+ # store the opentelemetry context so that it can be attached inside threads
1647
+ self.opentelemetry_context = context.get_current()
1639
1648
 
1640
1649
 
1641
1650
  @abstractmethod
@@ -1945,6 +1954,7 @@ def __managed_outbound_processing_worker(
1945
1954
  Consumes a fixed sized set of records by passing them to the wrapped function,
1946
1955
  while adhering to the defined API constraints.
1947
1956
  """
1957
+ context.attach(plugin_class_obj.opentelemetry_context)
1948
1958
  logger.debug(
1949
1959
  f"worker {worker_index} processing. Cancelled: {cancellation_token.is_set()}"
1950
1960
  )
@@ -2127,6 +2137,7 @@ def __managed_inbound_processing_worker(
2127
2137
  A worker thread for the managed_inbound_processing annotation.
2128
2138
  Passes single streams at a time to the wrapped function, adhering to concurrency constraints.
2129
2139
  """
2140
+ context.attach(plugin_class_obj.opentelemetry_context)
2130
2141
  while not cancellation_token.is_set():
2131
2142
  # Get our generator object out of the queue
2132
2143
  logger.debug(
@@ -2137,14 +2148,16 @@ def __managed_inbound_processing_worker(
2137
2148
  logger.debug(f"stream returned from queue: {stream}")
2138
2149
  # restore the first argument, was originally the dataframe/generator but now it's the appropriately sized dataframe
2139
2150
  try:
2140
- logger.debug(f"worker {worker_index} processing stream {stream.stream_name}, invoking plugin class method {method.__name__}")
2141
- result = method(plugin_class_obj, *(stream, *method_args), **method_kwargs)
2142
- logger.debug(f"worker {worker_index} completed processing stream {stream.stream_name}")
2143
- if result is not None and result is False:
2144
- logger.info(f"worker {worker_index} requested that {stream.stream_name} be not marked as complete")
2145
- else:
2146
- logger.info(f"worker {worker_index} marking stream {stream.stream_name} as complete")
2147
- plugin_class_obj._sync_request.mark_stream_complete(stream.stream_name)
2151
+ with tracer.start_as_current_span("managed_inbound_processing") as managed_inbound_processing_span:
2152
+ logger.debug(f"worker {worker_index} processing stream {stream.stream_name}, invoking plugin class method {method.__name__}")
2153
+ managed_inbound_processing_span.set_attribute("stream_name", stream.stream_name)
2154
+ result = method(plugin_class_obj, *(stream, *method_args), **method_kwargs)
2155
+ logger.debug(f"worker {worker_index} completed processing stream {stream.stream_name}")
2156
+ if result is not None and result is False:
2157
+ logger.info(f"worker {worker_index} requested that {stream.stream_name} be not marked as complete")
2158
+ else:
2159
+ logger.info(f"worker {worker_index} marking stream {stream.stream_name} as complete")
2160
+ plugin_class_obj._sync_request.mark_stream_complete(stream.stream_name)
2148
2161
  except InterruptedWhileWaitingException:
2149
2162
  # If an inbound run is cancelled while waiting for rate limiting, this should mean that
2150
2163
  # the cancellation is handled elsewhere, so we don't need to do anything special here other than stop waiting
@@ -24,7 +24,7 @@ from .configuration import (
24
24
  ConnectivityOption
25
25
  )
26
26
  from .forms import ConnectionMethod, FormInputField, FormOption
27
- from .logging import OmnataPluginLogHandler
27
+ from .logging import OmnataPluginLogHandler, logger, tracer
28
28
  from .omnata_plugin import (
29
29
  SnowflakeBillingEvent,
30
30
  BillingEventRequest,
@@ -36,13 +36,10 @@ from .omnata_plugin import (
36
36
  )
37
37
  from pydantic import TypeAdapter
38
38
  from .rate_limiting import ApiLimits, RateLimitState
39
-
40
- # set the logger class to our custom logger so that pydantic errors are handled correctly
41
- logger = logging.getLogger(__name__)
39
+ from opentelemetry import trace
42
40
 
43
41
  IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
44
42
 
45
-
46
43
  class PluginEntrypoint:
47
44
  """
48
45
  This class gives each plugin's stored procs an initial point of contact.
@@ -53,89 +50,107 @@ class PluginEntrypoint:
53
50
  self, plugin_fqn: str, session: Session, module_name: str, class_name: str
54
51
  ):
55
52
  logger.info(f"Initialising plugin entrypoint for {plugin_fqn}")
56
- self._session = session
57
- import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
58
- sys.path.append(os.path.join(import_dir, "app.zip"))
59
- module = importlib.import_module(module_name)
60
- class_obj = getattr(module, class_name)
61
- self._plugin_instance: OmnataPlugin = class_obj()
62
- self._plugin_instance._session = session # pylint: disable=protected-access
63
- # logging defaults
64
- snowflake_logger = logging.getLogger("snowflake")
65
- snowflake_logger.setLevel(logging.WARN) # we don't want snowflake queries being logged by default
66
- # the sync engine can tell the plugin to override log level via a session variable
67
- if session is not None:
68
- try:
69
- v = session.sql("select getvariable('LOG_LEVEL_OVERRIDES')").collect()
70
- result = v[0][0]
71
- if result is not None:
72
- log_level_overrides:Dict[str,str] = json.loads(result)
73
- for logger_name,level in log_level_overrides.items():
74
- logger_override = logging.getLogger(logger_name)
75
- logger_override.setLevel(level)
76
- logger_override.propagate = False
77
- for handler in logger_override.handlers:
78
- handler.setLevel(level)
79
- except Exception as e:
80
- logger.error(f"Error setting log level overrides: {str(e)}")
53
+ with tracer.start_as_current_span("plugin_initialization") as span:
54
+ self._session = session
55
+ import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
56
+ span.add_event("Adding plugin zip to path")
57
+ sys.path.append(os.path.join(import_dir, "app.zip"))
58
+ span.add_event("Importing plugin module")
59
+ module = importlib.import_module(module_name)
60
+ class_obj = getattr(module, class_name)
61
+ self._plugin_instance: OmnataPlugin = class_obj()
62
+ self._plugin_instance._session = session # pylint: disable=protected-access
63
+ # logging defaults
64
+ snowflake_logger = logging.getLogger("snowflake")
65
+ snowflake_logger.setLevel(logging.WARN) # we don't want snowflake queries being logged by default
66
+ # the sync engine can tell the plugin to override log level via a session variable
67
+ if session is not None:
68
+ try:
69
+ span.add_event("Checking log level overrides")
70
+ v = session.sql("select getvariable('LOG_LEVEL_OVERRIDES')").collect()
71
+ result = v[0][0]
72
+ if result is not None:
73
+ log_level_overrides:Dict[str,str] = json.loads(result)
74
+ span.add_event("Applying log level overrides",log_level_overrides)
75
+ for logger_name,level in log_level_overrides.items():
76
+ logger_override = logging.getLogger(logger_name)
77
+ logger_override.setLevel(level)
78
+ logger_override.propagate = False
79
+ for handler in logger_override.handlers:
80
+ handler.setLevel(level)
81
+ except Exception as e:
82
+ logger.error(f"Error setting log level overrides: {str(e)}")
81
83
 
82
84
 
83
85
  def sync(self, sync_request: Dict):
84
- logger.info("Entered sync method")
85
86
  request = TypeAdapter(SyncRequestPayload).validate_python(sync_request)
86
- connection_secrets = get_secrets(
87
- request.oauth_secret_name, request.other_secrets_name
88
- )
89
- omnata_log_handler = OmnataPluginLogHandler(
90
- session=self._session,
91
- sync_id=request.sync_id,
92
- sync_branch_id=request.sync_branch_id,
93
- connection_id=request.connection_id,
94
- sync_run_id=request.run_id,
95
- )
96
- omnata_log_handler.register(
97
- request.logging_level, self._plugin_instance.additional_loggers()
98
- )
99
- # construct some connection parameters for the purpose of getting the api limits
100
- connection_parameters = ConnectionConfigurationParameters(
101
- connection_method=request.connection_method,
102
- connectivity_option=request.connectivity_option,
103
- connection_parameters=request.connection_parameters,
104
- connection_secrets=connection_secrets
105
- )
106
- if request.oauth_secret_name is not None:
107
- connection_parameters.access_token_secret_name = request.oauth_secret_name
108
- all_api_limits = self._plugin_instance.api_limits(connection_parameters)
109
- logger.info(
110
- f"Default API limits: {json.dumps(to_jsonable_python(all_api_limits))}"
111
- )
112
- all_api_limits_by_category = {
113
- api_limit.endpoint_category: api_limit for api_limit in all_api_limits
114
- }
115
- all_api_limits_by_category.update(
116
- {
117
- k: v
118
- for k, v in [
119
- (x.endpoint_category, x) for x in request.api_limit_overrides
120
- ]
87
+ logger.add_extra('omnata.operation', 'sync')
88
+ logger.add_extra('omnata.sync.id', request.sync_id)
89
+ logger.add_extra('omnata.sync.direction', request.sync_direction)
90
+ logger.add_extra('omnata.connection.id', request.connection_id)
91
+ logger.add_extra('omnata.sync_run.id', request.run_id)
92
+ logger.add_extra('omnata.sync_branch.id', request.sync_branch_id)
93
+ logger.add_extra('omnata.sync_branch.name', request.sync_branch_name)
94
+ logger.info("Entered sync method")
95
+ with tracer.start_as_current_span("initialization") as span:
96
+ span.add_event("Fetching secrets")
97
+
98
+ connection_secrets = get_secrets(
99
+ request.oauth_secret_name, request.other_secrets_name
100
+ )
101
+ span.add_event("Configuring log handler")
102
+ omnata_log_handler = OmnataPluginLogHandler(
103
+ session=self._session,
104
+ sync_id=request.sync_id,
105
+ sync_branch_id=request.sync_branch_id,
106
+ connection_id=request.connection_id,
107
+ sync_run_id=request.run_id,
108
+ )
109
+
110
+ omnata_log_handler.register(
111
+ request.logging_level, self._plugin_instance.additional_loggers()
112
+ )
113
+ # construct some connection parameters for the purpose of getting the api limits
114
+ connection_parameters = ConnectionConfigurationParameters(
115
+ connection_method=request.connection_method,
116
+ connectivity_option=request.connectivity_option,
117
+ connection_parameters=request.connection_parameters,
118
+ connection_secrets=connection_secrets
119
+ )
120
+ if request.oauth_secret_name is not None:
121
+ connection_parameters.access_token_secret_name = request.oauth_secret_name
122
+ span.add_event("Configuring API Limits")
123
+ all_api_limits = self._plugin_instance.api_limits(connection_parameters)
124
+ logger.info(
125
+ f"Default API limits: {json.dumps(to_jsonable_python(all_api_limits))}"
126
+ )
127
+ all_api_limits_by_category = {
128
+ api_limit.endpoint_category: api_limit for api_limit in all_api_limits
121
129
  }
122
- )
123
- api_limits = list(all_api_limits_by_category.values())
124
- return_dict = {}
125
- logger.info(
126
- f"Rate limits state: {json.dumps(to_jsonable_python(request.rate_limits_state))}"
127
- )
128
- (rate_limit_state_all, rate_limit_state_this_branch) = RateLimitState.collapse(request.rate_limits_state,request.sync_id, request.sync_branch_name)
129
- # if any endpoint categories have no state, give them an empty state
130
- for api_limit in api_limits:
131
- if api_limit.endpoint_category not in rate_limit_state_all:
132
- rate_limit_state_all[api_limit.endpoint_category] = RateLimitState(
133
- wait_until=None, previous_request_timestamps=[]
134
- )
135
- if api_limit.endpoint_category not in rate_limit_state_this_branch:
136
- rate_limit_state_this_branch[api_limit.endpoint_category] = RateLimitState(
137
- wait_until=None, previous_request_timestamps=[]
138
- )
130
+ all_api_limits_by_category.update(
131
+ {
132
+ k: v
133
+ for k, v in [
134
+ (x.endpoint_category, x) for x in request.api_limit_overrides
135
+ ]
136
+ }
137
+ )
138
+ api_limits = list(all_api_limits_by_category.values())
139
+ return_dict = {}
140
+ logger.info(
141
+ f"Rate limits state: {json.dumps(to_jsonable_python(request.rate_limits_state))}"
142
+ )
143
+ (rate_limit_state_all, rate_limit_state_this_branch) = RateLimitState.collapse(request.rate_limits_state,request.sync_id, request.sync_branch_name)
144
+ # if any endpoint categories have no state, give them an empty state
145
+ for api_limit in api_limits:
146
+ if api_limit.endpoint_category not in rate_limit_state_all:
147
+ rate_limit_state_all[api_limit.endpoint_category] = RateLimitState(
148
+ wait_until=None, previous_request_timestamps=[]
149
+ )
150
+ if api_limit.endpoint_category not in rate_limit_state_this_branch:
151
+ rate_limit_state_this_branch[api_limit.endpoint_category] = RateLimitState(
152
+ wait_until=None, previous_request_timestamps=[]
153
+ )
139
154
 
140
155
  if request.sync_direction == "outbound":
141
156
  parameters = OutboundSyncConfigurationParameters(
@@ -169,11 +184,13 @@ class PluginEntrypoint:
169
184
  )
170
185
  try:
171
186
  self._plugin_instance._configuration_parameters = parameters
172
- with HttpRateLimiting(outbound_sync_request, parameters):
173
- self._plugin_instance.sync_outbound(parameters, outbound_sync_request)
187
+ with tracer.start_as_current_span("invoke_plugin") as span:
188
+ with HttpRateLimiting(outbound_sync_request, parameters):
189
+ self._plugin_instance.sync_outbound(parameters, outbound_sync_request)
174
190
  if self._plugin_instance.disable_background_workers is False:
175
- outbound_sync_request.apply_results_queue()
176
- outbound_sync_request.apply_rate_limit_state()
191
+ with tracer.start_as_current_span("results_finalization") as span:
192
+ outbound_sync_request.apply_results_queue()
193
+ outbound_sync_request.apply_rate_limit_state()
177
194
  if outbound_sync_request.deadline_reached:
178
195
  # if we actually hit the deadline, this is flagged by the cancellation checking worker and the cancellation
179
196
  # token is set. We throw it here as an error since that's currently how it flows back to the engine with a DELAYED state
@@ -227,19 +244,21 @@ class PluginEntrypoint:
227
244
  inbound_sync_request.update_activity("Invoking plugin")
228
245
  logger.info(f"inbound sync request: {inbound_sync_request}")
229
246
  # plugin_instance._inbound_sync_request = outbound_sync_request
230
- with HttpRateLimiting(inbound_sync_request, parameters):
231
- self._plugin_instance.sync_inbound(parameters, inbound_sync_request)
247
+ with tracer.start_as_current_span("invoke_plugin"):
248
+ with HttpRateLimiting(inbound_sync_request, parameters):
249
+ self._plugin_instance.sync_inbound(parameters, inbound_sync_request)
232
250
  logger.info("Finished invoking plugin")
233
251
  if self._plugin_instance.disable_background_workers is False:
234
- inbound_sync_request.update_activity("Staging remaining records")
235
- logger.info("Calling apply_results_queue")
236
- inbound_sync_request.apply_results_queue()
237
- try:
238
- # this is not critical, we wouldn't fail the sync over rate limit usage capture
239
- logger.info("Calling apply_rate_limit_state")
240
- inbound_sync_request.apply_rate_limit_state()
241
- except Exception as e:
242
- logger.error(f"Error applying rate limit state: {str(e)}")
252
+ with tracer.start_as_current_span("results_finalization") as span:
253
+ inbound_sync_request.update_activity("Staging remaining records")
254
+ logger.info("Calling apply_results_queue")
255
+ inbound_sync_request.apply_results_queue()
256
+ try:
257
+ # this is not critical, we wouldn't fail the sync over rate limit usage capture
258
+ logger.info("Calling apply_rate_limit_state")
259
+ inbound_sync_request.apply_rate_limit_state()
260
+ except Exception as e:
261
+ logger.error(f"Error applying rate limit state: {str(e)}")
243
262
  # here we used to do a final inbound_sync_request.apply_progress_updates(ignore_errors=False)
244
263
  # but it was erroring too much since there was usually a lot of DDL activity on the Snowflake side
245
264
  # so instead, we'll provide a final progress update via a return value from the proc
@@ -283,6 +302,14 @@ class PluginEntrypoint:
283
302
  sync_parameters: Dict,
284
303
  current_form_parameters: Optional[Dict],
285
304
  ):
305
+ if function_name is None:
306
+ function_name = f"{sync_direction}_configuration_form"
307
+ logger.add_extra('omnata.operation', 'configuration_form')
308
+ logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
309
+ logger.add_extra('omnata.connection.connection_method', connection_method)
310
+ logger.add_extra('omnata.configuration_form.function_name', function_name)
311
+ logger.add_extra('omnata.sync.direction', sync_direction)
312
+
286
313
  logger.info("Entered configuration_form method")
287
314
  sync_strategy = normalise_nulls(sync_strategy)
288
315
  oauth_secret_name = normalise_nulls(oauth_secret_name)
@@ -322,9 +349,10 @@ class PluginEntrypoint:
322
349
  parameters.access_token_secret_name = oauth_secret_name
323
350
  the_function = getattr(
324
351
  self._plugin_instance,
325
- function_name or f"{sync_direction}_configuration_form",
352
+ function_name
326
353
  )
327
- script_result = the_function(parameters)
354
+ with tracer.start_as_current_span("invoke_plugin"):
355
+ script_result = the_function(parameters)
328
356
  if isinstance(script_result, BaseModel):
329
357
  script_result = script_result.model_dump()
330
358
  elif isinstance(script_result, List):
@@ -342,6 +370,10 @@ class PluginEntrypoint:
342
370
  sync_parameters: Dict,
343
371
  selected_streams: Optional[List[str]], # None to return all streams without requiring schema
344
372
  ):
373
+ logger.add_extra('omnata.operation', 'list_streams')
374
+ logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
375
+ logger.add_extra('omnata.connection.connection_method', connection_method)
376
+ logger.add_extra('omnata.sync.direction', 'inbound')
345
377
  logger.debug("Entered list_streams method")
346
378
  oauth_secret_name = normalise_nulls(oauth_secret_name)
347
379
  other_secrets_name = normalise_nulls(other_secrets_name)
@@ -362,8 +394,8 @@ class PluginEntrypoint:
362
394
  )
363
395
  if oauth_secret_name is not None:
364
396
  parameters.access_token_secret_name = oauth_secret_name
365
-
366
- script_result = self._plugin_instance.inbound_stream_list(parameters)
397
+ with tracer.start_as_current_span("invoke_plugin"):
398
+ script_result = self._plugin_instance.inbound_stream_list(parameters)
367
399
  if isinstance(script_result, BaseModel):
368
400
  script_result = script_result.model_dump()
369
401
  elif isinstance(script_result, List):
@@ -393,20 +425,25 @@ class PluginEntrypoint:
393
425
  return results
394
426
 
395
427
  def connection_form(self,connectivity_option: str):
428
+ logger.add_extra('omnata.operation', 'connection_form')
429
+ logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
396
430
  connectivity_option = TypeAdapter(ConnectivityOption).validate_python(connectivity_option)
397
431
  logger.info("Entered connection_form method")
398
- if self._plugin_instance.connection_form.connection_form.__code__.co_argcount==1:
399
- form: List[ConnectionMethod] = self._plugin_instance.connection_form()
400
- else:
401
- form: List[ConnectionMethod] = self._plugin_instance.connection_form(connectivity_option)
432
+ with tracer.start_as_current_span("invoke_plugin"):
433
+ if self._plugin_instance.connection_form.__code__.co_argcount==1:
434
+ form: List[ConnectionMethod] = self._plugin_instance.connection_form()
435
+ else:
436
+ form: List[ConnectionMethod] = self._plugin_instance.connection_form(connectivity_option)
402
437
  return [f.model_dump() for f in form]
403
438
 
404
439
  def create_billing_events(self, session, event_request: Dict):
440
+ logger.add_extra('omnata.operation', 'create_billing_events')
405
441
  logger.info("Entered create_billing_events method")
406
442
  request = TypeAdapter(BillingEventRequest).validate_python(event_request)
407
- events: List[SnowflakeBillingEvent] = self._plugin_instance.create_billing_events(
408
- request
409
- )
443
+ with tracer.start_as_current_span("invoke_plugin"):
444
+ events: List[SnowflakeBillingEvent] = self._plugin_instance.create_billing_events(
445
+ request
446
+ )
410
447
  # create each billing event, waiting a second between each one
411
448
  first_time = True
412
449
  for billing_event in events:
@@ -474,6 +511,9 @@ class PluginEntrypoint:
474
511
  oauth_secret_name: Optional[str],
475
512
  other_secrets_name: Optional[str],
476
513
  ):
514
+ logger.add_extra('omnata.operation', 'connection_test')
515
+ logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
516
+ logger.add_extra('omnata.connection.connection_method', method)
477
517
  logger.info("Entered connect method")
478
518
  logger.info(f"Connection parameters: {connection_parameters}")
479
519
  connection_secrets = get_secrets(oauth_secret_name, other_secrets_name)
@@ -490,31 +530,35 @@ class PluginEntrypoint:
490
530
  )
491
531
  if oauth_secret_name is not None:
492
532
  parameters.access_token_secret_name = oauth_secret_name
493
- connect_response = self._plugin_instance.connect(
494
- parameters=parameters
495
- )
533
+ with tracer.start_as_current_span("invoke_plugin"):
534
+ connect_response = self._plugin_instance.connect(
535
+ parameters=parameters
536
+ )
496
537
  # the connect method can also return more network addresses. If so, we need to update the
497
538
  # network rule associated with the external access integration
498
539
  if connect_response is None:
499
540
  raise ValueError("Plugin did not return a ConnectResponse object from the connect method")
500
541
  if connect_response.network_addresses is not None:
501
- existing_rule_result = self._session.sql(
502
- f"desc network rule {network_rule_name}"
503
- ).collect()
504
- rule_values: List[str] = existing_rule_result[0].value_list.split(",")
505
- rule_values = [r for r in rule_values if r != '']
506
- logger.info(f"Existing rules for {network_rule_name}: {rule_values}")
507
- for network_address in connect_response.network_addresses:
508
- if network_address not in rule_values:
509
- rule_values.append(network_address)
510
- #if len(rule_values)==0:
511
- # logger.info("No network addresses for plugin, adding localhost")
512
- # rule_values.append("https://localhost")
513
- logger.info(f"New rules for {network_rule_name}: {rule_values}")
514
- rule_values_string = ",".join([f"'{value}'" for value in rule_values])
515
- self._session.sql(
516
- f"alter network rule {network_rule_name} set value_list = ({rule_values_string})"
517
- ).collect()
542
+ with tracer.start_as_current_span("network_rule_update") as network_rule_update_span:
543
+ network_rule_update_span.add_event("Retrieving existing network rule")
544
+ existing_rule_result = self._session.sql(
545
+ f"desc network rule {network_rule_name}"
546
+ ).collect()
547
+ rule_values: List[str] = existing_rule_result[0].value_list.split(",")
548
+ rule_values = [r for r in rule_values if r != '']
549
+ logger.info(f"Existing rules for {network_rule_name}: {rule_values}")
550
+ for network_address in connect_response.network_addresses:
551
+ if network_address not in rule_values:
552
+ rule_values.append(network_address)
553
+ #if len(rule_values)==0:
554
+ # logger.info("No network addresses for plugin, adding localhost")
555
+ # rule_values.append("https://localhost")
556
+ logger.info(f"New rules for {network_rule_name}: {rule_values}")
557
+ rule_values_string = ",".join([f"'{value}'" for value in rule_values])
558
+ network_rule_update_span.add_event("Updating network rule")
559
+ self._session.sql(
560
+ f"alter network rule {network_rule_name} set value_list = ({rule_values_string})"
561
+ ).collect()
518
562
 
519
563
  return connect_response.model_dump()
520
564
 
@@ -524,6 +568,9 @@ class PluginEntrypoint:
524
568
  connection_parameters: Dict,
525
569
  oauth_secret_name: Optional[str],
526
570
  other_secrets_name: Optional[str]):
571
+ logger.add_extra('omnata.operation', 'api_limits')
572
+ logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
573
+ logger.add_extra('omnata.connection.connection_method', method)
527
574
  logger.info("Entered api_limits method")
528
575
  connection_secrets = get_secrets(oauth_secret_name, other_secrets_name)
529
576
  from omnata_plugin_runtime.omnata_plugin import (
@@ -538,7 +585,8 @@ class PluginEntrypoint:
538
585
  )
539
586
  if oauth_secret_name is not None:
540
587
  connection_parameters.access_token_secret_name = oauth_secret_name
541
- response: List[ApiLimits] = self._plugin_instance.api_limits(connection_parameters)
588
+ with tracer.start_as_current_span("invoke_plugin"):
589
+ response: List[ApiLimits] = self._plugin_instance.api_limits(connection_parameters)
542
590
  return [api_limit.model_dump() for api_limit in response]
543
591
 
544
592
  def outbound_record_validator(
@@ -15,12 +15,11 @@ import logging
15
15
  from pydantic import Field, root_validator, PrivateAttr, field_serializer
16
16
  from pydantic_core import to_jsonable_python
17
17
  from .configuration import SubscriptableBaseModel
18
+ from .logging import logger, tracer
18
19
  import pytz
19
20
  from requests.adapters import HTTPAdapter
20
21
  from urllib3.util.retry import Retry
21
22
 
22
- logger = getLogger(__name__)
23
-
24
23
  TimeUnitType = Literal["second", "minute", "hour", "day"]
25
24
 
26
25
  HttpMethodType = Literal[
@@ -384,11 +383,12 @@ class RetryWithLogging(Retry):
384
383
  retry_after = self.get_retry_after(response)
385
384
  if retry_after:
386
385
  logger.info(f"Retrying after {retry_after} seconds due to Retry-After header")
387
- if self.thread_cancellation_token is None:
388
- time.sleep(retry_after)
389
- else:
390
- if self.thread_cancellation_token.wait(retry_after):
391
- raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting to expire")
386
+ with tracer.start_as_current_span("http_retry_wait"):
387
+ if self.thread_cancellation_token is None:
388
+ time.sleep(retry_after)
389
+ else:
390
+ if self.thread_cancellation_token.wait(retry_after):
391
+ raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting to expire")
392
392
  return True
393
393
  return False
394
394
 
@@ -505,8 +505,9 @@ class RateLimitedSession(requests.Session):
505
505
  raise InterruptedWhileWaitingException(message=f"The rate limiting wait time ({wait_time} seconds) would exceed the run deadline")
506
506
  logger.info(f"Waiting for {wait_time} seconds before retrying {method} request to {url}")
507
507
  # if wait() returns true, it means that the thread was cancelled
508
- if self.thread_cancellation_token.wait(wait_time):
509
- raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting to expire")
508
+ with tracer.start_as_current_span("http_retry_wait"):
509
+ if self.thread_cancellation_token.wait(wait_time):
510
+ raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting to expire")
510
511
  else:
511
512
  current_url_retries = self.increment_retries(url)
512
513
  if current_url_retries >= self.max_retries:
@@ -515,8 +516,9 @@ class RateLimitedSession(requests.Session):
515
516
  if datetime.datetime.now(pytz.UTC) + datetime.timedelta(seconds=backoff_time) > self.run_deadline:
516
517
  raise InterruptedWhileWaitingException(message=f"The rate limiting backoff time ({backoff_time} seconds) would exceed the run deadline")
517
518
  logger.info(f"Waiting for {backoff_time} seconds before retrying {method} request to {url}")
518
- if self.thread_cancellation_token.wait(backoff_time):
519
- raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting backoff")
519
+ with tracer.start_as_current_span("http_retry_wait"):
520
+ if self.thread_cancellation_token.wait(backoff_time):
521
+ raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting backoff")
520
522
  else:
521
523
  self.set_retries(url,0) # Reset retries if the request is successful
522
524
  return response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omnata-plugin-runtime
3
- Version: 0.7.0a184
3
+ Version: 0.8.0
4
4
  Summary: Classes and common runtime components for building and running Omnata Plugins
5
5
  Author: James Weakley
6
6
  Author-email: james.weakley@omnata.com
@@ -19,9 +19,11 @@ Requires-Dist: idna (<=3.7)
19
19
  Requires-Dist: jinja2 (>=3.1.2,<=3.1.4)
20
20
  Requires-Dist: markupsafe (<=2.1.3)
21
21
  Requires-Dist: numpy (<1.27.0)
22
+ Requires-Dist: opentelemetry-api (<=1.23.0)
22
23
  Requires-Dist: packaging (<=24.1)
23
24
  Requires-Dist: pandas (<=2.2.2)
24
25
  Requires-Dist: platformdirs (<=3.10.0)
26
+ Requires-Dist: protobuf (<=4.25.3)
25
27
  Requires-Dist: pyarrow (<=16.1.0)
26
28
  Requires-Dist: pycparser (<=2.21)
27
29
  Requires-Dist: pydantic (>=2,<=2.8.2)
@@ -34,10 +36,12 @@ Requires-Dist: requests (>=2,<=2.32.3)
34
36
  Requires-Dist: setuptools (<=72.1.0)
35
37
  Requires-Dist: snowflake-connector-python (>=3,<=3.12.0)
36
38
  Requires-Dist: snowflake-snowpark-python (==1.23.0)
39
+ Requires-Dist: snowflake-telemetry-python (<=0.5.0)
37
40
  Requires-Dist: tenacity (>=8,<=8.2.3)
38
41
  Requires-Dist: tomlkit (<=0.11.1)
39
42
  Requires-Dist: urllib3 (<=2.2.2)
40
43
  Requires-Dist: wheel (<=0.43.0)
44
+ Requires-Dist: wrapt (<=1.14.1)
41
45
  Description-Content-Type: text/markdown
42
46
 
43
47
  # omnata-plugin-runtime
@@ -0,0 +1,12 @@
1
+ omnata_plugin_runtime/__init__.py,sha256=MS9d1whnfT_B3-ThqZ7l63QeC_8OEKTuaYV5wTwRpBA,1576
2
+ omnata_plugin_runtime/api.py,sha256=tVi4KLL0v5N3yz3Ie0kSyFemryu572gCbtSRfWN6wBU,6523
3
+ omnata_plugin_runtime/configuration.py,sha256=6JmgE4SL3F5cGlDYqt17A1vTFu6nB74yWgEpQ5qV9ho,38380
4
+ omnata_plugin_runtime/forms.py,sha256=ueodN2GIMS5N9fqebpY4uNGJnjEb9HcuaVQVfWH-cGg,19838
5
+ omnata_plugin_runtime/logging.py,sha256=WBuZt8lF9E5oFWM4KYQbE8dDJ_HctJ1pN3BHwU6rcd0,4461
6
+ omnata_plugin_runtime/omnata_plugin.py,sha256=GAvFRnx02_beTuw1LhPgOBxS_cTJmjDXc4EwVk69ZY8,131191
7
+ omnata_plugin_runtime/plugin_entrypoints.py,sha256=sB_h6OBEMk7lTLIjdNrNo9Sthk8UE9PnK2AUcQJPe9I,32728
8
+ omnata_plugin_runtime/rate_limiting.py,sha256=eOWVRYWiqPlVeYzmB1exVXfXbrcpmYb7vtTi9B-4zkQ,25868
9
+ omnata_plugin_runtime-0.8.0.dist-info/LICENSE,sha256=rGaMQG3R3F5-JGDp_-rlMKpDIkg5n0SI4kctTk8eZSI,56
10
+ omnata_plugin_runtime-0.8.0.dist-info/METADATA,sha256=ezmYz0AgrYsHNQlaR3k9uimL8ag-RpoWbZE6AnlxB1I,2144
11
+ omnata_plugin_runtime-0.8.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
12
+ omnata_plugin_runtime-0.8.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- omnata_plugin_runtime/__init__.py,sha256=MS9d1whnfT_B3-ThqZ7l63QeC_8OEKTuaYV5wTwRpBA,1576
2
- omnata_plugin_runtime/api.py,sha256=tVi4KLL0v5N3yz3Ie0kSyFemryu572gCbtSRfWN6wBU,6523
3
- omnata_plugin_runtime/configuration.py,sha256=Yyz3trj7G6nh3JyEw6S5qlrXUfS_MB-vZgATcNdWzp0,38339
4
- omnata_plugin_runtime/forms.py,sha256=ueodN2GIMS5N9fqebpY4uNGJnjEb9HcuaVQVfWH-cGg,19838
5
- omnata_plugin_runtime/logging.py,sha256=bn7eKoNWvtuyTk7RTwBS9UARMtqkiICtgMtzq3KA2V0,3272
6
- omnata_plugin_runtime/omnata_plugin.py,sha256=aggjb_CTTjhgqjS8CHPOm4ENU0jNcYoT6LC8yI1IeF4,130048
7
- omnata_plugin_runtime/plugin_entrypoints.py,sha256=Ulu4udC4tfCH-EA3VGXAT_WKHw5yZ6_ulsL7SjAN0qo,28953
8
- omnata_plugin_runtime/rate_limiting.py,sha256=JukA0l7x7Klqz2b54mR-poP7NRxpUHgWSGp6h0B8u6Q,25612
9
- omnata_plugin_runtime-0.7.0a184.dist-info/LICENSE,sha256=rGaMQG3R3F5-JGDp_-rlMKpDIkg5n0SI4kctTk8eZSI,56
10
- omnata_plugin_runtime-0.7.0a184.dist-info/METADATA,sha256=CsI5s6i4uyWKk2g6aUHbSHq9WXkclnRVQL3rDQ7rY4s,1985
11
- omnata_plugin_runtime-0.7.0a184.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
12
- omnata_plugin_runtime-0.7.0a184.dist-info/RECORD,,