diff --git a/.github/workflows/addlicense.yml b/.github/workflows/addlicense.yml index 8d66691ff7..83e5b29ef4 100644 --- a/.github/workflows/addlicense.yml +++ b/.github/workflows/addlicense.yml @@ -39,7 +39,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 513e467f29..a65695e7c4 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -38,7 +38,7 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/build-ci-image.yml b/.github/workflows/build-ci-image.yml index ab183f48a2..061233b6dd 100644 --- a/.github/workflows/build-ci-image.yml +++ b/.github/workflows/build-ci-image.yml @@ -43,7 +43,7 @@ jobs: name: Docker Build ${{ matrix.platform }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 with: persist-credentials: false fetch-depth: 0 @@ -60,7 +60,7 @@ jobs: - name: Generate Docker Metadata (Tags and Labels) id: meta - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # 5.8.0 + uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # 5.9.0 with: images: ghcr.io/${{ steps.image-name.outputs.IMAGE_NAME }} flavor: | @@ -139,7 +139,7 @@ jobs: - name: Generate Docker Metadata (Tags and Labels) id: meta - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # 5.8.0 + uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # 5.9.0 with: images: ghcr.io/${{ steps.image-name.outputs.IMAGE_NAME }} flavor: | diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8b469eaacb..af4739f2a3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -69,14 +69,14 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 with: persist-credentials: false fetch-depth: 0 - name: Setup QEMU if: runner.os == 'Linux' - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # 3.6.0 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # 3.7.0 with: platforms: arm64 @@ -109,7 +109,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml index 8f74866d43..0f869f3b58 100644 --- a/.github/workflows/mega-linter.yml +++ b/.github/workflows/mega-linter.yml @@ -45,7 +45,7 @@ jobs: steps: # Git Checkout - name: Checkout Code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} fetch-depth: 0 # Required for pushing commits to PRs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e9ef7b2d4e..9e47302bd4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -93,7 +93,7 @@ jobs: - tests steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # 6.0.0 with: python-version: "3.13" @@ -127,7 +127,7 @@ jobs: - tests steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # 6.0.0 with: python-version: "3.13" @@ -166,7 +166,7 @@ jobs: --add-host=host.docker.internal:host-gateway timeout-minutes: 30 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -231,7 +231,7 @@ jobs: --add-host=host.docker.internal:host-gateway timeout-minutes: 30 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -294,14 +294,14 @@ jobs: runs-on: windows-2025 timeout-minutes: 30 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | git fetch --tags origin - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # 7.1.3 - name: Install Python run: | @@ -363,14 +363,14 @@ jobs: runs-on: windows-11-arm timeout-minutes: 30 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | git fetch --tags origin - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # 7.1.3 - name: Install Python run: | @@ -443,7 +443,7 @@ jobs: --add-host=host.docker.internal:host-gateway timeout-minutes: 30 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -526,7 +526,7 @@ jobs: --health-retries 10 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -606,7 +606,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -687,7 +687,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -772,7 +772,7 @@ jobs: # from every being executed as bash commands. steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -837,7 +837,7 @@ jobs: --add-host=host.docker.internal:host-gateway timeout-minutes: 30 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -927,7 +927,7 @@ jobs: KAFKA_CFG_INTER_BROKER_LISTENER_NAME: L3 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1005,7 +1005,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1083,7 +1083,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1161,7 +1161,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1244,7 +1244,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1327,7 +1327,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1406,7 +1406,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1487,7 +1487,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1567,7 +1567,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1647,7 +1647,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1726,7 +1726,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1804,7 +1804,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -1923,7 +1923,7 @@ jobs: --add-host=host.docker.internal:host-gateway steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -2003,7 +2003,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | @@ -2081,7 +2081,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 - name: Fetch git tags run: | diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index c373a38bb1..e4b0e38c9c 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -32,7 +32,7 @@ jobs: steps: # Git Checkout - name: Checkout Code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 5.0.1 with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} fetch-depth: 0 @@ -61,6 +61,6 @@ jobs: - name: Upload Trivy scan results to GitHub Security tab if: ${{ github.event_name == 'schedule' }} - uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # 4.31.2 + uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # 4.31.3 with: sarif_file: "trivy-results.sarif" diff --git a/newrelic/api/asgi_application.py b/newrelic/api/asgi_application.py index 669d3e6db5..6b9a31130e 100644 --- a/newrelic/api/asgi_application.py +++ b/newrelic/api/asgi_application.py @@ -132,10 +132,20 @@ async def send_inject_browser_agent(self, message): message_type = message["type"] if message_type == "http.response.start" and not self.initial_message: - headers = list(message.get("headers", ())) + # message["headers"] may be a generator, and consuming it via process_response will leave the original + # application with no headers. Fix this by preserving them in a list before consuming them. + if "headers" in message: + message["headers"] = headers = list(message["headers"]) + else: + headers = [] + + # Check if we should insert the HTML snippet based on the headers. + # Currently if there are no headers this will always be False, but call the function + # anyway in case this logic changes in the future. if not self.should_insert_html(headers): await self.abort() return + message["headers"] = headers self.initial_message = message elif message_type == "http.response.body" and self.initial_message: @@ -232,7 +242,13 @@ async def send(self, event): finally: self.__exit__(*sys.exc_info()) elif event["type"] == "http.response.start": - self.process_response(event["status"], event.get("headers", ())) + # event["headers"] may be a generator, and consuming it via process_response will leave the original + # ASGI application with no headers. Fix this by preserving them in a list before consuming them. + if "headers" in event: + event["headers"] = headers = list(event["headers"]) + else: + headers = [] + self.process_response(event["status"], headers) return await self._send(event) diff --git a/newrelic/api/transaction.py b/newrelic/api/transaction.py index 8a5ba37a26..3356b05022 100644 --- a/newrelic/api/transaction.py +++ b/newrelic/api/transaction.py @@ -1084,8 +1084,8 @@ def _make_sampling_decision(self): priority, sampled, full_granularity=True, - remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled, - remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled, + remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity._remote_parent_sampled, + remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.full_granularity._remote_parent_not_sampled, ) _logger.debug("Full granularity sampling decision was %s with priority=%s.", sampled, priority) if computed_sampled or not self.settings.distributed_tracing.sampler.partial_granularity.enabled: @@ -1101,8 +1101,8 @@ def _make_sampling_decision(self): priority, sampled, full_granularity=False, - remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled, - remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled, + remote_parent_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity._remote_parent_sampled, + remote_parent_not_sampled_setting=self.settings.distributed_tracing.sampler.partial_granularity._remote_parent_not_sampled, ) _logger.debug( "Partial granularity sampling decision was %s with priority=%s.", self._sampled, self._priority diff --git a/newrelic/config.py b/newrelic/config.py index 41d118961f..c3944aa28c 100644 --- a/newrelic/config.py +++ b/newrelic/config.py @@ -337,7 +337,11 @@ def _process_dt_setting(section, option_p1, option_p2, getter): while True: if len(fields) == 1: value = value1 or value2 or "default" - setattr(target, fields[0], value) + # Store the value at the underscored location so if option_p1 is + # distributed_tracing.sampler.full_granularity.remote_parent_sampled + # store it at location + # distributed_tracing.sampler.full_granularity._remote_parent_sampled + setattr(target, f"_{fields[0]}", value) break target = getattr(target, fields[0]) fields = fields[1].split(".", 1) @@ -360,6 +364,90 @@ def _process_dt_setting(section, option_p1, option_p2, getter): _raise_configuration_error(section, option_p1) +def _process_dt_hidden_setting(section, option, getter): + try: + # The type of a value is dictated by the getter + # function supplied. + + value = getattr(_config_object, getter)(section, option) + + # Now need to apply the option from the + # configuration file to the internal settings + # object. Walk the object path and assign it. + + target = _settings + fields = option.split(".", 1) + + while True: + if len(fields) == 1: + value = value or "default" + # Store the value at the underscored location so if option is + # distributed_tracing.sampler.full_granularity.remote_parent_sampled + # store it at location + # distributed_tracing.sampler.full_granularity._remote_parent_sampled + setattr(target, f"_{fields[0]}", value) + break + target = getattr(target, fields[0]) + fields = fields[1].split(".", 1) + + # Cache the configuration so can be dumped out to + # log file when whole main configuration has been + # processed. This ensures that the log file and log + # level entries have been set. + + _cache_object.append((option, value)) + + except configparser.NoSectionError: + pass + + except configparser.NoOptionError: + pass + + except Exception: + _raise_configuration_error(section, option) + + +def _process_dt_sampler_setting(section, option, getter): + try: + # The type of a value is dictated by the getter + # function supplied. + + value = getattr(_config_object, getter)(section, option) + + # Now need to apply the option from the + # configuration file to the internal settings + # object. Walk the object path and assign it. + + target = _settings + fields = option.split(".", 1) + + while True: + if len(fields) == 1: + setattr(target, f"{fields[0]}", value) + break + elif fields[0] in ("root", "remote_parent_sampled", "remote_parent_not_sampled"): + sampler = fields[1].split(".", 1)[0] + setattr(target, f"_{fields[0]}", sampler) + target = getattr(target, fields[0]) + fields = fields[1].split(".", 1) + + # Cache the configuration so can be dumped out to + # log file when whole main configuration has been + # processed. This ensures that the log file and log + # level entries have been set. + + _cache_object.append((option, value)) + + except configparser.NoSectionError: + pass + + except configparser.NoOptionError: + pass + + except Exception: + _raise_configuration_error(section, option) + + # Processing of all the settings for specified section except # for log file and log level which are applied separately to # ensure they are set as soon as possible. @@ -452,17 +540,37 @@ def _process_configuration(section): "distributed_tracing.sampler.remote_parent_sampled", "get", ) + _process_dt_sampler_setting( + section, "distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target", "getint" + ) _process_dt_setting( section, "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled", "distributed_tracing.sampler.remote_parent_not_sampled", "get", ) + _process_dt_sampler_setting( + section, + "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target", + "getint", + ) _process_setting(section, "distributed_tracing.sampler.full_granularity.enabled", "getboolean", None) _process_setting(section, "distributed_tracing.sampler.partial_granularity.enabled", "getboolean", None) _process_setting(section, "distributed_tracing.sampler.partial_granularity.type", "get", None) - _process_setting(section, "distributed_tracing.sampler.partial_granularity.remote_parent_sampled", "get", None) - _process_setting(section, "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled", "get", None) + _process_dt_hidden_setting(section, "distributed_tracing.sampler.partial_granularity.remote_parent_sampled", "get") + _process_dt_sampler_setting( + section, + "distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target", + "getint", + ) + _process_dt_hidden_setting( + section, "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled", "get" + ) + _process_dt_sampler_setting( + section, + "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target", + "getint", + ) _process_setting(section, "span_events.enabled", "getboolean", None) _process_setting(section, "span_events.max_samples_stored", "getint", None) _process_setting(section, "span_events.attributes.enabled", "getboolean", None) @@ -2141,6 +2249,10 @@ def _process_module_builtin_defaults(): "asyncio.base_events", "newrelic.hooks.coroutines_asyncio", "instrument_asyncio_base_events" ) + _process_module_definition("asyncio.events", "newrelic.hooks.coroutines_asyncio", "instrument_asyncio_events") + + _process_module_definition("asyncio.runners", "newrelic.hooks.coroutines_asyncio", "instrument_asyncio_runners") + _process_module_definition( "langchain_core.runnables.base", "newrelic.hooks.mlmodel_langchain", @@ -2728,8 +2840,6 @@ def _process_module_builtin_defaults(): "langchain_core.callbacks.manager", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_callbacks_manager" ) - _process_module_definition("asyncio.events", "newrelic.hooks.coroutines_asyncio", "instrument_asyncio_events") - _process_module_definition("asgiref.sync", "newrelic.hooks.adapter_asgiref", "instrument_asgiref_sync") _process_module_definition( diff --git a/newrelic/core/config.py b/newrelic/core/config.py index fe5c9b5872..230e8067e5 100644 --- a/newrelic/core/config.py +++ b/newrelic/core/config.py @@ -338,10 +338,44 @@ class DistributedTracingSamplerSettings(Settings): class DistributedTracingSamplerFullGranularitySettings(Settings): + _remote_parent_sampled = "default" + _remote_parent_not_sampled = "default" + + +class DistributedTracingSamplerFullGranularityRemoteParentSampledSettings: + pass + + +class DistributedTracingSamplerFullGranularityRemoteParentSampledAdaptiveSettings: + pass + + +class DistributedTracingSamplerFullGranularityRemoteParentNotSampledSettings: + pass + + +class DistributedTracingSamplerFullGranularityRemoteParentNotSampledAdaptiveSettings: pass class DistributedTracingSamplerPartialGranularitySettings(Settings): + _remote_parent_sampled = "default" + _remote_parent_not_sampled = "default" + + +class DistributedTracingSamplerPartialGranularityRemoteParentSampledSettings: + pass + + +class DistributedTracingSamplerPartialGranularityRemoteParentSampledAdaptiveSettings: + pass + + +class DistributedTracingSamplerPartialGranularityRemoteParentNotSampledSettings: + pass + + +class DistributedTracingSamplerPartialGranularityRemoteParentNotSampledAdaptiveSettings: pass @@ -516,7 +550,31 @@ class EventHarvestConfigHarvestLimitSettings(Settings): _settings.distributed_tracing = DistributedTracingSettings() _settings.distributed_tracing.sampler = DistributedTracingSamplerSettings() _settings.distributed_tracing.sampler.full_granularity = DistributedTracingSamplerFullGranularitySettings() +_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = ( + DistributedTracingSamplerFullGranularityRemoteParentSampledSettings() +) +_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive = ( + DistributedTracingSamplerFullGranularityRemoteParentSampledAdaptiveSettings() +) +_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = ( + DistributedTracingSamplerFullGranularityRemoteParentNotSampledSettings() +) +_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive = ( + DistributedTracingSamplerFullGranularityRemoteParentNotSampledAdaptiveSettings() +) _settings.distributed_tracing.sampler.partial_granularity = DistributedTracingSamplerPartialGranularitySettings() +_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled = ( + DistributedTracingSamplerPartialGranularityRemoteParentSampledSettings() +) +_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive = ( + DistributedTracingSamplerPartialGranularityRemoteParentSampledAdaptiveSettings() +) +_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled = ( + DistributedTracingSamplerPartialGranularityRemoteParentNotSampledSettings() +) +_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive = ( + DistributedTracingSamplerPartialGranularityRemoteParentNotSampledAdaptiveSettings() +) _settings.error_collector = ErrorCollectorSettings() _settings.error_collector.attributes = ErrorCollectorAttributesSettings() _settings.event_harvest_config = EventHarvestConfigSettings() @@ -561,6 +619,8 @@ class EventHarvestConfigHarvestLimitSettings(Settings): def _environ_as_int(name, default=0): val = os.environ.get(name, default) try: + if default is None and val is None: + return None return int(val) except ValueError: return default @@ -858,23 +918,70 @@ def default_otlp_host(host): _settings.distributed_tracing.sampler.full_granularity.enabled = _environ_as_bool( "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_ENABLED", default=True ) -_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled = os.environ.get( - "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED", None -) or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED", "default") -_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = os.environ.get( - "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", None -) or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default") +_settings.distributed_tracing.sampler.full_granularity._remote_parent_sampled = ( + ( + "adaptive" + if os.environ.get( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", + None, + ) + else None + ) + or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED", None) + or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED", "default") +) +_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target = _environ_as_int( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", None +) +_settings.distributed_tracing.sampler.full_granularity._remote_parent_not_sampled = ( + ( + "adaptive" + if os.environ.get( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", + None, + ) + else None + ) + or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", None) + or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED", "default") +) +_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target = ( + _environ_as_int( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", + None, + ) +) _settings.distributed_tracing.sampler.partial_granularity.enabled = _environ_as_bool( "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_ENABLED", default=False ) _settings.distributed_tracing.sampler.partial_granularity.type = os.environ.get( "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_TYPE", "essential" ) -_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled = os.environ.get( - "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED", "default" +_settings.distributed_tracing.sampler.partial_granularity._remote_parent_sampled = ( + "adaptive" + if os.environ.get( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", None + ) + else None +) or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED", "default") +_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target = ( + _environ_as_int( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", None + ) ) -_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled = os.environ.get( - "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", "default" +_settings.distributed_tracing.sampler.partial_granularity._remote_parent_not_sampled = ( + "adaptive" + if os.environ.get( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", + None, + ) + else None +) or os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED", "default") +_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target = ( + _environ_as_int( + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET", + None, + ) ) _settings.distributed_tracing.exclude_newrelic_header = False _settings.span_events.enabled = _environ_as_bool("NEW_RELIC_SPAN_EVENTS_ENABLED", default=True) diff --git a/newrelic/core/samplers/sampler_proxy.py b/newrelic/core/samplers/sampler_proxy.py index 2e8b7cf87f..70cb706267 100644 --- a/newrelic/core/samplers/sampler_proxy.py +++ b/newrelic/core/samplers/sampler_proxy.py @@ -21,10 +21,41 @@ def __init__(self, settings): else: sampling_target_period = settings.sampling_target_period_in_seconds adaptive_sampler = AdaptiveSampler(settings.sampling_target, sampling_target_period) - self._samplers = [adaptive_sampler] + self._samplers = {"global": adaptive_sampler} + # Add adaptive sampler instances for each config section if configured. + self.add_adaptive_sampler( + (True, 1), + settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target, + sampling_target_period, + ) + self.add_adaptive_sampler( + (True, 2), + settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target, + sampling_target_period, + ) + self.add_adaptive_sampler( + (False, 1), + settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target, + sampling_target_period, + ) + self.add_adaptive_sampler( + (False, 2), + settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target, + sampling_target_period, + ) + + def add_adaptive_sampler(self, key, sampling_target, sampling_target_period): + """ + Add an adaptive sampler instance to self._samplers if the sampling_target is specified. + """ + if sampling_target: + adaptive_sampler = AdaptiveSampler(sampling_target, sampling_target_period) + self._samplers[key] = adaptive_sampler def get_sampler(self, full_granularity, section): - return self._samplers[0] + # Return the sampler instance for the given config section. + # If no instance is present, return the global adaptive sampler instance instead. + return self._samplers.get((full_granularity, section)) or self._samplers["global"] def compute_sampled(self, full_granularity, section, *args, **kwargs): """ diff --git a/newrelic/hooks/coroutines_asyncio.py b/newrelic/hooks/coroutines_asyncio.py index 41fc776595..6f862d52dd 100644 --- a/newrelic/hooks/coroutines_asyncio.py +++ b/newrelic/hooks/coroutines_asyncio.py @@ -16,36 +16,73 @@ from newrelic.core.trace_cache import trace_cache -def remove_from_cache(task): +def remove_from_cache_callback(task): cache = trace_cache() cache.task_stop(task) -def propagate_task_context(task): +def wrap_create_task(task): trace_cache().task_start(task) - task.add_done_callback(remove_from_cache) + task.add_done_callback(remove_from_cache_callback) return task -def _bind_loop(loop, *args, **kwargs): +def _instrument_event_loop(loop): + if loop and hasattr(loop, "create_task") and not hasattr(loop.create_task, "__wrapped__"): + wrap_out_function(loop, "create_task", wrap_create_task) + + +def _bind_set_event_loop(loop, *args, **kwargs): return loop -def wrap_create_task(wrapped, instance, args, kwargs): - loop = _bind_loop(*args, **kwargs) +def wrap_set_event_loop(wrapped, instance, args, kwargs): + loop = _bind_set_event_loop(*args, **kwargs) - if loop and not hasattr(loop.create_task, "__wrapped__"): - wrap_out_function(loop, "create_task", propagate_task_context) + _instrument_event_loop(loop) return wrapped(*args, **kwargs) +def wrap__lazy_init(wrapped, instance, args, kwargs): + result = wrapped(*args, **kwargs) + # This logic can be used for uvloop, but should + # work for any valid custom loop factory. + + # A custom loop_factory will be used to create + # a new event loop instance. It will then run + # the main() coroutine on this event loop. Once + # this coroutine is complete, the event loop will + # be stopped and closed. + + # The new loop that is created and set as the + # running loop of the duration of the run() call. + # When the coroutine starts, it runs in the context + # that was active when run() was called. Any tasks + # created within this coroutine on this new event + # loop will inherit that context. + + # Note: The loop created by loop_factory is never + # set as the global current loop for the thread, + # even while it is running. + loop = instance._loop + _instrument_event_loop(loop) + + return result + + def instrument_asyncio_base_events(module): - wrap_out_function(module, "BaseEventLoop.create_task", propagate_task_context) + wrap_out_function(module, "BaseEventLoop.create_task", wrap_create_task) def instrument_asyncio_events(module): if hasattr(module, "_BaseDefaultEventLoopPolicy"): # Python >= 3.14 - wrap_function_wrapper(module, "_BaseDefaultEventLoopPolicy.set_event_loop", wrap_create_task) - else: # Python <= 3.13 - wrap_function_wrapper(module, "BaseDefaultEventLoopPolicy.set_event_loop", wrap_create_task) + wrap_function_wrapper(module, "_BaseDefaultEventLoopPolicy.set_event_loop", wrap_set_event_loop) + elif hasattr(module, "BaseDefaultEventLoopPolicy"): # Python <= 3.13 + wrap_function_wrapper(module, "BaseDefaultEventLoopPolicy.set_event_loop", wrap_set_event_loop) + + +# For Python >= 3.11 +def instrument_asyncio_runners(module): + if hasattr(module, "Runner") and hasattr(module.Runner, "_lazy_init"): + wrap_function_wrapper(module, "Runner._lazy_init", wrap__lazy_init) diff --git a/newrelic/hooks/framework_sanic.py b/newrelic/hooks/framework_sanic.py index 14077eb6d9..74d8ab678e 100644 --- a/newrelic/hooks/framework_sanic.py +++ b/newrelic/hooks/framework_sanic.py @@ -183,7 +183,7 @@ async def _nr_sanic_response_send(wrapped, instance, args, kwargs): transaction = current_transaction() result = wrapped(*args, **kwargs) if isawaitable(result): - await result + result = await result if transaction is None: return result diff --git a/tests/adapter_uvicorn/test_uvicorn.py b/tests/adapter_uvicorn/test_uvicorn.py index 0084be3e46..d5db2d6ca6 100644 --- a/tests/adapter_uvicorn/test_uvicorn.py +++ b/tests/adapter_uvicorn/test_uvicorn.py @@ -56,8 +56,8 @@ def app(request): return request.param -@pytest.fixture -def port(app): +@pytest.fixture(params=["asyncio", "uvloop", "none"], ids=["asyncio", "uvloop", "none"]) +def port(app, request): port = get_open_port() loops = [] @@ -72,7 +72,7 @@ def on_tick_sync(): async def on_tick(): on_tick_sync() - config = Config(app, host="127.0.0.1", port=port, loop="asyncio") + config = Config(app, host="127.0.0.1", port=port, loop=request.param) config.callback_notify = on_tick config.log_config = {"version": 1} config.disable_lifespan = True diff --git a/tests/agent_features/test_agent_control_health_check.py b/tests/agent_features/test_agent_control_health_check.py index e12f3a07f0..84058a1b28 100644 --- a/tests/agent_features/test_agent_control_health_check.py +++ b/tests/agent_features/test_agent_control_health_check.py @@ -38,7 +38,7 @@ def get_health_file_contents(tmp_path): return contents -@pytest.fixture(scope="module", autouse=True) +@pytest.fixture(autouse=True) def restore_settings_fixture(): # Backup settings from before this test file runs original_settings = global_settings() @@ -51,6 +51,10 @@ def restore_settings_fixture(): original_settings.__dict__.clear() original_settings.__dict__.update(backup) + # Re-initialize the agent to restore the settings + _reset_configuration_done() + initialize() + @pytest.mark.parametrize("file_uri", ["", "file://", "/test/dir", "foo:/test/dir"]) def test_invalid_file_directory_supplied(monkeypatch, file_uri): @@ -155,10 +159,18 @@ def test_no_override_on_unhealthy_shutdown(monkeypatch, tmp_path): def test_health_check_running_threads(monkeypatch, tmp_path): - running_threads = threading.enumerate() - # Only the main thread should be running since not agent control env vars are set - assert len(running_threads) == 1 + # If the Activate-Session thread is still active, give it time to close before we proceed + timeout = 30.0 + while len(threading.enumerate()) != 1 and timeout > 0: + time.sleep(0.1) + timeout -= 0.1 + # Only the main thread should be running since no agent control env vars are set + assert len(threading.enumerate()) == 1, ( + f"Expected only the main thread to be running before the test starts. Got: {threading.enumerate()}" + ) + + # Setup expected env vars to run agent control health check monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True") file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) @@ -180,6 +192,7 @@ def test_proxy_error_status(monkeypatch, tmp_path): file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) + # Re-initialize the agent to allow the health check thread to start _reset_configuration_done() initialize() @@ -209,6 +222,7 @@ def test_multiple_activations_running_threads(monkeypatch, tmp_path): file_path = tmp_path.as_uri() monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_path) + # Re-initialize the agent to allow the health check thread to start and assert that it did _reset_configuration_done() initialize() diff --git a/tests/agent_features/test_asgi_transaction.py b/tests/agent_features/test_asgi_transaction.py index e70ec95901..ac774689bd 100644 --- a/tests/agent_features/test_asgi_transaction.py +++ b/tests/agent_features/test_asgi_transaction.py @@ -19,6 +19,7 @@ from testing_support.fixtures import override_application_settings from testing_support.sample_asgi_applications import ( AppWithDescriptor, + asgi_application_generator_headers, simple_app_v2, simple_app_v2_init_exc, simple_app_v2_raw, @@ -37,6 +38,7 @@ simple_app_v3_wrapped = AsgiTest(simple_app_v3) simple_app_v2_wrapped = AsgiTest(simple_app_v2) simple_app_v2_init_exc = AsgiTest(simple_app_v2_init_exc) +asgi_application_generator_headers = AsgiTest(asgi_application_generator_headers) # Test naming scheme logic and ASGIApplicationWrapper for a single callable @@ -85,6 +87,28 @@ def test_double_callable_raw(): assert response.body == b"" +# Ensure headers object is preserved +@pytest.mark.parametrize("browser_monitoring", [True, False]) +@validate_transaction_metrics(name="", group="Uri") +def test_generator_headers(browser_monitoring): + """ + Both ASGIApplicationWrapper and ASGIBrowserMiddleware can cause headers to be lost if generators are + not handled properly. + + Ensure neither destroys headers by testing with and without the ASGIBrowserMiddleware, to make sure whichever + receives headers first properly preserves them in a list. + """ + + @override_application_settings({"browser_monitoring.enabled": browser_monitoring}) + def _test(): + response = asgi_application_generator_headers.make_request("GET", "/") + assert response.status == 200 + assert response.headers == {"x-my-header": "myvalue"} + assert response.body == b"" + + _test() + + # Test asgi_application decorator with parameters passed in on a single callable @pytest.mark.parametrize("name, group", ((None, "group"), ("name", "group"), ("", "group"))) def test_asgi_application_decorator_single_callable(name, group): diff --git a/tests/agent_features/test_distributed_tracing.py b/tests/agent_features/test_distributed_tracing.py index 33433ff7c9..0a64a4e97a 100644 --- a/tests/agent_features/test_distributed_tracing.py +++ b/tests/agent_features/test_distributed_tracing.py @@ -26,6 +26,7 @@ from testing_support.validators.validate_transaction_event_attributes import validate_transaction_event_attributes from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics +from newrelic.api.application import application_instance from newrelic.api.function_trace import function_trace from newrelic.common.object_wrapper import function_wrapper, transient_function_wrapper @@ -586,8 +587,8 @@ def test_distributed_trace_remote_parent_sampling_decision_full_granularity( test_settings = _override_settings.copy() test_settings.update( { - "distributed_tracing.sampler.full_granularity.remote_parent_sampled": remote_parent_sampled_setting, - "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled": remote_parent_not_sampled_setting, + "distributed_tracing.sampler.full_granularity._remote_parent_sampled": remote_parent_sampled_setting, + "distributed_tracing.sampler.full_granularity._remote_parent_not_sampled": remote_parent_not_sampled_setting, "span_events.enabled": True, } ) @@ -677,8 +678,8 @@ def test_distributed_trace_remote_parent_sampling_decision_partial_granularity( { "distributed_tracing.sampler.full_granularity.enabled": False, "distributed_tracing.sampler.partial_granularity.enabled": True, - "distributed_tracing.sampler.partial_granularity.remote_parent_sampled": remote_parent_sampled_setting, - "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled": remote_parent_not_sampled_setting, + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": remote_parent_sampled_setting, + "distributed_tracing.sampler.partial_granularity._remote_parent_not_sampled": remote_parent_not_sampled_setting, "span_events.enabled": True, } ) @@ -745,8 +746,8 @@ def test_distributed_trace_remote_parent_sampling_decision_between_full_and_part { "distributed_tracing.sampler.full_granularity.enabled": full_granularity_enabled, "distributed_tracing.sampler.partial_granularity.enabled": partial_granularity_enabled, - "distributed_tracing.sampler.full_granularity.remote_parent_sampled": full_granularity_remote_parent_sampled_setting, - "distributed_tracing.sampler.partial_granularity.remote_parent_sampled": partial_granularity_remote_parent_sampled_setting, + "distributed_tracing.sampler.full_granularity._remote_parent_sampled": full_granularity_remote_parent_sampled_setting, + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": partial_granularity_remote_parent_sampled_setting, "span_events.enabled": True, } ) @@ -815,7 +816,7 @@ def _test(): "distributed_tracing.sampler.full_granularity.enabled": False, "distributed_tracing.sampler.partial_granularity.enabled": True, "distributed_tracing.sampler.partial_granularity.type": "compact", - "distributed_tracing.sampler.partial_granularity.remote_parent_sampled": "always_on", + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": "always_on", "span_events.enabled": True, } )(_test) @@ -868,7 +869,7 @@ def _test(): "distributed_tracing.sampler.full_granularity.enabled": False, "distributed_tracing.sampler.partial_granularity.enabled": True, "distributed_tracing.sampler.partial_granularity.type": "compact", - "distributed_tracing.sampler.partial_granularity.remote_parent_sampled": "always_on", + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": "always_on", "span_events.enabled": True, } )(_test) @@ -918,7 +919,7 @@ def _test(): "distributed_tracing.sampler.full_granularity.enabled": False, "distributed_tracing.sampler.partial_granularity.enabled": True, "distributed_tracing.sampler.partial_granularity.type": "compact", - "distributed_tracing.sampler.partial_granularity.remote_parent_sampled": "always_on", + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": "always_on", "span_events.enabled": True, } )(_test) @@ -972,7 +973,7 @@ def _test(): "distributed_tracing.sampler.full_granularity.enabled": False, "distributed_tracing.sampler.partial_granularity.enabled": True, "distributed_tracing.sampler.partial_granularity.type": "reduced", - "distributed_tracing.sampler.partial_granularity.remote_parent_sampled": "always_on", + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": "always_on", "span_events.enabled": True, } )(_test) @@ -1026,9 +1027,118 @@ def _test(): "distributed_tracing.sampler.full_granularity.enabled": False, "distributed_tracing.sampler.partial_granularity.enabled": True, "distributed_tracing.sampler.partial_granularity.type": "essential", - "distributed_tracing.sampler.partial_granularity.remote_parent_sampled": "always_on", + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": "always_on", "span_events.enabled": True, } )(_test) _test() + + +@pytest.mark.parametrize( + "dt_settings,dt_headers,expected_sampling_instance_called,expected_adaptive_computed_count,expected_adaptive_sampled_count,expected_adaptive_sampling_target", + ( + ( + { + "distributed_tracing.sampler.full_granularity.enabled": False, + "distributed_tracing.sampler.partial_granularity.enabled": True, + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": "default", + "distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target": 5, + "distributed_tracing.sampler.partial_granularity._remote_parent_not_sampled": "default", + "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target": 6, + }, + {"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"}, + (False, 1), + 1, + 1, + 5, + ), + ( + { + "distributed_tracing.sampler.full_granularity.enabled": False, + "distributed_tracing.sampler.partial_granularity.enabled": True, + "distributed_tracing.sampler.partial_granularity._remote_parent_sampled": "default", + "distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target": 5, + "distributed_tracing.sampler.partial_granularity._remote_parent_not_sampled": "default", + "distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target": 6, + }, + {"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-00"}, + (False, 2), + 1, + 1, + 6, + ), + ( + { + "distributed_tracing.sampler.full_granularity.enabled": True, + "distributed_tracing.sampler.partial_granularity.enabled": False, + "distributed_tracing.sampler.full_granularity._remote_parent_sampled": "default", + "distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target": 5, + "distributed_tracing.sampler.full_granularity._remote_parent_not_sampled": "default", + "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target": 6, + }, + {"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"}, + (True, 1), + 1, + 1, + 5, + ), + ( + { + "distributed_tracing.sampler.full_granularity.enabled": True, + "distributed_tracing.sampler.partial_granularity.enabled": False, + "distributed_tracing.sampler.full_granularity._remote_parent_sampled": "default", + "distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target": 5, + "distributed_tracing.sampler.full_granularity._remote_parent_not_sampled": "default", + "distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target": 6, + }, + {"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-00"}, + (True, 2), + 1, + 1, + 6, + ), + ), +) +def test_distributed_trace_uses_sampling_instance( + dt_settings, + dt_headers, + expected_sampling_instance_called, + expected_adaptive_computed_count, + expected_adaptive_sampled_count, + expected_adaptive_sampling_target, +): + test_settings = _override_settings.copy() + test_settings.update(dt_settings) + function_called_decorator = validate_function_called( + "newrelic.core.samplers.adaptive_sampler", "AdaptiveSampler.compute_sampled" + ) + + @function_called_decorator + @override_application_settings(test_settings) + @background_task(name="test_distributed_trace_attributes") + def _test(): + txn = current_transaction() + application = txn._application._agent._applications.get(txn.settings.app_name) + # Re-initialize sampler proxy after overriding settings. + application.sampler.__init__(txn.settings) + + accept_distributed_trace_headers(dt_headers) + # Explicitly call this so we can assert sampling decision during the transaction + # as opposed to after it ends and we lose the application context. + txn._make_sampling_decision() + + assert ( + application.sampler._samplers[expected_sampling_instance_called].computed_count + == expected_adaptive_computed_count + ) + assert ( + application.sampler._samplers[expected_sampling_instance_called].sampled_count + == expected_adaptive_sampled_count + ) + assert ( + application.sampler._samplers[expected_sampling_instance_called].sampling_target + == expected_adaptive_sampling_target + ) + + _test() diff --git a/tests/agent_unittests/test_distributed_tracing_settings.py b/tests/agent_unittests/test_distributed_tracing_settings.py index 3668cfbe32..8b0402b47c 100644 --- a/tests/agent_unittests/test_distributed_tracing_settings.py +++ b/tests/agent_unittests/test_distributed_tracing_settings.py @@ -26,6 +26,46 @@ distributed_tracing.exclude_newrelic_header = true """ +INI_FILE_FULL_GRAN_CONFLICTS = b""" +[newrelic] +distributed_tracing.sampler.remote_parent_sampled = default +distributed_tracing.sampler.remote_parent_not_sampled = default +distributed_tracing.sampler.full_granularity.remote_parent_sampled = always_on +distributed_tracing.sampler.full_granularity.remote_parent_not_sampled = always_off +""" + +INI_FILE_FULL_GRAN_CONFLICTS_ADAPTIVE = b""" +[newrelic] +distributed_tracing.sampler.remote_parent_sampled = always_on +distributed_tracing.sampler.remote_parent_not_sampled = always_off +distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target = 10 +distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target = 20 +""" + +INI_FILE_FULL_GRAN_MULTIPLE_SAMPLERS = b""" +[newrelic] +distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target = 10 +distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target = 20 +distributed_tracing.sampler.full_granularity.remote_parent_sampled.trace_id_ratio_based.sampling_target = 10 +distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.trace_id_ratio_based.sampling_target = 20 +""" + +INI_FILE_PARTIAL_GRAN_CONFLICTS_ADAPTIVE = b""" +[newrelic] +distributed_tracing.sampler.partial_granularity.remote_parent_sampled = always_on +distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled = always_off +distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target = 10 +distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target = 20 +""" + +INI_FILE_PARTIAL_GRAN_MULTIPLE_SAMPLERS = b""" +[newrelic] +distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target = 10 +distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target = 20 +distributed_tracing.sampler.partial_granularity.remote_parent_sampled.trace_id_ratio_based.sampling_target = 10 +distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.trace_id_ratio_based.sampling_target = 20 +""" + # Tests for loading settings and testing for values precedence @pytest.mark.parametrize("ini,env,expected_format", ((INI_FILE_EMPTY, {}, False), (INI_FILE_W3C, {}, True))) @@ -35,9 +75,33 @@ def test_distributed_trace_setings(ini, env, expected_format, global_settings): @pytest.mark.parametrize( - "ini,env", + "ini,env,expected", ( - ( + ( # Defaults to adaptive (default) sampler. + INI_FILE_EMPTY, + {}, + ("default", "default", None, None), + ), + ( # More specific full granularity path overrides less specific path in ini file. + INI_FILE_FULL_GRAN_CONFLICTS, + {}, + ("always_on", "always_off", None, None), + ), + ( # More specific sampler path overrides less specific path in ini file. + INI_FILE_FULL_GRAN_CONFLICTS_ADAPTIVE, + {}, + ("adaptive", "adaptive", 10, 20), + ), + ( # ini file configuration takes precedence over env vars. + INI_FILE_FULL_GRAN_CONFLICTS_ADAPTIVE, + { + "NEW_RELIC_ENABLED": "true", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "50", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "30", + }, + ("adaptive", "adaptive", 10, 20), + ), + ( # More specific full granularity path overrides less specific path in env vars. INI_FILE_EMPTY, { "NEW_RELIC_ENABLED": "true", @@ -46,21 +110,115 @@ def test_distributed_trace_setings(ini, env, expected_format, global_settings): "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED": "always_on", "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED": "always_off", }, + ("always_on", "always_off", None, None), + ), + ( # Simple configuration works. + INI_FILE_EMPTY, + { + "NEW_RELIC_ENABLED": "true", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED": "always_on", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED": "always_off", + }, + ("always_on", "always_off", None, None), ), - ( + ( # More specific sampler path overrides less specific path in env vars. INI_FILE_EMPTY, { "NEW_RELIC_ENABLED": "true", "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED": "always_on", "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED": "always_off", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "20", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_FULL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "20", }, + ("adaptive", "adaptive", 20, 20), + ), + ( # Ignores other unknown samplers. + INI_FILE_FULL_GRAN_MULTIPLE_SAMPLERS, + {}, + ("adaptive", "adaptive", 10, 20), + ), + ), +) +def test_full_granularity_precedence(ini, env, global_settings, expected): + settings = global_settings() + + app_settings = finalize_application_settings(settings=settings) + + assert app_settings.distributed_tracing.sampler.full_granularity._remote_parent_sampled == expected[0] + assert app_settings.distributed_tracing.sampler.full_granularity._remote_parent_not_sampled == expected[1] + assert ( + app_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled.adaptive.sampling_target + == expected[2] + ) + assert ( + app_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled.adaptive.sampling_target + == expected[3] + ) + + +@pytest.mark.parametrize( + "ini,env,expected", + ( + ( # Defaults to adaptive (default) sampler. + INI_FILE_EMPTY, + {}, + ("default", "default", None, None), + ), + ( # More specific sampler path overrides less specific path in ini file. + INI_FILE_PARTIAL_GRAN_CONFLICTS_ADAPTIVE, + {}, + ("adaptive", "adaptive", 10, 20), + ), + ( # ini config takes precedence over env vars. + INI_FILE_PARTIAL_GRAN_CONFLICTS_ADAPTIVE, + { + "NEW_RELIC_ENABLED": "true", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED": "always_on", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED": "always_off", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "20", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "30", + }, + ("adaptive", "adaptive", 10, 20), + ), + ( # Simple configuration works. + INI_FILE_EMPTY, + { + "NEW_RELIC_ENABLED": "true", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED": "always_on", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED": "always_off", + }, + ("always_on", "always_off", None, None), + ), + ( # More specific sampler path overrides less specific path in env vars. + INI_FILE_EMPTY, + { + "NEW_RELIC_ENABLED": "true", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED": "always_on", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED": "always_off", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "10", + "NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_PARTIAL_GRANULARITY_REMOTE_PARENT_NOT_SAMPLED_ADAPTIVE_SAMPLING_TARGET": "20", + }, + ("adaptive", "adaptive", 10, 20), + ), + ( # Ignores other unknown samplers. + INI_FILE_PARTIAL_GRAN_MULTIPLE_SAMPLERS, + {}, + ("adaptive", "adaptive", 10, 20), ), ), ) -def test_full_granularity_precedence(ini, env, global_settings): +def test_partial_granularity_precedence(ini, env, global_settings, expected): settings = global_settings() app_settings = finalize_application_settings(settings=settings) - assert app_settings.distributed_tracing.sampler.full_granularity.remote_parent_sampled == "always_on" - assert app_settings.distributed_tracing.sampler.full_granularity.remote_parent_not_sampled == "always_off" + assert app_settings.distributed_tracing.sampler.partial_granularity._remote_parent_sampled == expected[0] + assert app_settings.distributed_tracing.sampler.partial_granularity._remote_parent_not_sampled == expected[1] + assert ( + app_settings.distributed_tracing.sampler.partial_granularity.remote_parent_sampled.adaptive.sampling_target + == expected[2] + ) + assert ( + app_settings.distributed_tracing.sampler.partial_granularity.remote_parent_not_sampled.adaptive.sampling_target + == expected[3] + ) diff --git a/tests/coroutines_asyncio/test_context_propagation.py b/tests/coroutines_asyncio/test_context_propagation.py index b338b6ec3e..eb5c358745 100644 --- a/tests/coroutines_asyncio/test_context_propagation.py +++ b/tests/coroutines_asyncio/test_context_propagation.py @@ -36,16 +36,31 @@ import uvloop loop_policies = (pytest.param(None, id="asyncio"), pytest.param(uvloop.EventLoopPolicy(), id="uvloop")) + uvloop_factory = (pytest.param(uvloop.new_event_loop, id="uvloop"), pytest.param(None, id="None")) except ImportError: loop_policies = (pytest.param(None, id="asyncio"),) + uvloop_factory = (pytest.param(None, id="None"),) + + +def loop_factories(): + import asyncio + + if sys.platform == "win32": + return (pytest.param(asyncio.ProactorEventLoop, id="asyncio.ProactorEventLoop"), *uvloop_factory) + else: + return (pytest.param(asyncio.SelectorEventLoop, id="asyncio.SelectorEventLoop"), *uvloop_factory) @pytest.fixture(autouse=True) def reset_event_loop(): - from asyncio import set_event_loop, set_event_loop_policy + try: + from asyncio import set_event_loop, set_event_loop_policy + + # Remove the loop policy to avoid side effects + set_event_loop_policy(None) + except ImportError: + from asyncio import set_event_loop - # Remove the loop policy to avoid side effects - set_event_loop_policy(None) set_event_loop(None) @@ -102,6 +117,7 @@ async def _test(asyncio, schedule, nr_enabled=True): return trace +@pytest.mark.skipif(sys.version_info >= (3, 16), reason="loop_policy is not available") @pytest.mark.parametrize("loop_policy", loop_policies) @pytest.mark.parametrize("schedule", ("create_task", "ensure_future")) @validate_transaction_metrics( @@ -166,10 +182,12 @@ def handle_exception(loop, context): memcache_trace("cmd"), ], ) -def test_two_transactions(event_loop, trace): +def test_two_transactions_with_global_event_loop(event_loop, trace): """ Instantiate a coroutine in one transaction and await it in another. This should not cause any errors. + This uses the global event loop policy, which has been deprecated + since Python 3.11 and is scheduled for removal in Python 3.16. """ import asyncio @@ -211,6 +229,99 @@ async def await_task(): event_loop.run_until_complete(asyncio.gather(afut, bfut)) +@pytest.mark.skipif(sys.version_info < (3, 11), reason="asyncio.Runner is not available") +@validate_transaction_metrics("await_task", background_task=True) +@validate_transaction_metrics("create_coro", background_task=True, index=-2) +@pytest.mark.parametrize("loop_factory", loop_factories()) +@pytest.mark.parametrize( + "trace", + [ + function_trace(name="simple_gen"), + external_trace(library="lib", url="http://foo.com"), + database_trace("select * from foo"), + datastore_trace("lib", "foo", "bar"), + message_trace("lib", "op", "typ", "name"), + memcache_trace("cmd"), + ], +) +def test_two_transactions_with_loop_factory(trace, loop_factory): + """ + Instantiate a coroutine in one transaction and await it in + another. This should not cause any errors. + Starting in Python 3.11, the asyncio.Runner class was added + as well as the loop_factory parameter. The loop_factory + parameter provides a replacement for loop policies (which + are scheduled for removal in Python 3.16). + """ + import asyncio + + @trace + async def task(): + pass + + @background_task(name="create_coro") + async def create_coro(): + return asyncio.create_task(task()) + + @background_task(name="await_task") + async def await_task(task_to_await): + return await task_to_await + + async def _main(): + _task = await create_coro() + return await await_task(_task) + + with asyncio.Runner(loop_factory=loop_factory) as runner: + runner.run(_main()) + + +@pytest.mark.skipif(sys.version_info < (3, 11), reason="loop_factory/asyncio.Runner is not available") +@pytest.mark.parametrize("loop_factory", loop_factories()) +@validate_transaction_metrics( + "test_context_propagation:test_context_propagation_with_loop_factory", + background_task=True, + scoped_metrics=(("Function/waiter2", 2), ("Function/waiter3", 2)), +) +@background_task() +def test_context_propagation_with_loop_factory(loop_factory): + import asyncio + + exceptions = [] + + def handle_exception(loop, context): + exceptions.append(context) + + # Call default handler for standard logging + loop.default_exception_handler(context) + + async def subtask(): + with FunctionTrace(name="waiter2", terminal=True): + pass + + await child() + + async def _task(trace): + assert current_trace() == trace + + await subtask() + + trace = current_trace() + + with asyncio.Runner(loop_factory=loop_factory) as runner: + assert trace == current_trace() + runner._loop.set_exception_handler(handle_exception) + runner.run(_task(trace)) + runner.run(_task(trace)) + + # The agent should have removed all traces from the cache since + # run_until_complete has terminated (all callbacks scheduled inside the + # task have run) + assert len(trace_cache()) == 1 # Sentinel is all that remains + + # # Assert that no exceptions have occurred + assert not exceptions, exceptions + + # Sentinel left in cache transaction exited async def sentinel_in_cache_txn_exited(asyncio, bg): event = asyncio.Event() diff --git a/tests/testing_support/asgi_testing.py b/tests/testing_support/asgi_testing.py index 821a20fe96..5c97be8860 100644 --- a/tests/testing_support/asgi_testing.py +++ b/tests/testing_support/asgi_testing.py @@ -106,7 +106,7 @@ def process_output(self): if self.response_state is ResponseState.NOT_STARTED: assert message["type"] == "http.response.start" response_status = message["status"] - response_headers = message.get("headers", response_headers) + response_headers = list(message.get("headers", response_headers)) self.response_state = ResponseState.BODY elif self.response_state is ResponseState.BODY: assert message["type"] == "http.response.body" diff --git a/tests/testing_support/sample_asgi_applications.py b/tests/testing_support/sample_asgi_applications.py index c1ef860763..e281a7cbf2 100644 --- a/tests/testing_support/sample_asgi_applications.py +++ b/tests/testing_support/sample_asgi_applications.py @@ -114,6 +114,23 @@ async def normal_asgi_application(scope, receive, send): await send({"type": "http.response.body", "body": output}) +@ASGIApplicationWrapper +async def asgi_application_generator_headers(scope, receive, send): + if scope["type"] == "lifespan": + return await handle_lifespan(scope, receive, send) + + if scope["type"] != "http": + raise ValueError("unsupported") + + def headers(): + yield (b"x-my-header", b"myvalue") + + await send({"type": "http.response.start", "status": 200, "headers": headers()}) + await send({"type": "http.response.body"}) + + assert current_transaction() is None + + async def handle_lifespan(scope, receive, send): """Handle lifespan protocol with no-ops to allow more compatibility.""" while True: diff --git a/tox.ini b/tox.ini index 39148b657f..98cea6ee29 100644 --- a/tox.ini +++ b/tox.ini @@ -113,11 +113,11 @@ envlist = python-adapter_gunicorn-{py38,py39,py310,py311,py312,py313}-aiohttp03-gunicornlatest, ;; Package not ready for Python 3.14 (aiohttp's worker not updated) ; python-adapter_gunicorn-py314-aiohttp03-gunicornlatest, - python-adapter_hypercorn-{py38,py39,py310,py311,py312,py313,py314}-hypercornlatest, - python-adapter_hypercorn-py38-hypercorn{0010,0011,0012,0013}, + python-adapter_hypercorn-{py310,py311,py312,py313,py314}-hypercornlatest, + python-adapter_hypercorn-{py38,py39}-hypercorn{0010,0011,0012,0013}, python-adapter_mcp-{py310,py311,py312,py313,py314}, - python-adapter_uvicorn-{py38,py39,py310,py311,py312,py313,py314}-uvicornlatest, - python-adapter_uvicorn-py38-uvicorn014, + python-adapter_uvicorn-{py39,py310,py311,py312,py313,py314}-uvicornlatest, + python-adapter_uvicorn-py38-uvicorn020, python-adapter_waitress-{py38,py39,py310,py311,py312,py313,py314}-waitresslatest, python-application_celery-{py38,py39,py310,py311,py312,py313,py314,pypy311}-celerylatest, python-application_celery-py311-celery{0504,0503,0502}, @@ -176,7 +176,8 @@ envlist = python-framework_strawberry-{py38,py39,py310,py311,py312}-strawberry02352, python-framework_strawberry-{py38,py39,py310,py311,py312,py313,py314}-strawberrylatest, python-framework_tornado-{py38,py39,py310,py311,py312,py313,py314}-tornadolatest, - python-framework_tornado-{py310,py311,py312,py313,py314}-tornadomaster, + ; Remove `python-framework_tornado-py314-tornadomaster` temporarily + python-framework_tornado-{py310,py311,py312,py313}-tornadomaster, python-logger_logging-{py38,py39,py310,py311,py312,py313,py314,pypy311}, python-logger_loguru-{py38,py39,py310,py311,py312,py313,py314,pypy311}-logurulatest, python-logger_structlog-{py38,py39,py310,py311,py312,py313,py314,pypy311}-structloglatest, @@ -231,16 +232,18 @@ deps = adapter_gunicorn-aiohttp03: aiohttp<4.0 adapter_gunicorn-gunicorn19: gunicorn<20 adapter_gunicorn-gunicornlatest: gunicorn - adapter_hypercorn-hypercornlatest: hypercorn[h3] + adapter_hypercorn-hypercornlatest: hypercorn[h3]!=0.18 adapter_hypercorn-hypercorn0013: hypercorn[h3]<0.14 adapter_hypercorn-hypercorn0012: hypercorn[h3]<0.13 adapter_hypercorn-hypercorn0011: hypercorn[h3]<0.12 adapter_hypercorn-hypercorn0010: hypercorn[h3]<0.11 adapter_hypercorn: niquests adapter_mcp: fastmcp - adapter_uvicorn-uvicorn014: uvicorn<0.15 + adapter_uvicorn-uvicorn020: uvicorn<0.21 + adapter_uvicorn-uvicorn020: uvloop<0.20 adapter_uvicorn-uvicornlatest: uvicorn adapter_uvicorn: typing-extensions + adapter_uvicorn: uvloop adapter_waitress: WSGIProxy2 adapter_waitress-waitresslatest: waitress agent_features: beautifulsoup4