diff --git a/examples/python/observability/requirements.txt b/examples/python/observability/requirements.txt index c20bd70143d..f7997817571 100644 --- a/examples/python/observability/requirements.txt +++ b/examples/python/observability/requirements.txt @@ -1,2 +1,3 @@ grpcio>=1.62.0 grpcio-observability>=1.62.0 +opentelemetry-sdk==1.21.0 diff --git a/src/python/grpcio_observability/README.rst b/src/python/grpcio_observability/README.rst index a597057198f..a4122f79bd8 100644 --- a/src/python/grpcio_observability/README.rst +++ b/src/python/grpcio_observability/README.rst @@ -62,7 +62,6 @@ gRPC Python Observability Depends on the following packages: :: grpcio - opentelemetry-sdk==1.21.0 opentelemetry-api==1.21.0 diff --git a/src/python/grpcio_observability/grpc_observability/_open_telemetry_observability.py b/src/python/grpcio_observability/grpc_observability/_open_telemetry_observability.py index e6f94e77211..a102509e5c8 100644 --- a/src/python/grpcio_observability/grpc_observability/_open_telemetry_observability.py +++ b/src/python/grpcio_observability/grpc_observability/_open_telemetry_observability.py @@ -62,7 +62,6 @@ class OpenTelemetryObservability(grpc._observability.ObservabilityPlugin): This is class is part of an EXPERIMENTAL API. Args: - exporter: Exporter used to export data. plugin: OpenTelemetryPlugin to enable. """ @@ -73,17 +72,13 @@ class OpenTelemetryObservability(grpc._observability.ObservabilityPlugin): self, *, plugins: Optional[Iterable[OpenTelemetryPlugin]] = None, - exporter: "grpc_observability.Exporter" = None, ): _plugins = [] if plugins: for plugin in plugins: _plugins.append(_OpenTelemetryPlugin(plugin)) - if exporter: - self.exporter = exporter - else: - self.exporter = _OpenTelemetryExporterDelegator(_plugins) + self.exporter = _OpenTelemetryExporterDelegator(_plugins) try: _cyobservability.activate_stats() diff --git a/src/python/grpcio_observability/grpc_observability/_open_telemetry_plugin.py b/src/python/grpcio_observability/grpc_observability/_open_telemetry_plugin.py index 212a9ec1b82..fd782bdef47 100644 --- a/src/python/grpcio_observability/grpc_observability/_open_telemetry_plugin.py +++ b/src/python/grpcio_observability/grpc_observability/_open_telemetry_plugin.py @@ -20,10 +20,10 @@ import grpc from grpc_observability import _open_telemetry_measures from grpc_observability._cyobservability import MetricsName from grpc_observability._observability import StatsData -from opentelemetry.sdk.metrics import Counter -from opentelemetry.sdk.metrics import Histogram -from opentelemetry.sdk.metrics import Meter -from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.metrics import Counter +from opentelemetry.metrics import Histogram +from opentelemetry.metrics import Meter +from opentelemetry.metrics import MeterProvider GRPC_METHOD_LABEL = "grpc.method" GRPC_TARGET_LABEL = "grpc.target" @@ -123,7 +123,7 @@ class OpenTelemetryPlugin: self, target: str # pylint: disable=unused-argument ) -> bool: """ - If set, this will be called per channel to decide whether to record the + Once overridden, this will be called per channel to decide whether to record the target attribute on client or to replace it with "other". This helps reduce the cardinality on metrics in cases where many channels are created with different targets in the same binary (which might happen @@ -142,7 +142,7 @@ class OpenTelemetryPlugin: self, method: str # pylint: disable=unused-argument ) -> bool: """ - If set, this will be called with a generic method type to decide whether to + Once overridden, this will be called with a generic method type to decide whether to record the method name or to replace it with "other". Note that pre-registered methods will always be recorded no matter what this diff --git a/src/python/grpcio_observability/setup.py b/src/python/grpcio_observability/setup.py index cac2417d919..4d4ebbeec57 100644 --- a/src/python/grpcio_observability/setup.py +++ b/src/python/grpcio_observability/setup.py @@ -290,7 +290,6 @@ setuptools.setup( install_requires=[ "grpcio=={version}".format(version=grpc_version.VERSION), "setuptools>=59.6.0", - "opentelemetry-sdk==1.21.0", "opentelemetry-api==1.21.0", ], cmdclass={ diff --git a/src/python/grpcio_tests/tests/observability/BUILD.bazel b/src/python/grpcio_tests/tests/observability/BUILD.bazel index bd4e10b450a..b7a531423a7 100644 --- a/src/python/grpcio_tests/tests/observability/BUILD.bazel +++ b/src/python/grpcio_tests/tests/observability/BUILD.bazel @@ -23,20 +23,6 @@ py_library( srcs = ["_from_observability_import_star.py"], ) -py_test( - name = "_observability_test", - size = "small", - srcs = ["_observability_test.py"], - imports = ["../../"], - main = "_observability_test.py", - deps = [ - ":test_server", - "//src/python/grpcio/grpc:grpcio", - "//src/python/grpcio_observability/grpc_observability:pyobservability", - "//src/python/grpcio_tests/tests/testing", - ], -) - py_test( name = "_open_telemetry_observability_test", size = "small", diff --git a/src/python/grpcio_tests/tests/observability/_observability_test.py b/src/python/grpcio_tests/tests/observability/_observability_test.py deleted file mode 100644 index 55450b035d9..00000000000 --- a/src/python/grpcio_tests/tests/observability/_observability_test.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright 2023 gRPC authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os -import sys -from typing import List -import unittest - -import grpc -import grpc_observability -from grpc_observability import _cyobservability -from grpc_observability import _observability - -from tests.observability import _test_server - -logger = logging.getLogger(__name__) - -STREAM_LENGTH = 5 - - -class TestExporter(_observability.Exporter): - def __init__( - self, - metrics: List[_observability.StatsData], - spans: List[_observability.TracingData], - ): - self.span_collecter = spans - self.metric_collecter = metrics - self._server = None - - def export_stats_data( - self, stats_data: List[_observability.StatsData] - ) -> None: - self.metric_collecter.extend(stats_data) - - def export_tracing_data( - self, tracing_data: List[_observability.TracingData] - ) -> None: - self.span_collecter.extend(tracing_data) - - -class _ClientUnaryUnaryInterceptor(grpc.UnaryUnaryClientInterceptor): - def intercept_unary_unary( - self, continuation, client_call_details, request_or_iterator - ): - response = continuation(client_call_details, request_or_iterator) - return response - - -class _ServerInterceptor(grpc.ServerInterceptor): - def intercept_service(self, continuation, handler_call_details): - return continuation(handler_call_details) - - -@unittest.skipIf( - os.name == "nt" or "darwin" in sys.platform, - "Observability is not supported in Windows and MacOS", -) -class ObservabilityTest(unittest.TestCase): - def setUp(self): - self.all_metric = [] - self.all_span = [] - self.test_exporter = TestExporter(self.all_metric, self.all_span) - self._server = None - self._port = None - - def tearDown(self): - if self._server: - self._server.stop(0) - - def testRecordUnaryUnary(self): - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server() - self._server = server - _test_server.unary_unary_call(port=port) - - self.assertGreater(len(self.all_metric), 0) - self._validate_metrics(self.all_metric) - - def testRecordUnaryUnaryWithClientInterceptor(self): - interceptor = _ClientUnaryUnaryInterceptor() - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server() - self._server = server - _test_server.intercepted_unary_unary_call( - port=port, interceptors=interceptor - ) - - self.assertGreater(len(self.all_metric), 0) - self._validate_metrics(self.all_metric) - - def testRecordUnaryUnaryWithServerInterceptor(self): - interceptor = _ServerInterceptor() - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server(interceptors=[interceptor]) - self._server = server - _test_server.unary_unary_call(port=port) - - self.assertGreater(len(self.all_metric), 0) - self._validate_metrics(self.all_metric) - - def testThrowErrorWhenCallingMultipleInit(self): - with self.assertRaises(ValueError): - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ) as o11y: - grpc._observability.observability_init(o11y) - - def testRecordUnaryStream(self): - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server() - self._server = server - _test_server.unary_stream_call(port=port) - - self.assertGreater(len(self.all_metric), 0) - self._validate_metrics(self.all_metric) - - def testRecordStreamUnary(self): - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server() - self._server = server - _test_server.stream_unary_call(port=port) - - self.assertTrue(len(self.all_metric) > 0) - self._validate_metrics(self.all_metric) - - def testRecordStreamStream(self): - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server() - self._server = server - _test_server.stream_stream_call(port=port) - - self.assertGreater(len(self.all_metric), 0) - self._validate_metrics(self.all_metric) - - def testNoRecordBeforeInit(self): - server, port = _test_server.start_server() - _test_server.unary_unary_call(port=port) - self.assertEqual(len(self.all_metric), 0) - server.stop(0) - - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server() - self._server = server - _test_server.unary_unary_call(port=port) - - self.assertGreater(len(self.all_metric), 0) - self._validate_metrics(self.all_metric) - - def testNoRecordAfterExit(self): - with grpc_observability.OpenTelemetryObservability( - exporter=self.test_exporter - ): - server, port = _test_server.start_server() - self._server = server - self._port = port - _test_server.unary_unary_call(port=port) - - self.assertGreater(len(self.all_metric), 0) - current_metric_len = len(self.all_metric) - self._validate_metrics(self.all_metric) - - _test_server.unary_unary_call(port=self._port) - self.assertEqual(len(self.all_metric), current_metric_len) - - def _validate_metrics( - self, metrics: List[_observability.StatsData] - ) -> None: - metric_names = set(metric.name for metric in metrics) - for name in _cyobservability.MetricsName: - if name not in metric_names: - logger.error( - "metric %s not found in exported metrics: %s!", - name, - metric_names, - ) - self.assertTrue(name in metric_names) - - -if __name__ == "__main__": - logging.basicConfig() - unittest.main(verbosity=2) diff --git a/src/python/grpcio_tests/tests/observability/_open_telemetry_observability_test.py b/src/python/grpcio_tests/tests/observability/_open_telemetry_observability_test.py index a3115b0108a..81c9750970f 100644 --- a/src/python/grpcio_tests/tests/observability/_open_telemetry_observability_test.py +++ b/src/python/grpcio_tests/tests/observability/_open_telemetry_observability_test.py @@ -21,6 +21,7 @@ import time from typing import Any, Callable, Dict, List, Optional, Set import unittest +import grpc import grpc_observability from grpc_observability import _open_telemetry_measures from grpc_observability._open_telemetry_plugin import GRPC_METHOD_LABEL @@ -100,6 +101,19 @@ class BaseTestOpenTelemetryPlugin(grpc_observability.OpenTelemetryPlugin): return self.provider +class _ClientUnaryUnaryInterceptor(grpc.UnaryUnaryClientInterceptor): + def intercept_unary_unary( + self, continuation, client_call_details, request_or_iterator + ): + response = continuation(client_call_details, request_or_iterator) + return response + + +class _ServerInterceptor(grpc.ServerInterceptor): + def intercept_service(self, continuation, handler_call_details): + return continuation(handler_call_details) + + @unittest.skipIf( os.name == "nt" or "darwin" in sys.platform, "Observability is not supported in Windows and MacOS", @@ -132,6 +146,42 @@ class OpenTelemetryObservabilityTest(unittest.TestCase): self._validate_metrics_exist(self.all_metrics) self._validate_all_metrics_names(self.all_metrics) + def testRecordUnaryUnaryWithClientInterceptor(self): + interceptor = _ClientUnaryUnaryInterceptor() + otel_plugin = BaseTestOpenTelemetryPlugin(self._provider) + with grpc_observability.OpenTelemetryObservability( + plugins=[otel_plugin] + ): + server, port = _test_server.start_server() + self._server = server + _test_server.intercepted_unary_unary_call( + port=port, interceptors=interceptor + ) + + self._validate_metrics_exist(self.all_metrics) + self._validate_all_metrics_names(self.all_metrics) + + def testRecordUnaryUnaryWithServerInterceptor(self): + interceptor = _ServerInterceptor() + otel_plugin = BaseTestOpenTelemetryPlugin(self._provider) + with grpc_observability.OpenTelemetryObservability( + plugins=[otel_plugin] + ): + server, port = _test_server.start_server(interceptors=[interceptor]) + self._server = server + _test_server.unary_unary_call(port=port) + + self._validate_metrics_exist(self.all_metrics) + self._validate_all_metrics_names(self.all_metrics) + + def testThrowErrorWhenCallingMultipleInit(self): + otel_plugin = BaseTestOpenTelemetryPlugin(self._provider) + with self.assertRaises(ValueError): + with grpc_observability.OpenTelemetryObservability( + plugins=[otel_plugin] + ) as o11y: + grpc._observability.observability_init(o11y) + def testRecordUnaryUnaryClientOnly(self): server, port = _test_server.start_server() self._server = server @@ -145,6 +195,41 @@ class OpenTelemetryObservabilityTest(unittest.TestCase): self._validate_metrics_exist(self.all_metrics) self._validate_client_metrics_names(self.all_metrics) + def testNoRecordBeforeInit(self): + server, port = _test_server.start_server() + _test_server.unary_unary_call(port=port) + self.assertEqual(len(self.all_metrics), 0) + server.stop(0) + + otel_plugin = BaseTestOpenTelemetryPlugin(self._provider) + with grpc_observability.OpenTelemetryObservability( + plugins=[otel_plugin] + ): + server, port = _test_server.start_server() + self._server = server + _test_server.unary_unary_call(port=port) + + self._validate_metrics_exist(self.all_metrics) + self._validate_all_metrics_names(self.all_metrics) + + def testNoRecordAfterExit(self): + otel_plugin = BaseTestOpenTelemetryPlugin(self._provider) + with grpc_observability.OpenTelemetryObservability( + plugins=[otel_plugin] + ): + server, port = _test_server.start_server() + self._server = server + self._port = port + _test_server.unary_unary_call(port=port) + + self._validate_metrics_exist(self.all_metrics) + self._validate_all_metrics_names(self.all_metrics) + + self.all_metrics = defaultdict(list) + _test_server.unary_unary_call(port=self._port) + with self.assertRaisesRegex(AssertionError, "No metrics was exported"): + self._validate_metrics_exist(self.all_metrics) + def testRecordUnaryStream(self): otel_plugin = BaseTestOpenTelemetryPlugin(self._provider) diff --git a/src/python/grpcio_tests/tests/tests.json b/src/python/grpcio_tests/tests/tests.json index 657cb94c028..9c42ec7baa0 100644 --- a/src/python/grpcio_tests/tests/tests.json +++ b/src/python/grpcio_tests/tests/tests.json @@ -10,7 +10,6 @@ "tests.interop._insecure_intraop_test.InsecureIntraopTest", "tests.interop._secure_intraop_test.SecureIntraopTest", "tests.observability._observability_api_test.AllTest", - "tests.observability._observability_test.ObservabilityTest", "tests.observability._open_telemetry_observability_test.OpenTelemetryObservabilityTest", "tests.protoc_plugin._python_plugin_test.ModuleMainTest", "tests.protoc_plugin._python_plugin_test.PythonPluginTest", diff --git a/tools/run_tests/helper_scripts/build_python.sh b/tools/run_tests/helper_scripts/build_python.sh index db71c656e50..dedbe02641f 100755 --- a/tools/run_tests/helper_scripts/build_python.sh +++ b/tools/run_tests/helper_scripts/build_python.sh @@ -217,7 +217,8 @@ pip_install_dir "$ROOT/src/python/grpcio_testing" # Build/install tests pip_install coverage==7.2.0 oauth2client==4.1.0 \ google-auth>=1.35.0 requests==2.31.0 \ - googleapis-common-protos>=1.5.5 rsa==4.0 absl-py==1.4.0 + googleapis-common-protos>=1.5.5 rsa==4.0 absl-py==1.4.0 \ + opentelemetry-sdk==1.21.0 $VENV_PYTHON "$ROOT/src/python/grpcio_tests/setup.py" preprocess $VENV_PYTHON "$ROOT/src/python/grpcio_tests/setup.py" build_package_protos pip_install_dir "$ROOT/src/python/grpcio_tests"