From 6959d55cf46506b3e0d8b1ad8b77b68b06fa446c Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Tue, 18 Feb 2025 13:38:13 +0100 Subject: [PATCH 1/6] Lazily import requests-related objects --- datadog_checks_base/datadog_checks/base/checks/base.py | 3 ++- .../datadog_checks/base/checks/openmetrics/base_check.py | 4 ++-- .../datadog_checks/base/checks/openmetrics/mixins.py | 7 +++++-- .../datadog_checks/base/checks/openmetrics/v2/base.py | 4 ++-- .../datadog_checks/base/checks/openmetrics/v2/scraper.py | 4 ++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/datadog_checks_base/datadog_checks/base/checks/base.py b/datadog_checks_base/datadog_checks/base/checks/base.py index 0e61550f9b071..ea83371e99108 100644 --- a/datadog_checks_base/datadog_checks/base/checks/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/base.py @@ -48,7 +48,6 @@ from ..utils.common import ensure_bytes, to_native_string from ..utils.diagnose import Diagnosis from ..utils.fips import enable_fips -from ..utils.http import RequestsWrapper from ..utils.limiter import Limiter from ..utils.metadata import MetadataManager from ..utils.secrets import SecretsSanitizer @@ -406,6 +405,8 @@ def http(self): Only new checks or checks on Agent 6.13+ can and should use this for HTTP requests. """ + from ..utils.http import RequestsWrapper + if not hasattr(self, '_http'): self._http = RequestsWrapper(self.instance or {}, self.init_config, self.HTTP_CONFIG_REMAPPER, self.log) diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py index 5d71cde0584fa..88c01d52ae018 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py @@ -3,8 +3,6 @@ # Licensed under a 3-clause BSD style license (see LICENSE) from copy import deepcopy -import requests - from ...errors import CheckException from ...utils.tracing import traced_class from .. import AgentCheck @@ -74,6 +72,8 @@ def __init__(self, *args, **kwargs): """ The base class for any Prometheus-based integration. """ + import requests + args = list(args) default_instances = kwargs.pop('default_instances', None) or {} default_namespace = kwargs.pop('default_namespace', None) diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py index 1a6cb13a09c34..48fa282f90ce4 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py @@ -10,7 +10,6 @@ from os.path import isfile from re import compile -import requests from prometheus_client.samples import Sample from datadog_checks.base.agent import datadog_agent @@ -18,7 +17,6 @@ from ...config import is_affirmative from ...errors import CheckException from ...utils.common import to_native_string -from ...utils.http import RequestsWrapper from .. import AgentCheck from ..libs.prometheus import text_fd_to_metric_families @@ -380,6 +378,9 @@ def get_http_handler(self, scraper_config): The http handler is cached using `prometheus_url` as key. The http handler doesn't use the cache if a bearer token is used to allow refreshing it. """ + + from ...utils.http import RequestsWrapper + prometheus_url = scraper_config['prometheus_url'] bearer_token = scraper_config['_bearer_token'] if prometheus_url in self._http_handlers and bearer_token is None: @@ -826,6 +827,8 @@ def poll(self, scraper_config, headers=None): Custom headers can be added to the default headers. """ + import requests + endpoint = scraper_config.get('prometheus_url') # Should we send a service check for when we make a request diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py index 0a502b95a4dea..172990b0984fb 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py @@ -4,8 +4,6 @@ from collections import ChainMap from contextlib import contextmanager -from requests.exceptions import RequestException - from ....errors import ConfigurationError from ....utils.tracing import traced_class from ... import AgentCheck @@ -62,6 +60,8 @@ def check(self, _): Another thing to note is that this check ignores its instance argument completely. We take care of instance-level customization at initialization time. """ + from requests.exceptions import RequestException + self.refresh_scrapers() for endpoint, scraper in self.scrapers.items(): diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py index 4d156ae9773ab..953a24448b6bb 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py @@ -11,7 +11,6 @@ from prometheus_client.openmetrics.parser import text_fd_to_metric_families as parse_openmetrics from prometheus_client.parser import text_fd_to_metric_families as parse_prometheus -from requests.exceptions import ConnectionError from datadog_checks.base.agent import datadog_agent @@ -19,7 +18,6 @@ from ....constants import ServiceCheck from ....errors import ConfigurationError from ....utils.functions import no_op, return_true -from ....utils.http import RequestsWrapper from .first_scrape_handler import first_scrape_handler from .labels import LabelAggregator, get_label_normalizer from .transform import MetricTransformer @@ -50,6 +48,7 @@ def __init__(self, check, config): """ The base class for any scraper overrides. """ + from ....utils.http import RequestsWrapper self.config = config @@ -393,6 +392,7 @@ def stream_connection_lines(self): """ Yield the connection line. """ + from requests.exceptions import ConnectionError try: with self.get_connection() as connection: From d52f132040ddce983ae01473ea5a81b8d2585ba1 Mon Sep 17 00:00:00 2001 From: Pierre Gimalac Date: Tue, 18 Feb 2025 15:46:14 +0100 Subject: [PATCH 2/6] refactor: move imports of prometheus inside functions using it --- .../datadog_checks/base/checks/libs/prometheus.py | 5 ++--- .../datadog_checks/base/checks/openmetrics/mixins.py | 4 ++-- .../datadog_checks/base/checks/openmetrics/v2/scraper.py | 5 ++--- .../datadog_checks/base/checks/openmetrics/v2/utils.py | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py b/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py index a6ce894a421ff..a84bd3028d3c8 100644 --- a/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py +++ b/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py @@ -4,9 +4,6 @@ from itertools import tee -from prometheus_client.metrics_core import Metric -from prometheus_client.parser import _parse_sample, _replace_help_escaping - def text_fd_to_metric_families(fd): raw_lines, input_lines = tee(fd, 2) @@ -32,6 +29,8 @@ def _parse_payload(fd): Yields Metric's. """ + from prometheus_client.metrics_core import Metric + from prometheus_client.parser import _parse_sample, _replace_help_escaping name = '' documentation = '' typ = 'untyped' diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py index 48fa282f90ce4..58c1ca3a3362a 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py @@ -10,8 +10,6 @@ from os.path import isfile from re import compile -from prometheus_client.samples import Sample - from datadog_checks.base.agent import datadog_agent from ...config import is_affirmative @@ -1063,6 +1061,8 @@ def _decumulate_histogram_buckets(self, metric): """ Decumulate buckets in a given histogram metric and adds the lower_bound label (le being upper_bound) """ + from prometheus_client.samples import Sample + bucket_values_by_context_upper_bound = {} for sample in metric.samples: if sample[self.SAMPLE_NAME].endswith("_bucket"): diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py index 953a24448b6bb..71884c05b88b0 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py @@ -9,9 +9,6 @@ from math import isinf, isnan from typing import List # noqa: F401 -from prometheus_client.openmetrics.parser import text_fd_to_metric_families as parse_openmetrics -from prometheus_client.parser import text_fd_to_metric_families as parse_prometheus - from datadog_checks.base.agent import datadog_agent from ....config import is_affirmative @@ -332,6 +329,8 @@ def parse_metrics(self): @property def parse_metric_families(self): + from prometheus_client.parser import text_fd_to_metric_families as parse_prometheus + from prometheus_client.openmetrics.parser import text_fd_to_metric_families as parse_openmetrics media_type = self._content_type.split(';')[0] # Setting `use_latest_spec` forces the use of the OpenMetrics format, otherwise # the format will be chosen based on the media type specified in the response's content-header. diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py index eb44d81cf358b..daf15dd6cea47 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py @@ -1,7 +1,6 @@ # (C) Datadog, Inc. 2020-present # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) -from prometheus_client.samples import Sample NEGATIVE_INFINITY = float('-inf') @@ -10,6 +9,7 @@ def decumulate_histogram_buckets(sample_data): """ Decumulate buckets in a given histogram metric and adds the lower_bound label (le being upper_bound) """ + from prometheus_client.samples import Sample # TODO: investigate performance optimizations new_sample_data = [] bucket_values_by_context_upper_bound = {} From c17d98d12eed7270506b0fc9ddfee943b24fc02b Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Tue, 25 Feb 2025 21:59:27 +0100 Subject: [PATCH 3/6] fix lint --- .../datadog_checks/base/checks/libs/prometheus.py | 1 + .../datadog_checks/base/checks/openmetrics/v2/scraper.py | 3 ++- .../datadog_checks/base/checks/openmetrics/v2/utils.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py b/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py index a84bd3028d3c8..9aa9d46f53dfd 100644 --- a/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py +++ b/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py @@ -31,6 +31,7 @@ def _parse_payload(fd): """ from prometheus_client.metrics_core import Metric from prometheus_client.parser import _parse_sample, _replace_help_escaping + name = '' documentation = '' typ = 'untyped' diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py index 71884c05b88b0..ec860e476158c 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py @@ -329,8 +329,9 @@ def parse_metrics(self): @property def parse_metric_families(self): - from prometheus_client.parser import text_fd_to_metric_families as parse_prometheus from prometheus_client.openmetrics.parser import text_fd_to_metric_families as parse_openmetrics + from prometheus_client.parser import text_fd_to_metric_families as parse_prometheus + media_type = self._content_type.split(';')[0] # Setting `use_latest_spec` forces the use of the OpenMetrics format, otherwise # the format will be chosen based on the media type specified in the response's content-header. diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py index daf15dd6cea47..6d1072d20c1ef 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py @@ -10,6 +10,7 @@ def decumulate_histogram_buckets(sample_data): Decumulate buckets in a given histogram metric and adds the lower_bound label (le being upper_bound) """ from prometheus_client.samples import Sample + # TODO: investigate performance optimizations new_sample_data = [] bucket_values_by_context_upper_bound = {} From a857e977fea153f88cfe31148460774c0134897f Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Mon, 3 Mar 2025 07:53:25 +0100 Subject: [PATCH 4/6] add changelog --- datadog_checks_base/changelog.d/19679.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 datadog_checks_base/changelog.d/19679.fixed diff --git a/datadog_checks_base/changelog.d/19679.fixed b/datadog_checks_base/changelog.d/19679.fixed new file mode 100644 index 0000000000000..8e1d2bc382e7f --- /dev/null +++ b/datadog_checks_base/changelog.d/19679.fixed @@ -0,0 +1 @@ +Import dependencies in base check classes lazily. With only the disk and network checks running, the Agent uses almost 50MB less memory after this change. From 16628a4c4c0fa598db509dd34ab64bbd9db6c99f Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Thu, 6 Mar 2025 12:48:07 +0100 Subject: [PATCH 5/6] docs --- datadog_checks_base/README.md | 13 +++++++++++++ .../datadog_checks/base/checks/base.py | 1 + .../datadog_checks/base/checks/libs/prometheus.py | 1 + .../base/checks/openmetrics/base_check.py | 1 + .../base/checks/openmetrics/mixins.py | 3 +++ .../base/checks/openmetrics/v2/base.py | 1 + .../base/checks/openmetrics/v2/scraper.py | 3 +++ .../base/checks/openmetrics/v2/utils.py | 1 + 8 files changed, 24 insertions(+) diff --git a/datadog_checks_base/README.md b/datadog_checks_base/README.md index f3e00d2d1e932..ae18f28a95297 100644 --- a/datadog_checks_base/README.md +++ b/datadog_checks_base/README.md @@ -29,6 +29,19 @@ install the toolkit locally and play with it: pip install datadog-checks-base ``` +## Performance Optimizations + +We strive to balance lean resource usage with a "batteries included" user experience. +This is why we import some of our dependencies inside functions that use them instead of the more conventional import section at the top of the file. + +Below are some examples for how much we shave off the Python heap for a given dependency: + +- `requests==2.32.3`: 3.6MB +- `RequestWrapper` class (`datadog_checks_base==37.7.0`): 2.9MB +- `prometheus-client==0.21.1`: around 1MB + +This translates into even bigger savings when we run in the Agent, something close to 50MB. + ## Troubleshooting Need help? Contact [Datadog support][8]. diff --git a/datadog_checks_base/datadog_checks/base/checks/base.py b/datadog_checks_base/datadog_checks/base/checks/base.py index 9ea463ffb194f..1c00c0db0c52e 100644 --- a/datadog_checks_base/datadog_checks/base/checks/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/base.py @@ -395,6 +395,7 @@ def http(self): Only new checks or checks on Agent 6.13+ can and should use this for HTTP requests. """ + # See Performance Optimizations in this package's README.md. from ..utils.http import RequestsWrapper if not hasattr(self, '_http'): diff --git a/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py b/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py index 9aa9d46f53dfd..3fe57e6191065 100644 --- a/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py +++ b/datadog_checks_base/datadog_checks/base/checks/libs/prometheus.py @@ -29,6 +29,7 @@ def _parse_payload(fd): Yields Metric's. """ + # See Performance Optimizations in this package's README.md. from prometheus_client.metrics_core import Metric from prometheus_client.parser import _parse_sample, _replace_help_escaping diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py index 88c01d52ae018..7fd6324a1bc90 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/base_check.py @@ -72,6 +72,7 @@ def __init__(self, *args, **kwargs): """ The base class for any Prometheus-based integration. """ + # See Performance Optimizations in this package's README.md. import requests args = list(args) diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py index 58c1ca3a3362a..ff2a801c5137a 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/mixins.py @@ -377,6 +377,7 @@ def get_http_handler(self, scraper_config): The http handler doesn't use the cache if a bearer token is used to allow refreshing it. """ + # See Performance Optimizations in this package's README.md. from ...utils.http import RequestsWrapper prometheus_url = scraper_config['prometheus_url'] @@ -825,6 +826,7 @@ def poll(self, scraper_config, headers=None): Custom headers can be added to the default headers. """ + # See Performance Optimizations in this package's README.md. import requests endpoint = scraper_config.get('prometheus_url') @@ -1061,6 +1063,7 @@ def _decumulate_histogram_buckets(self, metric): """ Decumulate buckets in a given histogram metric and adds the lower_bound label (le being upper_bound) """ + # See Performance Optimizations in this package's README.md. from prometheus_client.samples import Sample bucket_values_by_context_upper_bound = {} diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py index 172990b0984fb..d4a0722afc38a 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/base.py @@ -60,6 +60,7 @@ def check(self, _): Another thing to note is that this check ignores its instance argument completely. We take care of instance-level customization at initialization time. """ + # See Performance Optimizations in this package's README.md. from requests.exceptions import RequestException self.refresh_scrapers() diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py index ec860e476158c..ffcbb430f31ef 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper.py @@ -45,6 +45,7 @@ def __init__(self, check, config): """ The base class for any scraper overrides. """ + # See Performance Optimizations in this package's README.md. from ....utils.http import RequestsWrapper self.config = config @@ -329,6 +330,7 @@ def parse_metrics(self): @property def parse_metric_families(self): + # See Performance Optimizations in this package's README.md. from prometheus_client.openmetrics.parser import text_fd_to_metric_families as parse_openmetrics from prometheus_client.parser import text_fd_to_metric_families as parse_prometheus @@ -392,6 +394,7 @@ def stream_connection_lines(self): """ Yield the connection line. """ + # See Performance Optimizations in this package's README.md. from requests.exceptions import ConnectionError try: diff --git a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py index 6d1072d20c1ef..b613ec6c045bd 100644 --- a/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py +++ b/datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/utils.py @@ -9,6 +9,7 @@ def decumulate_histogram_buckets(sample_data): """ Decumulate buckets in a given histogram metric and adds the lower_bound label (le being upper_bound) """ + # See Performance Optimizations in this package's README.md. from prometheus_client.samples import Sample # TODO: investigate performance optimizations From 36483d22464a69ed278ac6536e289dc62328305d Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Thu, 6 Mar 2025 12:58:33 +0100 Subject: [PATCH 6/6] update changelog --- datadog_checks_base/changelog.d/{19679.fixed => 19781.fixed} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename datadog_checks_base/changelog.d/{19679.fixed => 19781.fixed} (100%) diff --git a/datadog_checks_base/changelog.d/19679.fixed b/datadog_checks_base/changelog.d/19781.fixed similarity index 100% rename from datadog_checks_base/changelog.d/19679.fixed rename to datadog_checks_base/changelog.d/19781.fixed