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