From bd51fcb7a3afb08aec975e7302c9cc36060bcacc Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 10 Sep 2024 08:44:47 -0700 Subject: [PATCH 1/2] Implement events sdk (#4176) --- CHANGELOG.md | 2 + .../src/opentelemetry/sdk/_events/__init__.py | 89 ++++++++ opentelemetry-sdk/tests/events/__init__.py | 13 ++ opentelemetry-sdk/tests/events/test_events.py | 202 ++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py create mode 100644 opentelemetry-sdk/tests/events/__init__.py create mode 100644 opentelemetry-sdk/tests/events/test_events.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7152dbaafcb..6c0d1711c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4154](https://github.com/open-telemetry/opentelemetry-python/pull/4154)) - sdk: Add support for log formatting ([#4137](https://github.com/open-telemetry/opentelemetry-python/pull/4166)) +- Implement events sdk + ([#4176](https://github.com/open-telemetry/opentelemetry-python/pull/4176)) ## Version 1.27.0/0.48b0 (2024-08-28) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py new file mode 100644 index 00000000000..ae16302546d --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py @@ -0,0 +1,89 @@ +# Copyright The OpenTelemetry 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 +from time import time_ns +from typing import Optional + +from opentelemetry import trace +from opentelemetry._events import Event +from opentelemetry._events import EventLogger as APIEventLogger +from opentelemetry._events import EventLoggerProvider as APIEventLoggerProvider +from opentelemetry._logs import NoOpLogger, SeverityNumber, get_logger_provider +from opentelemetry.sdk._logs import Logger, LoggerProvider, LogRecord +from opentelemetry.util.types import Attributes + +_logger = logging.getLogger(__name__) + + +class EventLogger(APIEventLogger): + def __init__( + self, + logger_provider: LoggerProvider, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + attributes: Optional[Attributes] = None, + ): + super().__init__( + name=name, + version=version, + schema_url=schema_url, + attributes=attributes, + ) + self._logger: Logger = logger_provider.get_logger( + name, version, schema_url, attributes + ) + + def emit(self, event: Event) -> None: + if isinstance(self._logger, NoOpLogger): + # Do nothing if SDK is disabled + return + span_context = trace.get_current_span().get_span_context() + log_record = LogRecord( + timestamp=event.timestamp or time_ns(), + observed_timestamp=None, + trace_id=event.trace_id or span_context.trace_id, + span_id=event.span_id or span_context.span_id, + trace_flags=event.trace_flags or span_context.trace_flags, + severity_text=None, + severity_number=event.severity_number or SeverityNumber.INFO, + body=event.body, + resource=getattr(self._logger, "resource", None), + attributes=event.attributes, + ) + self._logger.emit(log_record) + + +class EventLoggerProvider(APIEventLoggerProvider): + def __init__(self, logger_provider: Optional[LoggerProvider] = None): + self._logger_provider = logger_provider or get_logger_provider() + + def get_event_logger( + self, + name: str, + version: Optional[str] = None, + schema_url: Optional[str] = None, + attributes: Optional[Attributes] = None, + ) -> EventLogger: + if not name: + _logger.warning("EventLogger created with invalid name: %s", name) + return EventLogger( + self._logger_provider, name, version, schema_url, attributes + ) + + def shutdown(self): + self._logger_provider.shutdown() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + self._logger_provider.force_flush(timeout_millis) diff --git a/opentelemetry-sdk/tests/events/__init__.py b/opentelemetry-sdk/tests/events/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/opentelemetry-sdk/tests/events/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry 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. diff --git a/opentelemetry-sdk/tests/events/test_events.py b/opentelemetry-sdk/tests/events/test_events.py new file mode 100644 index 00000000000..7b8d42ff316 --- /dev/null +++ b/opentelemetry-sdk/tests/events/test_events.py @@ -0,0 +1,202 @@ +# Copyright The OpenTelemetry 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. + +# pylint: disable=protected-access,no-self-use + +import unittest +from unittest.mock import Mock, patch + +from opentelemetry._events import Event +from opentelemetry._logs import SeverityNumber, set_logger_provider +from opentelemetry.sdk._events import EventLoggerProvider +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs._internal import Logger, NoOpLogger +from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED + + +class TestEventLoggerProvider(unittest.TestCase): + def test_event_logger_provider(self): + logger_provider = LoggerProvider() + event_logger_provider = EventLoggerProvider( + logger_provider=logger_provider + ) + + self.assertEqual( + event_logger_provider._logger_provider, + logger_provider, + ) + + def test_event_logger_provider_default(self): + logger_provider = LoggerProvider() + set_logger_provider(logger_provider) + event_logger_provider = EventLoggerProvider() + + self.assertEqual( + event_logger_provider._logger_provider, + logger_provider, + ) + + def test_get_event_logger(self): + logger_provider = LoggerProvider() + event_logger = EventLoggerProvider(logger_provider).get_event_logger( + "name", + version="version", + schema_url="schema_url", + attributes={"key": "value"}, + ) + self.assertTrue( + event_logger._logger, + Logger, + ) + logger = event_logger._logger + self.assertEqual(logger._instrumentation_scope.name, "name") + self.assertEqual(logger._instrumentation_scope.version, "version") + self.assertEqual( + logger._instrumentation_scope.schema_url, "schema_url" + ) + self.assertEqual( + logger._instrumentation_scope.attributes, {"key": "value"} + ) + + @patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"}) + def test_get_event_logger_with_sdk_disabled(self): + logger_provider = LoggerProvider() + event_logger = EventLoggerProvider(logger_provider).get_event_logger( + "name", + version="version", + schema_url="schema_url", + attributes={"key": "value"}, + ) + self.assertIsInstance(event_logger._logger, NoOpLogger) + + def test_force_flush(self): + logger_provider = Mock() + event_logger = EventLoggerProvider(logger_provider) + event_logger.force_flush(1000) + logger_provider.force_flush.assert_called_once_with(1000) + + def test_shutdown(self): + logger_provider = Mock() + event_logger = EventLoggerProvider(logger_provider) + event_logger.shutdown() + logger_provider.shutdown.assert_called_once() + + @patch("opentelemetry.sdk._logs._internal.LoggerProvider.get_logger") + def test_event_logger(self, logger_mock): + logger_provider = LoggerProvider() + logger_mock_inst = Mock() + logger_mock.return_value = logger_mock_inst + EventLoggerProvider(logger_provider).get_event_logger( + "name", + version="version", + schema_url="schema_url", + attributes={"key": "value"}, + ) + logger_mock.assert_called_once_with( + "name", "version", "schema_url", {"key": "value"} + ) + + @patch("opentelemetry.sdk._events.LogRecord") + @patch("opentelemetry.sdk._logs._internal.LoggerProvider.get_logger") + def test_event_logger_emit(self, logger_mock, log_record_mock): + logger_provider = LoggerProvider() + logger_mock_inst = Mock() + logger_mock.return_value = logger_mock_inst + event_logger = EventLoggerProvider(logger_provider).get_event_logger( + "name", + version="version", + schema_url="schema_url", + attributes={"key": "value"}, + ) + logger_mock.assert_called_once_with( + "name", "version", "schema_url", {"key": "value"} + ) + now = Mock() + trace_id = Mock() + span_id = Mock() + trace_flags = Mock() + event = Event( + name="test_event", + timestamp=now, + trace_id=trace_id, + span_id=span_id, + trace_flags=trace_flags, + body="test body", + severity_number=SeverityNumber.ERROR, + attributes={ + "key": "val", + "foo": "bar", + "event.name": "not this one", + }, + ) + log_record_mock_inst = Mock() + log_record_mock.return_value = log_record_mock_inst + event_logger.emit(event) + log_record_mock.assert_called_once_with( + timestamp=now, + observed_timestamp=None, + trace_id=trace_id, + span_id=span_id, + trace_flags=trace_flags, + severity_text=None, + severity_number=SeverityNumber.ERROR, + body="test body", + resource=event_logger._logger.resource, + attributes={ + "key": "val", + "foo": "bar", + "event.name": "test_event", + }, + ) + logger_mock_inst.emit.assert_called_once_with(log_record_mock_inst) + + @patch("opentelemetry.sdk._events.LogRecord") + @patch("opentelemetry.sdk._logs._internal.LoggerProvider.get_logger") + def test_event_logger_emit_sdk_disabled( + self, logger_mock, log_record_mock + ): + logger_provider = LoggerProvider() + logger_mock_inst = Mock(spec=NoOpLogger) + logger_mock.return_value = logger_mock_inst + event_logger = EventLoggerProvider(logger_provider).get_event_logger( + "name", + version="version", + schema_url="schema_url", + attributes={"key": "value"}, + ) + logger_mock.assert_called_once_with( + "name", "version", "schema_url", {"key": "value"} + ) + now = Mock() + trace_id = Mock() + span_id = Mock() + trace_flags = Mock() + event = Event( + name="test_event", + timestamp=now, + trace_id=trace_id, + span_id=span_id, + trace_flags=trace_flags, + body="test body", + severity_number=SeverityNumber.ERROR, + attributes={ + "key": "val", + "foo": "bar", + "event.name": "not this one", + }, + ) + log_record_mock_inst = Mock() + log_record_mock.return_value = log_record_mock_inst + event_logger.emit(event) + logger_mock_inst.emit.assert_not_called() From a8aacb0c6f2f06bf19b501d98e62f7c0e667fa4c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 11 Sep 2024 16:55:35 -0600 Subject: [PATCH 2/2] Add more symbols to opentelemetry.util._importlib_metadata (#4181) Fixes #4180 --- .../opentelemetry/util/_importlib_metadata.py | 20 +++++++++++++------ .../tests/util/test__importlib_metadata.py | 3 +++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py index cbf09f3ef8c..2457630ba22 100644 --- a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py +++ b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py @@ -15,15 +15,23 @@ # FIXME: Use importlib.metadata when support for 3.11 is dropped if the rest of # the supported versions at that time have the same API. from importlib_metadata import ( # type: ignore + Distribution, EntryPoint, EntryPoints, + PackageNotFoundError, + distributions, entry_points, + requires, version, ) -# The importlib-metadata library has introduced breaking changes before to its -# API, this module is kept just to act as a layer between the -# importlib-metadata library and our project if in any case it is necessary to -# do so. - -__all__ = ["entry_points", "version", "EntryPoint", "EntryPoints"] +__all__ = [ + "entry_points", + "version", + "EntryPoint", + "EntryPoints", + "requires", + "Distribution", + "distributions", + "PackageNotFoundError", +] diff --git a/opentelemetry-api/tests/util/test__importlib_metadata.py b/opentelemetry-api/tests/util/test__importlib_metadata.py index 92a4e7dd62a..dce949c47d8 100644 --- a/opentelemetry-api/tests/util/test__importlib_metadata.py +++ b/opentelemetry-api/tests/util/test__importlib_metadata.py @@ -19,6 +19,7 @@ from opentelemetry.util._importlib_metadata import ( entry_points as importlib_metadata_entry_points, ) +from opentelemetry.util._importlib_metadata import version class TestEntryPoints(TestCase): @@ -106,3 +107,5 @@ def test_uniform_behavior(self): entry_points = importlib_metadata_entry_points(group="abc", name="abc") self.assertIsInstance(entry_points, EntryPoints) self.assertEqual(len(entry_points), 0) + + self.assertIsInstance(version("opentelemetry-api"), str)