Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 27782e8

Browse files
srikanthccvlzchen
authored andcommitted
Add OTLPHandler for standard library logging module (open-telemetry#1903)
1 parent 7d868af commit 27782e8

File tree

4 files changed

+198
-1
lines changed

4 files changed

+198
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9393
### Added
9494
- Add global LogEmitterProvider and convenience function get_log_emitter
9595
([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901))
96+
- Add OTLPHandler for standard library logging module
97+
([#1903](https://github.com/open-telemetry/opentelemetry-python/pull/1903))
9698

9799
### Changed
98100
- Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource

opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
from opentelemetry.sdk.environment_variables import (
2222
OTEL_PYTHON_LOG_EMITTER_PROVIDER,
2323
)
24-
from opentelemetry.sdk.logs.severity import SeverityNumber
24+
from opentelemetry.sdk.logs.severity import SeverityNumber, std_to_otlp
2525
from opentelemetry.sdk.resources import Resource
2626
from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
27+
from opentelemetry.trace import get_current_span
2728
from opentelemetry.trace.span import TraceFlags
2829
from opentelemetry.util._providers import _load_provider
2930
from opentelemetry.util.types import Attributes
@@ -111,6 +112,48 @@ def force_flush(self, timeout_millis: int = 30000):
111112
"""
112113

113114

115+
class OTLPHandler(logging.Handler):
116+
"""A handler class which writes logging records, in OTLP format, to
117+
a network destination or file.
118+
"""
119+
120+
def __init__(self, level=logging.NOTSET, log_emitter=None) -> None:
121+
super().__init__(level=level)
122+
self._log_emitter = log_emitter or get_log_emitter(__name__)
123+
124+
def _translate(self, record: logging.LogRecord) -> LogRecord:
125+
timestamp = int(record.created * 1e9)
126+
span_context = get_current_span().get_span_context()
127+
# TODO: attributes (or resource attributes?) from record metadata
128+
attributes: Attributes = {}
129+
severity_number = std_to_otlp(record.levelno)
130+
return LogRecord(
131+
timestamp=timestamp,
132+
trace_id=span_context.trace_id,
133+
span_id=span_context.span_id,
134+
trace_flags=span_context.trace_flags,
135+
severity_text=record.levelname,
136+
severity_number=severity_number,
137+
body=record.getMessage(),
138+
resource=self._log_emitter.resource,
139+
attributes=attributes,
140+
)
141+
142+
def emit(self, record: logging.LogRecord) -> None:
143+
"""
144+
Emit a record.
145+
146+
The record is translated to OTLP format, and then sent across the pipeline.
147+
"""
148+
self._log_emitter.emit(self._translate(record))
149+
150+
def flush(self) -> None:
151+
"""
152+
Flushes the logging output.
153+
"""
154+
self._log_emitter.flush()
155+
156+
114157
class LogEmitter:
115158
# TODO: Add multi_log_processor
116159
def __init__(
@@ -121,6 +164,10 @@ def __init__(
121164
self._resource = resource
122165
self._instrumentation_info = instrumentation_info
123166

167+
@property
168+
def resource(self):
169+
return self._resource
170+
124171
def emit(self, record: LogRecord):
125172
# TODO: multi_log_processor.emit
126173
pass
@@ -142,6 +189,10 @@ def __init__(
142189
if shutdown_on_exit:
143190
self._at_exit_handler = atexit.register(self.shutdown)
144191

192+
@property
193+
def resource(self):
194+
return self._resource
195+
145196
def get_log_emitter(
146197
self,
147198
instrumenting_module_name: str,

opentelemetry-sdk/src/opentelemetry/sdk/logs/severity.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,63 @@ class SeverityNumber(enum.Enum):
5454
FATAL2 = 22
5555
FATAL3 = 23
5656
FATAL4 = 24
57+
58+
59+
_STD_TO_OTLP = {
60+
10: SeverityNumber.DEBUG,
61+
11: SeverityNumber.DEBUG2,
62+
12: SeverityNumber.DEBUG3,
63+
13: SeverityNumber.DEBUG4,
64+
14: SeverityNumber.DEBUG4,
65+
15: SeverityNumber.DEBUG4,
66+
16: SeverityNumber.DEBUG4,
67+
17: SeverityNumber.DEBUG4,
68+
18: SeverityNumber.DEBUG4,
69+
19: SeverityNumber.DEBUG4,
70+
20: SeverityNumber.INFO,
71+
21: SeverityNumber.INFO2,
72+
22: SeverityNumber.INFO3,
73+
23: SeverityNumber.INFO4,
74+
24: SeverityNumber.INFO4,
75+
25: SeverityNumber.INFO4,
76+
26: SeverityNumber.INFO4,
77+
27: SeverityNumber.INFO4,
78+
28: SeverityNumber.INFO4,
79+
29: SeverityNumber.INFO4,
80+
30: SeverityNumber.WARN,
81+
31: SeverityNumber.WARN2,
82+
32: SeverityNumber.WARN3,
83+
33: SeverityNumber.WARN4,
84+
34: SeverityNumber.WARN4,
85+
35: SeverityNumber.WARN4,
86+
36: SeverityNumber.WARN4,
87+
37: SeverityNumber.WARN4,
88+
38: SeverityNumber.WARN4,
89+
39: SeverityNumber.WARN4,
90+
40: SeverityNumber.ERROR,
91+
41: SeverityNumber.ERROR2,
92+
42: SeverityNumber.ERROR3,
93+
43: SeverityNumber.ERROR4,
94+
44: SeverityNumber.ERROR4,
95+
45: SeverityNumber.ERROR4,
96+
46: SeverityNumber.ERROR4,
97+
47: SeverityNumber.ERROR4,
98+
48: SeverityNumber.ERROR4,
99+
49: SeverityNumber.ERROR4,
100+
50: SeverityNumber.FATAL,
101+
51: SeverityNumber.FATAL2,
102+
52: SeverityNumber.FATAL3,
103+
53: SeverityNumber.FATAL4,
104+
}
105+
106+
107+
def std_to_otlp(levelno: int) -> SeverityNumber:
108+
"""
109+
Map python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels
110+
to OTLP log severity number.
111+
"""
112+
if levelno < 10:
113+
return SeverityNumber.UNSPECIFIED
114+
if levelno > 53:
115+
return SeverityNumber.FATAL4
116+
return _STD_TO_OTLP[levelno]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
import logging
16+
import unittest
17+
from unittest.mock import Mock
18+
19+
from opentelemetry.sdk import trace
20+
from opentelemetry.sdk.logs import LogEmitter, OTLPHandler
21+
from opentelemetry.sdk.logs.severity import SeverityNumber
22+
from opentelemetry.trace import INVALID_SPAN_CONTEXT
23+
24+
25+
def get_logger(level=logging.NOTSET, log_emitter=None):
26+
logger = logging.getLogger(__name__)
27+
handler = OTLPHandler(level=level, log_emitter=log_emitter)
28+
logger.addHandler(handler)
29+
return logger
30+
31+
32+
class TestOTLPHandler(unittest.TestCase):
33+
def test_handler_default_log_level(self):
34+
emitter_mock = Mock(spec=LogEmitter)
35+
logger = get_logger(log_emitter=emitter_mock)
36+
# Make sure debug messages are ignored by default
37+
logger.debug("Debug message")
38+
self.assertEqual(emitter_mock.emit.call_count, 0)
39+
# Assert emit gets called for warning message
40+
logger.warning("Wanrning message")
41+
self.assertEqual(emitter_mock.emit.call_count, 1)
42+
43+
def test_handler_custom_log_level(self):
44+
emitter_mock = Mock(spec=LogEmitter)
45+
logger = get_logger(level=logging.ERROR, log_emitter=emitter_mock)
46+
logger.warning("Warning message test custom log level")
47+
# Make sure any log with level < ERROR is ignored
48+
self.assertEqual(emitter_mock.emit.call_count, 0)
49+
logger.error("Mumbai, we have a major problem")
50+
logger.critical("No Time For Caution")
51+
self.assertEqual(emitter_mock.emit.call_count, 2)
52+
53+
def test_log_record_no_span_context(self):
54+
emitter_mock = Mock(spec=LogEmitter)
55+
logger = get_logger(log_emitter=emitter_mock)
56+
# Assert emit gets called for warning message
57+
logger.warning("Wanrning message")
58+
args, _ = emitter_mock.emit.call_args_list[0]
59+
log_record = args[0]
60+
61+
self.assertIsNotNone(log_record)
62+
self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id)
63+
self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id)
64+
self.assertEqual(
65+
log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags
66+
)
67+
68+
def test_log_record_trace_correlation(self):
69+
emitter_mock = Mock(spec=LogEmitter)
70+
logger = get_logger(log_emitter=emitter_mock)
71+
72+
tracer = trace.TracerProvider().get_tracer(__name__)
73+
with tracer.start_as_current_span("test") as span:
74+
logger.critical("Critical message within span")
75+
76+
args, _ = emitter_mock.emit.call_args_list[0]
77+
log_record = args[0]
78+
self.assertEqual(log_record.body, "Critical message within span")
79+
self.assertEqual(log_record.severity_text, "CRITICAL")
80+
self.assertEqual(log_record.severity_number, SeverityNumber.FATAL)
81+
span_context = span.get_span_context()
82+
self.assertEqual(log_record.trace_id, span_context.trace_id)
83+
self.assertEqual(log_record.span_id, span_context.span_id)
84+
self.assertEqual(log_record.trace_flags, span_context.trace_flags)

0 commit comments

Comments
 (0)