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 = [
27
27
dependencies = [
28
28
" opentelemetry-api >= 1.31.0" ,
29
29
" opentelemetry-instrumentation ~= 0.57b0" ,
30
- " opentelemetry-semantic-conventions ~= 0.57b0"
30
+ " opentelemetry-semantic-conventions ~= 0.57b0" ,
31
+ " cachetools ~= 5.0"
31
32
]
32
33
33
34
[project .optional-dependencies ]
Original file line number Diff line number Diff line change 16
16
from typing import Dict , List , Optional
17
17
from uuid import UUID
18
18
19
+ from cachetools import TTLCache
19
20
from opentelemetry .semconv ._incubating .attributes import (
20
21
gen_ai_attributes as GenAI ,
21
22
)
@@ -42,8 +43,8 @@ def __init__(
42
43
self ._tracer = tracer
43
44
44
45
# 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 )
47
48
48
49
def _create_span (
49
50
self ,
@@ -92,12 +93,16 @@ def create_chat_span(
92
93
return span
93
94
94
95
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
+
96
100
for child_id in state .children :
97
101
child_state = self .spans .get (child_id )
98
102
if child_state :
99
103
child_state .span .end ()
100
104
del self .spans [child_id ]
105
+
101
106
state .span .end ()
102
107
del self .spans [run_id ]
103
108
Original file line number Diff line number Diff line change 1
1
import unittest .mock
2
2
import uuid
3
+ import time
3
4
4
5
import pytest
5
6
@@ -98,3 +99,28 @@ def test_end_span(self, handler):
98
99
child_mock_span .end .assert_called_once ()
99
100
assert run_id not in handler .spans
100
101
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