Skip to content

Commit 3d3ae4f

Browse files
authored
Merge pull request #3 from carolinecgilbert/loguru_handler_WORKING
Loguru handler working
2 parents 96548d6 + 21e411e commit 3d3ae4f

File tree

6 files changed

+519
-5
lines changed

6 files changed

+519
-5
lines changed

handlers/opentelemetry_loguru/__init__.py

Whitespace-only changes.

handlers/opentelemetry_loguru/src/__init__.py

Whitespace-only changes.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import traceback
2+
from datetime import datetime, timezone
3+
from typing import Dict
4+
5+
import loguru
6+
import traceback
7+
from os import environ
8+
from time import time_ns
9+
from typing import Any, Callable, Optional, Tuple, Union # noqa
10+
from opentelemetry._logs import (
11+
NoOpLogger,
12+
SeverityNumber,
13+
get_logger,
14+
get_logger_provider,
15+
std_to_otel,
16+
)
17+
from opentelemetry.attributes import BoundedAttributes
18+
from opentelemetry.sdk.environment_variables import (
19+
OTEL_ATTRIBUTE_COUNT_LIMIT,
20+
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
21+
)
22+
from opentelemetry.sdk.resources import Resource
23+
from opentelemetry.sdk.util import ns_to_iso_str
24+
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
25+
from opentelemetry.semconv.trace import SpanAttributes
26+
from opentelemetry.trace import (
27+
format_span_id,
28+
format_trace_id,
29+
get_current_span,
30+
)
31+
from opentelemetry.trace.span import TraceFlags
32+
from opentelemetry.util.types import Attributes
33+
34+
from opentelemetry._logs import Logger as APILogger
35+
from opentelemetry._logs import LoggerProvider as APILoggerProvider
36+
from opentelemetry._logs import LogRecord as APILogRecord
37+
38+
from opentelemetry._logs import std_to_otel
39+
from opentelemetry.sdk._logs._internal import LoggerProvider, LogRecord
40+
from opentelemetry.sdk._logs._internal.export import BatchLogRecordProcessor, LogExporter
41+
from opentelemetry.sdk.resources import Resource
42+
from opentelemetry.semconv.trace import SpanAttributes
43+
from opentelemetry.trace import get_current_span
44+
45+
from opentelemetry._logs.severity import SeverityNumber
46+
47+
import sys
48+
import json
49+
50+
_STD_TO_OTEL = {
51+
10: SeverityNumber.DEBUG,
52+
11: SeverityNumber.DEBUG2,
53+
12: SeverityNumber.DEBUG3,
54+
13: SeverityNumber.DEBUG4,
55+
14: SeverityNumber.DEBUG4,
56+
15: SeverityNumber.DEBUG4,
57+
16: SeverityNumber.DEBUG4,
58+
17: SeverityNumber.DEBUG4,
59+
18: SeverityNumber.DEBUG4,
60+
19: SeverityNumber.DEBUG4,
61+
20: SeverityNumber.INFO,
62+
21: SeverityNumber.INFO2,
63+
22: SeverityNumber.INFO3,
64+
23: SeverityNumber.INFO4,
65+
24: SeverityNumber.INFO4,
66+
25: SeverityNumber.INFO4,
67+
26: SeverityNumber.INFO4,
68+
27: SeverityNumber.INFO4,
69+
28: SeverityNumber.INFO4,
70+
29: SeverityNumber.INFO4,
71+
30: SeverityNumber.WARN,
72+
31: SeverityNumber.WARN2,
73+
32: SeverityNumber.WARN3,
74+
33: SeverityNumber.WARN4,
75+
34: SeverityNumber.WARN4,
76+
35: SeverityNumber.WARN4,
77+
36: SeverityNumber.WARN4,
78+
37: SeverityNumber.WARN4,
79+
38: SeverityNumber.WARN4,
80+
39: SeverityNumber.WARN4,
81+
40: SeverityNumber.ERROR,
82+
41: SeverityNumber.ERROR2,
83+
42: SeverityNumber.ERROR3,
84+
43: SeverityNumber.ERROR4,
85+
44: SeverityNumber.ERROR4,
86+
45: SeverityNumber.ERROR4,
87+
46: SeverityNumber.ERROR4,
88+
47: SeverityNumber.ERROR4,
89+
48: SeverityNumber.ERROR4,
90+
49: SeverityNumber.ERROR4,
91+
50: SeverityNumber.FATAL,
92+
51: SeverityNumber.FATAL2,
93+
52: SeverityNumber.FATAL3,
94+
53: SeverityNumber.FATAL4,
95+
}
96+
97+
98+
class LoguruHandler:
99+
100+
# this was largely inspired by the OpenTelemetry handler for stdlib `logging`:
101+
# https://github.com/open-telemetry/opentelemetry-python/blob/8f312c49a5c140c14d1829c66abfe4e859ad8fd7/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py#L318
102+
103+
def __init__(
104+
self,
105+
logger_provider=None,
106+
) -> None:
107+
108+
self._logger_provider = logger_provider or get_logger_provider()
109+
self._logger = get_logger(
110+
__name__, logger_provider=self._logger_provider
111+
)
112+
113+
114+
def _get_attributes(self, record) -> Attributes:
115+
attributes = {key:value for key, value in record.items()}
116+
117+
# Add standard code attributes for logs.
118+
attributes[SpanAttributes.CODE_FILEPATH] = record['file'] #This includes file and path -> (file, path)
119+
attributes[SpanAttributes.CODE_FUNCTION] = record['function']
120+
attributes[SpanAttributes.CODE_LINENO] = record['line']
121+
122+
if record['exception'] is not None:
123+
124+
attributes[SpanAttributes.EXCEPTION_TYPE] = record['exception'].type
125+
126+
attributes[SpanAttributes.EXCEPTION_MESSAGE] = record['exception'].value
127+
128+
attributes[SpanAttributes.EXCEPTION_STACKTRACE] = record['exception'].traceback
129+
130+
return attributes
131+
132+
def _loguru_to_otel(self, levelno: int) -> SeverityNumber:
133+
if levelno < 10 or levelno == 25:
134+
return SeverityNumber.UNSPECIFIED
135+
136+
elif levelno > 53:
137+
return SeverityNumber.FATAL4
138+
139+
return _STD_TO_OTEL[levelno]
140+
141+
142+
def _translate(self, record) -> LogRecord:
143+
144+
#Timestamp
145+
timestamp = record["time"]
146+
147+
#Observed timestamp
148+
observedTimestamp = time_ns()
149+
150+
#Span context
151+
spanContext = get_current_span().get_span_context()
152+
153+
#Setting the level name
154+
if record['level'].name == 'WARNING':
155+
levelName = 'WARN'
156+
elif record['level'].name == 'TRACE' or record['level'].name == 'SUCCESS':
157+
levelName = 'NOTSET'
158+
else:
159+
levelName = record['level'].name
160+
161+
#Severity number
162+
severityNumber = self._loguru_to_otel(int(record["level"].no))
163+
164+
#Getting attributes
165+
attributes = self._get_attributes(record)
166+
167+
168+
return LogRecord(
169+
timestamp = timestamp,
170+
observed_timestamp = observedTimestamp,
171+
trace_id = spanContext.trace_id,
172+
span_id = spanContext.span_id,
173+
trace_flags = spanContext.trace_flags,
174+
severity_text = levelName,
175+
severity_number = severityNumber,
176+
body=record['message'],
177+
resource = self._logger.resource,
178+
attributes=attributes
179+
)
180+
181+
def sink(self, record) -> None:
182+
183+
self._logger.emit(self._translate(record))
184+

instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ typing_extensions==4.9.0
1414
wrapt==1.16.0
1515
zipp==3.17.0
1616
structlog==24.1.0
17+
loguru==0.7.2
1718
-e opentelemetry-instrumentation
1819
-e instrumentation/opentelemetry-instrumentation-logging

instrumentation/opentelemetry-instrumentation-logging/tests/test_logging.py

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,35 @@
1515
import logging
1616
from typing import Optional
1717
from unittest import mock
18+
import unittest
19+
from collections import namedtuple
1820

1921
import pytest
2022

2123
# Imports for StructlogHandler tests
2224
from unittest.mock import Mock
2325
from handlers.opentelemetry_structlog.src.exporter import LogExporter
24-
from datetime import datetime, timezone, timedelta
26+
27+
from datetime import datetime, timezone
28+
2529
from unittest.mock import MagicMock, patch
2630

2731

2832

33+
from opentelemetry.semconv.trace import SpanAttributes
2934

3035
from opentelemetry.instrumentation.logging import ( # pylint: disable=no-name-in-module
3136
DEFAULT_LOGGING_FORMAT,
3237
LoggingInstrumentor,
3338
)
3439
from opentelemetry.test.test_base import TestBase
35-
from opentelemetry.trace import ProxyTracer, get_tracer
40+
from opentelemetry.trace import ProxyTracer, get_tracer, get_current_span, SpanContext, TraceFlags
3641

3742
from handlers.opentelemetry_structlog.src.exporter import StructlogHandler
43+
from handlers.opentelemetry_loguru.src.exporter import LoguruHandler, _STD_TO_OTEL
3844

39-
45+
from opentelemetry._logs import get_logger_provider, get_logger
46+
import time
4047

4148
class FakeTracerProvider:
4249
def get_tracer( # pylint: disable=no-self-use
@@ -295,7 +302,6 @@ def test_call_method_processes_log_correctly(self):
295302
# Assert that the logger's emit method was called with the processed event
296303
logger.emit.assert_called_once()
297304

298-
299305
def test_log_record_translation_attributes(self):
300306
"""Verify that event_dict translates correctly into a LogRecord with the correct attributes."""
301307
exporter = MagicMock()
@@ -381,4 +387,104 @@ def test_trace_context_propogation(self):
381387
actual_trace_id = format(log_record.trace_id, "032x")
382388
assert actual_trace_id == trace_id, "Trace ID should be propagated"
383389

384-
assert log_record.trace_flags == trace_sampled, "Trace flags should be propagated"
390+
assert log_record.trace_flags == trace_sampled, "Trace flags should be propagated"
391+
392+
393+
394+
class TestLoguruHandler(TestBase):
395+
def setUp(self):
396+
self.default_provider = get_logger_provider()
397+
self.custom_provider = MagicMock()
398+
self.record = {
399+
"time": 1581000000.000123,
400+
"level": MagicMock(name="ERROR", no=40),
401+
"message": "Test message",
402+
"file": "test_file.py",
403+
"function": "test_function",
404+
"line": 123,
405+
"exception": None
406+
}
407+
# self.span_context = SpanContext(
408+
# trace_id=1234,
409+
# span_id=5678,
410+
# trace_flags=TraceFlags(1),
411+
# is_remote=False
412+
# )
413+
self.span_context = get_current_span().get_span_context()
414+
self.current_span = MagicMock()
415+
self.current_span.get_span_context.return_value = self.span_context
416+
417+
def test_initialization_with_default_provider(self):
418+
handler = LoguruHandler()
419+
self.assertEqual(handler._logger_provider, self.default_provider)
420+
421+
def test_initialization_with_custom_provider(self):
422+
handler = LoguruHandler(logger_provider=self.custom_provider)
423+
self.assertEqual(handler._logger_provider, self.custom_provider)
424+
425+
def test_attributes_extraction_without_exception(self):
426+
attrs = LoguruHandler()._get_attributes(self.record)
427+
expected_attrs = {
428+
SpanAttributes.CODE_FILEPATH: 'test_file.py',
429+
SpanAttributes.CODE_FUNCTION: 'test_function',
430+
SpanAttributes.CODE_LINENO: 123
431+
}
432+
433+
self.assertEqual(attrs[SpanAttributes.CODE_FILEPATH], expected_attrs[SpanAttributes.CODE_FILEPATH])
434+
self.assertEqual(attrs[SpanAttributes.CODE_FUNCTION], expected_attrs[SpanAttributes.CODE_FUNCTION])
435+
self.assertEqual(attrs[SpanAttributes.CODE_LINENO], expected_attrs[SpanAttributes.CODE_LINENO])
436+
437+
@patch('traceback.format_exception')
438+
def test_attributes_extraction_with_exception(self, mock_format_exception):
439+
mock_format_exception.return_value = 'Exception traceback'
440+
exception = Exception("Test exception")
441+
442+
ExceptionRecord = namedtuple('ExceptionRecord', ['type', 'value', 'traceback'])
443+
444+
# Example usage:
445+
exception_record = ExceptionRecord(
446+
type=type(exception).__name__,
447+
value=str(exception),
448+
traceback=mock_format_exception(exception)
449+
)
450+
self.record['exception'] = exception_record
451+
# self.record['exception'].type = type(exception).__name__
452+
# self.record['exception'].value = str(exception)
453+
# self.record['exception'].traceback = mock_format_exception(exception)
454+
455+
attrs = LoguruHandler()._get_attributes(self.record)
456+
457+
expected_attrs = {
458+
SpanAttributes.CODE_FILEPATH: 'test_file.py',
459+
SpanAttributes.CODE_FUNCTION: 'test_function',
460+
SpanAttributes.CODE_LINENO: 123,
461+
SpanAttributes.EXCEPTION_TYPE: 'Exception',
462+
SpanAttributes.EXCEPTION_MESSAGE: 'Test exception',
463+
SpanAttributes.EXCEPTION_STACKTRACE: 'Exception traceback'
464+
}
465+
466+
self.assertEqual(attrs[SpanAttributes.EXCEPTION_TYPE], expected_attrs[SpanAttributes.EXCEPTION_TYPE])
467+
self.assertEqual(attrs[SpanAttributes.EXCEPTION_MESSAGE], expected_attrs[SpanAttributes.EXCEPTION_MESSAGE])
468+
self.assertEqual(attrs[SpanAttributes.EXCEPTION_STACKTRACE], expected_attrs[SpanAttributes.EXCEPTION_STACKTRACE])
469+
470+
@patch('opentelemetry.trace.get_current_span')
471+
def test_translation(self, mock_get_current_span):
472+
mock_get_current_span.return_value = self.current_span
473+
handler = LoguruHandler(logger_provider=self.custom_provider)
474+
log_record = handler._translate(self.record)
475+
self.assertEqual(log_record.trace_id, self.span_context.trace_id)
476+
self.assertEqual(log_record.span_id, self.span_context.span_id)
477+
self.assertEqual(log_record.trace_flags, self.span_context.trace_flags)
478+
self.assertEqual(log_record.severity_number, _STD_TO_OTEL[self.record["level"].no])
479+
self.assertEqual(log_record.body, self.record["message"])
480+
481+
@patch('opentelemetry._logs.Logger.emit')
482+
@patch('opentelemetry.trace.get_current_span')
483+
def test_sink(self, mock_get_current_span, mock_emit):
484+
mock_get_current_span.return_value = self.current_span
485+
handler = LoguruHandler(logger_provider=self.custom_provider)
486+
handler.sink(self.record)
487+
#mock_emit.assert_called_once()
488+
handler._logger.emit.assert_called_once()
489+
490+

0 commit comments

Comments
 (0)