monocle-apptrace 0.3.1b1__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 (46) 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 +8 -5
  6. monocle_apptrace/exporters/okahu/okahu_exporter.py +5 -2
  7. monocle_apptrace/instrumentation/common/__init__.py +1 -1
  8. monocle_apptrace/instrumentation/common/constants.py +12 -5
  9. monocle_apptrace/instrumentation/common/instrumentor.py +44 -22
  10. monocle_apptrace/instrumentation/common/span_handler.py +102 -50
  11. monocle_apptrace/instrumentation/common/tracing.md +68 -0
  12. monocle_apptrace/instrumentation/common/utils.py +114 -63
  13. monocle_apptrace/instrumentation/common/wrapper.py +202 -47
  14. monocle_apptrace/instrumentation/common/wrapper_method.py +15 -7
  15. monocle_apptrace/instrumentation/metamodel/aiohttp/__init__.py +0 -0
  16. monocle_apptrace/instrumentation/metamodel/aiohttp/_helper.py +66 -0
  17. monocle_apptrace/instrumentation/metamodel/aiohttp/entities/http.py +51 -0
  18. monocle_apptrace/instrumentation/metamodel/aiohttp/methods.py +13 -0
  19. monocle_apptrace/instrumentation/metamodel/anthropic/methods.py +4 -2
  20. monocle_apptrace/instrumentation/metamodel/flask/_helper.py +50 -3
  21. monocle_apptrace/instrumentation/metamodel/flask/entities/http.py +48 -0
  22. monocle_apptrace/instrumentation/metamodel/flask/methods.py +10 -1
  23. monocle_apptrace/instrumentation/metamodel/haystack/_helper.py +17 -4
  24. monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +5 -2
  25. monocle_apptrace/instrumentation/metamodel/haystack/methods.py +8 -4
  26. monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +12 -4
  27. monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +1 -1
  28. monocle_apptrace/instrumentation/metamodel/langchain/methods.py +6 -14
  29. monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +13 -9
  30. monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +1 -1
  31. monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +16 -15
  32. monocle_apptrace/instrumentation/metamodel/openai/_helper.py +26 -5
  33. monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +184 -26
  34. monocle_apptrace/instrumentation/metamodel/openai/methods.py +6 -8
  35. monocle_apptrace/instrumentation/metamodel/requests/_helper.py +31 -0
  36. monocle_apptrace/instrumentation/metamodel/requests/entities/http.py +51 -0
  37. monocle_apptrace/instrumentation/metamodel/requests/methods.py +2 -1
  38. monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +55 -5
  39. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/actionplanner_output_processor.py +13 -33
  40. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +24 -20
  41. monocle_apptrace/instrumentation/metamodel/teamsai/methods.py +54 -8
  42. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0.dist-info}/METADATA +22 -18
  43. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0.dist-info}/RECORD +46 -39
  44. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0.dist-info}/WHEEL +0 -0
  45. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0.dist-info}/licenses/LICENSE +0 -0
  46. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import os
3
- from importlib.metadata import version
3
+ from contextlib import contextmanager
4
4
  from opentelemetry.context import get_value, set_value, attach, detach
5
5
  from opentelemetry.sdk.trace import Span
6
6
  from opentelemetry.trace.status import Status, StatusCode
@@ -8,9 +8,9 @@ from monocle_apptrace.instrumentation.common.constants import (
8
8
  QUERY,
9
9
  service_name_map,
10
10
  service_type_map,
11
- MONOCLE_SDK_VERSION
11
+ MONOCLE_SDK_VERSION, MONOCLE_SDK_LANGUAGE, MONOCLE_DETECTED_SPAN_ERROR
12
12
  )
13
- from monocle_apptrace.instrumentation.common.utils import set_attribute, get_scopes, MonocleSpanException
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
15
15
 
16
16
  logger = logging.getLogger(__name__)
@@ -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:
@@ -39,23 +40,30 @@ class SpanHandler:
39
40
  pass
40
41
 
41
42
  def skip_span(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
42
- # If this is a workflow span type and a workflow span is already generated, then skip generating this span
43
- if to_wrap.get('span_type') == "workflow" and self.is_workflow_span_active():
44
- return True
45
43
  return False
46
44
 
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
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
- try:
55
- sdk_version = version("monocle_apptrace")
56
- span.set_attribute(MONOCLE_SDK_VERSION, sdk_version)
57
- except Exception as e:
58
- logger.warning("Exception finding monocle-apptrace version.")
64
+ span.set_attribute(MONOCLE_SDK_VERSION, get_monocle_version())
65
+ span.set_attribute(MONOCLE_SDK_LANGUAGE, "python")
66
+ span.set_attribute("span_source", source_path)
59
67
  for scope_key, scope_value in get_scopes().items():
60
68
  span.set_attribute(f"scope.{scope_key}", scope_value)
61
69
 
@@ -64,28 +72,38 @@ class SpanHandler:
64
72
  """ Set attributes of workflow if this is a root span"""
65
73
  SpanHandler.set_workflow_attributes(to_wrap, span)
66
74
  SpanHandler.set_app_hosting_identifier_attribute(span)
67
- span.set_status(StatusCode.OK)
68
75
 
76
+ @staticmethod
77
+ def set_non_workflow_properties(span: Span, to_wrap = None):
78
+ workflow_name = SpanHandler.get_workflow_name(span=span)
79
+ if workflow_name:
80
+ span.set_attribute("workflow.name", workflow_name)
81
+ span.set_attribute("span.type", "generic")
69
82
 
70
83
  def post_task_processing(self, to_wrap, wrapped, instance, args, kwargs, result, span:Span):
71
- if span.status.status_code == StatusCode.UNSET:
72
- span.set_status(StatusCode.OK)
84
+ pass
73
85
 
74
- def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span):
75
- self.hydrate_attributes(to_wrap, wrapped, instance, args, kwargs, result, span)
76
- 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)
77
95
 
78
- 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
79
98
  span_index = 0
80
99
  if SpanHandler.is_root_span(span):
81
100
  span_index = 2 # root span will have workflow and hosting entities pre-populated
82
- if '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:
83
102
  output_processor=to_wrap['output_processor']
84
- if 'type' in output_processor:
85
- span.set_attribute("span.type", output_processor['type'])
86
- else:
87
- logger.warning("type of span not found or incorrect written in entity json")
88
- 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:
89
107
  for processors in output_processor["attributes"]:
90
108
  for processor in processors:
91
109
  attribute = processor.get('attribute')
@@ -100,13 +118,12 @@ class SpanHandler:
100
118
  span.set_attribute(attribute_name, result)
101
119
  except MonocleSpanException as e:
102
120
  span.set_status(StatusCode.ERROR, e.message)
121
+ detected_error = True
103
122
  except Exception as e:
104
123
  logger.debug(f"Error processing accessor: {e}")
105
124
  else:
106
125
  logger.debug(f"{' and '.join([key for key in ['attribute', 'accessor'] if not processor.get(key)])} not found or incorrect in entity JSON")
107
126
  span_index += 1
108
- else:
109
- logger.debug("attributes not found or incorrect written in entity json")
110
127
 
111
128
  # set scopes as attributes by calling get_scopes()
112
129
  # scopes is a Mapping[str:object], iterate directly with .items()
@@ -115,16 +132,23 @@ class SpanHandler:
115
132
 
116
133
  if span_index > 0:
117
134
  span.set_attribute("entity.count", span_index)
135
+ return detected_error
118
136
 
119
-
120
- def hydrate_events(self, to_wrap, wrapped, instance, args, kwargs, result, span):
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
121
139
  if 'output_processor' in to_wrap and to_wrap["output_processor"] is not None:
122
140
  output_processor=to_wrap['output_processor']
123
- arguments = {"instance": instance, "args": args, "kwargs": kwargs, "result": result}
124
- 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):
125
147
  events = output_processor['events']
126
148
  for event in events:
127
149
  event_name = event.get("name")
150
+ if 'events.'+event_name in skip_processors and ex is None:
151
+ continue
128
152
  event_attributes = {}
129
153
  attributes = event.get("attributes", [])
130
154
  for attribute in attributes:
@@ -132,21 +156,32 @@ class SpanHandler:
132
156
  accessor = attribute.get("accessor")
133
157
  if accessor:
134
158
  try:
135
- if attribute_key is not None:
136
- event_attributes[attribute_key] = accessor(arguments)
137
- else:
138
- 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)
139
167
  except MonocleSpanException as e:
140
168
  span.set_status(StatusCode.ERROR, e.message)
169
+ detected_error = True
141
170
  except Exception as e:
142
171
  logger.debug(f"Error evaluating accessor for attribute '{attribute_key}': {e}")
143
- 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
144
178
 
145
179
  @staticmethod
146
180
  def set_workflow_attributes(to_wrap, span: Span):
147
181
  span_index = 1
148
182
  workflow_name = SpanHandler.get_workflow_name(span=span)
149
183
  if workflow_name:
184
+ span.update_name("workflow")
150
185
  span.set_attribute("span.type", "workflow")
151
186
  span.set_attribute(f"entity.{span_index}.name", workflow_name)
152
187
  workflow_type = SpanHandler.get_workflow_type(to_wrap)
@@ -186,26 +221,19 @@ class SpanHandler:
186
221
  @staticmethod
187
222
  def is_root_span(curr_span: Span) -> bool:
188
223
  try:
189
- if curr_span is not None and hasattr(curr_span, "parent"):
224
+ if curr_span is not None and hasattr(curr_span, "parent") or curr_span.context.trace_state:
190
225
  return curr_span.parent is None
191
226
  except Exception as e:
192
227
  logger.warning(f"Error finding root span: {e}")
193
228
 
194
- def is_non_workflow_root_span(self, curr_span: Span, to_wrap) -> bool:
195
- return SpanHandler.is_root_span(curr_span) and to_wrap.get("span_type") != "workflow"
196
-
197
- def is_workflow_span_active(self):
198
- return get_value(WORKFLOW_TYPE_KEY) is not None
199
-
200
229
  @staticmethod
201
230
  def attach_workflow_type(to_wrap=None, context=None):
202
231
  token = None
203
232
  if to_wrap:
204
- if to_wrap.get('span_type') == "workflow":
233
+ workflow_type = SpanHandler.get_workflow_type(to_wrap)
234
+ if workflow_type != WORKFLOW_TYPE_GENERIC:
205
235
  token = attach(set_value(WORKFLOW_TYPE_KEY,
206
236
  SpanHandler.get_workflow_type(to_wrap), context))
207
- else:
208
- token = attach(set_value(WORKFLOW_TYPE_KEY, WORKFLOW_TYPE_GENERIC, context))
209
237
  return token
210
238
 
211
239
  @staticmethod
@@ -213,8 +241,32 @@ class SpanHandler:
213
241
  if token:
214
242
  return detach(token)
215
243
 
244
+ @staticmethod
245
+ @contextmanager
246
+ def workflow_type(to_wrap=None, span:Span=None):
247
+ token = SpanHandler.attach_workflow_type(to_wrap)
248
+ try:
249
+ yield
250
+ finally:
251
+ SpanHandler.detach_workflow_type(token)
252
+
253
+
216
254
  class NonFrameworkSpanHandler(SpanHandler):
217
255
 
218
- # If the language framework is being executed, then skip generating direct openAI spans
219
- def skip_span(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
220
- return get_value(WORKFLOW_TYPE_KEY) in WORKFLOW_TYPE_MAP.values()
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
+
262
+ # If the language framework is being executed, then skip generating direct openAI attributes and events
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
@@ -0,0 +1,68 @@
1
+ # Monocle tracing: concepts and principles
2
+
3
+ ## Span
4
+ Span is an observation of a code/method executed. Each span has a unique ID. It records the start time and end time of the code's execution along with additional information relevant to that operation. Before the code execution starts, a span object is created in the memory of the host process executing this code. It'll capture the current time as start of time of span. At this stage the span is considered active. It'll stay active till the code execution ends. Once the code execution is complete, it'll record the current time as end time, capture any additional relevant information (eg argument, return value, environment setttings etc.). Now the span is marked as closed and it will be queued to be saved to some configured storage.
5
+ Note that the code that generated this span could in turn call other methods that are also instrumented. Those will generate spans of their own. These will be "child" spans which will refer to the span ID of the calling code as "parent" span. An initial span which has no parent is referred as "root" span.
6
+
7
+ ## Trace
8
+ A trace is a collection of spans with a common ID called traceID. When the first active span gets created, a new unique traceID is generated and assigned to that span. All the child spans generated by execution of other instrumented code/methods will share the same traceID. Once this top span ends, this trace ends. This way all the code executed as part of the top level instrumented code will have a common traceID to group them together. For example, consider following sequence where `f1()` is the first instrumented method is executed, it calls other instrumented methods `f2(),f3(),f4() and f5()`
9
+ ```
10
+ f1()--> f2() --> f3()
11
+ --> f4() --> f5()
12
+ ```
13
+ In the above sequence, all method execution will generate a span each and they all will have a common traceID. Now if a new instrumented methods is executed after f1() finishes, it will be the first active span in the process's execution context and a will get a new traceID.
14
+
15
+ ### Trace ID propogation
16
+ Each child span inherits the parent's trace ID. When spans running in same process, it captures it from process memory/context etc. But consider the above example again, where the `f4()-->f5()` code is not part of the process that executing f1(). It's a remote call, say over REST. From the overall application's point of view, the work done if `f4()` and `f5()` is part of `f1()` and you want same traceID associated with all spans. You want the instrumentation to seamlessly pass the tracedID over such remote calls and continue that instead of generating a new one. It's the responsibility of Monocle to provide such mechanism to make thsi trace ID propogation transparent to the application logic and architecture.
17
+
18
+ ## Propogation
19
+ When the execution logic spans mulitple processes using remote calling mechanisms like REST, you want the trace ID also to propogate from process that originated it to the one that's continueing the remote execution. Monocle supports seamlessly propogating traceID over REST if both the sides for the trace execution are instrumented.
20
+
21
+ ## Types of spans in Monocle
22
+ Monocle extends these generic span types by enriching additional attributes/data for genAI specific operations.
23
+ ### GenAI spans
24
+ There are the core spans that capture details of genAI component operations like call to an LLM or vectore store. The purpose of these spans is to capture the details the applications interaction with core genAI comoponents. These spans are triggered by pre-instrumented methods that handle such operations.
25
+ - Inference span
26
+ Represents interaction with LLMs, captures details like model, prompts, response and other metadata (eg tokens)
27
+ - Retrieval span
28
+ Represents interactions with vector stores like embedding creating, vector retrieval etc. Captures the model, search query, response, vector embedding etc.
29
+
30
+ ### anchor spans
31
+ These are the spans that are created by a top level method that anchors a higher level of abstraction for underlying core genAI APIs. For example a langchain.invoke() which under the cover calls langchain.llm_invoke() or langchain.vector_retrieval(). Consider following psuedo code of a langchain rag pattern API,
32
+ ```
33
+ response = rag_chain.invoke(prompt)
34
+ --> cleaned_prompt = llm1.chat(prompt)
35
+ --> context = vector_store.retrieve(cleaned_prompt)
36
+ --> response = llm2.chat(system_prompt+context+cleaned_prompt)
37
+ --> return response
38
+ ```
39
+ If we only instrument the top level invoke call, then we'll trace the top level prompt and response interaction between application and langchain. But we'll miss the details like how a system prompt was added and send to mulitple LLMs and what context was extracted from a vector store etc. On the other hand, if we only instrument the low level calls to LLM and vector, then we'll miss the fact that those are part of same RAG. Hence we instrument all of them. This exaple would genearte an anchor spna for `invoke()` method, a retrieval span for `retrieve()` method and two inference spans for each `chat()` method. All of these will have common traceID.
40
+ The anchor spans also provides an observation window of your application interaction with an high level SDK or service. It will illustrate facts such as how much time take by the genAI service invocation compared to other local logic.
41
+
42
+ ### Workflow spans
43
+ Workflow spans are synthetic spans that are created to trace the full trace. It captures the summary of the full trace including the time window, the process running this code (set as `workflow_name` in the API to enab le Monocle instrumentation) and runtime environment details such as hosting service (Azure function, Lambda function etc).
44
+ The workflow spans is generated when a new trace starts or when a trace is propogated. They provide the base line observation window for the entire trace or a fragment of trace executed in a process.
45
+ Consider following example,
46
+ ```
47
+ setup_monocle_telemetry(workflow='bot')
48
+ rag_chain.invoke()
49
+ --> context = retrieval()
50
+ --> new_prompt = REST --> azure.func.chat(prompt) -->
51
+ setup_monocle_telemetry(workflow='moderator')
52
+ return llm(moderator_system_prompt+prompt)
53
+ --> response = llm(new_prompt)
54
+ ```
55
+ This will generate following spans:
56
+ ```
57
+ Span{name='workflow.bot', type= workflow, traceID = xx1, spanID = yy0, parentID=None} ==> Workflow for new trace start
58
+ Span{name='chain.invoke', type=anchor, traceID = xx1, spanID = yy1, parentID=yy0} ==> anchor span for chain invoke
59
+ Span{name='chain.retrieval', type=retrieval, traceID = xx1, spanID = yy2, parentID = yy1} ==> Retrieval API span
60
+ Span{name='workflow.moderator', type=workflow, traceID = xx1, spanID = zz1, parentID=yy1} ==> Workflow for propogated trace fragement
61
+ Span{name='az.func.chat', type=anchor, traceID = xx1, spanID = zz2, parentID=zz1} ==> anchor span for az function invoke
62
+ Span{name='chain.infer', type=inference, traceID = xx1, spanID = zz2, parentID=zz2} ==> inference
63
+ Span{name='chain.infer',type=inference, traceID = xx1, spanID = yy3, parentID=yy1} ==> inference
64
+ ```
65
+
66
+ ## Scopes
67
+ Scope is an way of grouping across traces. It's a tag with a value that can either be specified or auto generated (GUID) by Monocle. There can be any number of scopes active in an application code at a given point in time. All the active scopes are recorded in every span that's emmitted.
68
+
@@ -1,26 +1,32 @@
1
1
  import logging, json
2
2
  import os
3
+ import traceback
3
4
  from typing import Callable, Generic, Optional, TypeVar, Mapping
4
- import threading, asyncio
5
5
 
6
6
  from opentelemetry.context import attach, detach, get_current, get_value, set_value, Context
7
- from opentelemetry.trace import NonRecordingSpan, Span, get_tracer
7
+ from opentelemetry.trace import NonRecordingSpan, Span
8
8
  from opentelemetry.trace.propagation import _SPAN_KEY
9
9
  from opentelemetry.sdk.trace import id_generator, TracerProvider
10
- from opentelemetry.propagate import inject, extract
10
+ from opentelemetry.propagate import extract
11
11
  from opentelemetry import baggage
12
- from monocle_apptrace.instrumentation.common.constants import MONOCLE_SCOPE_NAME_PREFIX, SCOPE_METHOD_FILE, SCOPE_CONFIG_PATH, llm_type_map
12
+ from monocle_apptrace.instrumentation.common.constants import MONOCLE_SCOPE_NAME_PREFIX, SCOPE_METHOD_FILE, SCOPE_CONFIG_PATH, llm_type_map, MONOCLE_SDK_VERSION, ADD_NEW_WORKFLOW
13
+ from importlib.metadata import version
13
14
 
14
15
  T = TypeVar('T')
15
16
  U = TypeVar('U')
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
19
- monocle_tracer_provider: TracerProvider = None
20
20
  embedding_model_context = {}
21
21
  scope_id_generator = id_generator.RandomIdGenerator()
22
22
  http_scopes:dict[str:str] = {}
23
23
 
24
+ try:
25
+ monocle_sdk_version = version("monocle_apptrace")
26
+ except Exception as e:
27
+ monocle_sdk_version = "unknown"
28
+ logger.warning("Exception finding monocle-apptrace version.")
29
+
24
30
  class MonocleSpanException(Exception):
25
31
  def __init__(self, err_message:str):
26
32
  """
@@ -36,14 +42,6 @@ class MonocleSpanException(Exception):
36
42
  """String representation of the exception."""
37
43
  return f"[Monocle Span Error: {self.message} {self.status}"
38
44
 
39
- def set_tracer_provider(tracer_provider: TracerProvider):
40
- global monocle_tracer_provider
41
- monocle_tracer_provider = tracer_provider
42
-
43
- def get_tracer_provider() -> TracerProvider:
44
- global monocle_tracer_provider
45
- return monocle_tracer_provider
46
-
47
45
  def set_span_attribute(span, name, value):
48
46
  if value is not None:
49
47
  if value != "":
@@ -86,7 +84,12 @@ def with_tracer_wrapper(func):
86
84
  except Exception as e:
87
85
  logger.error("Exception in attaching parent context: %s", e)
88
86
 
89
- val = func(tracer, handler, to_wrap, wrapped, instance, args, kwargs)
87
+ if traceback.extract_stack().__len__() > 2:
88
+ filename, line_number, _, _ = traceback.extract_stack()[-2]
89
+ source_path = f"{filename}:{line_number}"
90
+ else:
91
+ source_path = ""
92
+ val = func(tracer, handler, to_wrap, wrapped, instance, source_path, args, kwargs)
90
93
  return val
91
94
 
92
95
  return wrapper
@@ -96,8 +99,8 @@ def with_tracer_wrapper(func):
96
99
  def resolve_from_alias(my_map, alias):
97
100
  """Find a alias that is not none from list of aliases"""
98
101
 
99
- for i in alias and my_map[i] is not None:
100
- if i in my_map.keys():
102
+ for i in alias:
103
+ if i in my_map.keys() and my_map[i] is not None:
101
104
  return my_map[i]
102
105
  return None
103
106
 
@@ -233,9 +236,17 @@ def set_scopes_from_baggage(baggage_context:Context):
233
236
  scope_name = scope_key[len(MONOCLE_SCOPE_NAME_PREFIX):]
234
237
  set_scope(scope_name, scope_value)
235
238
 
239
+ def get_parent_span() -> Span:
240
+ parent_span: Span = None
241
+ _parent_span_context = get_current()
242
+ if _parent_span_context is not None and _parent_span_context.get(_SPAN_KEY, None):
243
+ parent_span = _parent_span_context.get(_SPAN_KEY, None)
244
+ return parent_span
245
+
236
246
  def extract_http_headers(headers) -> object:
237
247
  global http_scopes
238
248
  trace_context:Context = extract(headers, context=get_current())
249
+ trace_context = set_value(ADD_NEW_WORKFLOW, True, trace_context)
239
250
  imported_scope:dict[str, object] = {}
240
251
  for http_header, http_scope in http_scopes.items():
241
252
  if http_header in headers:
@@ -267,49 +278,57 @@ async def http_async_route_handler(func, *args, **kwargs):
267
278
  headers = kwargs['req'].headers
268
279
  else:
269
280
  headers = None
270
- return async_wrapper(func, None, None, headers, *args, **kwargs)
271
-
272
- def run_async_with_scope(method, current_context, exceptions, *args, **kwargs):
273
- token = None
274
281
  try:
275
- if current_context:
276
- token = attach(current_context)
277
- return asyncio.run(method(*args, **kwargs))
278
- except Exception as e:
279
- exceptions['exception'] = e
280
- raise e
282
+ if headers is not None:
283
+ token = extract_http_headers(headers)
284
+ return await func(*args, **kwargs)
281
285
  finally:
282
- if token:
283
- detach(token)
284
-
285
- def async_wrapper(method, scope_name=None, scope_value=None, headers=None, *args, **kwargs):
286
- try:
287
- run_loop = asyncio.get_running_loop()
288
- except RuntimeError:
289
- run_loop = None
286
+ if token is not None:
287
+ clear_http_scopes(token)
290
288
 
291
- token = None
292
- exceptions = {}
293
- if scope_name:
294
- token = set_scope(scope_name, scope_value)
295
- elif headers:
296
- token = extract_http_headers(headers)
297
- current_context = get_current()
298
- try:
299
- if run_loop and run_loop.is_running():
300
- results = []
301
- thread = threading.Thread(target=lambda: results.append(run_async_with_scope(method, current_context, exceptions, *args, **kwargs)))
302
- thread.start()
303
- thread.join()
304
- if 'exception' in exceptions:
305
- raise exceptions['exception']
306
- return_value = results[0] if len(results) > 0 else None
307
- return return_value
308
- else:
309
- return run_async_with_scope(method, None, exceptions, *args, **kwargs)
310
- finally:
311
- if token:
312
- remove_scope(token)
289
+ # def run_async_with_scope(method, current_context, exceptions, *args, **kwargs):
290
+ # token = None
291
+ # try:
292
+ # if current_context:
293
+ # token = attach(current_context)
294
+ # return asyncio.run(method(*args, **kwargs))
295
+ # except Exception as e:
296
+ # exceptions['exception'] = e
297
+ # raise e
298
+ # finally:
299
+ # if token:
300
+ # detach(token)
301
+
302
+ # async def async_wrapper(method, headers=None, *args, **kwargs):
303
+ # current_context = get_current()
304
+ # try:
305
+ # if run_loop and run_loop.is_running():
306
+ # results = []
307
+ # thread = threading.Thread(target=lambda: results.append(run_async_with_scope(method, current_context, exceptions, *args, **kwargs)))
308
+ # thread.start()
309
+ # thread.join()
310
+ # if 'exception' in exceptions:
311
+ # raise exceptions['exception']
312
+ # return_value = results[0] if len(results) > 0 else None
313
+ # return return_value
314
+ # else:
315
+ # return run_async_with_scope(method, None, exceptions, *args, **kwargs)
316
+ # finally:
317
+ # if token:
318
+ # remove_scope(token)
319
+
320
+ def get_monocle_version() -> str:
321
+ global monocle_sdk_version
322
+ return monocle_sdk_version
323
+
324
+ def add_monocle_trace_state(headers:dict[str:str]) -> None:
325
+ if headers is None:
326
+ return
327
+ monocle_trace_state = f"{MONOCLE_SDK_VERSION}={get_monocle_version()}"
328
+ if 'tracestate' in headers:
329
+ headers['tracestate'] = f"{headers['tracestate']},{monocle_trace_state}"
330
+ else:
331
+ headers['tracestate'] = monocle_trace_state
313
332
 
314
333
  class Option(Generic[T]):
315
334
  def __init__(self, value: Optional[T]):
@@ -343,14 +362,46 @@ def try_option(func: Callable[..., T], *args, **kwargs) -> Option[T]:
343
362
 
344
363
  def get_llm_type(instance):
345
364
  try:
346
- llm_type = llm_type_map.get(type(instance).__name__.lower())
365
+ t_name = type(instance).__name__.lower()
366
+ t_name = t_name.replace("async", "") if "async" in t_name else t_name
367
+ llm_type = llm_type_map.get(t_name)
347
368
  return llm_type
348
369
  except:
349
370
  pass
350
371
 
351
- def resolve_from_alias(my_map, alias):
352
- """Find a alias that is not none from list of aliases"""
353
- for i in alias:
354
- if i in my_map.keys():
355
- return my_map[i]
356
- return None
372
+ def get_status(arguments):
373
+ if arguments['exception'] is not None:
374
+ return 'error'
375
+ else:
376
+ return 'success'
377
+
378
+ def get_exception_status_code(arguments):
379
+ if arguments['exception'] is not None and hasattr(arguments['exception'], 'code'):
380
+ return arguments['exception'].code
381
+ else:
382
+ return 'error'
383
+
384
+ def get_exception_message(arguments):
385
+ if arguments['exception'] is not None:
386
+ if hasattr(arguments['exception'], 'message'):
387
+ return arguments['exception'].message
388
+ else:
389
+ return arguments['exception'].__str__()
390
+ else:
391
+ return ''
392
+
393
+ def patch_instance_method(obj, method_name, func):
394
+ """
395
+ Patch a special method (like __iter__) for a single instance.
396
+
397
+ Args:
398
+ obj: the instance to patch
399
+ method_name: the name of the method (e.g., '__iter__')
400
+ func: the new function, expecting (self, ...)
401
+ """
402
+ cls = obj.__class__
403
+ # Dynamically create a new class that inherits from obj's class
404
+ new_cls = type(f"Patched{cls.__name__}", (cls,), {
405
+ method_name: func
406
+ })
407
+ obj.__class__ = new_cls