monocle-apptrace 0.3.0b6__py3-none-any.whl → 0.3.1__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 (50) hide show
  1. monocle_apptrace/__init__.py +1 -0
  2. monocle_apptrace/exporters/aws/s3_exporter.py +20 -6
  3. monocle_apptrace/exporters/aws/s3_exporter_opendal.py +22 -11
  4. monocle_apptrace/exporters/azure/blob_exporter.py +22 -8
  5. monocle_apptrace/exporters/azure/blob_exporter_opendal.py +23 -8
  6. monocle_apptrace/exporters/exporter_processor.py +128 -3
  7. monocle_apptrace/exporters/file_exporter.py +16 -0
  8. monocle_apptrace/exporters/monocle_exporters.py +15 -3
  9. monocle_apptrace/exporters/okahu/okahu_exporter.py +8 -6
  10. monocle_apptrace/instrumentation/__init__.py +1 -0
  11. monocle_apptrace/instrumentation/common/__init__.py +2 -0
  12. monocle_apptrace/instrumentation/common/constants.py +7 -1
  13. monocle_apptrace/instrumentation/common/instrumentor.py +105 -20
  14. monocle_apptrace/instrumentation/common/span_handler.py +46 -28
  15. monocle_apptrace/instrumentation/common/tracing.md +68 -0
  16. monocle_apptrace/instrumentation/common/utils.py +70 -26
  17. monocle_apptrace/instrumentation/common/wrapper.py +27 -23
  18. monocle_apptrace/instrumentation/common/wrapper_method.py +5 -2
  19. monocle_apptrace/instrumentation/metamodel/anthropic/__init__.py +0 -0
  20. monocle_apptrace/instrumentation/metamodel/anthropic/_helper.py +64 -0
  21. monocle_apptrace/instrumentation/metamodel/anthropic/entities/__init__.py +0 -0
  22. monocle_apptrace/instrumentation/metamodel/anthropic/entities/inference.py +72 -0
  23. monocle_apptrace/instrumentation/metamodel/anthropic/methods.py +24 -0
  24. monocle_apptrace/instrumentation/metamodel/botocore/entities/inference.py +2 -2
  25. monocle_apptrace/instrumentation/metamodel/botocore/handlers/botocore_span_handler.py +2 -1
  26. monocle_apptrace/instrumentation/metamodel/flask/_helper.py +45 -3
  27. monocle_apptrace/instrumentation/metamodel/flask/entities/http.py +49 -0
  28. monocle_apptrace/instrumentation/metamodel/flask/methods.py +10 -1
  29. monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +4 -1
  30. monocle_apptrace/instrumentation/metamodel/haystack/methods.py +1 -4
  31. monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +12 -4
  32. monocle_apptrace/instrumentation/metamodel/langchain/methods.py +6 -14
  33. monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +2 -15
  34. monocle_apptrace/instrumentation/metamodel/openai/_helper.py +9 -4
  35. monocle_apptrace/instrumentation/metamodel/openai/methods.py +16 -2
  36. monocle_apptrace/instrumentation/metamodel/requests/_helper.py +31 -0
  37. monocle_apptrace/instrumentation/metamodel/requests/entities/http.py +51 -0
  38. monocle_apptrace/instrumentation/metamodel/requests/methods.py +2 -1
  39. monocle_apptrace/instrumentation/metamodel/teamsai/__init__.py +0 -0
  40. monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +58 -0
  41. monocle_apptrace/instrumentation/metamodel/teamsai/entities/__init__.py +0 -0
  42. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/__init__.py +0 -0
  43. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/actionplanner_output_processor.py +80 -0
  44. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +70 -0
  45. monocle_apptrace/instrumentation/metamodel/teamsai/methods.py +26 -0
  46. {monocle_apptrace-0.3.0b6.dist-info → monocle_apptrace-0.3.1.dist-info}/METADATA +2 -1
  47. {monocle_apptrace-0.3.0b6.dist-info → monocle_apptrace-0.3.1.dist-info}/RECORD +50 -35
  48. {monocle_apptrace-0.3.0b6.dist-info → monocle_apptrace-0.3.1.dist-info}/WHEEL +0 -0
  49. {monocle_apptrace-0.3.0b6.dist-info → monocle_apptrace-0.3.1.dist-info}/licenses/LICENSE +0 -0
  50. {monocle_apptrace-0.3.0b6.dist-info → monocle_apptrace-0.3.1.dist-info}/licenses/NOTICE +0 -0
@@ -3,6 +3,7 @@ import inspect
3
3
  from typing import Collection, Dict, List, Union
4
4
  import random
5
5
  import uuid
6
+ import inspect
6
7
  from opentelemetry import trace
7
8
  from contextlib import contextmanager
8
9
  from opentelemetry.context import attach, get_value, set_value, get_current, detach
@@ -13,21 +14,22 @@ from opentelemetry.sdk.trace import TracerProvider, Span, id_generator
13
14
  from opentelemetry.sdk.resources import SERVICE_NAME, Resource
14
15
  from opentelemetry.sdk.trace import Span, TracerProvider
15
16
  from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanProcessor
17
+ from opentelemetry.sdk.trace.export import SpanExporter
16
18
  from opentelemetry.trace import get_tracer
17
19
  from wrapt import wrap_function_wrapper
18
20
  from opentelemetry.trace.propagation import set_span_in_context, _SPAN_KEY
19
21
  from monocle_apptrace.exporters.monocle_exporters import get_monocle_exporter
20
- from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
22
+ from monocle_apptrace.instrumentation.common.span_handler import SpanHandler, NonFrameworkSpanHandler
21
23
  from monocle_apptrace.instrumentation.common.wrapper_method import (
22
24
  DEFAULT_METHODS_LIST,
23
25
  WrapperMethod,
24
26
  MONOCLE_SPAN_HANDLERS
25
27
  )
26
- from monocle_apptrace.instrumentation.common.wrapper import scope_wrapper, ascope_wrapper
28
+ from monocle_apptrace.instrumentation.common.wrapper import scope_wrapper, ascope_wrapper, wrapper_processor
27
29
  from monocle_apptrace.instrumentation.common.utils import (
28
30
  set_scope, remove_scope, http_route_handler, load_scopes, async_wrapper, http_async_route_handler
29
31
  )
30
- from monocle_apptrace.instrumentation.common.constants import MONOCLE_INSTRUMENTOR, WORKFLOW_TYPE_KEY
32
+ from monocle_apptrace.instrumentation.common.constants import MONOCLE_INSTRUMENTOR, WORKFLOW_TYPE_GENERIC
31
33
  from functools import wraps
32
34
  logger = logging.getLogger(__name__)
33
35
 
@@ -39,19 +41,22 @@ monocle_tracer_provider: TracerProvider = None
39
41
 
40
42
  class MonocleInstrumentor(BaseInstrumentor):
41
43
  workflow_name: str = ""
42
- user_wrapper_methods: list[Union[dict,WrapperMethod]] = []
44
+ user_wrapper_methods: list[Union[dict,WrapperMethod]] = [],
45
+ exporters: list[SpanExporter] = [],
43
46
  instrumented_method_list: list[object] = []
44
- handlers:Dict[str,SpanHandler] = {} # dict of handlers
47
+ handlers:Dict[str,SpanHandler] = None # dict of handlers
45
48
  union_with_default_methods: bool = False
46
49
 
47
50
  def __init__(
48
51
  self,
49
52
  handlers,
50
53
  user_wrapper_methods: list[Union[dict,WrapperMethod]] = None,
54
+ exporters: list[SpanExporter] = None,
51
55
  union_with_default_methods: bool = True
52
56
  ) -> None:
53
57
  self.user_wrapper_methods = user_wrapper_methods or []
54
58
  self.handlers = handlers
59
+ self.exporters = exporters
55
60
  if self.handlers is not None:
56
61
  for key, val in MONOCLE_SPAN_HANDLERS.items():
57
62
  if key not in self.handlers:
@@ -65,13 +70,11 @@ class MonocleInstrumentor(BaseInstrumentor):
65
70
  def instrumented_endpoint_invoke(to_wrap,wrapped, span_name, instance,fn):
66
71
  @wraps(fn)
67
72
  def with_instrumentation(*args, **kwargs):
68
- handler = SpanHandler()
69
- with tracer.start_as_current_span(span_name) as span:
70
- response = fn(*args, **kwargs)
71
- handler.hydrate_span(to_wrap, wrapped=wrapped, instance=instance, args=args, kwargs=kwargs,
72
- result=response, span=span)
73
- return response
74
-
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)
75
78
  return with_instrumentation
76
79
  return instrumented_endpoint_invoke
77
80
 
@@ -157,11 +160,35 @@ def setup_monocle_telemetry(
157
160
  span_processors: List[SpanProcessor] = None,
158
161
  span_handlers: Dict[str,SpanHandler] = None,
159
162
  wrapper_methods: List[Union[dict,WrapperMethod]] = None,
160
- union_with_default_methods: bool = True) -> None:
163
+ union_with_default_methods: bool = True,
164
+ monocle_exporters_list:str = None) -> None:
165
+ """
166
+ Set up Monocle telemetry for the application.
167
+
168
+ Parameters
169
+ ----------
170
+ workflow_name : str
171
+ The name of the workflow to be used as the service name in telemetry.
172
+ span_processors : List[SpanProcessor], optional
173
+ Custom span processors to use instead of the default ones. If None,
174
+ BatchSpanProcessors with Monocle exporters will be used. This can't be combined with `monocle_exporters_list`.
175
+ span_handlers : Dict[str, SpanHandler], optional
176
+ Dictionary of span handlers to be used by the instrumentor, mapping handler names to handler objects.
177
+ wrapper_methods : List[Union[dict, WrapperMethod]], optional
178
+ Custom wrapper methods for instrumentation. If None, default methods will be used.
179
+ union_with_default_methods : bool, default=True
180
+ If True, combine the provided wrapper_methods with the default methods.
181
+ If False, only use the provided wrapper_methods.
182
+ monocle_exporters_list : str, optional
183
+ Comma-separated list of exporters to use. This will override the env setting MONOCLE_EXPORTERS.
184
+ Supported exporters are: s3, blob, okahu, file, memory, console. This can't be combined with `span_processors`.
185
+ """
161
186
  resource = Resource(attributes={
162
187
  SERVICE_NAME: workflow_name
163
188
  })
164
- exporters = get_monocle_exporter()
189
+ if span_processors and monocle_exporters_list:
190
+ raise ValueError("span_processors and monocle_exporters_list can't be used together")
191
+ exporters:List[SpanExporter] = get_monocle_exporter(monocle_exporters_list)
165
192
  span_processors = span_processors or [BatchSpanProcessor(exporter) for exporter in exporters]
166
193
  set_tracer_provider(TracerProvider(resource=resource))
167
194
  attach(set_value("workflow_name", workflow_name))
@@ -176,7 +203,7 @@ def setup_monocle_telemetry(
176
203
  get_tracer_provider().add_span_processor(processor)
177
204
  if is_proxy_provider:
178
205
  trace.set_tracer_provider(get_tracer_provider())
179
- instrumentor = MonocleInstrumentor(user_wrapper_methods=wrapper_methods or [],
206
+ instrumentor = MonocleInstrumentor(user_wrapper_methods=wrapper_methods or [], exporters=exporters,
180
207
  handlers=span_handlers, union_with_default_methods = union_with_default_methods)
181
208
  # instrumentor.app_name = workflow_name
182
209
  if not instrumentor.is_instrumented_by_opentelemetry:
@@ -196,19 +223,37 @@ def set_context_properties(properties: dict) -> None:
196
223
  attach(set_value(SESSION_PROPERTIES_KEY, properties))
197
224
 
198
225
  def start_trace():
226
+ """
227
+ Starts a new trace. All the spans created after this call will be part of the same trace.
228
+ Returns:
229
+ Token: A token representing the attached context for the workflow span.
230
+ This token is to be used later to stop the current trace.
231
+ Returns None if tracing fails.
232
+
233
+ Raises:
234
+ Exception: The function catches all exceptions internally and logs a warning.
235
+ """
199
236
  try:
200
237
  tracer = get_tracer(instrumenting_module_name= MONOCLE_INSTRUMENTOR, tracer_provider= get_tracer_provider())
201
238
  span = tracer.start_span(name = "workflow")
202
239
  updated_span_context = set_span_in_context(span=span)
203
240
  SpanHandler.set_default_monocle_attributes(span)
204
241
  SpanHandler.set_workflow_properties(span)
205
- token = SpanHandler.attach_workflow_type(context=updated_span_context)
242
+ token = attach(updated_span_context)
206
243
  return token
207
244
  except:
208
245
  logger.warning("Failed to start trace")
209
246
  return None
210
247
 
211
248
  def stop_trace(token) -> None:
249
+ """
250
+ Stop the active trace and detach workflow type if token is provided. All the spans created after this will not be part of the trace.
251
+ Args:
252
+ token: The token that was returned when the trace was started. Used to detach
253
+ workflow type. Can be None in which case only the span is ended.
254
+ Returns:
255
+ None
256
+ """
212
257
  try:
213
258
  _parent_span_context = get_current()
214
259
  if _parent_span_context is not None:
@@ -216,7 +261,7 @@ def stop_trace(token) -> None:
216
261
  if parent_span is not None:
217
262
  parent_span.end()
218
263
  if token is not None:
219
- SpanHandler.detach_workflow_type(token)
264
+ detach(token)
220
265
  except:
221
266
  logger.warning("Failed to stop trace")
222
267
 
@@ -229,32 +274,67 @@ def is_valid_trace_id_uuid(traceId: str) -> bool:
229
274
  return False
230
275
 
231
276
  def start_scope(scope_name: str, scope_value:str = None) -> object:
277
+ """
278
+ Start a new scope with the given name and and optional value. If no value is provided, a random UUID will be generated.
279
+ All the spans, across traces created after this call will have the scope attached until the scope is stopped.
280
+ Args:
281
+ scope_name: The name of the scope.
282
+ scope_value: Optional value of the scope. If None, a random UUID will be generated.
283
+ Returns:
284
+ Token: A token representing the attached context for the scope. This token is to be used later to stop the current scope.
285
+ """
232
286
  return set_scope(scope_name, scope_value)
233
287
 
234
288
  def stop_scope(token:object) -> None:
289
+ """
290
+ Stop the active scope. All the spans created after this will not have the scope attached.
291
+ Args:
292
+ token: The token that was returned when the scope was started.
293
+ Returns:
294
+ None
295
+ """
235
296
  remove_scope(token)
236
297
  return
237
298
 
299
+ @contextmanager
300
+ def monocle_trace():
301
+ """
302
+ Context manager to start and stop a scope. All the spans, across traces created within the encapsulated code will have same trace ID
303
+ """
304
+ token = start_trace()
305
+ try:
306
+ yield
307
+ finally:
308
+ stop_trace(token)
309
+
238
310
  @contextmanager
239
311
  def monocle_trace_scope(scope_name: str, scope_value:str = None):
312
+ """
313
+ Context manager to start and stop a scope. All the spans, across traces created within the encapsulated code will have the scope attached.
314
+ Args:
315
+ scope_name: The name of the scope.
316
+ scope_value: Optional value of the scope. If None, a random UUID will be generated."""
240
317
  token = start_scope(scope_name, scope_value)
241
318
  try:
242
319
  yield
243
320
  finally:
244
321
  stop_scope(token)
245
322
 
246
- def monocle_trace_scope_method(scope_name: str):
323
+ def monocle_trace_scope_method(scope_name: str, scope_value:str=None):
324
+ """
325
+ Decorator to start and stop a scope for a method. All the spans, across traces created in the method will have the scope attached.
326
+ """
247
327
  def decorator(func):
248
328
  if inspect.iscoroutinefunction(func):
249
329
  @wraps(func)
250
330
  async def wrapper(*args, **kwargs):
251
- result = async_wrapper(func, scope_name, None, *args, **kwargs)
331
+ result = async_wrapper(func, scope_name, scope_value, None, *args, **kwargs)
252
332
  return result
253
333
  return wrapper
254
334
  else:
255
335
  @wraps(func)
256
336
  def wrapper(*args, **kwargs):
257
- token = start_scope(scope_name)
337
+ token = start_scope(scope_name, scope_value)
258
338
  try:
259
339
  result = func(*args, **kwargs)
260
340
  return result
@@ -264,6 +344,10 @@ def monocle_trace_scope_method(scope_name: str):
264
344
  return decorator
265
345
 
266
346
  def monocle_trace_http_route(func):
347
+ """
348
+ Decorator to start and stop a continue traces and scope for a http route. It will also initiate new scopes from the http headers if configured in ``monocle_scopes.json``
349
+ All the spans, across traces created in the route will have the scope attached.
350
+ """
267
351
  if inspect.iscoroutinefunction(func):
268
352
  @wraps(func)
269
353
  async def wrapper(*args, **kwargs):
@@ -286,3 +370,4 @@ class FixedIdGenerator(id_generator.IdGenerator):
286
370
 
287
371
  def generate_trace_id(self) -> int:
288
372
  return self.trace_id
373
+
@@ -1,16 +1,16 @@
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
7
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
11
+ MONOCLE_SDK_VERSION, MONOCLE_SDK_LANGUAGE
12
12
  )
13
- from monocle_apptrace.instrumentation.common.utils import set_attribute, get_scopes
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__)
@@ -39,9 +39,9 @@ class SpanHandler:
39
39
  pass
40
40
 
41
41
  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
42
+ return False
43
+
44
+ def skip_processor(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
45
45
  return False
46
46
 
47
47
  def pre_task_processing(self, to_wrap, wrapped, instance, args,kwargs, span):
@@ -51,11 +51,8 @@ class SpanHandler:
51
51
  @staticmethod
52
52
  def set_default_monocle_attributes(span: Span):
53
53
  """ 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.")
54
+ span.set_attribute(MONOCLE_SDK_VERSION, get_monocle_version())
55
+ span.set_attribute(MONOCLE_SDK_LANGUAGE, "python")
59
56
  for scope_key, scope_value in get_scopes().items():
60
57
  span.set_attribute(f"scope.{scope_key}", scope_value)
61
58
 
@@ -65,8 +62,17 @@ class SpanHandler:
65
62
  SpanHandler.set_workflow_attributes(to_wrap, span)
66
63
  SpanHandler.set_app_hosting_identifier_attribute(span)
67
64
 
68
- def post_task_processing(self, to_wrap, wrapped, instance, args, kwargs, result, span):
69
- pass
65
+ span.set_status(StatusCode.OK)
66
+
67
+ @staticmethod
68
+ def set_non_workflow_properties(span: Span, to_wrap = None):
69
+ workflow_name = SpanHandler.get_workflow_name(span=span)
70
+ if workflow_name:
71
+ span.set_attribute("workflow.name", workflow_name)
72
+
73
+ 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)
70
76
 
71
77
  def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span):
72
78
  self.hydrate_attributes(to_wrap, wrapped, instance, args, kwargs, result, span)
@@ -76,7 +82,8 @@ class SpanHandler:
76
82
  span_index = 0
77
83
  if SpanHandler.is_root_span(span):
78
84
  span_index = 2 # root span will have workflow and hosting entities pre-populated
79
- if 'output_processor' in to_wrap and to_wrap["output_processor"] is not None:
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):
80
87
  output_processor=to_wrap['output_processor']
81
88
  if 'type' in output_processor:
82
89
  span.set_attribute("span.type", output_processor['type'])
@@ -95,6 +102,8 @@ class SpanHandler:
95
102
  result = accessor(arguments)
96
103
  if result and isinstance(result, (str, list)):
97
104
  span.set_attribute(attribute_name, result)
105
+ except MonocleSpanException as e:
106
+ span.set_status(StatusCode.ERROR, e.message)
98
107
  except Exception as e:
99
108
  logger.debug(f"Error processing accessor: {e}")
100
109
  else:
@@ -102,6 +111,8 @@ class SpanHandler:
102
111
  span_index += 1
103
112
  else:
104
113
  logger.debug("attributes not found or incorrect written in entity json")
114
+ else:
115
+ span.set_attribute("span.type", "generic")
105
116
 
106
117
  # set scopes as attributes by calling get_scopes()
107
118
  # scopes is a Mapping[str:object], iterate directly with .items()
@@ -113,7 +124,8 @@ class SpanHandler:
113
124
 
114
125
 
115
126
  def hydrate_events(self, to_wrap, wrapped, instance, args, kwargs, result, span):
116
- if 'output_processor' in to_wrap and to_wrap["output_processor"] is not None:
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):
117
129
  output_processor=to_wrap['output_processor']
118
130
  arguments = {"instance": instance, "args": args, "kwargs": kwargs, "result": result}
119
131
  if 'events' in output_processor:
@@ -131,6 +143,8 @@ class SpanHandler:
131
143
  event_attributes[attribute_key] = accessor(arguments)
132
144
  else:
133
145
  event_attributes.update(accessor(arguments))
146
+ except MonocleSpanException as e:
147
+ span.set_status(StatusCode.ERROR, e.message)
134
148
  except Exception as e:
135
149
  logger.debug(f"Error evaluating accessor for attribute '{attribute_key}': {e}")
136
150
  span.add_event(name=event_name, attributes=event_attributes)
@@ -140,6 +154,7 @@ class SpanHandler:
140
154
  span_index = 1
141
155
  workflow_name = SpanHandler.get_workflow_name(span=span)
142
156
  if workflow_name:
157
+ span.update_name("workflow")
143
158
  span.set_attribute("span.type", "workflow")
144
159
  span.set_attribute(f"entity.{span_index}.name", workflow_name)
145
160
  workflow_type = SpanHandler.get_workflow_type(to_wrap)
@@ -179,26 +194,19 @@ class SpanHandler:
179
194
  @staticmethod
180
195
  def is_root_span(curr_span: Span) -> bool:
181
196
  try:
182
- if curr_span is not None and hasattr(curr_span, "parent"):
197
+ if curr_span is not None and hasattr(curr_span, "parent") or curr_span.context.trace_state:
183
198
  return curr_span.parent is None
184
199
  except Exception as e:
185
200
  logger.warning(f"Error finding root span: {e}")
186
201
 
187
- def is_non_workflow_root_span(self, curr_span: Span, to_wrap) -> bool:
188
- return SpanHandler.is_root_span(curr_span) and to_wrap.get("span_type") != "workflow"
189
-
190
- def is_workflow_span_active(self):
191
- return get_value(WORKFLOW_TYPE_KEY) is not None
192
-
193
202
  @staticmethod
194
203
  def attach_workflow_type(to_wrap=None, context=None):
195
204
  token = None
196
205
  if to_wrap:
197
- if to_wrap.get('span_type') == "workflow":
206
+ workflow_type = SpanHandler.get_workflow_type(to_wrap)
207
+ if workflow_type != WORKFLOW_TYPE_GENERIC:
198
208
  token = attach(set_value(WORKFLOW_TYPE_KEY,
199
209
  SpanHandler.get_workflow_type(to_wrap), context))
200
- else:
201
- token = attach(set_value(WORKFLOW_TYPE_KEY, WORKFLOW_TYPE_GENERIC, context))
202
210
  return token
203
211
 
204
212
  @staticmethod
@@ -206,8 +214,18 @@ class SpanHandler:
206
214
  if token:
207
215
  return detach(token)
208
216
 
217
+ @staticmethod
218
+ @contextmanager
219
+ def workflow_type(to_wrap=None):
220
+ token = SpanHandler.attach_workflow_type(to_wrap)
221
+ try:
222
+ yield
223
+ finally:
224
+ SpanHandler.detach_workflow_type(token)
225
+
226
+
209
227
  class NonFrameworkSpanHandler(SpanHandler):
210
228
 
211
- # If the language framework is being executed, then skip generating direct openAI spans
212
- def skip_span(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
229
+ # 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:
213
231
  return get_value(WORKFLOW_TYPE_KEY) in WORKFLOW_TYPE_MAP.values()
@@ -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
+
@@ -9,7 +9,8 @@ from opentelemetry.trace.propagation import _SPAN_KEY
9
9
  from opentelemetry.sdk.trace import id_generator, TracerProvider
10
10
  from opentelemetry.propagate import inject, 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')
@@ -21,6 +22,27 @@ embedding_model_context = {}
21
22
  scope_id_generator = id_generator.RandomIdGenerator()
22
23
  http_scopes:dict[str:str] = {}
23
24
 
25
+ try:
26
+ monocle_sdk_version = version("monocle_apptrace")
27
+ except Exception as e:
28
+ monocle_sdk_version = "unknown"
29
+ logger.warning("Exception finding monocle-apptrace version.")
30
+
31
+ class MonocleSpanException(Exception):
32
+ def __init__(self, err_message:str):
33
+ """
34
+ Monocle exeption to indicate error in span processing.
35
+ Parameters:
36
+ - err_message (str): Error message.
37
+ - status (str): Status code
38
+ """
39
+ super().__init__(err_message)
40
+ self.message = err_message
41
+
42
+ def __str__(self):
43
+ """String representation of the exception."""
44
+ return f"[Monocle Span Error: {self.message} {self.status}"
45
+
24
46
  def set_tracer_provider(tracer_provider: TracerProvider):
25
47
  global monocle_tracer_provider
26
48
  monocle_tracer_provider = tracer_provider
@@ -81,8 +103,8 @@ def with_tracer_wrapper(func):
81
103
  def resolve_from_alias(my_map, alias):
82
104
  """Find a alias that is not none from list of aliases"""
83
105
 
84
- for i in alias and my_map[i] is not None:
85
- if i in my_map.keys():
106
+ for i in alias:
107
+ if i in my_map.keys() and my_map[i] is not None:
86
108
  return my_map[i]
87
109
  return None
88
110
 
@@ -221,6 +243,7 @@ def set_scopes_from_baggage(baggage_context:Context):
221
243
  def extract_http_headers(headers) -> object:
222
244
  global http_scopes
223
245
  trace_context:Context = extract(headers, context=get_current())
246
+ trace_context = set_value(ADD_NEW_WORKFLOW, True, trace_context)
224
247
  imported_scope:dict[str, object] = {}
225
248
  for http_header, http_scope in http_scopes.items():
226
249
  if http_header in headers:
@@ -252,35 +275,62 @@ async def http_async_route_handler(func, *args, **kwargs):
252
275
  headers = kwargs['req'].headers
253
276
  else:
254
277
  headers = None
255
- return async_wrapper(func, None, headers, *args, **kwargs)
278
+ return async_wrapper(func, None, None, headers, *args, **kwargs)
256
279
 
257
- def run_async_with_scope(method, scope_name, headers, *args, **kwargs):
280
+ def run_async_with_scope(method, current_context, exceptions, *args, **kwargs):
258
281
  token = None
259
- if scope_name:
260
- token = set_scope(scope_name)
261
- elif headers:
262
- token = extract_http_headers(headers)
263
282
  try:
283
+ if current_context:
284
+ token = attach(current_context)
264
285
  return asyncio.run(method(*args, **kwargs))
286
+ except Exception as e:
287
+ exceptions['exception'] = e
288
+ raise e
265
289
  finally:
266
290
  if token:
267
- remove_scope(token)
291
+ detach(token)
268
292
 
269
- def async_wrapper(method, scope_name=None, headers=None, *args, **kwargs):
293
+ def async_wrapper(method, scope_name=None, scope_value=None, headers=None, *args, **kwargs):
270
294
  try:
271
295
  run_loop = asyncio.get_running_loop()
272
296
  except RuntimeError:
273
297
  run_loop = None
274
298
 
275
- if run_loop and run_loop.is_running():
276
- results = []
277
- thread = threading.Thread(target=lambda: results.append(run_async_with_scope(method, scope_name, headers, *args, **kwargs)))
278
- thread.start()
279
- thread.join()
280
- return_value = results[0] if len(results) > 0 else None
281
- return return_value
299
+ token = None
300
+ exceptions = {}
301
+ if scope_name:
302
+ token = set_scope(scope_name, scope_value)
303
+ elif headers:
304
+ token = extract_http_headers(headers)
305
+ current_context = get_current()
306
+ try:
307
+ if run_loop and run_loop.is_running():
308
+ results = []
309
+ thread = threading.Thread(target=lambda: results.append(run_async_with_scope(method, current_context, exceptions, *args, **kwargs)))
310
+ thread.start()
311
+ thread.join()
312
+ if 'exception' in exceptions:
313
+ raise exceptions['exception']
314
+ return_value = results[0] if len(results) > 0 else None
315
+ return return_value
316
+ else:
317
+ return run_async_with_scope(method, None, exceptions, *args, **kwargs)
318
+ finally:
319
+ if token:
320
+ remove_scope(token)
321
+
322
+ def get_monocle_version() -> str:
323
+ global monocle_sdk_version
324
+ return monocle_sdk_version
325
+
326
+ def add_monocle_trace_state(headers:dict[str:str]) -> None:
327
+ if headers is None:
328
+ return
329
+ monocle_trace_state = f"{MONOCLE_SDK_VERSION}={get_monocle_version()}"
330
+ if 'tracestate' in headers:
331
+ headers['tracestate'] = f"{headers['tracestate']},{monocle_trace_state}"
282
332
  else:
283
- return run_async_with_scope(method, scope_name, headers, *args, **kwargs)
333
+ headers['tracestate'] = monocle_trace_state
284
334
 
285
335
  class Option(Generic[T]):
286
336
  def __init__(self, value: Optional[T]):
@@ -314,14 +364,8 @@ def try_option(func: Callable[..., T], *args, **kwargs) -> Option[T]:
314
364
 
315
365
  def get_llm_type(instance):
316
366
  try:
367
+ t_name = type(instance).__name__.lower()
317
368
  llm_type = llm_type_map.get(type(instance).__name__.lower())
318
369
  return llm_type
319
370
  except:
320
371
  pass
321
-
322
- def resolve_from_alias(my_map, alias):
323
- """Find a alias that is not none from list of aliases"""
324
- for i in alias:
325
- if i in my_map.keys():
326
- return my_map[i]
327
- return None