Skip to content

Commit e083565

Browse files
CG: loguru finalized with documentation
1 parent a097455 commit e083565

File tree

5 files changed

+199
-39
lines changed

5 files changed

+199
-39
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Loguru Handler for OpenTelemetry
2+
3+
This project provides a Loguru handler for OpenTelemetry applications. The handler converts Loguru logs into the OpenTelemetry Logs Protocol (OTLP) format for export to a collector.
4+
5+
## Usage
6+
7+
To use the Loguru handler in your OpenTelemetry application, follow these steps:
8+
9+
1. Import the necessary modules:
10+
11+
```python
12+
import loguru
13+
from handlers.opentelemetry_loguru.src.exporter import LoguruHandler
14+
from opentelemetry.sdk._logs._internal.export import LogExporter
15+
from opentelemetry.sdk.resources import Resource
16+
```
17+
18+
2. Initialize the LoguruHandler with your service name, server hostname, and LogExporter instance:
19+
20+
```python
21+
service_name = "my_service"
22+
server_hostname = "my_server"
23+
exporter = LogExporter() # Initialize your LogExporter instance
24+
handler = LoguruHandler(service_name, server_hostname, exporter)
25+
```
26+
27+
3. Add the handler to your Loguru logger:
28+
29+
```python
30+
logger = loguru.logger
31+
logger.add(handler.sink)
32+
```
33+
34+
4. Use the logger as usual with Loguru:
35+
36+
```python
37+
logger.warning("This is a test log message.")
38+
```
39+
## OpenTelemetry Application Example with Handler
40+
See the loguru handler demo in the examples directory of this repository for a step-by-step guide on using the handler in an OpenTelemetry application.
41+
42+
## Customization
43+
44+
The LoguruHandler supports customization through its constructor parameters:
45+
46+
- `service_name`: The name of your service.
47+
- `server_hostname`: The hostname of the server where the logs originate.
48+
- `exporter`: An instance of your LogExporter for exporting logs to a collector.
49+
50+
## Notes
51+
52+
- This handler automatically converts Loguru logs into the OTLP format for compatibility with OpenTelemetry.
53+
- It extracts attributes from Loguru logs and maps them to OpenTelemetry attributes for log records.

handlers/opentelemetry_loguru/src/exporter.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,30 +94,48 @@
9494
53: SeverityNumber.FATAL4,
9595
}
9696

97-
97+
EXCLUDE_ATTR = ("elapsed", "exception", "extra", "file", "level", "process", "thread", "time")
9898
class LoguruHandler:
9999

100100
# this was largely inspired by the OpenTelemetry handler for stdlib `logging`:
101101
# https://github.com/open-telemetry/opentelemetry-python/blob/8f312c49a5c140c14d1829c66abfe4e859ad8fd7/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py#L318
102102

103103
def __init__(
104104
self,
105-
logger_provider=None,
105+
service_name: str,
106+
server_hostname: str,
107+
exporter: LogExporter,
106108
) -> None:
107-
108-
self._logger_provider = logger_provider or get_logger_provider()
109-
self._logger = get_logger(
110-
__name__, logger_provider=self._logger_provider
109+
logger_provider = LoggerProvider(
110+
resource=Resource.create(
111+
{
112+
"service.name": service_name,
113+
"service.instance.id": server_hostname,
114+
}
115+
),
116+
)
117+
118+
logger_provider.add_log_record_processor(
119+
BatchLogRecordProcessor(exporter, max_export_batch_size=1)
111120
)
121+
122+
self._logger_provider = logger_provider
123+
self._logger = logger_provider.get_logger(__name__)
112124

113125

114126
def _get_attributes(self, record) -> Attributes:
115-
attributes = {key:value for key, value in record.items()}
127+
attributes = {key:value for key, value in record.items() if key not in EXCLUDE_ATTR}
116128

117129
# Add standard code attributes for logs.
118-
attributes[SpanAttributes.CODE_FILEPATH] = record['file'] #This includes file and path -> (file, path)
130+
attributes[SpanAttributes.CODE_FILEPATH] = record['file'].path #This includes file and path -> (file, path)
119131
attributes[SpanAttributes.CODE_FUNCTION] = record['function']
120132
attributes[SpanAttributes.CODE_LINENO] = record['line']
133+
134+
attributes['process_name'] = (record['process']).name
135+
attributes['process_id'] = (record['process']).id
136+
attributes['thread_name'] = (record['thread']).name
137+
attributes['thread_id'] = (record['thread']).id
138+
attributes['file'] = record['file'].name
121139

122140
if record['exception'] is not None:
123141

@@ -138,11 +156,13 @@ def _loguru_to_otel(self, levelno: int) -> SeverityNumber:
138156

139157
return _STD_TO_OTEL[levelno]
140158

159+
160+
141161

142162
def _translate(self, record) -> LogRecord:
143163

144164
#Timestamp
145-
timestamp = record["time"]
165+
timestamp = int((record["time"].timestamp()) * 1e9)
146166

147167
#Observed timestamp
148168
observedTimestamp = time_ns()
@@ -179,6 +199,6 @@ def _translate(self, record) -> LogRecord:
179199
)
180200

181201
def sink(self, record) -> None:
182-
183-
self._logger.emit(self._translate(record))
202+
print("\n BALLIN HERE\n")
203+
self._logger.emit(self._translate(record.record))
184204

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Structlog Handler for OpenTelemetry
2+
This project provides a Structlog handler for OpenTelemetry applications. The handler converts Structlog logs into the OpenTelemetry Logs Protocol (OTLP) format for export to a collector.
3+
4+
## Usage
5+
6+
To use the Structlog handler in your OpenTelemetry application, follow these steps:
7+
8+
1. Import the necessary modules:
9+
10+
```python
11+
import structlog
12+
from opentelemetry.sdk._logs._internal.export import LogExporter
13+
from opentelemetry.sdk.resources import Resource
14+
from handlers.opentelemetry_structlog.src.exporter import StructlogHandler
15+
```
16+
17+
2. Initialize the StructlogHandler with your service name, server hostname, and LogExporter instance:
18+
19+
```python
20+
service_name = "my_service"
21+
server_hostname = "my_server"
22+
exporter = LogExporter() # Initialize your LogExporter instance
23+
handler = StructlogHandler(service_name, server_hostname, exporter)
24+
```
25+
26+
3. Add the handler to your Structlog logger:
27+
28+
```python
29+
structlog.configure(
30+
processors=[structlog.processors.JSONRenderer()],
31+
logger_factory=structlog.stdlib.LoggerFactory(),
32+
wrapper_class=structlog.stdlib.BoundLogger,
33+
cache_logger_on_first_use=True,
34+
context_class=dict,
35+
**handler.wrap_for_structlog(),
36+
)
37+
```
38+
39+
4. Use the logger as usual with Structlog:
40+
41+
```python
42+
logger = structlog.get_logger()
43+
logger.info("This is a test log message.")
44+
```
45+
## OpenTelemetry Application Example with Handler
46+
See the structlog handler demo in the examples directory of this repository for a step-by-step guide on using the handler in an OpenTelemetry application.
47+
48+
## Customization
49+
50+
The StructlogHandler supports customization through its constructor parameters:
51+
52+
- `service_name`: The name of your service.
53+
- `server_hostname`: The hostname of the server where the logs originate.
54+
- `exporter`: An instance of your LogExporter for exporting logs to a collector.
55+
56+
## Notes
57+
58+
- This handler automatically converts the `timestamp` key in the `event_dict` to ISO 8601 format for better compatibility.
59+
- It performs operations similar to `structlog.processors.ExceptionRenderer`, so avoid using `ExceptionRenderer` in the same pipeline.
60+
```

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ wrapt==1.16.0
1515
zipp==3.17.0
1616
structlog==24.1.0
1717
loguru==0.7.2
18+
opentelemetry-exporter-otlp==1.24.0
1819
-e opentelemetry-instrumentation
1920
-e instrumentation/opentelemetry-instrumentation-logging

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

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
from datetime import datetime, timezone
2828

2929
from unittest.mock import MagicMock, patch
30-
30+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
31+
OTLPLogExporter,
32+
)
3133

3234

3335
from opentelemetry.semconv.trace import SpanAttributes
@@ -389,41 +391,58 @@ def test_trace_context_propogation(self):
389391

390392
assert log_record.trace_flags == trace_sampled, "Trace flags should be propagated"
391393

392-
394+
class TimestampRecord:
395+
def __init__(self, data):
396+
self.timestam = data
397+
def timestamp(self):
398+
return self.timestam
393399

394400
class TestLoguruHandler(TestBase):
395401
def setUp(self):
396402
self.default_provider = get_logger_provider()
397403
self.custom_provider = MagicMock()
404+
405+
RecordFile = namedtuple('RecordFile', ['path', 'name'])
406+
file_record = RecordFile(
407+
path="test_file.py",
408+
name = "test_file.py"
409+
)
410+
411+
RecordProcess = namedtuple('RecordProcess', ['name', 'id'])
412+
process_record = RecordProcess(
413+
name = "MainProcess",
414+
id = 1
415+
)
416+
417+
RecordThread = namedtuple('RecordThread', ['name', 'id'])
418+
thread_record = RecordThread(
419+
name = "MainThread",
420+
id = 1
421+
)
422+
423+
timeRec = TimestampRecord(data=2.38763786)
424+
398425
self.record = {
399-
"time": 1581000000.000123,
426+
"time": timeRec,
400427
"level": MagicMock(name="ERROR", no=40),
401428
"message": "Test message",
402-
"file": "test_file.py",
429+
"file": file_record,
430+
"process": process_record,
431+
"thread": thread_record,
403432
"function": "test_function",
404433
"line": 123,
405434
"exception": None
406435
}
407-
# self.span_context = SpanContext(
408-
# trace_id=1234,
409-
# span_id=5678,
410-
# trace_flags=TraceFlags(1),
411-
# is_remote=False
412-
# )
436+
413437
self.span_context = get_current_span().get_span_context()
414438
self.current_span = MagicMock()
415-
self.current_span.get_span_context.return_value = self.span_context
439+
self.current_span.get_span_context.return_value = self.span_context
416440

417-
def test_initialization_with_default_provider(self):
418-
handler = LoguruHandler()
419-
self.assertEqual(handler._logger_provider, self.default_provider)
441+
def test_attributes_extraction_without_exception(self):
442+
handler = LoguruHandler(service_name="flask-loguru-demo", server_hostname="instance-1", exporter=OTLPLogExporter(insecure=True))
420443

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)
444+
attrs = handler._get_attributes(self.record)
424445

425-
def test_attributes_extraction_without_exception(self):
426-
attrs = LoguruHandler()._get_attributes(self.record)
427446
expected_attrs = {
428447
SpanAttributes.CODE_FILEPATH: 'test_file.py',
429448
SpanAttributes.CODE_FUNCTION: 'test_function',
@@ -448,11 +467,11 @@ def test_attributes_extraction_with_exception(self, mock_format_exception):
448467
traceback=mock_format_exception(exception)
449468
)
450469
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)
470+
454471

455-
attrs = LoguruHandler()._get_attributes(self.record)
472+
handler = LoguruHandler(service_name="flask-loguru-demo", server_hostname="instance-1", exporter=OTLPLogExporter(insecure=True))
473+
474+
attrs = handler._get_attributes(self.record)
456475

457476
expected_attrs = {
458477
SpanAttributes.CODE_FILEPATH: 'test_file.py',
@@ -470,7 +489,9 @@ def test_attributes_extraction_with_exception(self, mock_format_exception):
470489
@patch('opentelemetry.trace.get_current_span')
471490
def test_translation(self, mock_get_current_span):
472491
mock_get_current_span.return_value = self.current_span
473-
handler = LoguruHandler(logger_provider=self.custom_provider)
492+
493+
handler = LoguruHandler(service_name="flask-loguru-demo", server_hostname="instance-1", exporter=OTLPLogExporter(insecure=True))
494+
474495
log_record = handler._translate(self.record)
475496
self.assertEqual(log_record.trace_id, self.span_context.trace_id)
476497
self.assertEqual(log_record.span_id, self.span_context.span_id)
@@ -482,9 +503,14 @@ def test_translation(self, mock_get_current_span):
482503
@patch('opentelemetry.trace.get_current_span')
483504
def test_sink(self, mock_get_current_span, mock_emit):
484505
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()
506+
507+
handler = LoguruHandler(service_name="flask-loguru-demo", server_hostname="instance-1", exporter=OTLPLogExporter(insecure=True))
508+
509+
MessageRecord = namedtuple('MessageRecord', ['record'])
510+
message = MessageRecord(
511+
record=self.record
512+
)
513+
514+
handler.sink(message)
489515

490516

0 commit comments

Comments
 (0)