Skip to content

Commit 5c49e58

Browse files
initial commit
1 parent e3cb053 commit 5c49e58

File tree

4 files changed

+67
-31
lines changed

4 files changed

+67
-31
lines changed

instrumentation-genai/opentelemetry-instrumentation-langchain/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## Unreleased
99

1010
- Added span support for genAI langchain llm invocation.
11-
([#3665](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3665))
11+
([#3665](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3665))
12+
- Use weak reference in langchain instrumentation span map.
13+
([#3735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3735))

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.2.0",
3132
]
3233

3334
[project.optional-dependencies]

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

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
# limitations under the License.
1414

1515
from dataclasses import dataclass, field
16+
from threading import RLock
1617
from typing import Dict, List, Optional
1718
from uuid import UUID
1819

20+
from cachetools import TTLCache
1921
from opentelemetry.semconv._incubating.attributes import (
2022
gen_ai_attributes as GenAI,
2123
)
@@ -40,10 +42,11 @@ def __init__(
4042
tracer: Tracer,
4143
) -> None:
4244
self._tracer = tracer
45+
self._lock = RLock()
4346

4447
# 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] = {}
48+
# Using a TTL cache to avoid memory leaks in long-running processes where end_span might not be called.
49+
self.spans: TTLCache[UUID, _SpanState] = TTLCache(maxsize=1024, ttl=3600)
4750

4851
def _create_span(
4952
self,
@@ -52,23 +55,24 @@ def _create_span(
5255
span_name: str,
5356
kind: SpanKind = SpanKind.INTERNAL,
5457
) -> Span:
55-
if parent_run_id is not None and parent_run_id in self.spans:
56-
parent_state = self.spans[parent_run_id]
57-
parent_span = parent_state.span
58-
ctx = set_span_in_context(parent_span)
59-
span = self._tracer.start_span(
60-
name=span_name, kind=kind, context=ctx
61-
)
62-
parent_state.children.append(run_id)
63-
else:
64-
# top-level or missing parent
65-
span = self._tracer.start_span(name=span_name, kind=kind)
66-
set_span_in_context(span)
67-
68-
span_state = _SpanState(span=span)
69-
self.spans[run_id] = span_state
70-
71-
return span
58+
with self._lock:
59+
if parent_run_id is not None and parent_run_id in self.spans:
60+
parent_state = self.spans[parent_run_id]
61+
parent_span = parent_state.span
62+
ctx = set_span_in_context(parent_span)
63+
span = self._tracer.start_span(
64+
name=span_name, kind=kind, context=ctx
65+
)
66+
parent_state.children.append(run_id)
67+
else:
68+
# top-level or missing parent
69+
span = self._tracer.start_span(name=span_name, kind=kind)
70+
set_span_in_context(span)
71+
72+
span_state = _SpanState(span=span)
73+
self.spans[run_id] = span_state
74+
75+
return span
7276

7377
def create_chat_span(
7478
self,
@@ -92,18 +96,25 @@ def create_chat_span(
9296
return span
9397

9498
def end_span(self, run_id: UUID) -> None:
95-
state = self.spans[run_id]
96-
for child_id in state.children:
97-
child_state = self.spans.get(child_id)
98-
if child_state:
99-
child_state.span.end()
100-
del self.spans[child_id]
101-
state.span.end()
102-
del self.spans[run_id]
99+
with self._lock:
100+
state = self.spans.get(run_id)
101+
if not state:
102+
return
103+
# End children first (make a copy to avoid modification during iteration)
104+
for child_id in list(state.children):
105+
child_state = self.spans.get(child_id)
106+
if child_state:
107+
child_state.span.end()
108+
# Use pop to avoid KeyError if already expired
109+
self.spans.pop(child_id, None)
110+
state.span.end()
111+
# Use pop to avoid KeyError if already expired
112+
self.spans.pop(run_id, None)
103113

104114
def get_span(self, run_id: UUID) -> Optional[Span]:
105-
state = self.spans.get(run_id)
106-
return state.span if state else None
115+
with self._lock:
116+
state = self.spans.get(run_id)
117+
return state.span if state else None
107118

108119
def handle_error(self, error: BaseException, run_id: UUID):
109120
span = self.get_span(run_id)

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

Lines changed: 22 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,24 @@ 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+
def test_ttl_cache_expires_spans(self, tracer):
104+
# Arrange - Create handler with short TTL
105+
from cachetools import TTLCache
106+
107+
handler = _SpanManager(tracer=tracer)
108+
# Replace the cache with one that has a very short TTL
109+
handler.spans = TTLCache(maxsize=1024, ttl=0.05)
110+
111+
run_id = uuid.uuid4()
112+
mock_span = unittest.mock.Mock(spec=Span)
113+
handler.spans[run_id] = _SpanState(span=mock_span)
114+
115+
# Assert - Span exists immediately
116+
assert run_id in handler.spans
117+
118+
# Act - Wait for TTL to expire
119+
time.sleep(0.1)
120+
121+
# Assert - Span is automatically removed by TTL
122+
assert run_id not in handler.spans

0 commit comments

Comments
 (0)