omnata-plugin-runtime 0.8.0a187__tar.gz → 0.8.0a189__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omnata-plugin-runtime
3
- Version: 0.8.0a187
3
+ Version: 0.8.0a189
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "omnata-plugin-runtime"
3
- version = "0.8.0-a187"
3
+ version = "0.8.0-a189"
4
4
  description = "Classes and common runtime components for building and running Omnata Plugins"
5
5
  authors = ["James Weakley <james.weakley@omnata.com>"]
6
6
  readme = "README.md"
@@ -19,7 +19,9 @@ if tuple(sys.version_info[:2]) >= (3, 9):
19
19
  else:
20
20
  # Python 3.8 and below
21
21
  from typing_extensions import Annotated
22
+ from opentelemetry import trace
22
23
 
24
+ tracer = trace.get_tracer(__name__)
23
25
 
24
26
  class MapperType(str, Enum):
25
27
  FIELD_MAPPING_SELECTOR = "field_mapping_selector"
@@ -871,7 +873,7 @@ InboundSyncStreamsConfiguration.model_rebuild()
871
873
  StoredFieldMappings.model_rebuild()
872
874
  OutboundSyncConfigurationParameters.model_rebuild()
873
875
 
874
-
876
+ @tracer.start_as_current_span("get_secrets")
875
877
  def get_secrets(oauth_secret_name: Optional[str], other_secrets_name: Optional[str]
876
878
  ) -> Dict[str, StoredConfigurationValue]:
877
879
  connection_secrets = {}
@@ -36,9 +36,10 @@ from .omnata_plugin import (
36
36
  )
37
37
  from pydantic import TypeAdapter
38
38
  from .rate_limiting import ApiLimits, RateLimitState
39
+ from opentelemetry import trace
39
40
 
40
41
  IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
41
-
42
+ tracer = trace.get_tracer(__name__)
42
43
 
43
44
  class PluginEntrypoint:
44
45
  """
@@ -50,46 +51,39 @@ class PluginEntrypoint:
50
51
  self, plugin_fqn: str, session: Session, module_name: str, class_name: str
51
52
  ):
52
53
  logger.info(f"Initialising plugin entrypoint for {plugin_fqn}")
53
- self._session = session
54
- import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
55
- sys.path.append(os.path.join(import_dir, "app.zip"))
56
- module = importlib.import_module(module_name)
57
- class_obj = getattr(module, class_name)
58
- self._plugin_instance: OmnataPlugin = class_obj()
59
- self._plugin_instance._session = session # pylint: disable=protected-access
60
- # logging defaults
61
- snowflake_logger = logging.getLogger("snowflake")
62
- snowflake_logger.setLevel(logging.WARN) # we don't want snowflake queries being logged by default
63
- # the sync engine can tell the plugin to override log level via a session variable
64
- if session is not None:
65
- try:
66
- v = session.sql("select getvariable('LOG_LEVEL_OVERRIDES')").collect()
67
- result = v[0][0]
68
- if result is not None:
69
- log_level_overrides:Dict[str,str] = json.loads(result)
70
- for logger_name,level in log_level_overrides.items():
71
- logger_override = logging.getLogger(logger_name)
72
- logger_override.setLevel(level)
73
- logger_override.propagate = False
74
- for handler in logger_override.handlers:
75
- handler.setLevel(level)
76
- except Exception as e:
77
- logger.error(f"Error setting log level overrides: {str(e)}")
54
+ with tracer.start_as_current_span("plugin_initialization") as span:
55
+ self._session = session
56
+ import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
57
+ span.add_event("Adding plugin zip to path")
58
+ sys.path.append(os.path.join(import_dir, "app.zip"))
59
+ span.add_event("Importing plugin module")
60
+ module = importlib.import_module(module_name)
61
+ class_obj = getattr(module, class_name)
62
+ self._plugin_instance: OmnataPlugin = class_obj()
63
+ self._plugin_instance._session = session # pylint: disable=protected-access
64
+ # logging defaults
65
+ snowflake_logger = logging.getLogger("snowflake")
66
+ snowflake_logger.setLevel(logging.WARN) # we don't want snowflake queries being logged by default
67
+ # the sync engine can tell the plugin to override log level via a session variable
68
+ if session is not None:
69
+ try:
70
+ span.add_event("Checking log level overrides")
71
+ v = session.sql("select getvariable('LOG_LEVEL_OVERRIDES')").collect()
72
+ result = v[0][0]
73
+ if result is not None:
74
+ log_level_overrides:Dict[str,str] = json.loads(result)
75
+ span.add_event("Applying log level overrides",log_level_overrides)
76
+ for logger_name,level in log_level_overrides.items():
77
+ logger_override = logging.getLogger(logger_name)
78
+ logger_override.setLevel(level)
79
+ logger_override.propagate = False
80
+ for handler in logger_override.handlers:
81
+ handler.setLevel(level)
82
+ except Exception as e:
83
+ logger.error(f"Error setting log level overrides: {str(e)}")
78
84
 
79
85
 
80
86
  def sync(self, sync_request: Dict):
81
- logger.info("Entered sync method")
82
- request = TypeAdapter(SyncRequestPayload).validate_python(sync_request)
83
- connection_secrets = get_secrets(
84
- request.oauth_secret_name, request.other_secrets_name
85
- )
86
- omnata_log_handler = OmnataPluginLogHandler(
87
- session=self._session,
88
- sync_id=request.sync_id,
89
- sync_branch_id=request.sync_branch_id,
90
- connection_id=request.connection_id,
91
- sync_run_id=request.run_id,
92
- )
93
87
  logger.add_extra('omnata.operation', 'sync')
94
88
  logger.add_extra('omnata.sync.id', request.sync_id)
95
89
  logger.add_extra('omnata.sync.direction', request.sync_direction)
@@ -97,50 +91,67 @@ class PluginEntrypoint:
97
91
  logger.add_extra('omnata.sync.run_id', request.run_id)
98
92
  logger.add_extra('omnata.sync_branch.id', request.sync_branch_id)
99
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")
100
97
 
101
- omnata_log_handler.register(
102
- request.logging_level, self._plugin_instance.additional_loggers()
103
- )
104
- # construct some connection parameters for the purpose of getting the api limits
105
- connection_parameters = ConnectionConfigurationParameters(
106
- connection_method=request.connection_method,
107
- connectivity_option=request.connectivity_option,
108
- connection_parameters=request.connection_parameters,
109
- connection_secrets=connection_secrets
110
- )
111
- if request.oauth_secret_name is not None:
112
- connection_parameters.access_token_secret_name = request.oauth_secret_name
113
- all_api_limits = self._plugin_instance.api_limits(connection_parameters)
114
- logger.info(
115
- f"Default API limits: {json.dumps(to_jsonable_python(all_api_limits))}"
116
- )
117
- all_api_limits_by_category = {
118
- api_limit.endpoint_category: api_limit for api_limit in all_api_limits
119
- }
120
- all_api_limits_by_category.update(
121
- {
122
- k: v
123
- for k, v in [
124
- (x.endpoint_category, x) for x in request.api_limit_overrides
125
- ]
98
+ request = TypeAdapter(SyncRequestPayload).validate_python(sync_request)
99
+ connection_secrets = get_secrets(
100
+ request.oauth_secret_name, request.other_secrets_name
101
+ )
102
+ span.add_event("Configuring log handler")
103
+ omnata_log_handler = OmnataPluginLogHandler(
104
+ session=self._session,
105
+ sync_id=request.sync_id,
106
+ sync_branch_id=request.sync_branch_id,
107
+ connection_id=request.connection_id,
108
+ sync_run_id=request.run_id,
109
+ )
110
+
111
+ omnata_log_handler.register(
112
+ request.logging_level, self._plugin_instance.additional_loggers()
113
+ )
114
+ # construct some connection parameters for the purpose of getting the api limits
115
+ connection_parameters = ConnectionConfigurationParameters(
116
+ connection_method=request.connection_method,
117
+ connectivity_option=request.connectivity_option,
118
+ connection_parameters=request.connection_parameters,
119
+ connection_secrets=connection_secrets
120
+ )
121
+ if request.oauth_secret_name is not None:
122
+ connection_parameters.access_token_secret_name = request.oauth_secret_name
123
+ span.add_event("Configuring API Limits")
124
+ all_api_limits = self._plugin_instance.api_limits(connection_parameters)
125
+ logger.info(
126
+ f"Default API limits: {json.dumps(to_jsonable_python(all_api_limits))}"
127
+ )
128
+ all_api_limits_by_category = {
129
+ api_limit.endpoint_category: api_limit for api_limit in all_api_limits
126
130
  }
127
- )
128
- api_limits = list(all_api_limits_by_category.values())
129
- return_dict = {}
130
- logger.info(
131
- f"Rate limits state: {json.dumps(to_jsonable_python(request.rate_limits_state))}"
132
- )
133
- (rate_limit_state_all, rate_limit_state_this_branch) = RateLimitState.collapse(request.rate_limits_state,request.sync_id, request.sync_branch_name)
134
- # if any endpoint categories have no state, give them an empty state
135
- for api_limit in api_limits:
136
- if api_limit.endpoint_category not in rate_limit_state_all:
137
- rate_limit_state_all[api_limit.endpoint_category] = RateLimitState(
138
- wait_until=None, previous_request_timestamps=[]
139
- )
140
- if api_limit.endpoint_category not in rate_limit_state_this_branch:
141
- rate_limit_state_this_branch[api_limit.endpoint_category] = RateLimitState(
142
- wait_until=None, previous_request_timestamps=[]
143
- )
131
+ all_api_limits_by_category.update(
132
+ {
133
+ k: v
134
+ for k, v in [
135
+ (x.endpoint_category, x) for x in request.api_limit_overrides
136
+ ]
137
+ }
138
+ )
139
+ api_limits = list(all_api_limits_by_category.values())
140
+ return_dict = {}
141
+ logger.info(
142
+ f"Rate limits state: {json.dumps(to_jsonable_python(request.rate_limits_state))}"
143
+ )
144
+ (rate_limit_state_all, rate_limit_state_this_branch) = RateLimitState.collapse(request.rate_limits_state,request.sync_id, request.sync_branch_name)
145
+ # if any endpoint categories have no state, give them an empty state
146
+ for api_limit in api_limits:
147
+ if api_limit.endpoint_category not in rate_limit_state_all:
148
+ rate_limit_state_all[api_limit.endpoint_category] = RateLimitState(
149
+ wait_until=None, previous_request_timestamps=[]
150
+ )
151
+ if api_limit.endpoint_category not in rate_limit_state_this_branch:
152
+ rate_limit_state_this_branch[api_limit.endpoint_category] = RateLimitState(
153
+ wait_until=None, previous_request_timestamps=[]
154
+ )
144
155
 
145
156
  if request.sync_direction == "outbound":
146
157
  parameters = OutboundSyncConfigurationParameters(
@@ -174,11 +185,13 @@ class PluginEntrypoint:
174
185
  )
175
186
  try:
176
187
  self._plugin_instance._configuration_parameters = parameters
177
- with HttpRateLimiting(outbound_sync_request, parameters):
178
- self._plugin_instance.sync_outbound(parameters, outbound_sync_request)
188
+ with tracer.start_as_current_span("invoke_plugin") as span:
189
+ with HttpRateLimiting(outbound_sync_request, parameters):
190
+ self._plugin_instance.sync_outbound(parameters, outbound_sync_request)
179
191
  if self._plugin_instance.disable_background_workers is False:
180
- outbound_sync_request.apply_results_queue()
181
- outbound_sync_request.apply_rate_limit_state()
192
+ with tracer.start_as_current_span("results_finalization") as span:
193
+ outbound_sync_request.apply_results_queue()
194
+ outbound_sync_request.apply_rate_limit_state()
182
195
  if outbound_sync_request.deadline_reached:
183
196
  # if we actually hit the deadline, this is flagged by the cancellation checking worker and the cancellation
184
197
  # 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
@@ -232,19 +245,21 @@ class PluginEntrypoint:
232
245
  inbound_sync_request.update_activity("Invoking plugin")
233
246
  logger.info(f"inbound sync request: {inbound_sync_request}")
234
247
  # plugin_instance._inbound_sync_request = outbound_sync_request
235
- with HttpRateLimiting(inbound_sync_request, parameters):
236
- self._plugin_instance.sync_inbound(parameters, inbound_sync_request)
248
+ with tracer.start_as_current_span("invoke_plugin"):
249
+ with HttpRateLimiting(inbound_sync_request, parameters):
250
+ self._plugin_instance.sync_inbound(parameters, inbound_sync_request)
237
251
  logger.info("Finished invoking plugin")
238
252
  if self._plugin_instance.disable_background_workers is False:
239
- inbound_sync_request.update_activity("Staging remaining records")
240
- logger.info("Calling apply_results_queue")
241
- inbound_sync_request.apply_results_queue()
242
- try:
243
- # this is not critical, we wouldn't fail the sync over rate limit usage capture
244
- logger.info("Calling apply_rate_limit_state")
245
- inbound_sync_request.apply_rate_limit_state()
246
- except Exception as e:
247
- logger.error(f"Error applying rate limit state: {str(e)}")
253
+ with tracer.start_as_current_span("results_finalization") as span:
254
+ inbound_sync_request.update_activity("Staging remaining records")
255
+ logger.info("Calling apply_results_queue")
256
+ inbound_sync_request.apply_results_queue()
257
+ try:
258
+ # this is not critical, we wouldn't fail the sync over rate limit usage capture
259
+ logger.info("Calling apply_rate_limit_state")
260
+ inbound_sync_request.apply_rate_limit_state()
261
+ except Exception as e:
262
+ logger.error(f"Error applying rate limit state: {str(e)}")
248
263
  # here we used to do a final inbound_sync_request.apply_progress_updates(ignore_errors=False)
249
264
  # but it was erroring too much since there was usually a lot of DDL activity on the Snowflake side
250
265
  # so instead, we'll provide a final progress update via a return value from the proc
@@ -288,6 +303,8 @@ class PluginEntrypoint:
288
303
  sync_parameters: Dict,
289
304
  current_form_parameters: Optional[Dict],
290
305
  ):
306
+ if function_name is None:
307
+ function_name = f"{sync_direction}_configuration_form"
291
308
  logger.add_extra('omnata.operation', 'configuration_form')
292
309
  logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
293
310
  logger.add_extra('omnata.connection.connection_method', connection_method)
@@ -333,9 +350,10 @@ class PluginEntrypoint:
333
350
  parameters.access_token_secret_name = oauth_secret_name
334
351
  the_function = getattr(
335
352
  self._plugin_instance,
336
- function_name or f"{sync_direction}_configuration_form",
353
+ function_name
337
354
  )
338
- script_result = the_function(parameters)
355
+ with tracer.start_as_current_span("invoke_plugin"):
356
+ script_result = the_function(parameters)
339
357
  if isinstance(script_result, BaseModel):
340
358
  script_result = script_result.model_dump()
341
359
  elif isinstance(script_result, List):
@@ -377,8 +395,8 @@ class PluginEntrypoint:
377
395
  )
378
396
  if oauth_secret_name is not None:
379
397
  parameters.access_token_secret_name = oauth_secret_name
380
-
381
- script_result = self._plugin_instance.inbound_stream_list(parameters)
398
+ with tracer.start_as_current_span("invoke_plugin"):
399
+ script_result = self._plugin_instance.inbound_stream_list(parameters)
382
400
  if isinstance(script_result, BaseModel):
383
401
  script_result = script_result.model_dump()
384
402
  elif isinstance(script_result, List):
@@ -412,19 +430,21 @@ class PluginEntrypoint:
412
430
  logger.add_extra('omnata.connection.connectivity_option', connectivity_option)
413
431
  connectivity_option = TypeAdapter(ConnectivityOption).validate_python(connectivity_option)
414
432
  logger.info("Entered connection_form method")
415
- if self._plugin_instance.connection_form.__code__.co_argcount==1:
416
- form: List[ConnectionMethod] = self._plugin_instance.connection_form()
417
- else:
418
- form: List[ConnectionMethod] = self._plugin_instance.connection_form(connectivity_option)
433
+ with tracer.start_as_current_span("invoke_plugin"):
434
+ if self._plugin_instance.connection_form.__code__.co_argcount==1:
435
+ form: List[ConnectionMethod] = self._plugin_instance.connection_form()
436
+ else:
437
+ form: List[ConnectionMethod] = self._plugin_instance.connection_form(connectivity_option)
419
438
  return [f.model_dump() for f in form]
420
439
 
421
440
  def create_billing_events(self, session, event_request: Dict):
422
441
  logger.add_extra('omnata.operation', 'create_billing_events')
423
442
  logger.info("Entered create_billing_events method")
424
443
  request = TypeAdapter(BillingEventRequest).validate_python(event_request)
425
- events: List[SnowflakeBillingEvent] = self._plugin_instance.create_billing_events(
426
- request
427
- )
444
+ with tracer.start_as_current_span("invoke_plugin"):
445
+ events: List[SnowflakeBillingEvent] = self._plugin_instance.create_billing_events(
446
+ request
447
+ )
428
448
  # create each billing event, waiting a second between each one
429
449
  first_time = True
430
450
  for billing_event in events:
@@ -511,31 +531,35 @@ class PluginEntrypoint:
511
531
  )
512
532
  if oauth_secret_name is not None:
513
533
  parameters.access_token_secret_name = oauth_secret_name
514
- connect_response = self._plugin_instance.connect(
515
- parameters=parameters
516
- )
534
+ with tracer.start_as_current_span("invoke_plugin"):
535
+ connect_response = self._plugin_instance.connect(
536
+ parameters=parameters
537
+ )
517
538
  # the connect method can also return more network addresses. If so, we need to update the
518
539
  # network rule associated with the external access integration
519
540
  if connect_response is None:
520
541
  raise ValueError("Plugin did not return a ConnectResponse object from the connect method")
521
542
  if connect_response.network_addresses is not None:
522
- existing_rule_result = self._session.sql(
523
- f"desc network rule {network_rule_name}"
524
- ).collect()
525
- rule_values: List[str] = existing_rule_result[0].value_list.split(",")
526
- rule_values = [r for r in rule_values if r != '']
527
- logger.info(f"Existing rules for {network_rule_name}: {rule_values}")
528
- for network_address in connect_response.network_addresses:
529
- if network_address not in rule_values:
530
- rule_values.append(network_address)
531
- #if len(rule_values)==0:
532
- # logger.info("No network addresses for plugin, adding localhost")
533
- # rule_values.append("https://localhost")
534
- logger.info(f"New rules for {network_rule_name}: {rule_values}")
535
- rule_values_string = ",".join([f"'{value}'" for value in rule_values])
536
- self._session.sql(
537
- f"alter network rule {network_rule_name} set value_list = ({rule_values_string})"
538
- ).collect()
543
+ with tracer.start_as_current_span("network_rule_update") as network_rule_update_span:
544
+ network_rule_update_span.add_event("Retrieving existing network rule")
545
+ existing_rule_result = self._session.sql(
546
+ f"desc network rule {network_rule_name}"
547
+ ).collect()
548
+ rule_values: List[str] = existing_rule_result[0].value_list.split(",")
549
+ rule_values = [r for r in rule_values if r != '']
550
+ logger.info(f"Existing rules for {network_rule_name}: {rule_values}")
551
+ for network_address in connect_response.network_addresses:
552
+ if network_address not in rule_values:
553
+ rule_values.append(network_address)
554
+ #if len(rule_values)==0:
555
+ # logger.info("No network addresses for plugin, adding localhost")
556
+ # rule_values.append("https://localhost")
557
+ logger.info(f"New rules for {network_rule_name}: {rule_values}")
558
+ rule_values_string = ",".join([f"'{value}'" for value in rule_values])
559
+ network_rule_update_span.add_event("Updating network rule")
560
+ self._session.sql(
561
+ f"alter network rule {network_rule_name} set value_list = ({rule_values_string})"
562
+ ).collect()
539
563
 
540
564
  return connect_response.model_dump()
541
565
 
@@ -562,7 +586,8 @@ class PluginEntrypoint:
562
586
  )
563
587
  if oauth_secret_name is not None:
564
588
  connection_parameters.access_token_secret_name = oauth_secret_name
565
- response: List[ApiLimits] = self._plugin_instance.api_limits(connection_parameters)
589
+ with tracer.start_as_current_span("invoke_plugin"):
590
+ response: List[ApiLimits] = self._plugin_instance.api_limits(connection_parameters)
566
591
  return [api_limit.model_dump() for api_limit in response]
567
592
 
568
593
  def outbound_record_validator(