Skip to content

Commit b380e53

Browse files
Events API implementation (open-telemetry#4054)
1 parent 6e1429e commit b380e53

File tree

7 files changed

+370
-0
lines changed

7 files changed

+370
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Implementation of Events API
11+
([#4054](https://github.com/open-telemetry/opentelemetry-python/pull/4054))
1012
- Make log sdk add `exception.message` to logRecord for exceptions whose argument
1113
is an exception not a string message
1214
([#4122](https://github.com/open-telemetry/opentelemetry-python/pull/4122))
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from abc import ABC, abstractmethod
16+
from logging import getLogger
17+
from os import environ
18+
from typing import Any, Optional, cast
19+
20+
from opentelemetry._logs import LogRecord
21+
from opentelemetry._logs.severity import SeverityNumber
22+
from opentelemetry.environment_variables import (
23+
_OTEL_PYTHON_EVENT_LOGGER_PROVIDER,
24+
)
25+
from opentelemetry.trace.span import TraceFlags
26+
from opentelemetry.util._once import Once
27+
from opentelemetry.util._providers import _load_provider
28+
from opentelemetry.util.types import Attributes
29+
30+
_logger = getLogger(__name__)
31+
32+
33+
class Event(LogRecord):
34+
35+
def __init__(
36+
self,
37+
name: str,
38+
timestamp: Optional[int] = None,
39+
trace_id: Optional[int] = None,
40+
span_id: Optional[int] = None,
41+
trace_flags: Optional["TraceFlags"] = None,
42+
body: Optional[Any] = None,
43+
severity_number: Optional[SeverityNumber] = None,
44+
attributes: Optional[Attributes] = None,
45+
):
46+
attributes = attributes or {}
47+
event_attributes = {**attributes, "event.name": name}
48+
super().__init__(
49+
timestamp=timestamp,
50+
trace_id=trace_id,
51+
span_id=span_id,
52+
trace_flags=trace_flags,
53+
body=body, # type: ignore
54+
severity_number=severity_number,
55+
attributes=event_attributes,
56+
)
57+
self.name = name
58+
59+
60+
class EventLogger(ABC):
61+
62+
def __init__(
63+
self,
64+
name: str,
65+
version: Optional[str] = None,
66+
schema_url: Optional[str] = None,
67+
attributes: Optional[Attributes] = None,
68+
):
69+
self._name = name
70+
self._version = version
71+
self._schema_url = schema_url
72+
self._attributes = attributes
73+
74+
@abstractmethod
75+
def emit(self, event: "Event") -> None:
76+
"""Emits a :class:`Event` representing an event."""
77+
78+
79+
class NoOpEventLogger(EventLogger):
80+
81+
def emit(self, event: Event) -> None:
82+
pass
83+
84+
85+
class ProxyEventLogger(EventLogger):
86+
def __init__(
87+
self,
88+
name: str,
89+
version: Optional[str] = None,
90+
schema_url: Optional[str] = None,
91+
attributes: Optional[Attributes] = None,
92+
):
93+
super().__init__(
94+
name=name,
95+
version=version,
96+
schema_url=schema_url,
97+
attributes=attributes,
98+
)
99+
self._real_event_logger: Optional[EventLogger] = None
100+
self._noop_event_logger = NoOpEventLogger(name)
101+
102+
@property
103+
def _event_logger(self) -> EventLogger:
104+
if self._real_event_logger:
105+
return self._real_event_logger
106+
107+
if _EVENT_LOGGER_PROVIDER:
108+
self._real_event_logger = _EVENT_LOGGER_PROVIDER.get_event_logger(
109+
self._name,
110+
self._version,
111+
self._schema_url,
112+
self._attributes,
113+
)
114+
return self._real_event_logger
115+
return self._noop_event_logger
116+
117+
def emit(self, event: Event) -> None:
118+
self._event_logger.emit(event)
119+
120+
121+
class EventLoggerProvider(ABC):
122+
123+
@abstractmethod
124+
def get_event_logger(
125+
self,
126+
name: str,
127+
version: Optional[str] = None,
128+
schema_url: Optional[str] = None,
129+
attributes: Optional[Attributes] = None,
130+
) -> EventLogger:
131+
"""Returns an EventLoggerProvider for use."""
132+
133+
134+
class NoOpEventLoggerProvider(EventLoggerProvider):
135+
136+
def get_event_logger(
137+
self,
138+
name: str,
139+
version: Optional[str] = None,
140+
schema_url: Optional[str] = None,
141+
attributes: Optional[Attributes] = None,
142+
) -> EventLogger:
143+
return NoOpEventLogger(
144+
name, version=version, schema_url=schema_url, attributes=attributes
145+
)
146+
147+
148+
class ProxyEventLoggerProvider(EventLoggerProvider):
149+
150+
def get_event_logger(
151+
self,
152+
name: str,
153+
version: Optional[str] = None,
154+
schema_url: Optional[str] = None,
155+
attributes: Optional[Attributes] = None,
156+
) -> EventLogger:
157+
if _EVENT_LOGGER_PROVIDER:
158+
return _EVENT_LOGGER_PROVIDER.get_event_logger(
159+
name,
160+
version=version,
161+
schema_url=schema_url,
162+
attributes=attributes,
163+
)
164+
return ProxyEventLogger(
165+
name,
166+
version=version,
167+
schema_url=schema_url,
168+
attributes=attributes,
169+
)
170+
171+
172+
_EVENT_LOGGER_PROVIDER_SET_ONCE = Once()
173+
_EVENT_LOGGER_PROVIDER: Optional[EventLoggerProvider] = None
174+
_PROXY_EVENT_LOGGER_PROVIDER = ProxyEventLoggerProvider()
175+
176+
177+
def get_event_logger_provider() -> EventLoggerProvider:
178+
179+
global _EVENT_LOGGER_PROVIDER # pylint: disable=global-variable-not-assigned
180+
if _EVENT_LOGGER_PROVIDER is None:
181+
if _OTEL_PYTHON_EVENT_LOGGER_PROVIDER not in environ:
182+
return _PROXY_EVENT_LOGGER_PROVIDER
183+
184+
event_logger_provider: EventLoggerProvider = _load_provider( # type: ignore
185+
_OTEL_PYTHON_EVENT_LOGGER_PROVIDER, "event_logger_provider"
186+
)
187+
188+
_set_event_logger_provider(event_logger_provider, log=False)
189+
190+
return cast("EventLoggerProvider", _EVENT_LOGGER_PROVIDER)
191+
192+
193+
def _set_event_logger_provider(
194+
event_logger_provider: EventLoggerProvider, log: bool
195+
) -> None:
196+
def set_elp() -> None:
197+
global _EVENT_LOGGER_PROVIDER # pylint: disable=global-statement
198+
_EVENT_LOGGER_PROVIDER = event_logger_provider
199+
200+
did_set = _EVENT_LOGGER_PROVIDER_SET_ONCE.do_once(set_elp)
201+
202+
if log and did_set:
203+
_logger.warning(
204+
"Overriding of current EventLoggerProvider is not allowed"
205+
)
206+
207+
208+
def set_event_logger_provider(
209+
event_logger_provider: EventLoggerProvider,
210+
) -> None:
211+
212+
_set_event_logger_provider(event_logger_provider, log=True)
213+
214+
215+
def get_event_logger(
216+
name: str,
217+
version: Optional[str] = None,
218+
schema_url: Optional[str] = None,
219+
attributes: Optional[Attributes] = None,
220+
event_logger_provider: Optional[EventLoggerProvider] = None,
221+
) -> "EventLogger":
222+
if event_logger_provider is None:
223+
event_logger_provider = get_event_logger_provider()
224+
return event_logger_provider.get_event_logger(
225+
name,
226+
version,
227+
schema_url,
228+
attributes,
229+
)

opentelemetry-api/src/opentelemetry/environment_variables/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,8 @@
8181
"""
8282
.. envvar:: OTEL_PYTHON_LOGGER_PROVIDER
8383
"""
84+
85+
_OTEL_PYTHON_EVENT_LOGGER_PROVIDER = "OTEL_PYTHON_EVENT_LOGGER_PROVIDER"
86+
"""
87+
.. envvar:: OTEL_PYTHON_EVENT_LOGGER_PROVIDER
88+
"""
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import unittest
2+
3+
from opentelemetry._events import Event
4+
5+
6+
class TestEvent(unittest.TestCase):
7+
def test_event(self):
8+
event = Event("example", 123, attributes={"key": "value"})
9+
self.assertEqual(event.name, "example")
10+
self.assertEqual(event.timestamp, 123)
11+
self.assertEqual(
12+
event.attributes, {"key": "value", "event.name": "example"}
13+
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# type:ignore
2+
import unittest
3+
from unittest.mock import Mock, patch
4+
5+
import opentelemetry._events as events
6+
from opentelemetry._events import (
7+
get_event_logger_provider,
8+
set_event_logger_provider,
9+
)
10+
from opentelemetry.test.globals_test import EventsGlobalsTest
11+
12+
13+
class TestGlobals(EventsGlobalsTest, unittest.TestCase):
14+
def test_set_event_logger_provider(self):
15+
elp_mock = Mock()
16+
# pylint: disable=protected-access
17+
self.assertIsNone(events._EVENT_LOGGER_PROVIDER)
18+
set_event_logger_provider(elp_mock)
19+
self.assertIs(events._EVENT_LOGGER_PROVIDER, elp_mock)
20+
self.assertIs(get_event_logger_provider(), elp_mock)
21+
22+
def test_get_event_logger_provider(self):
23+
# pylint: disable=protected-access
24+
self.assertIsNone(events._EVENT_LOGGER_PROVIDER)
25+
26+
self.assertIsInstance(
27+
get_event_logger_provider(), events.ProxyEventLoggerProvider
28+
)
29+
30+
events._EVENT_LOGGER_PROVIDER = None
31+
32+
with patch.dict(
33+
"os.environ",
34+
{
35+
"OTEL_PYTHON_EVENT_LOGGER_PROVIDER": "test_event_logger_provider"
36+
},
37+
):
38+
39+
with patch("opentelemetry._events._load_provider", Mock()):
40+
with patch(
41+
"opentelemetry._events.cast",
42+
Mock(**{"return_value": "test_event_logger_provider"}),
43+
):
44+
self.assertEqual(
45+
get_event_logger_provider(),
46+
"test_event_logger_provider",
47+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# pylint: disable=W0212,W0222,W0221
2+
import typing
3+
import unittest
4+
5+
import opentelemetry._events as events
6+
from opentelemetry.test.globals_test import EventsGlobalsTest
7+
from opentelemetry.util.types import Attributes
8+
9+
10+
class TestProvider(events.NoOpEventLoggerProvider):
11+
def get_event_logger(
12+
self,
13+
name: str,
14+
version: typing.Optional[str] = None,
15+
schema_url: typing.Optional[str] = None,
16+
attributes: typing.Optional[Attributes] = None,
17+
) -> events.EventLogger:
18+
return LoggerTest(name)
19+
20+
21+
class LoggerTest(events.NoOpEventLogger):
22+
def emit(self, event: events.Event) -> None:
23+
pass
24+
25+
26+
class TestProxy(EventsGlobalsTest, unittest.TestCase):
27+
def test_proxy_logger(self):
28+
provider = events.get_event_logger_provider()
29+
# proxy provider
30+
self.assertIsInstance(provider, events.ProxyEventLoggerProvider)
31+
32+
# provider returns proxy logger
33+
event_logger = provider.get_event_logger("proxy-test")
34+
self.assertIsInstance(event_logger, events.ProxyEventLogger)
35+
36+
# set a real provider
37+
events.set_event_logger_provider(TestProvider())
38+
39+
# get_logger_provider() now returns the real provider
40+
self.assertIsInstance(events.get_event_logger_provider(), TestProvider)
41+
42+
# logger provider now returns real instance
43+
self.assertIsInstance(
44+
events.get_event_logger_provider().get_event_logger("fresh"),
45+
LoggerTest,
46+
)
47+
48+
# references to the old provider still work but return real logger now
49+
real_logger = provider.get_event_logger("proxy-test")
50+
self.assertIsInstance(real_logger, LoggerTest)

0 commit comments

Comments
 (0)