omnata-plugin-runtime 0.7.0a184__py3-none-any.whl → 0.8.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,,