55import os
66import atexit
77import weakref
8-
8+ import time
99from enum import Enum
1010from typing import Optional
1111
@@ -40,6 +40,7 @@ def start_span(name: str):
4040class QuotientAttributes (str , Enum ):
4141 app_name = "app.name"
4242 environment = "app.environment"
43+ detections = "quotient.detections"
4344
4445
4546class QuotientAttributesSpanProcessor (SpanProcessor ):
@@ -54,17 +55,22 @@ class QuotientAttributesSpanProcessor(SpanProcessor):
5455
5556 app_name : str
5657 environment : str
58+ detections : str
5759
58- def __init__ (self , app_name : str , environment : str ):
60+ def __init__ (self , app_name : str , environment : str , detections : Optional [ str ] = None ):
5961 self .app_name = app_name
6062 self .environment = environment
63+ self .detections = detections
6164
6265 def on_start (self , span : Span , parent_context : Optional [otel_context .Context ] = None ) -> None :
6366 attributes = {
6467 QuotientAttributes .app_name : self .app_name ,
6568 QuotientAttributes .environment : self .environment ,
6669 }
6770
71+ if self .detections is not None :
72+ attributes [QuotientAttributes .detections ] = self .detections
73+
6874 span .set_attributes (attributes )
6975 super ().on_start (span , parent_context )
7076
@@ -78,10 +84,10 @@ def __init__(self, client):
7884 self ._app_name = None
7985 self ._environment = None
8086 self ._instruments = None
81-
87+ self . _detections = None
8288 atexit .register (self ._cleanup )
8389
84- def configure (self , app_name : str , environment : str , instruments : Optional [list ] = None ):
90+ def configure (self , app_name : str , environment : str , instruments : Optional [list ] = None , detections : Optional [ list ] = None ):
8591 """
8692 Configure the tracing resource with app_name, environment, and instruments.
8793 This allows the trace decorator to be used without parameters.
@@ -100,14 +106,16 @@ def configure(self, app_name: str, environment: str, instruments: Optional[list]
100106 self ._app_name = app_name
101107 self ._environment = environment
102108 self ._instruments = instruments
109+ self ._detections = "," .join (detections ) if detections else None
103110
104- def init (self , app_name : str , environment : str , instruments : Optional [list ] = None ):
111+ def init (self , app_name : str , environment : str , instruments : Optional [list ] = None , detections : Optional [ list ] = None ):
105112 """
106113 Initialize tracing with app_name, environment, and instruments.
107114 This is a convenience method that calls configure and then sets up the collector.
108115 """
109- self .configure (app_name , environment , instruments )
110- self ._setup_auto_collector (app_name , environment , instruments )
116+ self .configure (app_name , environment , instruments , detections )
117+ detections_str = "," .join (detections ) if detections else None
118+ self ._setup_auto_collector (app_name , environment , instruments , detections_str )
111119
112120 def get_vector_db_instrumentors (self ):
113121 """
@@ -147,7 +155,7 @@ def _create_otlp_exporter(self, endpoint: str, headers: dict):
147155 return OTLPSpanExporter (endpoint = endpoint , headers = headers )
148156
149157 @functools .lru_cache ()
150- def _setup_auto_collector (self , app_name : str , environment : str , instruments : Optional [list ] = None ):
158+ def _setup_auto_collector (self , app_name : str , environment : str , instruments : Optional [tuple ] = None , detections : Optional [ str ] = None ):
151159 """
152160 Automatically setup OTLP exporter to send traces to collector
153161 """
@@ -186,6 +194,7 @@ def _setup_auto_collector(self, app_name: str, environment: str, instruments: Op
186194 quotient_attributes_span_processor = QuotientAttributesSpanProcessor (
187195 app_name = app_name ,
188196 environment = environment ,
197+ detections = detections ,
189198 )
190199 tracer_provider .add_span_processor (quotient_attributes_span_processor )
191200 tracer_provider .add_span_processor (span_processor )
@@ -235,18 +244,22 @@ def sync_func_wrapper(*args, **kwargs):
235244 app_name = self ._app_name ,
236245 environment = self ._environment ,
237246 instruments = tuple (self ._instruments ) if self ._instruments is not None else None ,
247+ detections = self ._detections ,
238248 )
239249
240250 # if there is no tracer, just run the function normally
241251 if self .tracer is None :
242252 return func (* args , ** kwargs )
243253
244- with self .tracer .start_as_current_span (name ):
254+ with self .tracer .start_as_current_span (name ) as root_span :
245255 try :
246256 result = func (* args , ** kwargs )
247257 except Exception as e :
248258 raise e
249259 finally :
260+ trace_id = root_span .get_span_context ().trace_id
261+ self ._create_end_of_trace_span (trace_id )
262+
250263 # here we can log the call once we have the result.
251264 # TODO: add otel support for quotient logging
252265 pass
@@ -259,17 +272,21 @@ async def async_func_wrapper(*args, **kwargs):
259272 app_name = self ._app_name ,
260273 environment = self ._environment ,
261274 instruments = tuple (self ._instruments ) if self ._instruments is not None else None ,
275+ detections = self ._detections ,
262276 )
263277
264278 if self .tracer is None :
265279 return await func (* args , ** kwargs )
266280
267- with self .tracer .start_as_current_span (name ):
281+ with self .tracer .start_as_current_span (name ) as root_span :
268282 try :
269283 result = await func (* args , ** kwargs )
270284 except Exception as e :
271285 raise e
272286 finally :
287+ trace_id = root_span .get_span_context ().trace_id
288+ self ._create_end_of_trace_span (trace_id )
289+
273290 # here we can log the call once we have the result.
274291 # TODO: add otel support for quotient logging
275292 pass
@@ -316,3 +333,14 @@ def force_flush(self):
316333 logger .info ("Forced flush of pending spans" )
317334 except Exception as e :
318335 logger .error (f"Failed to force flush spans: { str (e )} " )
336+
337+ def _create_end_of_trace_span (self , trace_id ):
338+ """Create an end-of-trace marker span"""
339+ try :
340+ with self .tracer .start_as_current_span ("quotient.end_of_trace" ) as span :
341+ span .set_attribute ("quotient.trace.complete" , True )
342+ span .set_attribute ("quotient.trace.marker" , True )
343+ span .set_attribute ("quotient.trace.id" , format (trace_id , '032x' ))
344+ span .set_attribute ("quotient.marker.timestamp" , time .time_ns ())
345+ except Exception as _ :
346+ pass
0 commit comments