File tree Expand file tree Collapse file tree 3 files changed +36
-4
lines changed
instrumentation-genai/opentelemetry-instrumentation-langchain
src/opentelemetry/instrumentation/langchain Expand file tree Collapse file tree 3 files changed +36
-4
lines changed Original file line number Diff line number Diff line change @@ -27,7 +27,8 @@ classifiers = [
2727dependencies = [
2828 " opentelemetry-api >= 1.31.0" ,
2929 " opentelemetry-instrumentation ~= 0.57b0" ,
30- " opentelemetry-semantic-conventions ~= 0.57b0"
30+ " opentelemetry-semantic-conventions ~= 0.57b0" ,
31+ " cachetools ~= 5.0"
3132]
3233
3334[project .optional-dependencies ]
Original file line number Diff line number Diff line change 1616from typing import Dict , List , Optional
1717from uuid import UUID
1818
19+ from cachetools import TTLCache
1920from opentelemetry .semconv ._incubating .attributes import (
2021 gen_ai_attributes as GenAI ,
2122)
@@ -42,8 +43,8 @@ def __init__(
4243 self ._tracer = tracer
4344
4445 # Map from run_id -> _SpanState, to keep track of spans and parent/child relationships
45- # TODO: Use weak references or a TTL cache to avoid memory leaks in long-running processes. See #3735
46- self .spans : Dict [UUID , _SpanState ] = {}
46+ # Using a TTL cache to avoid memory leaks in long-running processes where end_span might not be called.
47+ self .spans : TTLCache [UUID , _SpanState ] = TTLCache ( maxsize = 1024 , ttl = 3600 )
4748
4849 def _create_span (
4950 self ,
@@ -92,12 +93,16 @@ def create_chat_span(
9293 return span
9394
9495 def end_span (self , run_id : UUID ) -> None :
95- state = self .spans [run_id ]
96+ state = self .spans .get (run_id )
97+ if not state :
98+ return
99+
96100 for child_id in state .children :
97101 child_state = self .spans .get (child_id )
98102 if child_state :
99103 child_state .span .end ()
100104 del self .spans [child_id ]
105+
101106 state .span .end ()
102107 del self .spans [run_id ]
103108
Original file line number Diff line number Diff line change 11import unittest .mock
22import uuid
3+ import time
34
45import pytest
56
@@ -98,3 +99,28 @@ def test_end_span(self, handler):
9899 child_mock_span .end .assert_called_once ()
99100 assert run_id not in handler .spans
100101 assert child_run_id not in handler .spans
102+
103+ @unittest .mock .patch (
104+ "opentelemetry.instrumentation.langchain.span_manager.TTLCache" ,
105+ )
106+ def test_span_ttl_expiration (self , mock_ttlc_class , tracer ):
107+ # Arrange
108+ from cachetools import TTLCache
109+
110+ mock_ttlc_class .side_effect = lambda maxsize , ttl : TTLCache (
111+ maxsize = 1024 , ttl = 0.01
112+ )
113+ handler = _SpanManager (tracer = tracer )
114+ run_id = uuid .uuid4 ()
115+
116+ # Act
117+ handler ._create_span (run_id , None , "test_span" )
118+
119+ # Assert: Span is present immediately after creation
120+ assert run_id in handler .spans
121+
122+ # Wait
123+ time .sleep (0.02 )
124+
125+ # Assert: Span is gone after TTL expiration
126+ assert run_id not in handler .spans
You can’t perform that action at this time.
0 commit comments