Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events API implementation #4054

Merged
merged 35 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
dae55a5
initial commit
soumyadeepm04 Jul 14, 2024
078d75c
lint
soumyadeepm04 Jul 14, 2024
7df9ed2
Merge branch 'main' into eventapi
soumyadeepm04 Jul 16, 2024
f028535
resolve comments
soumyadeepm04 Jul 19, 2024
722fb54
Merge branch 'eventapi' of https://github.com/soumyadeepm04/opentelem…
soumyadeepm04 Jul 19, 2024
0b3eec2
Merge branch 'main' into eventapi
xrmx Jul 22, 2024
32baf7c
Merge branch 'main' into eventapi
emdneto Jul 22, 2024
da611a8
updated CHANGELOG
soumyadeepm04 Jul 23, 2024
100826d
Merge branch 'eventapi' of https://github.com/soumyadeepm04/opentelem…
soumyadeepm04 Jul 23, 2024
79af742
Merge branch 'main' into eventapi
emdneto Jul 23, 2024
2d186bd
Merge branch 'main' into eventapi
soumyadeepm04 Jul 25, 2024
7a4db6c
initial full implementation
soumyadeepm04 Jul 27, 2024
a991447
Merge branch 'eventapi' of https://github.com/soumyadeepm04/opentelem…
soumyadeepm04 Jul 27, 2024
7111579
Merge branch 'main' into eventapi
soumyadeepm04 Jul 27, 2024
1115efc
lint
soumyadeepm04 Jul 27, 2024
a20d31e
Merge branch 'eventapi' of https://github.com/soumyadeepm04/opentelem…
soumyadeepm04 Jul 27, 2024
e328c9d
lint
soumyadeepm04 Jul 27, 2024
0d1dd73
lint
soumyadeepm04 Jul 27, 2024
bfe1fb0
rename file
soumyadeepm04 Jul 27, 2024
235eec5
lint
soumyadeepm04 Jul 27, 2024
f25db47
Merge branch 'main' into eventapi
lzchen Jul 30, 2024
cfc3792
fix comments
soumyadeepm04 Aug 13, 2024
5589341
Merge branch 'eventapi' of https://github.com/soumyadeepm04/opentelem…
soumyadeepm04 Aug 13, 2024
2ff0a4e
fix lint
soumyadeepm04 Aug 13, 2024
1742aaa
fix comments
soumyadeepm04 Aug 13, 2024
f597da6
fix lint
soumyadeepm04 Aug 13, 2024
c3642ed
fix lint
soumyadeepm04 Aug 13, 2024
e379cb4
Merge branch 'main' into eventapi
lzchen Aug 13, 2024
f5928e8
Merge branch 'main' into eventapi
xrmx Aug 19, 2024
3259a1b
Merge branch 'main' into eventapi
xrmx Aug 21, 2024
ad8244d
Update opentelemetry-api/src/opentelemetry/_events/__init__.py
soumyadeepm04 Aug 21, 2024
9e07117
fix attributes
soumyadeepm04 Aug 21, 2024
68831d3
Merge branch 'eventapi' of https://github.com/soumyadeepm04/opentelem…
soumyadeepm04 Aug 21, 2024
5b10d0b
fix lint
soumyadeepm04 Aug 21, 2024
2cf8bd9
Merge branch 'main' into eventapi
lzchen Aug 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
soumyadeepm04 marked this conversation as resolved.
Show resolved Hide resolved
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,
lzchen marked this conversation as resolved.
Show resolved Hide resolved
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,
lzchen marked this conversation as resolved.
Show resolved Hide resolved
attributes=event_attributes,
)
self.name = name


class EventLogger(ABC):
xrmx marked this conversation as resolved.
Show resolved Hide resolved

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


soumyadeepm04 marked this conversation as resolved.
Show resolved Hide resolved
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