Skip to content

Commit

Permalink
Events API implementation (#4054)
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyadeepm04 committed Aug 21, 2024
1 parent 6e1429e commit b380e53
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Implementation of Events API
([#4054](https://github.com/open-telemetry/opentelemetry-python/pull/4054))
- Make log sdk add `exception.message` to logRecord for exceptions whose argument
is an exception not a string message
([#4122](https://github.com/open-telemetry/opentelemetry-python/pull/4122))
Expand Down
229 changes: 229 additions & 0 deletions opentelemetry-api/src/opentelemetry/_events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# 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.

from abc import ABC, abstractmethod
from logging import getLogger
from os import environ
from typing import Any, Optional, cast

from opentelemetry._logs import LogRecord
from opentelemetry._logs.severity import SeverityNumber
from opentelemetry.environment_variables import (
_OTEL_PYTHON_EVENT_LOGGER_PROVIDER,
)
from opentelemetry.trace.span import TraceFlags
from opentelemetry.util._once import Once
from opentelemetry.util._providers import _load_provider
from opentelemetry.util.types import Attributes

_logger = getLogger(__name__)


class Event(LogRecord):

def __init__(
self,
name: str,
timestamp: Optional[int] = None,
trace_id: Optional[int] = None,
span_id: Optional[int] = None,
trace_flags: Optional["TraceFlags"] = None,
body: Optional[Any] = None,
severity_number: Optional[SeverityNumber] = None,
attributes: Optional[Attributes] = None,
):
attributes = attributes or {}
event_attributes = {**attributes, "event.name": name}
super().__init__(
timestamp=timestamp,
trace_id=trace_id,
span_id=span_id,
trace_flags=trace_flags,
body=body, # type: ignore
severity_number=severity_number,
attributes=event_attributes,
)
self.name = name


class EventLogger(ABC):

def __init__(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = None,
):
self._name = name
self._version = version
self._schema_url = schema_url
self._attributes = attributes

@abstractmethod
def emit(self, event: "Event") -> None:
"""Emits a :class:`Event` representing an event."""


class NoOpEventLogger(EventLogger):

def emit(self, event: Event) -> None:
pass


class ProxyEventLogger(EventLogger):
def __init__(
self,
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._real_event_logger: Optional[EventLogger] = None
self._noop_event_logger = NoOpEventLogger(name)

@property
def _event_logger(self) -> EventLogger:
if self._real_event_logger:
return self._real_event_logger

if _EVENT_LOGGER_PROVIDER:
self._real_event_logger = _EVENT_LOGGER_PROVIDER.get_event_logger(
self._name,
self._version,
self._schema_url,
self._attributes,
)
return self._real_event_logger
return self._noop_event_logger

def emit(self, event: Event) -> None:
self._event_logger.emit(event)


class EventLoggerProvider(ABC):

@abstractmethod
def get_event_logger(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = None,
) -> EventLogger:
"""Returns an EventLoggerProvider for use."""


class NoOpEventLoggerProvider(EventLoggerProvider):

def get_event_logger(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = None,
) -> EventLogger:
return NoOpEventLogger(
name, version=version, schema_url=schema_url, attributes=attributes
)


class ProxyEventLoggerProvider(EventLoggerProvider):

def get_event_logger(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = None,
) -> EventLogger:
if _EVENT_LOGGER_PROVIDER:
return _EVENT_LOGGER_PROVIDER.get_event_logger(
name,
version=version,
schema_url=schema_url,
attributes=attributes,
)
return ProxyEventLogger(
name,
version=version,
schema_url=schema_url,
attributes=attributes,
)


_EVENT_LOGGER_PROVIDER_SET_ONCE = Once()
_EVENT_LOGGER_PROVIDER: Optional[EventLoggerProvider] = None
_PROXY_EVENT_LOGGER_PROVIDER = ProxyEventLoggerProvider()


def get_event_logger_provider() -> EventLoggerProvider:

global _EVENT_LOGGER_PROVIDER # pylint: disable=global-variable-not-assigned
if _EVENT_LOGGER_PROVIDER is None:
if _OTEL_PYTHON_EVENT_LOGGER_PROVIDER not in environ:
return _PROXY_EVENT_LOGGER_PROVIDER

event_logger_provider: EventLoggerProvider = _load_provider( # type: ignore
_OTEL_PYTHON_EVENT_LOGGER_PROVIDER, "event_logger_provider"
)

_set_event_logger_provider(event_logger_provider, log=False)

return cast("EventLoggerProvider", _EVENT_LOGGER_PROVIDER)


def _set_event_logger_provider(
event_logger_provider: EventLoggerProvider, log: bool
) -> None:
def set_elp() -> None:
global _EVENT_LOGGER_PROVIDER # pylint: disable=global-statement
_EVENT_LOGGER_PROVIDER = event_logger_provider

did_set = _EVENT_LOGGER_PROVIDER_SET_ONCE.do_once(set_elp)

if log and did_set:
_logger.warning(
"Overriding of current EventLoggerProvider is not allowed"
)


def set_event_logger_provider(
event_logger_provider: EventLoggerProvider,
) -> None:

_set_event_logger_provider(event_logger_provider, log=True)


def get_event_logger(
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = None,
event_logger_provider: Optional[EventLoggerProvider] = None,
) -> "EventLogger":
if event_logger_provider is None:
event_logger_provider = get_event_logger_provider()
return event_logger_provider.get_event_logger(
name,
version,
schema_url,
attributes,
)
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,8 @@
"""
.. envvar:: OTEL_PYTHON_LOGGER_PROVIDER
"""

_OTEL_PYTHON_EVENT_LOGGER_PROVIDER = "OTEL_PYTHON_EVENT_LOGGER_PROVIDER"
"""
.. envvar:: OTEL_PYTHON_EVENT_LOGGER_PROVIDER
"""
13 changes: 13 additions & 0 deletions opentelemetry-api/tests/events/test_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import unittest

from opentelemetry._events import Event


class TestEvent(unittest.TestCase):
def test_event(self):
event = Event("example", 123, attributes={"key": "value"})
self.assertEqual(event.name, "example")
self.assertEqual(event.timestamp, 123)
self.assertEqual(
event.attributes, {"key": "value", "event.name": "example"}
)
47 changes: 47 additions & 0 deletions opentelemetry-api/tests/events/test_event_logger_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# type:ignore
import unittest
from unittest.mock import Mock, patch

import opentelemetry._events as events
from opentelemetry._events import (
get_event_logger_provider,
set_event_logger_provider,
)
from opentelemetry.test.globals_test import EventsGlobalsTest


class TestGlobals(EventsGlobalsTest, unittest.TestCase):
def test_set_event_logger_provider(self):
elp_mock = Mock()
# pylint: disable=protected-access
self.assertIsNone(events._EVENT_LOGGER_PROVIDER)
set_event_logger_provider(elp_mock)
self.assertIs(events._EVENT_LOGGER_PROVIDER, elp_mock)
self.assertIs(get_event_logger_provider(), elp_mock)

def test_get_event_logger_provider(self):
# pylint: disable=protected-access
self.assertIsNone(events._EVENT_LOGGER_PROVIDER)

self.assertIsInstance(
get_event_logger_provider(), events.ProxyEventLoggerProvider
)

events._EVENT_LOGGER_PROVIDER = None

with patch.dict(
"os.environ",
{
"OTEL_PYTHON_EVENT_LOGGER_PROVIDER": "test_event_logger_provider"
},
):

with patch("opentelemetry._events._load_provider", Mock()):
with patch(
"opentelemetry._events.cast",
Mock(**{"return_value": "test_event_logger_provider"}),
):
self.assertEqual(
get_event_logger_provider(),
"test_event_logger_provider",
)
50 changes: 50 additions & 0 deletions opentelemetry-api/tests/events/test_proxy_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# pylint: disable=W0212,W0222,W0221
import typing
import unittest

import opentelemetry._events as events
from opentelemetry.test.globals_test import EventsGlobalsTest
from opentelemetry.util.types import Attributes


class TestProvider(events.NoOpEventLoggerProvider):
def get_event_logger(
self,
name: str,
version: typing.Optional[str] = None,
schema_url: typing.Optional[str] = None,
attributes: typing.Optional[Attributes] = None,
) -> events.EventLogger:
return LoggerTest(name)


class LoggerTest(events.NoOpEventLogger):
def emit(self, event: events.Event) -> None:
pass


class TestProxy(EventsGlobalsTest, unittest.TestCase):
def test_proxy_logger(self):
provider = events.get_event_logger_provider()
# proxy provider
self.assertIsInstance(provider, events.ProxyEventLoggerProvider)

# provider returns proxy logger
event_logger = provider.get_event_logger("proxy-test")
self.assertIsInstance(event_logger, events.ProxyEventLogger)

# set a real provider
events.set_event_logger_provider(TestProvider())

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

# logger provider now returns real instance
self.assertIsInstance(
events.get_event_logger_provider().get_event_logger("fresh"),
LoggerTest,
)

# references to the old provider still work but return real logger now
real_logger = provider.get_event_logger("proxy-test")
self.assertIsInstance(real_logger, LoggerTest)
Loading

0 comments on commit b380e53

Please sign in to comment.