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+
315from typing import Any , Dict , List , Optional
416from uuid import UUID
517
618from langchain_core .callbacks import BaseCallbackHandler # type: ignore
719from langchain_core .messages import BaseMessage # type: ignore
820from langchain_core .outputs import LLMResult # type: ignore
921
10- from opentelemetry .context import Context , get_current
22+ from opentelemetry .instrumentation . langchain . span_manager import SpanManager
1123from opentelemetry .instrumentation .langchain .utils import dont_throw
1224from opentelemetry .semconv ._incubating .attributes import (
1325 gen_ai_attributes as GenAI ,
1426)
1527from opentelemetry .semconv .attributes import (
1628 error_attributes as ErrorAttributes ,
1729)
18- from opentelemetry .trace import Span , SpanKind , Tracer , set_span_in_context
30+ from opentelemetry .trace import Tracer
1931from opentelemetry .trace .status import Status , StatusCode
2032
2133
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-
3034class OpenTelemetryLangChainCallbackHandler (BaseCallbackHandler ): # type: ignore[misc]
3135 """
3236 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__(
3741 tracer : Tracer ,
3842 ) -> None :
3943 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 )
6844
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 ,
8247 )
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
10348
10449 @dont_throw
10550 def on_chat_model_start (
@@ -114,7 +59,7 @@ def on_chat_model_start(
11459 ** kwargs : Any ,
11560 ) -> None :
11661 name = serialized .get ("name" ) or kwargs .get ("name" ) or "ChatLLM"
117- span = self ._create_llm_span (
62+ span = self .span_manager . create_llm_span (
11863 run_id = run_id ,
11964 parent_run_id = parent_run_id ,
12065 name = name ,
@@ -170,7 +115,7 @@ def on_llm_end(
170115 parent_run_id : Optional [UUID ] = None ,
171116 ** kwargs : Any ,
172117 ) -> None :
173- span = self ._get_span (run_id )
118+ span = self .span_manager . get_span (run_id )
174119
175120 finish_reasons : List [str ] = []
176121 for generation in getattr (response , "generations" , []): # type: ignore
@@ -218,7 +163,7 @@ def on_llm_end(
218163 )
219164
220165 # End the LLM span
221- self ._end_span (run_id )
166+ self .span_manager . end_span (run_id )
222167
223168 @dont_throw
224169 def on_llm_error (
@@ -232,9 +177,9 @@ def on_llm_error(
232177 self ._handle_error (error , run_id )
233178
234179 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 )
236181 span .set_status (Status (StatusCode .ERROR , str (error )))
237182 span .set_attribute (
238183 ErrorAttributes .ERROR_TYPE , type (error ).__qualname__
239184 )
240- self ._end_span (run_id )
185+ self .span_manager . end_span (run_id )
0 commit comments