Skip to content

Commit 0a74f67

Browse files
committed
fix(fastapi-instrumentation): properly remove app from instrumented list to avoid memory leaks
1 parent c1a6895 commit 0a74f67

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ def uninstrument_app(app: fastapi.FastAPI):
358358
app.middleware_stack = app.build_middleware_stack()
359359
app._is_instrumented_by_opentelemetry = False
360360

361+
# Remove the app from the set of instrumented apps to prevent memory leaks
362+
if app in _InstrumentedFastAPI._instrumented_fastapi_apps:
363+
_InstrumentedFastAPI._instrumented_fastapi_apps.remove(app)
364+
361365
def instrumentation_dependencies(self) -> Collection[str]:
362366
return _instruments
363367

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
15+
import sys
16+
import unittest
17+
18+
import fastapi
19+
20+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
21+
22+
23+
class TestFastAPIMemoryLeak(unittest.TestCase):
24+
"""Test for memory leak in FastAPIInstrumentor.uninstrument_app()"""
25+
26+
def test_refcount_after_uninstrument(self):
27+
"""Test that refcount is restored after uninstrument_app()"""
28+
app = fastapi.FastAPI()
29+
30+
# Instrument the app
31+
FastAPIInstrumentor.instrument_app(app)
32+
refcount_after_instrument = sys.getrefcount(app)
33+
34+
# Uninstrument the app
35+
FastAPIInstrumentor.uninstrument_app(app)
36+
refcount_after_uninstrument = sys.getrefcount(app)
37+
38+
# The refcount should be reduced after uninstrument (may not be exactly initial due to Python internals)
39+
self.assertLess(
40+
refcount_after_uninstrument,
41+
refcount_after_instrument,
42+
"Refcount should be reduced after uninstrument_app()",
43+
)
44+
45+
# Verify that the app was removed from the set
46+
from opentelemetry.instrumentation.fastapi import _InstrumentedFastAPI
47+
48+
self.assertNotIn(
49+
app,
50+
_InstrumentedFastAPI._instrumented_fastapi_apps,
51+
"App should be removed from _instrumented_fastapi_apps after uninstrument_app()",
52+
)
53+
54+
def test_multiple_instrument_uninstrument_cycles(self):
55+
"""Test that multiple instrument/uninstrument cycles don't leak memory"""
56+
app = fastapi.FastAPI()
57+
58+
initial_refcount = sys.getrefcount(app)
59+
60+
# Perform multiple instrument/uninstrument cycles
61+
for i in range(5):
62+
FastAPIInstrumentor.instrument_app(app)
63+
FastAPIInstrumentor.uninstrument_app(app)
64+
65+
final_refcount = sys.getrefcount(app)
66+
67+
# The refcount should not grow significantly after multiple cycles
68+
# (may not be exactly initial due to Python internals)
69+
self.assertLessEqual(
70+
final_refcount,
71+
initial_refcount
72+
+ 2, # Allow small increase due to Python internals
73+
f"Refcount after {i+1} instrument/uninstrument cycles should not grow significantly",
74+
)
75+
76+
# Verify that the app is not in the set
77+
from opentelemetry.instrumentation.fastapi import _InstrumentedFastAPI
78+
79+
self.assertNotIn(
80+
app,
81+
_InstrumentedFastAPI._instrumented_fastapi_apps,
82+
"App should not be in _instrumented_fastapi_apps after uninstrument_app()",
83+
)
84+
85+
def test_multiple_apps_instrument_uninstrument(self):
86+
"""Test that multiple apps can be instrumented and uninstrumented without leaks"""
87+
apps = [fastapi.FastAPI() for _ in range(3)]
88+
initial_refcounts = [sys.getrefcount(app) for app in apps]
89+
90+
# Instrument all apps
91+
for app in apps:
92+
FastAPIInstrumentor.instrument_app(app)
93+
94+
# Uninstrument all apps
95+
for app in apps:
96+
FastAPIInstrumentor.uninstrument_app(app)
97+
98+
# Check that refcounts are not significantly increased
99+
for i, app in enumerate(apps):
100+
final_refcount = sys.getrefcount(app)
101+
self.assertLessEqual(
102+
final_refcount,
103+
initial_refcounts[i]
104+
+ 2, # Allow small increase due to Python internals
105+
f"App {i} refcount should not grow significantly",
106+
)
107+
108+
# Verify that no apps are in the set
109+
from opentelemetry.instrumentation.fastapi import _InstrumentedFastAPI
110+
111+
for app in apps:
112+
self.assertNotIn(
113+
app,
114+
_InstrumentedFastAPI._instrumented_fastapi_apps,
115+
"All apps should be removed from _instrumented_fastapi_apps",
116+
)
117+
118+
def test_demonstrate_fix(self):
119+
"""Demonstrate the fix for the memory leak issue"""
120+
app = fastapi.FastAPI()
121+
122+
# Before the fix: app would remain in _instrumented_fastapi_apps after uninstrument_app()
123+
# After the fix: app should be removed from _instrumented_fastapi_apps
124+
125+
FastAPIInstrumentor.instrument_app(app)
126+
from opentelemetry.instrumentation.fastapi import _InstrumentedFastAPI
127+
128+
# Verify app is in the set after instrumentation
129+
self.assertIn(app, _InstrumentedFastAPI._instrumented_fastapi_apps)
130+
131+
FastAPIInstrumentor.uninstrument_app(app)
132+
133+
# Verify app is removed from the set after uninstrumentation
134+
self.assertNotIn(app, _InstrumentedFastAPI._instrumented_fastapi_apps)
135+
self.assertEqual(
136+
len(_InstrumentedFastAPI._instrumented_fastapi_apps), 0
137+
)
138+
139+
140+
if __name__ == "__main__":
141+
unittest.main()

0 commit comments

Comments
 (0)