monocle-apptrace 0.3.1b1__py3-none-any.whl → 0.4.0b1__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 (43) 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 +8 -1
  9. monocle_apptrace/instrumentation/common/instrumentor.py +44 -22
  10. monocle_apptrace/instrumentation/common/span_handler.py +67 -41
  11. monocle_apptrace/instrumentation/common/tracing.md +68 -0
  12. monocle_apptrace/instrumentation/common/utils.py +86 -63
  13. monocle_apptrace/instrumentation/common/wrapper.py +185 -46
  14. monocle_apptrace/instrumentation/common/wrapper_method.py +12 -6
  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 +4 -1
  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/methods.py +6 -14
  28. monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +13 -9
  29. monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +16 -15
  30. monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +174 -26
  31. monocle_apptrace/instrumentation/metamodel/openai/methods.py +0 -2
  32. monocle_apptrace/instrumentation/metamodel/requests/_helper.py +31 -0
  33. monocle_apptrace/instrumentation/metamodel/requests/entities/http.py +51 -0
  34. monocle_apptrace/instrumentation/metamodel/requests/methods.py +2 -1
  35. monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +19 -1
  36. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/actionplanner_output_processor.py +1 -1
  37. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +24 -18
  38. monocle_apptrace/instrumentation/metamodel/teamsai/methods.py +42 -8
  39. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0b1.dist-info}/METADATA +1 -1
  40. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0b1.dist-info}/RECORD +43 -36
  41. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0b1.dist-info}/WHEEL +0 -0
  42. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0b1.dist-info}/licenses/LICENSE +0 -0
  43. {monocle_apptrace-0.3.1b1.dist-info → monocle_apptrace-0.4.0b1.dist-info}/licenses/NOTICE +0 -0
@@ -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
 
@@ -236,6 +239,7 @@ def set_scopes_from_baggage(baggage_context:Context):
236
239
  def extract_http_headers(headers) -> object:
237
240
  global http_scopes
238
241
  trace_context:Context = extract(headers, context=get_current())
242
+ trace_context = set_value(ADD_NEW_WORKFLOW, True, trace_context)
239
243
  imported_scope:dict[str, object] = {}
240
244
  for http_header, http_scope in http_scopes.items():
241
245
  if http_header in headers:
@@ -267,49 +271,57 @@ async def http_async_route_handler(func, *args, **kwargs):
267
271
  headers = kwargs['req'].headers
268
272
  else:
269
273
  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
274
  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
275
+ if headers is not None:
276
+ token = extract_http_headers(headers)
277
+ return await func(*args, **kwargs)
281
278
  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
279
+ if token is not None:
280
+ clear_http_scopes(token)
290
281
 
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)
282
+ # def run_async_with_scope(method, current_context, exceptions, *args, **kwargs):
283
+ # token = None
284
+ # try:
285
+ # if current_context:
286
+ # token = attach(current_context)
287
+ # return asyncio.run(method(*args, **kwargs))
288
+ # except Exception as e:
289
+ # exceptions['exception'] = e
290
+ # raise e
291
+ # finally:
292
+ # if token:
293
+ # detach(token)
294
+
295
+ # async def async_wrapper(method, headers=None, *args, **kwargs):
296
+ # current_context = get_current()
297
+ # try:
298
+ # if run_loop and run_loop.is_running():
299
+ # results = []
300
+ # thread = threading.Thread(target=lambda: results.append(run_async_with_scope(method, current_context, exceptions, *args, **kwargs)))
301
+ # thread.start()
302
+ # thread.join()
303
+ # if 'exception' in exceptions:
304
+ # raise exceptions['exception']
305
+ # return_value = results[0] if len(results) > 0 else None
306
+ # return return_value
307
+ # else:
308
+ # return run_async_with_scope(method, None, exceptions, *args, **kwargs)
309
+ # finally:
310
+ # if token:
311
+ # remove_scope(token)
312
+
313
+ def get_monocle_version() -> str:
314
+ global monocle_sdk_version
315
+ return monocle_sdk_version
316
+
317
+ def add_monocle_trace_state(headers:dict[str:str]) -> None:
318
+ if headers is None:
319
+ return
320
+ monocle_trace_state = f"{MONOCLE_SDK_VERSION}={get_monocle_version()}"
321
+ if 'tracestate' in headers:
322
+ headers['tracestate'] = f"{headers['tracestate']},{monocle_trace_state}"
323
+ else:
324
+ headers['tracestate'] = monocle_trace_state
313
325
 
314
326
  class Option(Generic[T]):
315
327
  def __init__(self, value: Optional[T]):
@@ -343,14 +355,25 @@ def try_option(func: Callable[..., T], *args, **kwargs) -> Option[T]:
343
355
 
344
356
  def get_llm_type(instance):
345
357
  try:
346
- llm_type = llm_type_map.get(type(instance).__name__.lower())
358
+ t_name = type(instance).__name__.lower()
359
+ t_name = t_name.replace("async", "") if "async" in t_name else t_name
360
+ llm_type = llm_type_map.get(t_name)
347
361
  return llm_type
348
362
  except:
349
363
  pass
350
364
 
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
365
+ def patch_instance_method(obj, method_name, func):
366
+ """
367
+ Patch a special method (like __iter__) for a single instance.
368
+
369
+ Args:
370
+ obj: the instance to patch
371
+ method_name: the name of the method (e.g., '__iter__')
372
+ func: the new function, expecting (self, ...)
373
+ """
374
+ cls = obj.__class__
375
+ # Dynamically create a new class that inherits from obj's class
376
+ new_cls = type(f"Patched{cls.__name__}", (cls,), {
377
+ method_name: func
378
+ })
379
+ obj.__class__ = new_cls
@@ -6,76 +6,168 @@ from opentelemetry.context import set_value, attach, detach, get_value
6
6
  from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
7
7
  from monocle_apptrace.instrumentation.common.utils import (
8
8
  get_fully_qualified_class_name,
9
+ set_scopes,
9
10
  with_tracer_wrapper,
10
11
  set_scope,
11
- remove_scope,
12
- async_wrapper
12
+ remove_scope
13
13
  )
14
- from monocle_apptrace.instrumentation.common.constants import WORKFLOW_TYPE_KEY
14
+ from monocle_apptrace.instrumentation.common.constants import WORKFLOW_TYPE_KEY, ADD_NEW_WORKFLOW
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
- def wrapper_processor(async_task: bool, tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, args, kwargs):
18
- # Some Langchain objects are wrapped elsewhere, so we ignore them here
19
- if instance.__class__.__name__ in ("AgentExecutor"):
20
- return wrapped(*args, **kwargs)
17
+ def get_auto_close_span(to_wrap, kwargs):
18
+ try:
19
+ if to_wrap.get("output_processor") and to_wrap.get("output_processor").get("is_auto_close"):
20
+ return to_wrap.get("output_processor").get("is_auto_close")(kwargs)
21
+ return True
22
+ except Exception as e:
23
+ logger.warning("Warning: Error occurred in get_auto_close_span: %s", str(e))
24
+ return True
25
+
26
+ def pre_process_span(name, tracer, handler, add_workflow_span, to_wrap, wrapped, instance, args, kwargs, span, source_path):
27
+ SpanHandler.set_default_monocle_attributes(span, source_path)
28
+ if SpanHandler.is_root_span(span) or add_workflow_span:
29
+ # This is a direct API call of a non-framework type
30
+ SpanHandler.set_workflow_properties(span, to_wrap)
31
+ else:
32
+ SpanHandler.set_non_workflow_properties(span)
33
+ handler.pre_task_processing(to_wrap, wrapped, instance, args, kwargs, span)
21
34
 
22
- if hasattr(instance, "name") and instance.name:
23
- name = f"{to_wrap.get('span_name')}.{instance.name.lower()}"
24
- elif to_wrap.get("span_name"):
35
+ def post_process_span(handler, to_wrap, wrapped, instance, args, kwargs, return_value, span):
36
+ if not (SpanHandler.is_root_span(span) or get_value(ADD_NEW_WORKFLOW) == True):
37
+ handler.hydrate_span(to_wrap, wrapped, instance, args, kwargs, return_value, span)
38
+ handler.post_task_processing(to_wrap, wrapped, instance, args, kwargs, return_value, span)
39
+
40
+ def get_span_name(to_wrap, instance):
41
+ if to_wrap.get("span_name"):
25
42
  name = to_wrap.get("span_name")
26
43
  else:
27
44
  name = get_fully_qualified_class_name(instance)
45
+ return name
46
+
47
+ def monocle_wrapper_span_processor(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, add_workflow_span, args, kwargs):
48
+ # Main span processing logic
49
+ name = get_span_name(to_wrap, instance)
50
+ return_value = None
51
+ span_status = None
52
+ if(get_auto_close_span(to_wrap, kwargs)):
53
+ with tracer.start_as_current_span(name) as span:
54
+ pre_process_span(name, tracer, handler, add_workflow_span, to_wrap, wrapped, instance, args, kwargs, span, source_path)
55
+
56
+ if SpanHandler.is_root_span(span) or add_workflow_span:
57
+ # Recursive call for the actual span
58
+ return_value, span_status = monocle_wrapper_span_processor(tracer, handler, to_wrap, wrapped, instance, source_path, False, args, kwargs)
59
+ span.set_status(span_status)
60
+ else:
61
+ with SpanHandler.workflow_type(to_wrap):
62
+ return_value = wrapped(*args, **kwargs)
63
+ post_process_span(handler, to_wrap, wrapped, instance, args, kwargs, return_value, span)
64
+ span_status = span.status
65
+ else:
66
+ span = tracer.start_span(name)
67
+
68
+ pre_process_span(name, tracer, handler, add_workflow_span, to_wrap, wrapped, instance, args, kwargs, span, source_path)
69
+
70
+ def post_process_span_internal(ret_val):
71
+ nonlocal handler, to_wrap, wrapped, instance, args, kwargs, span
72
+ post_process_span(handler, to_wrap, wrapped, instance, args, kwargs, ret_val, span)
73
+ span.end()
74
+
75
+ with SpanHandler.workflow_type(to_wrap):
76
+ return_value = wrapped(*args, **kwargs)
77
+ if to_wrap.get("output_processor") and to_wrap.get("output_processor").get("response_processor"):
78
+ # Process the stream
79
+ to_wrap.get("output_processor").get("response_processor")(to_wrap, return_value, post_process_span_internal)
80
+ else:
81
+ span.end()
82
+ span_status = span.status
83
+ return return_value, span_status
28
84
 
85
+ def monocle_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
29
86
  return_value = None
30
87
  token = None
31
88
  try:
32
89
  handler.pre_tracing(to_wrap, wrapped, instance, args, kwargs)
33
- skip_scan:bool = to_wrap.get('skip_span') or handler.skip_span(to_wrap, wrapped, instance, args, kwargs)
34
- if not to_wrap.get('skip_span'):
35
- token = SpanHandler.attach_workflow_type(to_wrap=to_wrap)
36
- if skip_scan:
37
- if async_task:
38
- return_value = async_wrapper(wrapped, None, None, None, *args, **kwargs)
39
- else:
40
- return_value = wrapped(*args, **kwargs)
90
+ if to_wrap.get('skip_span', False) or handler.skip_span(to_wrap, wrapped, instance, args, kwargs):
91
+ return_value = wrapped(*args, **kwargs)
41
92
  else:
42
- return_value = span_processor(name, async_task, tracer, handler, to_wrap, wrapped, instance, args, kwargs)
93
+ add_workflow_span = get_value(ADD_NEW_WORKFLOW) == True
94
+ token = attach(set_value(ADD_NEW_WORKFLOW, False))
95
+ try:
96
+ return_value, span_status = monocle_wrapper_span_processor(tracer, handler, to_wrap, wrapped, instance, source_path, add_workflow_span, args, kwargs)
97
+ finally:
98
+ detach(token)
43
99
  return return_value
44
100
  finally:
45
- handler.detach_workflow_type(token)
46
101
  handler.post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value)
47
102
 
48
- def span_processor(name: str, async_task: bool, tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, args, kwargs):
49
- # For singleton spans, eg OpenAI inference generate a workflow span to format the workflow specific attributes
103
+
104
+ async def amonocle_wrapper_span_processor(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, add_workflow_span, args, kwargs):
105
+ # Main span processing logic
106
+ name = get_span_name(to_wrap, instance)
50
107
  return_value = None
51
- with tracer.start_as_current_span(name) as span:
52
- # Since Spanhandler can be overridden, ensure we set default monocle attributes.
53
- SpanHandler.set_default_monocle_attributes(span)
54
- if SpanHandler.is_root_span(span):
55
- SpanHandler.set_workflow_properties(span, to_wrap)
56
- if handler.is_non_workflow_root_span(span, to_wrap):
57
- # This is a direct API call of a non-framework type, call the span_processor recursively for the actual span
58
- return_value = span_processor(name, async_task, tracer, handler, to_wrap, wrapped, instance, args, kwargs)
108
+ span_status = None
109
+ if(get_auto_close_span(to_wrap, kwargs)):
110
+ with tracer.start_as_current_span(name) as span:
111
+ pre_process_span(name, tracer, handler, add_workflow_span, to_wrap, wrapped, instance, args, kwargs, span, source_path)
112
+
113
+ if SpanHandler.is_root_span(span) or add_workflow_span:
114
+ # Recursive call for the actual span
115
+ return_value, span_status = await amonocle_wrapper_span_processor(tracer, handler, to_wrap, wrapped, instance, source_path, False, args, kwargs)
116
+ span.set_status(span_status)
117
+ else:
118
+ with SpanHandler.workflow_type(to_wrap):
119
+ return_value = await wrapped(*args, **kwargs)
120
+ span_status = span.status
121
+ post_process_span(handler, to_wrap, wrapped, instance, args, kwargs, return_value, span)
122
+ else:
123
+ span = tracer.start_span(name)
124
+
125
+ pre_process_span(name, tracer, handler, add_workflow_span, to_wrap, wrapped, instance, args, kwargs, span, source_path)
126
+
127
+ def post_process_span_internal(ret_val):
128
+ nonlocal handler, to_wrap, wrapped, instance, args, kwargs, span
129
+ post_process_span(handler, to_wrap, wrapped, instance, args, kwargs, ret_val, span)
130
+ span.end()
131
+
132
+ with SpanHandler.workflow_type(to_wrap):
133
+ return_value = await wrapped(*args, **kwargs)
134
+
135
+ if to_wrap.get("output_processor") and to_wrap.get("output_processor").get("response_processor"):
136
+ # Process the stream
137
+ to_wrap.get("output_processor").get("response_processor")(to_wrap, return_value, post_process_span_internal)
138
+ else:
139
+ span.end()
140
+ span_status = span.status
141
+ return return_value, span.status
142
+
143
+ async def amonocle_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
144
+ return_value = None
145
+ token = None
146
+ try:
147
+ handler.pre_tracing(to_wrap, wrapped, instance, args, kwargs)
148
+ if to_wrap.get('skip_span', False) or handler.skip_span(to_wrap, wrapped, instance, args, kwargs):
149
+ return_value = await wrapped(*args, **kwargs)
59
150
  else:
60
- handler.pre_task_processing(to_wrap, wrapped, instance, args, kwargs, span)
61
- if async_task:
62
- return_value = async_wrapper(wrapped, None, None, None, *args, **kwargs)
63
- else:
64
- return_value = wrapped(*args, **kwargs)
65
- handler.hydrate_span(to_wrap, wrapped, instance, args, kwargs, return_value, span)
66
- handler.post_task_processing(to_wrap, wrapped, instance, args, kwargs, return_value, span)
67
- return return_value
151
+ add_workflow_span = get_value(ADD_NEW_WORKFLOW) == True
152
+ token = attach(set_value(ADD_NEW_WORKFLOW, False))
153
+ try:
154
+ return_value, span_status = await amonocle_wrapper_span_processor(tracer, handler, to_wrap, wrapped, instance, source_path, add_workflow_span, args, kwargs)
155
+ finally:
156
+ detach(token)
157
+ return return_value
158
+ finally:
159
+ handler.post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value)
68
160
 
69
161
  @with_tracer_wrapper
70
- def task_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, args, kwargs):
71
- return wrapper_processor(False, tracer, handler, to_wrap, wrapped, instance, args, kwargs)
162
+ def task_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
163
+ return monocle_wrapper(tracer, handler, to_wrap, wrapped, instance, source_path, args, kwargs)
72
164
 
73
165
  @with_tracer_wrapper
74
- async def atask_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, args, kwargs):
75
- return wrapper_processor(True, tracer, handler, to_wrap, wrapped, instance, args, kwargs)
166
+ async def atask_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
167
+ return await amonocle_wrapper(tracer, handler, to_wrap, wrapped, instance, source_path, args, kwargs)
76
168
 
77
169
  @with_tracer_wrapper
78
- def scope_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, args, kwargs):
170
+ def scope_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
79
171
  scope_name = to_wrap.get('scope_name', None)
80
172
  if scope_name:
81
173
  token = set_scope(scope_name)
@@ -85,8 +177,55 @@ def scope_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instan
85
177
  return return_value
86
178
 
87
179
  @with_tracer_wrapper
88
- async def ascope_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, args, kwargs):
180
+ async def ascope_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
89
181
  scope_name = to_wrap.get('scope_name', None)
90
182
  scope_value = to_wrap.get('scope_value', None)
91
- return_value = async_wrapper(wrapped, scope_name, scope_value, None, *args, **kwargs)
92
- return return_value
183
+ token = None
184
+ try:
185
+ if scope_name:
186
+ token = set_scope(scope_name, scope_value)
187
+ return_value = await wrapped(*args, **kwargs)
188
+ return return_value
189
+ finally:
190
+ if token:
191
+ remove_scope(token)
192
+
193
+ @with_tracer_wrapper
194
+ def scopes_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
195
+ scope_values = to_wrap.get('scope_values', None)
196
+ scope_values = evaluate_scope_values(args, kwargs, scope_values)
197
+ token = None
198
+ try:
199
+ if scope_values:
200
+ token = set_scopes(scope_values)
201
+ return_value = wrapped(*args, **kwargs)
202
+ return return_value
203
+ finally:
204
+ if token:
205
+ remove_scope(token)
206
+
207
+ @with_tracer_wrapper
208
+ async def ascopes_wrapper(tracer: Tracer, handler: SpanHandler, to_wrap, wrapped, instance, source_path, args, kwargs):
209
+ scope_values = to_wrap.get('scope_values', None)
210
+ scope_values = evaluate_scope_values(args, kwargs, scope_values)
211
+ token = None
212
+ try:
213
+ if scope_values:
214
+ token = set_scopes(scope_values)
215
+ return_value = await wrapped(*args, **kwargs)
216
+ return return_value
217
+ finally:
218
+ if token:
219
+ remove_scope(token)
220
+
221
+ def evaluate_scope_values(args, kwargs, scope_values):
222
+ if callable(scope_values):
223
+ try:
224
+ scope_values = scope_values(args, kwargs)
225
+ except Exception as e:
226
+ logger.warning("Warning: Error occurred in evaluate_scope_values: %s", str(e))
227
+ scope_values = None
228
+ if isinstance(scope_values, dict):
229
+ return scope_values
230
+ return None
231
+
@@ -12,12 +12,13 @@ from monocle_apptrace.instrumentation.metamodel.haystack.methods import (HAYSTAC
12
12
  from monocle_apptrace.instrumentation.metamodel.openai.methods import (OPENAI_METHODS,)
13
13
  from monocle_apptrace.instrumentation.metamodel.langgraph.methods import LANGGRAPH_METHODS
14
14
  from monocle_apptrace.instrumentation.metamodel.flask.methods import (FLASK_METHODS, )
15
- from monocle_apptrace.instrumentation.metamodel.flask._helper import FlaskSpanHandler
15
+ from monocle_apptrace.instrumentation.metamodel.flask._helper import FlaskSpanHandler, FlaskResponseSpanHandler
16
16
  from monocle_apptrace.instrumentation.metamodel.requests.methods import (REQUESTS_METHODS, )
17
17
  from monocle_apptrace.instrumentation.metamodel.requests._helper import RequestSpanHandler
18
18
  from monocle_apptrace.instrumentation.metamodel.teamsai.methods import (TEAMAI_METHODS, )
19
19
  from monocle_apptrace.instrumentation.metamodel.anthropic.methods import (ANTHROPIC_METHODS, )
20
-
20
+ from monocle_apptrace.instrumentation.metamodel.aiohttp.methods import (AIOHTTP_METHODS, )
21
+ from monocle_apptrace.instrumentation.metamodel.aiohttp._helper import aiohttpSpanHandler
21
22
  class WrapperMethod:
22
23
  def __init__(
23
24
  self,
@@ -29,7 +30,8 @@ class WrapperMethod:
29
30
  wrapper_method = task_wrapper,
30
31
  span_handler = 'default',
31
32
  scope_name: str = None,
32
- span_type: str = None
33
+ span_type: str = None,
34
+ scope_values = None,
33
35
  ):
34
36
  self.package = package
35
37
  self.object = object_name
@@ -37,10 +39,11 @@ class WrapperMethod:
37
39
  self.span_name = span_name
38
40
  self.output_processor=output_processor
39
41
  self.span_type = span_type
42
+ self.scope_values = scope_values
40
43
 
41
44
  self.span_handler:SpanHandler.__class__ = span_handler
42
45
  self.scope_name = scope_name
43
- if scope_name:
46
+ if scope_name and not scope_values:
44
47
  self.wrapper_method = scope_wrapper
45
48
  else:
46
49
  self.wrapper_method = wrapper_method
@@ -56,19 +59,22 @@ class WrapperMethod:
56
59
  'wrapper_method': self.wrapper_method,
57
60
  'span_handler': self.span_handler,
58
61
  'scope_name': self.scope_name,
59
- 'span_type': self.span_type
62
+ 'span_type': self.span_type,
63
+ 'scope_values': self.scope_values,
60
64
  }
61
65
  return instance_dict
62
66
 
63
67
  def get_span_handler(self) -> SpanHandler:
64
68
  return self.span_handler()
65
69
 
66
- DEFAULT_METHODS_LIST = LANGCHAIN_METHODS + LLAMAINDEX_METHODS + HAYSTACK_METHODS + BOTOCORE_METHODS + FLASK_METHODS + REQUESTS_METHODS + LANGGRAPH_METHODS + OPENAI_METHODS + TEAMAI_METHODS + ANTHROPIC_METHODS
70
+ DEFAULT_METHODS_LIST = LANGCHAIN_METHODS + LLAMAINDEX_METHODS + HAYSTACK_METHODS + BOTOCORE_METHODS + FLASK_METHODS + REQUESTS_METHODS + LANGGRAPH_METHODS + OPENAI_METHODS + TEAMAI_METHODS + ANTHROPIC_METHODS + AIOHTTP_METHODS
67
71
 
68
72
  MONOCLE_SPAN_HANDLERS: Dict[str, SpanHandler] = {
69
73
  "default": SpanHandler(),
74
+ "aiohttp_handler": aiohttpSpanHandler(),
70
75
  "botocore_handler": BotoCoreSpanHandler(),
71
76
  "flask_handler": FlaskSpanHandler(),
77
+ "flask_response_handler": FlaskResponseSpanHandler(),
72
78
  "request_handler": RequestSpanHandler(),
73
79
  "non_framework_handler": NonFrameworkSpanHandler()
74
80
  }