monocle-apptrace 0.3.1__py3-none-any.whl → 0.4.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.

Potentially problematic release.


This version of monocle-apptrace might be problematic. Click here for more details.

Files changed (38) hide show
  1. monocle_apptrace/exporters/aws/s3_exporter.py +3 -1
  2. monocle_apptrace/exporters/azure/blob_exporter.py +2 -2
  3. monocle_apptrace/exporters/base_exporter.py +10 -4
  4. monocle_apptrace/exporters/file_exporter.py +19 -4
  5. monocle_apptrace/exporters/monocle_exporters.py +3 -3
  6. monocle_apptrace/exporters/okahu/okahu_exporter.py +5 -2
  7. monocle_apptrace/instrumentation/common/constants.py +9 -5
  8. monocle_apptrace/instrumentation/common/instrumentor.py +24 -13
  9. monocle_apptrace/instrumentation/common/span_handler.py +79 -38
  10. monocle_apptrace/instrumentation/common/utils.py +90 -54
  11. monocle_apptrace/instrumentation/common/wrapper.py +193 -40
  12. monocle_apptrace/instrumentation/common/wrapper_method.py +13 -6
  13. monocle_apptrace/instrumentation/metamodel/aiohttp/__init__.py +0 -0
  14. monocle_apptrace/instrumentation/metamodel/aiohttp/_helper.py +66 -0
  15. monocle_apptrace/instrumentation/metamodel/aiohttp/entities/http.py +51 -0
  16. monocle_apptrace/instrumentation/metamodel/aiohttp/methods.py +13 -0
  17. monocle_apptrace/instrumentation/metamodel/flask/_helper.py +8 -3
  18. monocle_apptrace/instrumentation/metamodel/flask/entities/http.py +0 -1
  19. monocle_apptrace/instrumentation/metamodel/haystack/_helper.py +17 -4
  20. monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +1 -1
  21. monocle_apptrace/instrumentation/metamodel/haystack/methods.py +8 -1
  22. monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +1 -1
  23. monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +13 -9
  24. monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +1 -1
  25. monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +14 -0
  26. monocle_apptrace/instrumentation/metamodel/openai/_helper.py +26 -5
  27. monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +184 -26
  28. monocle_apptrace/instrumentation/metamodel/openai/methods.py +6 -6
  29. monocle_apptrace/instrumentation/metamodel/requests/_helper.py +1 -1
  30. monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +55 -5
  31. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/actionplanner_output_processor.py +13 -33
  32. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +24 -20
  33. monocle_apptrace/instrumentation/metamodel/teamsai/methods.py +54 -8
  34. {monocle_apptrace-0.3.1.dist-info → monocle_apptrace-0.4.0.dist-info}/METADATA +22 -18
  35. {monocle_apptrace-0.3.1.dist-info → monocle_apptrace-0.4.0.dist-info}/RECORD +38 -34
  36. {monocle_apptrace-0.3.1.dist-info → monocle_apptrace-0.4.0.dist-info}/WHEEL +0 -0
  37. {monocle_apptrace-0.3.1.dist-info → monocle_apptrace-0.4.0.dist-info}/licenses/LICENSE +0 -0
  38. {monocle_apptrace-0.3.1.dist-info → monocle_apptrace-0.4.0.dist-info}/licenses/NOTICE +0 -0
@@ -129,6 +129,8 @@ class S3SpanExporter(SpanExporterBase):
129
129
  # Serialize spans to JSON or any other format you prefer
130
130
  valid_json_list = []
131
131
  for span in spans:
132
+ if self.skip_export(span):
133
+ continue
132
134
  try:
133
135
  valid_json_list.append(span.to_json(indent=0).replace("\n", ""))
134
136
  except json.JSONDecodeError as e:
@@ -169,7 +171,7 @@ class S3SpanExporter(SpanExporterBase):
169
171
  Key=file_name,
170
172
  Body=span_data_batch
171
173
  )
172
- logger.info(f"Span batch uploaded to AWS S3 as {file_name}.")
174
+ logger.debug(f"Span batch uploaded to AWS S3 as {file_name}.")
173
175
 
174
176
  async def force_flush(self, timeout_millis: int = 30000) -> bool:
175
177
  await self.__export_spans() # Export any remaining spans in the queue
@@ -82,8 +82,8 @@ class AzureBlobSpanExporter(SpanExporterBase):
82
82
  # With Monocle,OpenTelemetry is always loaded. If the Azure trace package is installed, then it triggers the blob trace generation on every blob operation.
83
83
  # Thus, the Monocle span write ends up generating a blob span which again comes back to the exporter .. and would result in an infinite loop.
84
84
  # To avoid this, we check if the span has the Monocle SDK version attribute and skip it if it doesn't. That way the blob span genearted by Azure library are not exported.
85
- if not span.attributes.get(MONOCLE_SDK_VERSION):
86
- continue # TODO: All exporters to use same base class and check it there
85
+ if self.skip_export(span):
86
+ continue
87
87
  self.export_queue.append(span)
88
88
  if len(self.export_queue) >= self.max_batch_size:
89
89
  await self.__export_spans()
@@ -1,20 +1,21 @@
1
- import time
1
+ import time, os
2
2
  import random
3
3
  import logging
4
4
  from abc import ABC, abstractmethod
5
5
  from opentelemetry.sdk.trace import ReadableSpan
6
- from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
6
+ from opentelemetry.sdk.trace.export import SpanExportResult
7
+ from monocle_apptrace.instrumentation.common.constants import MONOCLE_SDK_VERSION
7
8
  from typing import Sequence
8
- import asyncio
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
12
  class SpanExporterBase(ABC):
13
- def __init__(self):
13
+ def __init__(self, export_monocle_only: bool = True):
14
14
  self.backoff_factor = 2
15
15
  self.max_retries = 10
16
16
  self.export_queue = []
17
17
  self.last_export_time = time.time()
18
+ self.export_monocle_only = export_monocle_only or os.environ.get("MONOCLE_EXPORTS_ONLY", True)
18
19
 
19
20
  @abstractmethod
20
21
  async def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
@@ -27,6 +28,11 @@ class SpanExporterBase(ABC):
27
28
  def shutdown(self) -> None:
28
29
  pass
29
30
 
31
+ def skip_export(self, span:ReadableSpan) -> bool:
32
+ if self.export_monocle_only and (not span.attributes.get(MONOCLE_SDK_VERSION)):
33
+ return True
34
+ return False
35
+
30
36
  @staticmethod
31
37
  def retry_with_backoff(retries=3, backoff_in_seconds=1, max_backoff_in_seconds=32, exceptions=(Exception,)):
32
38
  def decorator(func):
@@ -7,12 +7,13 @@ from typing import Optional, Callable, Sequence
7
7
  from opentelemetry.sdk.trace import ReadableSpan
8
8
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
9
9
  from opentelemetry.sdk.resources import SERVICE_NAME
10
+ from monocle_apptrace.exporters.base_exporter import SpanExporterBase
10
11
  from monocle_apptrace.exporters.exporter_processor import ExportTaskProcessor
11
12
 
12
13
  DEFAULT_FILE_PREFIX:str = "monocle_trace_"
13
14
  DEFAULT_TIME_FORMAT:str = "%Y-%m-%d_%H.%M.%S"
14
15
 
15
- class FileSpanExporter(SpanExporter):
16
+ class FileSpanExporter(SpanExporterBase):
16
17
  current_trace_id: int = None
17
18
  current_file_path: str = None
18
19
 
@@ -28,6 +29,7 @@ class FileSpanExporter(SpanExporter):
28
29
  + linesep,
29
30
  task_processor: Optional[ExportTaskProcessor] = None
30
31
  ):
32
+ super().__init__()
31
33
  self.out_handle:TextIOWrapper = None
32
34
  self.formatter = formatter
33
35
  self.service_name = service_name
@@ -39,21 +41,32 @@ class FileSpanExporter(SpanExporter):
39
41
  self.task_processor.start()
40
42
 
41
43
  def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
44
+ is_root_span = any(not span.parent for span in spans)
42
45
  if self.task_processor is not None and callable(getattr(self.task_processor, 'queue_task', None)):
43
46
  # Check if any span is a root span (no parent)
44
- is_root_span = any(not span.parent for span in spans)
45
47
  self.task_processor.queue_task(self._process_spans, spans, is_root_span)
46
48
  return SpanExportResult.SUCCESS
47
49
  else:
48
- return self._process_spans(spans)
50
+ return self._process_spans(spans, is_root_span=is_root_span)
49
51
 
50
52
  def _process_spans(self, spans: Sequence[ReadableSpan], is_root_span: bool = False) -> SpanExportResult:
53
+ first_span:bool = True
51
54
  for span in spans:
55
+ if self.skip_export(span):
56
+ continue
52
57
  if span.context.trace_id != self.current_trace_id:
53
58
  self.rotate_file(span.resource.attributes[SERVICE_NAME],
54
59
  span.context.trace_id)
60
+ first_span = True
61
+ if first_span:
62
+ first_span = False
63
+ else:
64
+ self.out_handle.write(",")
55
65
  self.out_handle.write(self.formatter(span))
56
- self.out_handle.flush()
66
+ if is_root_span:
67
+ self.reset_handle()
68
+ else:
69
+ self.out_handle.flush()
57
70
  return SpanExportResult.SUCCESS
58
71
 
59
72
  def rotate_file(self, trace_name:str, trace_id:int) -> None:
@@ -62,6 +75,7 @@ class FileSpanExporter(SpanExporter):
62
75
  self.file_prefix + trace_name + "_" + hex(trace_id) + "_"
63
76
  + datetime.now().strftime(self.time_format) + ".json")
64
77
  self.out_handle = open(self.current_file_path, "w", encoding='UTF-8')
78
+ self.out_handle.write("[")
65
79
  self.current_trace_id = trace_id
66
80
 
67
81
  def force_flush(self, timeout_millis: int = 30000) -> bool:
@@ -70,6 +84,7 @@ class FileSpanExporter(SpanExporter):
70
84
 
71
85
  def reset_handle(self) -> None:
72
86
  if self.out_handle is not None:
87
+ self.out_handle.write("]")
73
88
  self.out_handle.close()
74
89
  self.out_handle = None
75
90
 
@@ -1,6 +1,6 @@
1
1
  from typing import Dict, Any, List
2
2
  import os
3
- import logging
3
+ import logging, warnings
4
4
  from importlib import import_module
5
5
  from opentelemetry.sdk.trace.export import SpanExporter, ConsoleSpanExporter
6
6
  from monocle_apptrace.exporters.exporter_processor import LambdaExportTaskProcessor, is_aws_lambda_environment
@@ -34,7 +34,7 @@ def get_monocle_exporter(exporters_list:str=None) -> List[SpanExporter]:
34
34
  try:
35
35
  exporter_class_path = monocle_exporters[exporter_name]
36
36
  except KeyError:
37
- logger.debug(f"Unsupported Monocle span exporter '{exporter_name}', skipping.")
37
+ warnings.warn(f"Unsupported Monocle span exporter '{exporter_name}', skipping.")
38
38
  continue
39
39
  try:
40
40
  exporter_module = import_module(exporter_class_path["module"])
@@ -45,7 +45,7 @@ def get_monocle_exporter(exporters_list:str=None) -> List[SpanExporter]:
45
45
  else:
46
46
  exporters.append(exporter_class())
47
47
  except Exception as ex:
48
- logger.debug(
48
+ warnings.warn(
49
49
  f"Unable to initialize Monocle span exporter '{exporter_name}', error: {ex}. Using ConsoleSpanExporter as a fallback.")
50
50
  exporters.append(ConsoleSpanExporter())
51
51
  continue
@@ -6,7 +6,7 @@ import requests
6
6
  from opentelemetry.sdk.trace import ReadableSpan
7
7
  from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult, ConsoleSpanExporter
8
8
  from requests.exceptions import ReadTimeout
9
-
9
+ from monocle_apptrace.exporters.base_exporter import SpanExporterBase
10
10
  from monocle_apptrace.exporters.exporter_processor import ExportTaskProcessor
11
11
 
12
12
  REQUESTS_SUCCESS_STATUS_CODES = (200, 202)
@@ -15,7 +15,7 @@ OKAHU_PROD_INGEST_ENDPOINT = "https://ingest.okahu.co/api/v1/trace/ingest"
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
- class OkahuSpanExporter(SpanExporter):
18
+ class OkahuSpanExporter(SpanExporterBase):
19
19
  def __init__(
20
20
  self,
21
21
  endpoint: Optional[str] = None,
@@ -24,6 +24,7 @@ class OkahuSpanExporter(SpanExporter):
24
24
  task_processor: ExportTaskProcessor = None
25
25
  ):
26
26
  """Okahu exporter."""
27
+ super().__init__()
27
28
  okahu_endpoint: str = os.environ.get("OKAHU_INGESTION_ENDPOINT", OKAHU_PROD_INGEST_ENDPOINT)
28
29
  self.endpoint = endpoint or okahu_endpoint
29
30
  api_key: str = os.environ.get("OKAHU_API_KEY")
@@ -58,6 +59,8 @@ class OkahuSpanExporter(SpanExporter):
58
59
 
59
60
  # append the batch object with all the spans object
60
61
  for span in spans:
62
+ if self.skip_export(span):
63
+ continue
61
64
  # create a object from serialized span
62
65
  obj = json.loads(span.to_json())
63
66
  if obj["parent_id"] is None:
@@ -14,10 +14,10 @@ GITHUB_CODESPACE_IDENTIFIER_ENV_NAME = "GITHUB_REPOSITORY"
14
14
  # Azure naming reference can be found here
15
15
  # https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations
16
16
  # https://docs.aws.amazon.com/resource-explorer/latest/userguide/supported-resource-types.html#services-lookoutmetrics
17
- AZURE_FUNCTION_NAME = "azure.func"
18
- AZURE_APP_SERVICE_NAME = "azure.asp"
19
- AZURE_ML_SERVICE_NAME = "azure.mlw"
20
- AWS_LAMBDA_SERVICE_NAME = "aws.lambda"
17
+ AZURE_FUNCTION_NAME = "azure_func"
18
+ AZURE_APP_SERVICE_NAME = "azure_webapp"
19
+ AZURE_ML_SERVICE_NAME = "azure_ml"
20
+ AWS_LAMBDA_SERVICE_NAME = "aws_lambda"
21
21
  GITHUB_CODESPACE_SERVICE_NAME = "github_codespace"
22
22
 
23
23
  # Env variables to identify infra service type
@@ -51,7 +51,9 @@ llm_type_map = {
51
51
  "openaigenerator": "openai",
52
52
  "bedrockruntime":"aws_bedrock",
53
53
  "sagemakerruntime":"aws_sagemaker",
54
+ "anthropic": "anthropic",
54
55
  "chatanthropic":"anthropic",
56
+ "anthropicchatgenerator":"anthropic",
55
57
  }
56
58
 
57
59
  MONOCLE_INSTRUMENTOR = "monocle_apptrace"
@@ -74,4 +76,6 @@ WORKFLOW_TYPE_KEY = "monocle.workflow_type"
74
76
  ADD_NEW_WORKFLOW = "monocle.add_new_workflow"
75
77
  WORKFLOW_TYPE_GENERIC = "workflow.generic"
76
78
  MONOCLE_SDK_VERSION = "monocle_apptrace.version"
77
- MONOCLE_SDK_LANGUAGE = "monocle_apptrace.language"
79
+ MONOCLE_SDK_LANGUAGE = "monocle_apptrace.language"
80
+ MONOCLE_DETECTED_SPAN_ERROR = "monocle_apptrace.detected_span_error"
81
+ HTTP_SUCCESS_CODES = ('200', '201', '202', '204', '205', '206')
@@ -25,9 +25,9 @@ from monocle_apptrace.instrumentation.common.wrapper_method import (
25
25
  WrapperMethod,
26
26
  MONOCLE_SPAN_HANDLERS
27
27
  )
28
- from monocle_apptrace.instrumentation.common.wrapper import scope_wrapper, ascope_wrapper, wrapper_processor
28
+ from monocle_apptrace.instrumentation.common.wrapper import scope_wrapper, ascope_wrapper, monocle_wrapper, amonocle_wrapper
29
29
  from monocle_apptrace.instrumentation.common.utils import (
30
- set_scope, remove_scope, http_route_handler, load_scopes, async_wrapper, http_async_route_handler
30
+ set_scope, remove_scope, http_route_handler, load_scopes, http_async_route_handler
31
31
  )
32
32
  from monocle_apptrace.instrumentation.common.constants import MONOCLE_INSTRUMENTOR, WORKFLOW_TYPE_GENERIC
33
33
  from functools import wraps
@@ -68,13 +68,20 @@ class MonocleInstrumentor(BaseInstrumentor):
68
68
 
69
69
  def get_instrumentor(self, tracer):
70
70
  def instrumented_endpoint_invoke(to_wrap,wrapped, span_name, instance,fn):
71
- @wraps(fn)
72
- def with_instrumentation(*args, **kwargs):
73
- async_task = inspect.iscoroutinefunction(fn)
74
- boto_method_to_wrap = to_wrap.copy()
75
- boto_method_to_wrap['skip_span'] = False
76
- return wrapper_processor(async_task, tracer, NonFrameworkSpanHandler(),
77
- boto_method_to_wrap, fn, instance, args, kwargs)
71
+ if inspect.iscoroutinefunction(fn):
72
+ @wraps(fn)
73
+ async def with_instrumentation(*args, **kwargs):
74
+ boto_method_to_wrap = to_wrap.copy()
75
+ boto_method_to_wrap['skip_span'] = False
76
+ return await amonocle_wrapper(tracer, NonFrameworkSpanHandler(),
77
+ boto_method_to_wrap, fn, instance, "", args, kwargs)
78
+ else:
79
+ @wraps(fn)
80
+ def with_instrumentation(*args, **kwargs):
81
+ boto_method_to_wrap = to_wrap.copy()
82
+ boto_method_to_wrap['skip_span'] = False
83
+ return monocle_wrapper(tracer, NonFrameworkSpanHandler(),
84
+ boto_method_to_wrap, fn, instance, "", args, kwargs)
78
85
  return with_instrumentation
79
86
  return instrumented_endpoint_invoke
80
87
 
@@ -241,8 +248,8 @@ def start_trace():
241
248
  SpanHandler.set_workflow_properties(span)
242
249
  token = attach(updated_span_context)
243
250
  return token
244
- except:
245
- logger.warning("Failed to start trace")
251
+ except Exception as e:
252
+ logger.warning("Failed to start trace {e}")
246
253
  return None
247
254
 
248
255
  def stop_trace(token) -> None:
@@ -328,8 +335,12 @@ def monocle_trace_scope_method(scope_name: str, scope_value:str=None):
328
335
  if inspect.iscoroutinefunction(func):
329
336
  @wraps(func)
330
337
  async def wrapper(*args, **kwargs):
331
- result = async_wrapper(func, scope_name, scope_value, None, *args, **kwargs)
332
- return result
338
+ token = start_scope(scope_name, scope_value)
339
+ try:
340
+ result = await func(*args, **kwargs)
341
+ return result
342
+ finally:
343
+ stop_scope(token)
333
344
  return wrapper
334
345
  else:
335
346
  @wraps(func)
@@ -8,7 +8,7 @@ from monocle_apptrace.instrumentation.common.constants import (
8
8
  QUERY,
9
9
  service_name_map,
10
10
  service_type_map,
11
- MONOCLE_SDK_VERSION, MONOCLE_SDK_LANGUAGE
11
+ MONOCLE_SDK_VERSION, MONOCLE_SDK_LANGUAGE, MONOCLE_DETECTED_SPAN_ERROR
12
12
  )
13
13
  from monocle_apptrace.instrumentation.common.utils import set_attribute, get_scopes, MonocleSpanException, get_monocle_version
14
14
  from monocle_apptrace.instrumentation.common.constants import WORKFLOW_TYPE_KEY, WORKFLOW_TYPE_GENERIC
@@ -18,7 +18,8 @@ logger = logging.getLogger(__name__)
18
18
  WORKFLOW_TYPE_MAP = {
19
19
  "llama_index": "workflow.llamaindex",
20
20
  "langchain": "workflow.langchain",
21
- "haystack": "workflow.haystack"
21
+ "haystack": "workflow.haystack",
22
+ "teams.ai": "workflow.teams_ai",
22
23
  }
23
24
 
24
25
  class SpanHandler:
@@ -41,18 +42,28 @@ class SpanHandler:
41
42
  def skip_span(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
42
43
  return False
43
44
 
44
- def skip_processor(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
45
- return False
45
+ def skip_processor(self, to_wrap, wrapped, instance, span, args, kwargs) -> list[str]:
46
+ return []
47
+
48
+ def set_span_type(self, to_wrap, wrapped, instance, output_processor, span:Span, args, kwargs) -> str:
49
+ span_type:str = None
50
+ if 'type' in output_processor:
51
+ span_type = output_processor['type']
52
+ span.set_attribute("span.type", span_type)
53
+ else:
54
+ logger.warning("type of span not found or incorrect written in entity json")
55
+ return span_type
46
56
 
47
57
  def pre_task_processing(self, to_wrap, wrapped, instance, args,kwargs, span):
48
58
  if "pipeline" in to_wrap['package']:
49
59
  set_attribute(QUERY, args[0]['prompt_builder']['question'])
50
60
 
51
61
  @staticmethod
52
- def set_default_monocle_attributes(span: Span):
62
+ def set_default_monocle_attributes(span: Span, source_path = "" ):
53
63
  """ Set default monocle attributes for all spans """
54
64
  span.set_attribute(MONOCLE_SDK_VERSION, get_monocle_version())
55
65
  span.set_attribute(MONOCLE_SDK_LANGUAGE, "python")
66
+ span.set_attribute("span_source", source_path)
56
67
  for scope_key, scope_value in get_scopes().items():
57
68
  span.set_attribute(f"scope.{scope_key}", scope_value)
58
69
 
@@ -62,34 +73,37 @@ class SpanHandler:
62
73
  SpanHandler.set_workflow_attributes(to_wrap, span)
63
74
  SpanHandler.set_app_hosting_identifier_attribute(span)
64
75
 
65
- span.set_status(StatusCode.OK)
66
-
67
76
  @staticmethod
68
77
  def set_non_workflow_properties(span: Span, to_wrap = None):
69
78
  workflow_name = SpanHandler.get_workflow_name(span=span)
70
79
  if workflow_name:
71
80
  span.set_attribute("workflow.name", workflow_name)
81
+ span.set_attribute("span.type", "generic")
72
82
 
73
83
  def post_task_processing(self, to_wrap, wrapped, instance, args, kwargs, result, span:Span):
74
- if span.status.status_code == StatusCode.UNSET:
75
- span.set_status(StatusCode.OK)
84
+ pass
76
85
 
77
- def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span):
78
- self.hydrate_attributes(to_wrap, wrapped, instance, args, kwargs, result, span)
79
- self.hydrate_events(to_wrap, wrapped, instance, args, kwargs, result, span)
86
+ def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span, parent_span = None, ex:Exception = None) -> bool:
87
+ try:
88
+ detected_error_in_attribute = self.hydrate_attributes(to_wrap, wrapped, instance, args, kwargs, result, span)
89
+ detected_error_in_event = self.hydrate_events(to_wrap, wrapped, instance, args, kwargs, result, span, parent_span, ex)
90
+ if detected_error_in_attribute or detected_error_in_event:
91
+ span.set_attribute(MONOCLE_DETECTED_SPAN_ERROR, True)
92
+ finally:
93
+ if span.status.status_code == StatusCode.UNSET and ex is None:
94
+ span.set_status(StatusCode.OK)
80
95
 
81
- def hydrate_attributes(self, to_wrap, wrapped, instance, args, kwargs, result, span):
96
+ def hydrate_attributes(self, to_wrap, wrapped, instance, args, kwargs, result, span:Span) -> bool:
97
+ detected_error:bool = False
82
98
  span_index = 0
83
99
  if SpanHandler.is_root_span(span):
84
100
  span_index = 2 # root span will have workflow and hosting entities pre-populated
85
- if not self.skip_processor(to_wrap, wrapped, instance, args, kwargs) and (
86
- 'output_processor' in to_wrap and to_wrap["output_processor"] is not None):
101
+ if 'output_processor' in to_wrap and to_wrap["output_processor"] is not None:
87
102
  output_processor=to_wrap['output_processor']
88
- if 'type' in output_processor:
89
- span.set_attribute("span.type", output_processor['type'])
90
- else:
91
- logger.warning("type of span not found or incorrect written in entity json")
92
- if 'attributes' in output_processor:
103
+ self.set_span_type(to_wrap, wrapped, instance, output_processor, span, args, kwargs)
104
+ skip_processors:list[str] = self.skip_processor(to_wrap, wrapped, instance, span, args, kwargs) or []
105
+
106
+ if 'attributes' in output_processor and 'attributes' not in skip_processors:
93
107
  for processors in output_processor["attributes"]:
94
108
  for processor in processors:
95
109
  attribute = processor.get('attribute')
@@ -104,15 +118,12 @@ class SpanHandler:
104
118
  span.set_attribute(attribute_name, result)
105
119
  except MonocleSpanException as e:
106
120
  span.set_status(StatusCode.ERROR, e.message)
121
+ detected_error = True
107
122
  except Exception as e:
108
123
  logger.debug(f"Error processing accessor: {e}")
109
124
  else:
110
125
  logger.debug(f"{' and '.join([key for key in ['attribute', 'accessor'] if not processor.get(key)])} not found or incorrect in entity JSON")
111
126
  span_index += 1
112
- else:
113
- logger.debug("attributes not found or incorrect written in entity json")
114
- else:
115
- span.set_attribute("span.type", "generic")
116
127
 
117
128
  # set scopes as attributes by calling get_scopes()
118
129
  # scopes is a Mapping[str:object], iterate directly with .items()
@@ -121,17 +132,23 @@ class SpanHandler:
121
132
 
122
133
  if span_index > 0:
123
134
  span.set_attribute("entity.count", span_index)
135
+ return detected_error
124
136
 
125
-
126
- def hydrate_events(self, to_wrap, wrapped, instance, args, kwargs, result, span):
127
- if not self.skip_processor(to_wrap, wrapped, instance, args, kwargs) and (
128
- 'output_processor' in to_wrap and to_wrap["output_processor"] is not None):
137
+ def hydrate_events(self, to_wrap, wrapped, instance, args, kwargs, ret_result, span, parent_span=None, ex:Exception=None) -> bool:
138
+ detected_error:bool = False
139
+ if 'output_processor' in to_wrap and to_wrap["output_processor"] is not None:
129
140
  output_processor=to_wrap['output_processor']
130
- arguments = {"instance": instance, "args": args, "kwargs": kwargs, "result": result}
131
- if 'events' in output_processor:
141
+ skip_processors:list[str] = self.skip_processor(to_wrap, wrapped, instance, span, args, kwargs) or []
142
+
143
+ arguments = {"instance": instance, "args": args, "kwargs": kwargs, "result": ret_result, "exception":ex}
144
+ # Process events if they are defined in the output_processor.
145
+ # In case of inference.modelapi skip the event processing unless the span has an exception
146
+ if 'events' in output_processor and ('events' not in skip_processors or ex is not None):
132
147
  events = output_processor['events']
133
148
  for event in events:
134
149
  event_name = event.get("name")
150
+ if 'events.'+event_name in skip_processors and ex is None:
151
+ continue
135
152
  event_attributes = {}
136
153
  attributes = event.get("attributes", [])
137
154
  for attribute in attributes:
@@ -139,15 +156,25 @@ class SpanHandler:
139
156
  accessor = attribute.get("accessor")
140
157
  if accessor:
141
158
  try:
142
- if attribute_key is not None:
143
- event_attributes[attribute_key] = accessor(arguments)
144
- else:
145
- event_attributes.update(accessor(arguments))
159
+ result = accessor(arguments)
160
+ if result and isinstance(result, dict):
161
+ result = dict((key, value) for key, value in result.items() if value is not None)
162
+ if result and isinstance(result, (str, list, dict)):
163
+ if attribute_key is not None:
164
+ event_attributes[attribute_key] = result
165
+ else:
166
+ event_attributes.update(result)
146
167
  except MonocleSpanException as e:
147
168
  span.set_status(StatusCode.ERROR, e.message)
169
+ detected_error = True
148
170
  except Exception as e:
149
171
  logger.debug(f"Error evaluating accessor for attribute '{attribute_key}': {e}")
150
- span.add_event(name=event_name, attributes=event_attributes)
172
+ matching_timestamp = getattr(ret_result, "timestamps", {}).get(event_name, None)
173
+ if isinstance(matching_timestamp, int):
174
+ span.add_event(name=event_name, attributes=event_attributes, timestamp=matching_timestamp)
175
+ else:
176
+ span.add_event(name=event_name, attributes=event_attributes)
177
+ return detected_error
151
178
 
152
179
  @staticmethod
153
180
  def set_workflow_attributes(to_wrap, span: Span):
@@ -216,7 +243,7 @@ class SpanHandler:
216
243
 
217
244
  @staticmethod
218
245
  @contextmanager
219
- def workflow_type(to_wrap=None):
246
+ def workflow_type(to_wrap=None, span:Span=None):
220
247
  token = SpanHandler.attach_workflow_type(to_wrap)
221
248
  try:
222
249
  yield
@@ -226,6 +253,20 @@ class SpanHandler:
226
253
 
227
254
  class NonFrameworkSpanHandler(SpanHandler):
228
255
 
256
+ def get_workflow_name_in_progress(self) -> str:
257
+ return get_value(WORKFLOW_TYPE_KEY)
258
+
259
+ def is_framework_span_in_progess(self) -> bool:
260
+ return self.get_workflow_name_in_progress() in WORKFLOW_TYPE_MAP.values()
261
+
229
262
  # If the language framework is being executed, then skip generating direct openAI attributes and events
230
- def skip_processor(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
231
- return get_value(WORKFLOW_TYPE_KEY) in WORKFLOW_TYPE_MAP.values()
263
+ def skip_processor(self, to_wrap, wrapped, instance, span, args, kwargs) -> list[str]:
264
+ if self.is_framework_span_in_progess():
265
+ return ["attributes", "events"]
266
+
267
+ def set_span_type(self, to_wrap, wrapped, instance, output_processor, span:Span, args, kwargs) -> str:
268
+ span_type = super().set_span_type(to_wrap, wrapped, instance, output_processor, span, args, kwargs)
269
+ if self.is_framework_span_in_progess() and span_type is not None:
270
+ span_type = span_type+".modelapi"
271
+ span.set_attribute("span.type", span_type)
272
+ return span_type