Skip to content

Commit 0465cb3

Browse files
committed
initial commit
1 parent c1a9f1f commit 0465cb3

File tree

3 files changed

+36
-4
lines changed

3 files changed

+36
-4
lines changed

instrumentation-genai/opentelemetry-instrumentation-langchain/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ classifiers = [
2727
dependencies = [
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]

instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/span_manager.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Dict, List, Optional
1717
from uuid import UUID
1818

19+
from cachetools import TTLCache
1920
from 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

instrumentation-genai/opentelemetry-instrumentation-langchain/tests/test_span_manager.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest.mock
22
import uuid
3+
import time
34

45
import 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

0 commit comments

Comments
 (0)