Skip to content

Commit 2672bd5

Browse files
authored
Merge branch 'main' into jacksonweber/populate-synthetic-attributes
2 parents 1385bce + 032d6c6 commit 2672bd5

31 files changed

+1775
-11
lines changed

.github/component_owners.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,11 @@ components:
3333
- dorkolog
3434

3535
propagator/opentelemetry-propagator-aws-xray:
36-
- NathanielRN
36+
- jj22ee
3737

3838
sdk-extension/opentelemetry-sdk-extension-aws:
39-
- NathanielRN
40-
- Kausik-A
4139
- srprash
40+
- jj22ee
4241

4342
instrumentation/opentelemetry-instrumentation-tortoiseorm:
4443
- tonybaloney

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818

1919
### Fixed
2020

21+
- `opentelemetry-instrumentation-fastapi`: Fix memory leak in `uninstrument_app()` by properly removing apps from the tracking set
22+
([#3688](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3688)
2123
- `opentelemetry-instrumentation-tornado` Fix server (request) duration metric calculation
2224
([#3679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3679))
2325
- `opentelemetry-instrumentation`: Avoid calls to `context.detach` with `None` token.
@@ -27,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2729

2830
- `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.11.0
2931
([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685))
32+
- `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics
33+
([#3666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3666))
34+
- `opentelemetry-sdk-extension-aws` Add AWS X-Ray Remote Sampler with initial Rules Poller implementation
35+
([#3366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3366))
3036

3137
## Version 1.36.0/0.57b0 (2025-07-29)
3238

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
186186
import logging
187187
import types
188188
from typing import Collection, Literal
189+
from weakref import WeakSet as _WeakSet
189190

190191
import fastapi
191192
from starlette.applications import Starlette
@@ -358,6 +359,11 @@ def uninstrument_app(app: fastapi.FastAPI):
358359
app.middleware_stack = app.build_middleware_stack()
359360
app._is_instrumented_by_opentelemetry = False
360361

362+
# Remove the app from the set of instrumented apps to avoid calling uninstrument twice
363+
# if the instrumentation is later disabled or such
364+
# Use discard to avoid KeyError if already GC'ed
365+
_InstrumentedFastAPI._instrumented_fastapi_apps.discard(app)
366+
361367
def instrumentation_dependencies(self) -> Collection[str]:
362368
return _instruments
363369

@@ -388,7 +394,11 @@ def _instrument(self, **kwargs):
388394
fastapi.FastAPI = _InstrumentedFastAPI
389395

390396
def _uninstrument(self, **kwargs):
391-
for instance in _InstrumentedFastAPI._instrumented_fastapi_apps:
397+
# Create a copy of the set to avoid RuntimeError during iteration
398+
instances_to_uninstrument = list(
399+
_InstrumentedFastAPI._instrumented_fastapi_apps
400+
)
401+
for instance in instances_to_uninstrument:
392402
self.uninstrument_app(instance)
393403
_InstrumentedFastAPI._instrumented_fastapi_apps.clear()
394404
fastapi.FastAPI = self._original_fastapi
@@ -406,7 +416,8 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
406416
_http_capture_headers_sanitize_fields: list[str] | None = None
407417
_exclude_spans: list[Literal["receive", "send"]] | None = None
408418

409-
_instrumented_fastapi_apps = set()
419+
# Track instrumented app instances using weak references to avoid GC leaks
420+
_instrumented_fastapi_apps = _WeakSet()
410421
_sem_conv_opt_in_mode = _StabilityMode.DEFAULT
411422

412423
def __init__(self, *args, **kwargs):
@@ -426,10 +437,6 @@ def __init__(self, *args, **kwargs):
426437
)
427438
_InstrumentedFastAPI._instrumented_fastapi_apps.add(self)
428439

429-
def __del__(self):
430-
if self in _InstrumentedFastAPI._instrumented_fastapi_apps:
431-
_InstrumentedFastAPI._instrumented_fastapi_apps.remove(self)
432-
433440

434441
def _get_route_details(scope):
435442
"""

instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
# pylint: disable=too-many-lines
1616

17+
import gc as _gc
1718
import unittest
19+
import weakref as _weakref
1820
from contextlib import ExitStack
1921
from timeit import default_timer
2022
from unittest.mock import Mock, call, patch
@@ -1400,6 +1402,16 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
14001402
)
14011403

14021404

1405+
class TestFastAPIGarbageCollection(unittest.TestCase):
1406+
def test_fastapi_app_is_collected_after_instrument(self):
1407+
app = fastapi.FastAPI()
1408+
otel_fastapi.FastAPIInstrumentor().instrument_app(app)
1409+
app_ref = _weakref.ref(app)
1410+
del app
1411+
_gc.collect()
1412+
self.assertIsNone(app_ref())
1413+
1414+
14031415
@patch.dict(
14041416
"os.environ",
14051417
{

instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
# pylint: disable=too-many-lines
15+
1416
"""
1517
Instrument to report system (CPU, memory, network) and
1618
process (CPU, memory, garbage collection) metrics. By default, the
@@ -45,6 +47,8 @@
4547
"process.runtime.cpu.time": ["user", "system"],
4648
"process.runtime.gc_count": None,
4749
"cpython.gc.collections": None,
50+
"cpython.gc.collected_objects": None,
51+
"cpython.gc.uncollectable_objects": None,
4852
"process.runtime.thread_count": None,
4953
"process.runtime.cpu.utilization": None,
5054
"process.runtime.context_switches": ["involuntary", "voluntary"],
@@ -138,6 +142,8 @@
138142
"process.runtime.cpu.time": ["user", "system"],
139143
"process.runtime.gc_count": None,
140144
"cpython.gc.collections": None,
145+
"cpython.gc.collected_objects": None,
146+
"cpython.gc.uncollectable_objects": None,
141147
"process.runtime.thread_count": None,
142148
"process.runtime.cpu.utilization": None,
143149
"process.runtime.context_switches": ["involuntary", "voluntary"],
@@ -199,6 +205,8 @@ def __init__(
199205
self._runtime_cpu_time_labels = self._labels.copy()
200206
self._runtime_gc_count_labels = self._labels.copy()
201207
self._runtime_gc_collections_labels = self._labels.copy()
208+
self._runtime_gc_collected_objects_labels = self._labels.copy()
209+
self._runtime_gc_uncollectable_objects_labels = self._labels.copy()
202210
self._runtime_thread_count_labels = self._labels.copy()
203211
self._runtime_cpu_utilization_labels = self._labels.copy()
204212
self._runtime_context_switches_labels = self._labels.copy()
@@ -486,6 +494,32 @@ def _instrument(self, **kwargs: Any):
486494
unit="{collection}",
487495
)
488496

497+
if "cpython.gc.collected_objects" in self._config:
498+
if self._python_implementation == "pypy":
499+
_logger.warning(
500+
"The cpython.gc.collected_objects metric won't be collected because the interpreter is PyPy"
501+
)
502+
else:
503+
self._meter.create_observable_counter(
504+
name="cpython.gc.collected_objects",
505+
callbacks=[self._get_runtime_gc_collected_objects],
506+
description="The total number of objects collected since interpreter start.",
507+
unit="{object}",
508+
)
509+
510+
if "cpython.gc.uncollectable_objects" in self._config:
511+
if self._python_implementation == "pypy":
512+
_logger.warning(
513+
"The cpython.gc.uncollectable_objects metric won't be collected because the interpreter is PyPy"
514+
)
515+
else:
516+
self._meter.create_observable_counter(
517+
name="cpython.gc.uncollectable_objects",
518+
callbacks=[self._get_runtime_gc_uncollectable_objects],
519+
description="The total number of uncollectable objects found since interpreter start.",
520+
unit="{object}",
521+
)
522+
489523
if "process.runtime.thread_count" in self._config:
490524
self._meter.create_observable_up_down_counter(
491525
name=f"process.runtime.{self._python_implementation}.thread_count",
@@ -911,6 +945,32 @@ def _get_runtime_gc_collections(
911945
stat["collections"], self._runtime_gc_collections_labels.copy()
912946
)
913947

948+
def _get_runtime_gc_collected_objects(
949+
self, options: CallbackOptions
950+
) -> Iterable[Observation]:
951+
"""Observer callback for garbage collection collected objects"""
952+
for index, stat in enumerate(gc.get_stats()):
953+
self._runtime_gc_collected_objects_labels["generation"] = str(
954+
index
955+
)
956+
yield Observation(
957+
stat["collected"],
958+
self._runtime_gc_collected_objects_labels.copy(),
959+
)
960+
961+
def _get_runtime_gc_uncollectable_objects(
962+
self, options: CallbackOptions
963+
) -> Iterable[Observation]:
964+
"""Observer callback for garbage collection uncollectable objects"""
965+
for index, stat in enumerate(gc.get_stats()):
966+
self._runtime_gc_uncollectable_objects_labels["generation"] = str(
967+
index
968+
)
969+
yield Observation(
970+
stat["uncollectable"],
971+
self._runtime_gc_uncollectable_objects_labels.copy(),
972+
)
973+
914974
def _get_runtime_thread_count(
915975
self, options: CallbackOptions
916976
) -> Iterable[Observation]:

instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ def test_system_metrics_instrument(self):
139139
observer_names.append(
140140
"cpython.gc.collections",
141141
)
142+
observer_names.append(
143+
"cpython.gc.collected_objects",
144+
)
145+
observer_names.append(
146+
"cpython.gc.uncollectable_objects",
147+
)
142148
if sys.platform != "darwin":
143149
observer_names.append("system.network.connections")
144150

@@ -983,6 +989,54 @@ def test_runtime_get_gc_collections(self, mock_gc_get_stats):
983989
expected_gc_collections,
984990
)
985991

992+
@mock.patch("gc.get_stats")
993+
@skipIf(
994+
python_implementation().lower() == "pypy", "not supported for pypy"
995+
)
996+
def test_runtime_get_gc_collected_objects(self, mock_gc_get_stats):
997+
mock_gc_get_stats.configure_mock(
998+
**{
999+
"return_value": [
1000+
{"collections": 10, "collected": 100, "uncollectable": 1},
1001+
{"collections": 20, "collected": 200, "uncollectable": 2},
1002+
{"collections": 30, "collected": 300, "uncollectable": 3},
1003+
]
1004+
}
1005+
)
1006+
expected_gc_collected_objects = [
1007+
_SystemMetricsResult({"generation": "0"}, 100),
1008+
_SystemMetricsResult({"generation": "1"}, 200),
1009+
_SystemMetricsResult({"generation": "2"}, 300),
1010+
]
1011+
self._test_metrics(
1012+
"cpython.gc.collected_objects",
1013+
expected_gc_collected_objects,
1014+
)
1015+
1016+
@mock.patch("gc.get_stats")
1017+
@skipIf(
1018+
python_implementation().lower() == "pypy", "not supported for pypy"
1019+
)
1020+
def test_runtime_get_gc_uncollectable_objects(self, mock_gc_get_stats):
1021+
mock_gc_get_stats.configure_mock(
1022+
**{
1023+
"return_value": [
1024+
{"collections": 10, "collected": 100, "uncollectable": 1},
1025+
{"collections": 20, "collected": 200, "uncollectable": 2},
1026+
{"collections": 30, "collected": 300, "uncollectable": 3},
1027+
]
1028+
}
1029+
)
1030+
expected_gc_uncollectable_objects = [
1031+
_SystemMetricsResult({"generation": "0"}, 1),
1032+
_SystemMetricsResult({"generation": "1"}, 2),
1033+
_SystemMetricsResult({"generation": "2"}, 3),
1034+
]
1035+
self._test_metrics(
1036+
"cpython.gc.uncollectable_objects",
1037+
expected_gc_uncollectable_objects,
1038+
)
1039+
9861040
@mock.patch("psutil.Process.num_ctx_switches")
9871041
def test_runtime_context_switches(self, mock_process_num_ctx_switches):
9881042
PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"])

sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ classifiers = [
2525
"Programming Language :: Python :: 3.13",
2626
]
2727
dependencies = [
28-
"opentelemetry-sdk ~= 1.12",
28+
"opentelemetry-api ~= 1.23",
29+
"opentelemetry-sdk ~= 1.23",
30+
"opentelemetry-instrumentation ~= 0.44b0",
31+
"opentelemetry-semantic-conventions ~= 0.44b0",
32+
"requests ~= 2.28",
2933
]
3034

3135
[project.entry-points.opentelemetry_id_generator]
@@ -38,6 +42,10 @@ aws_eks = "opentelemetry.sdk.extension.aws.resource.eks:AwsEksResourceDetector"
3842
aws_elastic_beanstalk = "opentelemetry.sdk.extension.aws.resource.beanstalk:AwsBeanstalkResourceDetector"
3943
aws_lambda = "opentelemetry.sdk.extension.aws.resource._lambda:AwsLambdaResourceDetector"
4044

45+
# TODO: Uncomment this when Sampler implementation is complete
46+
# [project.entry-points.opentelemetry_sampler]
47+
# aws_xray_remote_sampler = "opentelemetry.sdk.extension.aws.trace.sampler:AwsXRayRemoteSampler"
48+
4149
[project.urls]
4250
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/sdk-extension/opentelemetry-sdk-extension-aws"
4351
Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
# pylint: disable=no-name-in-module
16+
from opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler import (
17+
_AwsXRayRemoteSampler,
18+
)
19+
20+
__all__ = ["_AwsXRayRemoteSampler"]

0 commit comments

Comments
 (0)