3
3
import time
4
4
from typing import Any
5
5
from typing import Dict
6
+ from typing import List
6
7
from typing import Optional
8
+ from typing import Tuple
7
9
from typing import Union
8
10
9
11
import ddtrace
13
15
from ddtrace ._trace .context import Context
14
16
from ddtrace .ext import SpanTypes
15
17
from ddtrace .internal import atexit
18
+ from ddtrace .internal import core
16
19
from ddtrace .internal import forksafe
17
20
from ddtrace .internal ._rand import rand64bits
18
21
from ddtrace .internal .compat import ensure_text
45
48
from ddtrace .llmobs ._constants import SPAN_START_WHILE_DISABLED_WARNING
46
49
from ddtrace .llmobs ._constants import TAGS
47
50
from ddtrace .llmobs ._evaluators .runner import EvaluatorRunner
48
- from ddtrace .llmobs ._trace_processor import LLMObsTraceProcessor
49
51
from ddtrace .llmobs ._utils import AnnotationContext
50
52
from ddtrace .llmobs ._utils import _get_llmobs_parent_id
51
53
from ddtrace .llmobs ._utils import _get_ml_app
52
54
from ddtrace .llmobs ._utils import _get_session_id
55
+ from ddtrace .llmobs ._utils import _get_span_name
53
56
from ddtrace .llmobs ._utils import _inject_llmobs_parent_id
54
57
from ddtrace .llmobs ._utils import safe_json
55
58
from ddtrace .llmobs ._utils import validate_prompt
60
63
from ddtrace .llmobs .utils import Messages
61
64
from ddtrace .propagation .http import HTTPPropagator
62
65
66
+ from ..constants import ERROR_MSG
67
+ from ..constants import ERROR_STACK
68
+ from ..constants import ERROR_TYPE
69
+ from . import _constants as constants
70
+
63
71
64
72
log = get_logger (__name__ )
65
73
@@ -81,34 +89,157 @@ class LLMObs(Service):
81
89
def __init__ (self , tracer = None ):
82
90
super (LLMObs , self ).__init__ ()
83
91
self .tracer = tracer or ddtrace .tracer
84
- self ._llmobs_span_writer = None
85
-
86
92
self ._llmobs_span_writer = LLMObsSpanWriter (
87
93
is_agentless = config ._llmobs_agentless_enabled ,
88
94
interval = float (os .getenv ("_DD_LLMOBS_WRITER_INTERVAL" , 1.0 )),
89
95
timeout = float (os .getenv ("_DD_LLMOBS_WRITER_TIMEOUT" , 5.0 )),
90
96
)
91
-
92
97
self ._llmobs_eval_metric_writer = LLMObsEvalMetricWriter (
93
98
site = config ._dd_site ,
94
99
api_key = config ._dd_api_key ,
95
100
interval = float (os .getenv ("_DD_LLMOBS_WRITER_INTERVAL" , 1.0 )),
96
101
timeout = float (os .getenv ("_DD_LLMOBS_WRITER_TIMEOUT" , 5.0 )),
97
102
)
98
-
99
103
self ._evaluator_runner = EvaluatorRunner (
100
104
interval = float (os .getenv ("_DD_LLMOBS_EVALUATOR_INTERVAL" , 1.0 )),
101
105
llmobs_service = self ,
102
106
)
103
107
104
- self ._trace_processor = LLMObsTraceProcessor (self ._llmobs_span_writer , self ._evaluator_runner )
105
108
forksafe .register (self ._child_after_fork )
106
109
107
110
self ._annotations = []
108
111
self ._annotation_context_lock = forksafe .RLock ()
109
- self .tracer .on_start_span (self ._do_annotations )
110
112
111
- def _do_annotations (self , span ):
113
+ # Register hooks for span events
114
+ core .on ("trace.span_start" , self ._do_annotations )
115
+ core .on ("trace.span_finish" , self ._on_span_finish )
116
+
117
+ def _on_span_finish (self , span ):
118
+ if self .enabled and span .span_type == SpanTypes .LLM :
119
+ self ._submit_llmobs_span (span )
120
+
121
+ def _submit_llmobs_span (self , span : Span ) -> None :
122
+ """Generate and submit an LLMObs span event to be sent to LLMObs."""
123
+ span_event = None
124
+ is_llm_span = span ._get_ctx_item (SPAN_KIND ) == "llm"
125
+ is_ragas_integration_span = False
126
+ try :
127
+ span_event , is_ragas_integration_span = self ._llmobs_span_event (span )
128
+ self ._llmobs_span_writer .enqueue (span_event )
129
+ except (KeyError , TypeError ):
130
+ log .error (
131
+ "Error generating LLMObs span event for span %s, likely due to malformed span" , span , exc_info = True
132
+ )
133
+ finally :
134
+ if not span_event or not is_llm_span or is_ragas_integration_span :
135
+ return
136
+ if self ._evaluator_runner :
137
+ self ._evaluator_runner .enqueue (span_event , span )
138
+
139
+ @classmethod
140
+ def _llmobs_span_event (cls , span : Span ) -> Tuple [Dict [str , Any ], bool ]:
141
+ """Span event object structure."""
142
+ span_kind = span ._get_ctx_item (SPAN_KIND )
143
+ if not span_kind :
144
+ raise KeyError ("Span kind not found in span context" )
145
+ meta : Dict [str , Any ] = {"span.kind" : span_kind , "input" : {}, "output" : {}}
146
+ if span_kind in ("llm" , "embedding" ) and span ._get_ctx_item (MODEL_NAME ) is not None :
147
+ meta ["model_name" ] = span ._get_ctx_item (MODEL_NAME )
148
+ meta ["model_provider" ] = (span ._get_ctx_item (MODEL_PROVIDER ) or "custom" ).lower ()
149
+ meta ["metadata" ] = span ._get_ctx_item (METADATA ) or {}
150
+ if span ._get_ctx_item (INPUT_PARAMETERS ):
151
+ meta ["input" ]["parameters" ] = span ._get_ctx_item (INPUT_PARAMETERS )
152
+ if span_kind == "llm" and span ._get_ctx_item (INPUT_MESSAGES ) is not None :
153
+ meta ["input" ]["messages" ] = span ._get_ctx_item (INPUT_MESSAGES )
154
+ if span ._get_ctx_item (INPUT_VALUE ) is not None :
155
+ meta ["input" ]["value" ] = safe_json (span ._get_ctx_item (INPUT_VALUE ))
156
+ if span_kind == "llm" and span ._get_ctx_item (OUTPUT_MESSAGES ) is not None :
157
+ meta ["output" ]["messages" ] = span ._get_ctx_item (OUTPUT_MESSAGES )
158
+ if span_kind == "embedding" and span ._get_ctx_item (INPUT_DOCUMENTS ) is not None :
159
+ meta ["input" ]["documents" ] = span ._get_ctx_item (INPUT_DOCUMENTS )
160
+ if span ._get_ctx_item (OUTPUT_VALUE ) is not None :
161
+ meta ["output" ]["value" ] = safe_json (span ._get_ctx_item (OUTPUT_VALUE ))
162
+ if span_kind == "retrieval" and span ._get_ctx_item (OUTPUT_DOCUMENTS ) is not None :
163
+ meta ["output" ]["documents" ] = span ._get_ctx_item (OUTPUT_DOCUMENTS )
164
+ if span ._get_ctx_item (INPUT_PROMPT ) is not None :
165
+ prompt_json_str = span ._get_ctx_item (INPUT_PROMPT )
166
+ if span_kind != "llm" :
167
+ log .warning (
168
+ "Dropping prompt on non-LLM span kind, annotating prompts is only supported for LLM span kinds."
169
+ )
170
+ else :
171
+ meta ["input" ]["prompt" ] = prompt_json_str
172
+ if span .error :
173
+ meta .update (
174
+ {
175
+ ERROR_MSG : span .get_tag (ERROR_MSG ),
176
+ ERROR_STACK : span .get_tag (ERROR_STACK ),
177
+ ERROR_TYPE : span .get_tag (ERROR_TYPE ),
178
+ }
179
+ )
180
+ if not meta ["input" ]:
181
+ meta .pop ("input" )
182
+ if not meta ["output" ]:
183
+ meta .pop ("output" )
184
+ metrics = span ._get_ctx_item (METRICS ) or {}
185
+ ml_app = _get_ml_app (span )
186
+
187
+ is_ragas_integration_span = False
188
+
189
+ if ml_app .startswith (constants .RAGAS_ML_APP_PREFIX ):
190
+ is_ragas_integration_span = True
191
+
192
+ span ._set_ctx_item (ML_APP , ml_app )
193
+ parent_id = str (_get_llmobs_parent_id (span ) or "undefined" )
194
+
195
+ llmobs_span_event = {
196
+ "trace_id" : "{:x}" .format (span .trace_id ),
197
+ "span_id" : str (span .span_id ),
198
+ "parent_id" : parent_id ,
199
+ "name" : _get_span_name (span ),
200
+ "start_ns" : span .start_ns ,
201
+ "duration" : span .duration_ns ,
202
+ "status" : "error" if span .error else "ok" ,
203
+ "meta" : meta ,
204
+ "metrics" : metrics ,
205
+ }
206
+ session_id = _get_session_id (span )
207
+ if session_id is not None :
208
+ span ._set_ctx_item (SESSION_ID , session_id )
209
+ llmobs_span_event ["session_id" ] = session_id
210
+
211
+ llmobs_span_event ["tags" ] = cls ._llmobs_tags (
212
+ span , ml_app , session_id , is_ragas_integration_span = is_ragas_integration_span
213
+ )
214
+ return llmobs_span_event , is_ragas_integration_span
215
+
216
+ @staticmethod
217
+ def _llmobs_tags (
218
+ span : Span , ml_app : str , session_id : Optional [str ] = None , is_ragas_integration_span : bool = False
219
+ ) -> List [str ]:
220
+ tags = {
221
+ "version" : config .version or "" ,
222
+ "env" : config .env or "" ,
223
+ "service" : span .service or "" ,
224
+ "source" : "integration" ,
225
+ "ml_app" : ml_app ,
226
+ "ddtrace.version" : ddtrace .__version__ ,
227
+ "language" : "python" ,
228
+ "error" : span .error ,
229
+ }
230
+ err_type = span .get_tag (ERROR_TYPE )
231
+ if err_type :
232
+ tags ["error_type" ] = err_type
233
+ if session_id :
234
+ tags ["session_id" ] = session_id
235
+ if is_ragas_integration_span :
236
+ tags [constants .RUNNER_IS_INTEGRATION_SPAN_TAG ] = "ragas"
237
+ existing_tags = span ._get_ctx_item (TAGS )
238
+ if existing_tags is not None :
239
+ tags .update (existing_tags )
240
+ return ["{}:{}" .format (k , v ) for k , v in tags .items ()]
241
+
242
+ def _do_annotations (self , span : Span ) -> None :
112
243
# get the current span context
113
244
# only do the annotations if it matches the context
114
245
if span .span_type != SpanTypes .LLM : # do this check to avoid the warning log in `annotate`
@@ -120,20 +251,14 @@ def _do_annotations(self, span):
120
251
if current_context_id == context_id :
121
252
self .annotate (span , ** annotation_kwargs )
122
253
123
- def _child_after_fork (self ):
254
+ def _child_after_fork (self ) -> None :
124
255
self ._llmobs_span_writer = self ._llmobs_span_writer .recreate ()
125
256
self ._llmobs_eval_metric_writer = self ._llmobs_eval_metric_writer .recreate ()
126
257
self ._evaluator_runner = self ._evaluator_runner .recreate ()
127
- self ._trace_processor ._span_writer = self ._llmobs_span_writer
128
- self ._trace_processor ._evaluator_runner = self ._evaluator_runner
129
258
if self .enabled :
130
259
self ._start_service ()
131
260
132
261
def _start_service (self ) -> None :
133
- tracer_filters = self .tracer ._filters
134
- if not any (isinstance (tracer_filter , LLMObsTraceProcessor ) for tracer_filter in tracer_filters ):
135
- tracer_filters += [self ._trace_processor ]
136
- self .tracer .configure (settings = {"FILTERS" : tracer_filters })
137
262
try :
138
263
self ._llmobs_span_writer .start ()
139
264
self ._llmobs_eval_metric_writer .start ()
@@ -160,11 +285,7 @@ def _stop_service(self) -> None:
160
285
except ServiceStatusError :
161
286
log .debug ("Error stopping LLMObs writers" )
162
287
163
- try :
164
- forksafe .unregister (self ._child_after_fork )
165
- self .tracer .shutdown ()
166
- except Exception :
167
- log .warning ("Failed to shutdown tracer" , exc_info = True )
288
+ forksafe .unregister (self ._child_after_fork )
168
289
169
290
@classmethod
170
291
def enable (
@@ -265,7 +386,6 @@ def disable(cls) -> None:
265
386
266
387
cls ._instance .stop ()
267
388
cls .enabled = False
268
- cls ._instance .tracer .deregister_on_start_span (cls ._instance ._do_annotations )
269
389
telemetry_writer .product_activated (TELEMETRY_APM_PRODUCT .LLMOBS , False )
270
390
271
391
log .debug ("%s disabled" , cls .__name__ )
0 commit comments