Skip to content

Commit 4cff733

Browse files
authored
Merge branch 'master' into 2025/change/makePOSTGRESSIZEconfigurable
2 parents b3f2daa + 8e85054 commit 4cff733

File tree

2 files changed

+184
-35
lines changed

2 files changed

+184
-35
lines changed

packages/service-library/src/servicelib/logging_utils.py

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ def format(self, record) -> str:
9797
if hasattr(record, "file_name_override"):
9898
record.filename = record.file_name_override
9999

100-
for name in LogExtra.__optional_keys__: # pylint: disable=no-member
100+
optional_keys = LogExtra.__optional_keys__ | frozenset( # pylint: disable=no-member
101+
["otelTraceID", "otelSpanID"]
102+
)
103+
for name in optional_keys:
101104
if not hasattr(record, name):
102105
setattr(record, name, None)
103106

@@ -113,22 +116,6 @@ def format(self, record) -> str:
113116

114117
# SEE https://docs.python.org/3/library/logging.html#logrecord-attributes
115118
_DEFAULT_FORMATTING: Final[str] = " | ".join(
116-
[
117-
"log_level=%(levelname)s",
118-
"log_timestamp=%(asctime)s",
119-
"log_source=%(name)s:%(funcName)s(%(lineno)d)",
120-
"log_uid=%(log_uid)s",
121-
"log_oec=%(log_oec)s",
122-
"log_msg=%(message)s",
123-
]
124-
)
125-
126-
_LOCAL_FORMATTING: Final[str] = (
127-
"%(levelname)s: [%(asctime)s/%(processName)s] [%(name)s:%(funcName)s(%(lineno)d)] - %(message)s"
128-
)
129-
130-
# Tracing format strings
131-
_TRACING_FORMATTING: Final[str] = " | ".join(
132119
[
133120
"log_level=%(levelname)s",
134121
"log_timestamp=%(asctime)s",
@@ -137,38 +124,25 @@ def format(self, record) -> str:
137124
"log_oec=%(log_oec)s",
138125
"log_trace_id=%(otelTraceID)s",
139126
"log_span_id=%(otelSpanID)s",
140-
"log_resource.service.name=%(otelServiceName)s",
141-
"log_trace_sampled=%(otelTraceSampled)s",
142127
"log_msg=%(message)s",
143128
]
144129
)
145130

146-
_LOCAL_TRACING_FORMATTING: Final[str] = (
131+
_LOCAL_FORMATTING: Final[str] = (
147132
"%(levelname)s: [%(asctime)s/%(processName)s] "
148-
"[log_trace_id=%(otelTraceID)s log_span_id=%(otelSpanID)s "
149-
"log_resource.service.name=%(otelServiceName)s log_trace_sampled=%(otelTraceSampled)s] "
133+
"[log_trace_id=%(otelTraceID)s|log_span_id=%(otelSpanID)s] "
150134
"[%(name)s:%(funcName)s(%(lineno)d)] - %(message)s"
151135
)
152136

153137
# Graylog Grok pattern extractor:
154-
# log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{DATA:log_source} \| (log_uid=%{WORD:log_uid} \| )?log_msg=%{GREEDYDATA:log_msg}
138+
# log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{NOTSPACE:log_source} \| log_uid=%{NOTSPACE:log_uid} \| log_oec=%{NOTSPACE:log_oec} \| log_trace_id=%{NOTSPACE:log_trace_id} \| log_span_id=%{NOTSPACE:log_span_id} \| log_msg=%{GREEDYDATA:log_msg}
155139

156140

157141
def _setup_logging_formatter(
158142
*,
159-
tracing_settings: TracingSettings | None,
160143
log_format_local_dev_enabled: bool,
161144
) -> logging.Formatter:
162-
if log_format_local_dev_enabled:
163-
fmt = (
164-
_LOCAL_TRACING_FORMATTING
165-
if tracing_settings is not None
166-
else _LOCAL_FORMATTING
167-
)
168-
else:
169-
fmt = (
170-
_TRACING_FORMATTING if tracing_settings is not None else _DEFAULT_FORMATTING
171-
)
145+
fmt = _LOCAL_FORMATTING if log_format_local_dev_enabled else _DEFAULT_FORMATTING
172146

173147
return CustomFormatter(
174148
fmt, log_format_local_dev_enabled=log_format_local_dev_enabled
@@ -234,7 +208,6 @@ def _configure_common_logging_settings(
234208
setup_log_tracing(tracing_settings=tracing_settings)
235209

236210
return _setup_logging_formatter(
237-
tracing_settings=tracing_settings,
238211
log_format_local_dev_enabled=log_format_local_dev_enabled,
239212
)
240213

packages/service-library/tests/test_logging_utils.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# pylint:disable=redefined-outer-name
22
# pylint:disable=unused-argument
33

4+
import io
45
import logging
6+
import re
57
from collections.abc import Iterable
68
from contextlib import suppress
79
from pathlib import Path
@@ -10,6 +12,8 @@
1012
import pytest
1113
from faker import Faker
1214
from servicelib.logging_utils import (
15+
_DEFAULT_FORMATTING,
16+
CustomFormatter,
1317
LogExtra,
1418
LogLevelInt,
1519
LogMessageStr,
@@ -573,3 +577,175 @@ def _raise_test_exception():
573577

574578
# Check that the message was logged and cleanup happened
575579
assert "Message before exception" in caplog.text
580+
581+
582+
def _create_grok_regex_pattern() -> str:
583+
"""Convert Grok pattern to regex for testing."""
584+
# The Grok pattern from the comment:
585+
# log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{NOTSPACE:log_source} \| log_uid=%{NOTSPACE:log_uid} \| log_oec=%{NOTSPACE:log_oec} \| log_trace_id=%{NOTSPACE:log_trace_id} \| log_span_id=%{NOTSPACE:log_span_id} \| log_msg=%{GREEDYDATA:log_msg}
586+
587+
grok_to_regex = {
588+
r"%{WORD:log_level}": r"(?P<log_level>\w+)",
589+
r"%{TIMESTAMP_ISO8601:log_timestamp}": r"(?P<log_timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})",
590+
r"%{NOTSPACE:log_source}": r"(?P<log_source>\S+)",
591+
r"%{NOTSPACE:log_uid}": r"(?P<log_uid>\S+)",
592+
r"%{NOTSPACE:log_oec}": r"(?P<log_oec>\S+)",
593+
r"%{NOTSPACE:log_trace_id}": r"(?P<log_trace_id>\S+)",
594+
r"%{NOTSPACE:log_span_id}": r"(?P<log_span_id>\S+)",
595+
r"%{GREEDYDATA:log_msg}": r"(?P<log_msg>.*)",
596+
}
597+
598+
grok_pattern = r"log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{NOTSPACE:log_source} \| log_uid=%{NOTSPACE:log_uid} \| log_oec=%{NOTSPACE:log_oec} \| log_trace_id=%{NOTSPACE:log_trace_id} \| log_span_id=%{NOTSPACE:log_span_id} \| log_msg=%{GREEDYDATA:log_msg}"
599+
600+
# Convert to regex
601+
regex_pattern = grok_pattern
602+
for grok, regex in grok_to_regex.items():
603+
regex_pattern = regex_pattern.replace(grok, regex)
604+
605+
return regex_pattern
606+
607+
608+
def _create_test_log_record(
609+
name: str,
610+
level: int,
611+
func_name: str,
612+
lineno: int,
613+
message: str,
614+
*,
615+
user_id: int | str | None = None,
616+
error_code: str | None = None,
617+
trace_id: str | None = None,
618+
span_id: str | None = None,
619+
) -> logging.LogRecord:
620+
"""Create a test LogRecord with optional extra fields."""
621+
from servicelib.logging_utils import get_log_record_extra
622+
623+
record = logging.LogRecord(
624+
name=name,
625+
level=level,
626+
pathname="/path/to/file.py",
627+
lineno=lineno,
628+
msg=message,
629+
args=(),
630+
exc_info=None,
631+
func=func_name,
632+
)
633+
634+
# Add extra fields if provided
635+
extra = get_log_record_extra(user_id=user_id, error_code=error_code)
636+
if extra:
637+
for key, value in extra.items():
638+
setattr(record, key, value)
639+
640+
# Add OpenTelemetry trace ID
641+
record.otelTraceID = trace_id # type: ignore[attr-defined]
642+
record.otelSpanID = span_id # type: ignore[attr-defined]
643+
644+
return record
645+
646+
647+
def test_grok_pattern_parsing(caplog: pytest.LogCaptureFixture) -> None:
648+
"""
649+
Test that the Graylog Grok pattern correctly parses logs formatted with _DEFAULT_FORMATTING.
650+
651+
This test validates that the Grok pattern defined in the comment can correctly
652+
parse logs formatted with _DEFAULT_FORMATTING.
653+
654+
WARNING: If log formatting changes, the Grok pattern in Graylog must be updated accordingly.
655+
"""
656+
657+
# Create a custom handler with the default formatter
658+
log_stream = io.StringIO()
659+
handler = logging.StreamHandler(log_stream)
660+
formatter = CustomFormatter(_DEFAULT_FORMATTING, log_format_local_dev_enabled=False)
661+
handler.setFormatter(formatter)
662+
663+
# Create test log record with all fields populated
664+
test_message = (
665+
"This is a test log message with special chars: []{} and new line\nembedded"
666+
)
667+
record = _create_test_log_record(
668+
name="test.module.submodule",
669+
level=logging.INFO,
670+
func_name="test_function",
671+
lineno=42,
672+
message=test_message,
673+
user_id=12345,
674+
error_code="OEC001",
675+
trace_id="1234567890abcdef1234567890abcdef",
676+
span_id="987654321",
677+
)
678+
679+
# Format the record
680+
formatted_log = formatter.format(record)
681+
682+
# Test that the formatted log matches the Grok pattern
683+
regex_pattern = _create_grok_regex_pattern()
684+
match = re.match(regex_pattern, formatted_log)
685+
686+
assert (
687+
match is not None
688+
), f"Grok pattern did not match formatted log. Log: {formatted_log!r}"
689+
690+
# Verify extracted fields match expected values
691+
groups = match.groupdict()
692+
693+
assert groups["log_level"] == "INFO"
694+
assert groups["log_source"] == "test.module.submodule:test_function(42)"
695+
assert groups["log_uid"] == "12345"
696+
assert groups["log_oec"] == "OEC001"
697+
assert groups["log_trace_id"] == "1234567890abcdef1234567890abcdef"
698+
assert groups["log_span_id"] == "987654321"
699+
700+
# Verify the message is correctly escaped (newlines become \\n)
701+
expected_message = test_message.replace("\n", "\\n")
702+
assert groups["log_msg"] == expected_message
703+
704+
# Verify timestamp format is ISO8601-like (as expected by Python logging)
705+
timestamp_pattern = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}"
706+
assert re.match(timestamp_pattern, groups["log_timestamp"])
707+
708+
709+
def test_grok_pattern_parsing_with_none_values(
710+
caplog: pytest.LogCaptureFixture,
711+
) -> None:
712+
"""
713+
Test Grok pattern parsing when optional fields are None.
714+
715+
WARNING: If log formatting changes, the Grok pattern in Graylog must be updated accordingly.
716+
"""
717+
718+
# Create a custom handler with the default formatter
719+
handler = logging.StreamHandler(io.StringIO())
720+
formatter = CustomFormatter(_DEFAULT_FORMATTING, log_format_local_dev_enabled=False)
721+
handler.setFormatter(formatter)
722+
723+
# Create test log record with None values for optional fields
724+
record = _create_test_log_record(
725+
name="test.module",
726+
level=logging.ERROR,
727+
func_name="error_function",
728+
lineno=100,
729+
message="Error message",
730+
user_id=None,
731+
error_code=None,
732+
trace_id=None,
733+
span_id=None,
734+
)
735+
736+
formatted_log = formatter.format(record)
737+
regex_pattern = _create_grok_regex_pattern()
738+
match = re.match(regex_pattern, formatted_log)
739+
740+
assert (
741+
match is not None
742+
), f"Grok pattern did not match log with None values. Log: {formatted_log!r}"
743+
744+
groups = match.groupdict()
745+
assert groups["log_level"] == "ERROR"
746+
assert groups["log_source"] == "test.module:error_function(100)"
747+
assert groups["log_uid"] == "None"
748+
assert groups["log_oec"] == "None"
749+
assert groups["log_trace_id"] == "None"
750+
assert groups["log_span_id"] == "None"
751+
assert groups["log_msg"] == "Error message"

0 commit comments

Comments
 (0)