1
- import time
2
- from dataclasses import dataclass , field
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
3
15
from typing import Any , Dict , List , Optional
4
16
from uuid import UUID
5
17
6
18
from langchain_core .callbacks import BaseCallbackHandler # type: ignore
7
19
from langchain_core .messages import BaseMessage # type: ignore
8
20
from langchain_core .outputs import LLMResult # type: ignore
9
21
10
- from opentelemetry .context import Context , get_current
22
+ from opentelemetry .instrumentation . langchain . span_manager import SpanManager
11
23
from opentelemetry .instrumentation .langchain .utils import dont_throw
12
24
from opentelemetry .semconv ._incubating .attributes import (
13
25
gen_ai_attributes as GenAI ,
14
26
)
15
27
from opentelemetry .semconv .attributes import (
16
28
error_attributes as ErrorAttributes ,
17
29
)
18
- from opentelemetry .trace import Span , SpanKind , Tracer , set_span_in_context
30
+ from opentelemetry .trace import Tracer
19
31
from opentelemetry .trace .status import Status , StatusCode
20
32
21
33
22
- @dataclass
23
- class _SpanState :
24
- span : Span
25
- span_context : Context
26
- start_time : float = field (default_factory = time .time )
27
- children : List [UUID ] = field (default_factory = list )
28
-
29
-
30
34
class OpenTelemetryLangChainCallbackHandler (BaseCallbackHandler ): # type: ignore[misc]
31
35
"""
32
36
A callback handler for LangChain that uses OpenTelemetry to create spans for LLM calls and chains, tools etc,. in future.
@@ -37,69 +41,10 @@ def __init__(
37
41
tracer : Tracer ,
38
42
) -> None :
39
43
super ().__init__ () # type: ignore
40
- self ._tracer = tracer
41
-
42
- # Map from run_id -> _SpanState, to keep track of spans and parent/child relationships
43
- self .spans : Dict [UUID , _SpanState ] = {}
44
- self .run_inline = True # Whether to run the callback inline.
45
-
46
- def _create_span (
47
- self ,
48
- run_id : UUID ,
49
- parent_run_id : Optional [UUID ],
50
- span_name : str ,
51
- kind : SpanKind = SpanKind .INTERNAL ,
52
- ) -> Span :
53
- if parent_run_id is not None and parent_run_id in self .spans :
54
- parent_span = self .spans [parent_run_id ].span
55
- ctx = set_span_in_context (parent_span )
56
- span = self ._tracer .start_span (
57
- name = span_name , kind = kind , context = ctx
58
- )
59
- else :
60
- # top-level or missing parent
61
- span = self ._tracer .start_span (name = span_name , kind = kind )
62
-
63
- span_state = _SpanState (span = span , span_context = get_current ())
64
- self .spans [run_id ] = span_state
65
-
66
- if parent_run_id is not None and parent_run_id in self .spans :
67
- self .spans [parent_run_id ].children .append (run_id )
68
44
69
- return span
70
-
71
- def _create_llm_span (
72
- self ,
73
- run_id : UUID ,
74
- parent_run_id : Optional [UUID ],
75
- name : str ,
76
- ) -> Span :
77
- span = self ._create_span (
78
- run_id = run_id ,
79
- parent_run_id = parent_run_id ,
80
- span_name = f"{ name } .{ GenAI .GenAiOperationNameValues .CHAT .value } " ,
81
- kind = SpanKind .CLIENT ,
45
+ self .span_manager = SpanManager (
46
+ tracer = tracer ,
82
47
)
83
- span .set_attribute (
84
- GenAI .GEN_AI_OPERATION_NAME ,
85
- GenAI .GenAiOperationNameValues .CHAT .value ,
86
- )
87
- span .set_attribute (GenAI .GEN_AI_SYSTEM , name )
88
-
89
- return span
90
-
91
- def _end_span (self , run_id : UUID ) -> None :
92
- state = self .spans [run_id ]
93
- for child_id in state .children :
94
- child_state = self .spans .get (child_id )
95
- if child_state :
96
- # Always end child spans as OpenTelemetry spans don't expose end_time directly
97
- child_state .span .end ()
98
- # Always end the span as OpenTelemetry spans don't expose end_time directly
99
- state .span .end ()
100
-
101
- def _get_span (self , run_id : UUID ) -> Span :
102
- return self .spans [run_id ].span
103
48
104
49
@dont_throw
105
50
def on_chat_model_start (
@@ -114,7 +59,7 @@ def on_chat_model_start(
114
59
** kwargs : Any ,
115
60
) -> None :
116
61
name = serialized .get ("name" ) or kwargs .get ("name" ) or "ChatLLM"
117
- span = self ._create_llm_span (
62
+ span = self .span_manager . create_llm_span (
118
63
run_id = run_id ,
119
64
parent_run_id = parent_run_id ,
120
65
name = name ,
@@ -170,7 +115,7 @@ def on_llm_end(
170
115
parent_run_id : Optional [UUID ] = None ,
171
116
** kwargs : Any ,
172
117
) -> None :
173
- span = self ._get_span (run_id )
118
+ span = self .span_manager . get_span (run_id )
174
119
175
120
finish_reasons : List [str ] = []
176
121
for generation in getattr (response , "generations" , []): # type: ignore
@@ -218,7 +163,7 @@ def on_llm_end(
218
163
)
219
164
220
165
# End the LLM span
221
- self ._end_span (run_id )
166
+ self .span_manager . end_span (run_id )
222
167
223
168
@dont_throw
224
169
def on_llm_error (
@@ -232,9 +177,9 @@ def on_llm_error(
232
177
self ._handle_error (error , run_id )
233
178
234
179
def _handle_error (self , error : BaseException , run_id : UUID ):
235
- span = self ._get_span (run_id )
180
+ span = self .span_manager . get_span (run_id )
236
181
span .set_status (Status (StatusCode .ERROR , str (error )))
237
182
span .set_attribute (
238
183
ErrorAttributes .ERROR_TYPE , type (error ).__qualname__
239
184
)
240
- self ._end_span (run_id )
185
+ self .span_manager . end_span (run_id )
0 commit comments