Skip to content

Commit

Permalink
Add ProxyLoggerProvider, ProxyLogger and associated tests (#3575)
Browse files Browse the repository at this point in the history
* 3060 adds ProxyLoggerProvider, ProxyLogger and associated tests

* Updates changelog for 3060 (add proxy classes for logging)

* Removes proxy classes from module level log import following review

* Moves changelog entry to unreleased

* Fixes build failures

* Fix mypy and lint

---------
  • Loading branch information
garry-cairns committed Feb 9, 2024
1 parent 39f0f82 commit 3500f5c
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3623](https://github.com/open-telemetry/opentelemetry-python/pull/3623))
- Improve Resource Detector timeout messaging
([#3645](https://github.com/open-telemetry/opentelemetry-python/pull/3645))
- Add Proxy classes for logging
([#3575](https://github.com/open-telemetry/opentelemetry-python/pull/3575))

## Version 1.22.0/0.43b0 (2023-12-15)

Expand Down
61 changes: 54 additions & 7 deletions opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,37 @@ def emit(self, record: "LogRecord") -> None:
pass


class ProxyLogger(Logger):
def __init__( # pylint: disable=super-init-not-called
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
):
self._name = name
self._version = version
self._schema_url = schema_url
self._real_logger: Optional[Logger] = None
self._noop_logger = NoOpLogger(name)

@property
def _logger(self) -> Logger:
if self._real_logger:
return self._real_logger

if _LOGGER_PROVIDER:
self._real_logger = _LOGGER_PROVIDER.get_logger(
self._name,
self._version,
self._schema_url,
)
return self._real_logger
return self._noop_logger

def emit(self, record: LogRecord) -> None:
self._logger.emit(record)


class LoggerProvider(ABC):
"""
LoggerProvider is the entry point of the API. It provides access to Logger instances.
Expand Down Expand Up @@ -166,21 +197,37 @@ def get_logger(
return NoOpLogger(name, version=version, schema_url=schema_url)


# TODO: ProxyLoggerProvider
class ProxyLoggerProvider(LoggerProvider):
def get_logger(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> Logger:
if _LOGGER_PROVIDER:
return _LOGGER_PROVIDER.get_logger(
name,
version=version,
schema_url=schema_url,
)
return ProxyLogger(
name,
version=version,
schema_url=schema_url,
)


_LOGGER_PROVIDER_SET_ONCE = Once()
_LOGGER_PROVIDER = None
_LOGGER_PROVIDER: Optional[LoggerProvider] = None
_PROXY_LOGGER_PROVIDER = ProxyLoggerProvider()


def get_logger_provider() -> LoggerProvider:
"""Gets the current global :class:`~.LoggerProvider` object."""
global _LOGGER_PROVIDER # pylint: disable=global-statement
global _LOGGER_PROVIDER # pylint: disable=global-variable-not-assigned
if _LOGGER_PROVIDER is None:
if _OTEL_PYTHON_LOGGER_PROVIDER not in environ:
# TODO: return proxy
_LOGGER_PROVIDER = NoOpLoggerProvider()
return _LOGGER_PROVIDER
return _PROXY_LOGGER_PROVIDER

logger_provider: LoggerProvider = _load_provider( # type: ignore
_OTEL_PYTHON_LOGGER_PROVIDER, "logger_provider"
Expand All @@ -194,7 +241,7 @@ def get_logger_provider() -> LoggerProvider:
def _set_logger_provider(logger_provider: LoggerProvider, log: bool) -> None:
def set_lp() -> None:
global _LOGGER_PROVIDER # pylint: disable=global-statement
_LOGGER_PROVIDER = logger_provider # type: ignore
_LOGGER_PROVIDER = logger_provider

did_set = _LOGGER_PROVIDER_SET_ONCE.do_once(set_lp)

Expand Down
2 changes: 1 addition & 1 deletion opentelemetry-api/tests/logs/test_logger_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_get_logger_provider(self):
assert logs_internal._LOGGER_PROVIDER is None

assert isinstance(
get_logger_provider(), logs_internal.NoOpLoggerProvider
get_logger_provider(), logs_internal.ProxyLoggerProvider
)

logs_internal._LOGGER_PROVIDER = None
Expand Down
63 changes: 63 additions & 0 deletions opentelemetry-api/tests/logs/test_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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=W0212,W0222,W0221
import typing
import unittest

import opentelemetry._logs._internal as _logs_internal
from opentelemetry import _logs
from opentelemetry.sdk._logs import LogRecord # type: ignore
from opentelemetry.test.globals_test import LoggingGlobalsTest


class TestProvider(_logs.NoOpLoggerProvider):
def get_logger(
self,
name: str,
version: typing.Optional[str] = None,
schema_url: typing.Optional[str] = None,
) -> _logs.Logger:
return TestLogger(name)


class TestLogger(_logs.NoOpLogger):
def emit(self, *args, **kwargs):
return LogRecord(timestamp=0)


class TestProxy(LoggingGlobalsTest, unittest.TestCase):
def test_proxy_logger(self):
provider = _logs.get_logger_provider()
# proxy provider
self.assertIsInstance(provider, _logs_internal.ProxyLoggerProvider)

# provider returns proxy logger
logger = provider.get_logger("proxy-test")
self.assertIsInstance(logger, _logs_internal.ProxyLogger)

# set a real provider
_logs.set_logger_provider(TestProvider())

# get_logger_provider() now returns the real provider
self.assertIsInstance(_logs.get_logger_provider(), TestProvider)

# logger provider now returns real instance
self.assertIsInstance(
_logs.get_logger_provider().get_logger("fresh"), TestLogger
)

# references to the old provider still work but return real logger now
real_logger = provider.get_logger("proxy-test")
self.assertIsInstance(real_logger, TestLogger)
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def reset_logging_globals() -> None:
"""WARNING: only use this for tests."""
logging_api._LOGGER_PROVIDER_SET_ONCE = Once() # type: ignore[attr-defined]
logging_api._LOGGER_PROVIDER = None # type: ignore[attr-defined]
# logging_api._PROXY_LOGGER_PROVIDER = _ProxyLoggerProvider() # type: ignore[attr-defined]
logging_api._PROXY_LOGGER_PROVIDER = logging_api.ProxyLoggerProvider() # type: ignore[attr-defined]


class TraceGlobalsTest(unittest.TestCase):
Expand Down Expand Up @@ -73,3 +73,18 @@ def setUp(self) -> None:
def tearDown(self) -> None:
super().tearDown()
reset_metrics_globals()


class LoggingGlobalsTest(unittest.TestCase):
"""Resets logging API globals in setUp/tearDown
Use as a base class or mixin for your test that modifies logging API globals.
"""

def setUp(self) -> None:
super().setUp()
reset_logging_globals()

def tearDown(self) -> None:
super().tearDown()
reset_logging_globals()

0 comments on commit 3500f5c

Please sign in to comment.