[Python O11y] Remove exporter from public API and refactor tests (#35792)

<!--

If you know who should review your pull request, please assign it to that
person, otherwise the pull request would get assigned randomly.

If your pull request is for a specific language, please add the appropriate
lang label.

-->

Closes #35792

PiperOrigin-RevId: 604695910
pull/35821/head^2
Xuan Wang 1 year ago committed by Copybara-Service
parent 3ddc48bf62
commit 9aa3c5835a
  1. 1
      examples/python/observability/requirements.txt
  2. 1
      src/python/grpcio_observability/README.rst
  3. 7
      src/python/grpcio_observability/grpc_observability/_open_telemetry_observability.py
  4. 12
      src/python/grpcio_observability/grpc_observability/_open_telemetry_plugin.py
  5. 1
      src/python/grpcio_observability/setup.py
  6. 14
      src/python/grpcio_tests/tests/observability/BUILD.bazel
  7. 208
      src/python/grpcio_tests/tests/observability/_observability_test.py
  8. 85
      src/python/grpcio_tests/tests/observability/_open_telemetry_observability_test.py
  9. 1
      src/python/grpcio_tests/tests/tests.json
  10. 3
      tools/run_tests/helper_scripts/build_python.sh

@ -1,2 +1,3 @@
grpcio>=1.62.0
grpcio-observability>=1.62.0
opentelemetry-sdk==1.21.0

@ -62,7 +62,6 @@ gRPC Python Observability Depends on the following packages:
::
grpcio
opentelemetry-sdk==1.21.0
opentelemetry-api==1.21.0

@ -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()

@ -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

@ -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={

@ -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",

@ -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)

@ -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)

@ -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",

@ -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"

Loading…
Cancel
Save