Skip to content

Commit b310993

Browse files
committed
Apply code review comments
1 parent e854fa1 commit b310993

File tree

5 files changed

+131
-337
lines changed

5 files changed

+131
-337
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ readme = "README.md"
1414
requires-python = ">=3.7"
1515
dependencies = [
1616
"backports.zoneinfo >= 0.2.1; python_version < '3.9'",
17-
"clp-ffi-py == 0.1.0b1",
17+
"clp-ffi-py == 0.0.14",
1818
"typing-extensions >= 3.7.4",
1919
"tzlocal == 5.1; python_version < '3.8'",
2020
"tzlocal >= 5.2; python_version >= '3.8'",
Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
2-
from typing import Any, Dict, Optional
2+
from typing import Any, Dict
33

4-
ZONED_TIMESTAMP_KEY: str = "zoned_timestamp"
5-
ZONED_TIMESTAMP_UTC_EPOCH_MS_KEY: str = "utc_epoch_ms"
6-
ZONED_TIMESTAMP_TZ_KEY: str = "timezone"
4+
from clp_logging.utils import Timestamp
5+
6+
TIMESTAMP_KEY: str = "timestamp"
7+
TIMESTAMP_UNIX_TS_MS: str = "unix_ts_ms"
8+
TIMESTAMP_UTC_OFFSET_SEC: str = "utc_offset_sec"
79

810
LEVEL_KEY: str = "level"
911
LEVEL_NO_KEY: str = "no"
@@ -13,23 +15,19 @@
1315
SOURCE_CONTEXT_PATH_KEY: str = "path"
1416
SOURCE_CONTEXT_LINE_KEY: str = "line"
1517

16-
LOGLIB_GENERATED_MSG_KEY: str = "loglib_generated_msg"
17-
18-
1918
class AutoGeneratedKeyValuePairsBuffer:
2019
"""
2120
A reusable buffer for auto-generated key-value pairs.
2221
23-
This buffer maintains a predefined dictionary for common metadata fields,
24-
to enable efficient reuse without creating new dictionaries for each log
25-
event.
22+
This buffer maintains a predefined dictionary for common metadata fields, to
23+
enable efficient reuse without creating new dictionaries for each log event.
2624
"""
2725

2826
def __init__(self) -> None:
2927
self._buf: Dict[str, Any] = {
30-
ZONED_TIMESTAMP_KEY: {
31-
ZONED_TIMESTAMP_UTC_EPOCH_MS_KEY: None,
32-
ZONED_TIMESTAMP_TZ_KEY: None,
28+
TIMESTAMP_KEY: {
29+
TIMESTAMP_UNIX_TS_MS: None,
30+
TIMESTAMP_UTC_OFFSET_SEC: None,
3331
},
3432
LEVEL_KEY: {
3533
LEVEL_NO_KEY: None,
@@ -41,22 +39,19 @@ def __init__(self) -> None:
4139
},
4240
}
4341

44-
def generate(
45-
self, timestamp: int, timezone: Optional[str], record: logging.LogRecord
46-
) -> Dict[str, Any]:
42+
def generate(self, ts: Timestamp, record: logging.LogRecord) -> Dict[str, Any]:
4743
"""
4844
Generates the auto-generated key-value pairs by populating the
4945
underlying buffer with the given log event metadata.
5046
51-
:param timestamp: The log event's millisecond Unix timestamp.
52-
:param timezone: The timezone of the log event, or `None` if UTC.
47+
:param ts: The timestamp assigned to the log event.
5348
:param record: The LogRecord containing metadata for the log event.
5449
:return: The populated underlying buffer as the auto-generated key-value
5550
pairs.
5651
"""
5752

58-
self._buf[ZONED_TIMESTAMP_KEY][ZONED_TIMESTAMP_UTC_EPOCH_MS_KEY] = timestamp
59-
self._buf[ZONED_TIMESTAMP_KEY][ZONED_TIMESTAMP_TZ_KEY] = timezone
53+
self._buf[TIMESTAMP_KEY][TIMESTAMP_UNIX_TS_MS] = ts.get_unix_ts()
54+
self._buf[TIMESTAMP_KEY][TIMESTAMP_UTC_OFFSET_SEC] = ts.get_utc_offset()
6055

6156
# NOTE: We don't add all the metadata contained in `record`. Instead, we only add the
6257
# following fields:
@@ -70,22 +65,3 @@ def generate(
7065
self._buf[SOURCE_CONTEXT_KEY][SOURCE_CONTEXT_LINE_KEY] = record.lineno
7166

7267
return self._buf
73-
74-
75-
def create_loglib_generated_log_event_as_auto_generated_kv_pairs(
76-
timestamp: int, timezone: Optional[str], msg: str
77-
) -> Dict[str, Any]:
78-
"""
79-
:param timestamp: The log event's millisecond Unix timestamp.
80-
:param timezone: The timezone of the log event, or `None` if UTC.
81-
:param msg: The log message generated by the logging library.
82-
:return: The auto-generated key-value pairs that represent a log event generated by the logging
83-
library itself.
84-
"""
85-
return {
86-
ZONED_TIMESTAMP_KEY: {
87-
ZONED_TIMESTAMP_UTC_EPOCH_MS_KEY: timestamp,
88-
ZONED_TIMESTAMP_TZ_KEY: timezone,
89-
},
90-
LOGLIB_GENERATED_MSG_KEY: msg,
91-
}

src/clp_logging/handlers.py

Lines changed: 52 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import warnings
77
from abc import ABCMeta, abstractmethod
8+
from contextlib import nullcontext
89
from math import floor
910
from pathlib import Path
1011
from queue import Empty, Queue
@@ -18,10 +19,7 @@
1819
from clp_ffi_py.utils import serialize_dict_to_msgpack
1920
from zstandard import FLUSH_FRAME, ZstdCompressionWriter, ZstdCompressor
2021

21-
from clp_logging.auto_generated_kv_pairs_utils import (
22-
AutoGeneratedKeyValuePairsBuffer,
23-
create_loglib_generated_log_event_as_auto_generated_kv_pairs,
24-
)
22+
from clp_logging.auto_generated_kv_pairs_utils import AutoGeneratedKeyValuePairsBuffer
2523
from clp_logging.protocol import (
2624
BYTE_ORDER,
2725
EOF_CHAR,
@@ -31,6 +29,7 @@
3129
UINT_MAX,
3230
ULONG_MAX,
3331
)
32+
from clp_logging.utils import Timestamp
3433

3534
# TODO: lock writes to zstream if GIL ever goes away
3635
# Note: no need to quote "Queue[Tuple[int, bytes]]" in python 3.9
@@ -228,7 +227,7 @@ def __init__(
228227
self.hard_timeout_thread: Optional[Timer] = None
229228
self.soft_timeout_thread: Optional[Timer] = None
230229

231-
def set_ostream(self, ostream: Union[ZstdCompressionWriter, IO[bytes], Serializer]) -> None:
230+
def set_ostream(self, ostream: Union[ZstdCompressionWriter, IO[bytes]]) -> None:
232231
self.ostream = ostream
233232

234233
def timeout(self) -> None:
@@ -829,26 +828,21 @@ class ClpKeyValuePairStreamHandler(logging.Handler):
829828
:param stream: A writable byte output stream to which the handler will write the serialized IR
830829
byte sequences.
831830
:param enable_compression: Whether to compress the serialized IR byte sequences using zstd.
832-
:param loglevel_timeout: Customized timeout configuration.
833831
"""
834832

835833
def __init__(
836834
self,
837835
stream: IO[bytes],
838836
enable_compression: bool = True,
839-
timezone: Optional[str] = None,
840-
loglevel_timeout: Optional[CLPLogLevelTimeout] = None,
841837
) -> None:
842838
super().__init__()
843839

844840
self._enable_compression: bool = enable_compression
845-
self._tz: Optional[str] = timezone
846-
self._loglevel_timeout: Optional[CLPLogLevelTimeout] = loglevel_timeout
847841
self._serializer: Optional[Serializer] = None
848842
self._formatter: Optional[logging.Formatter] = None
849843
self._ostream: IO[bytes] = stream
850844

851-
self._auto_generated_kv_pairs_buf: AutoGeneratedKeyValuePairsBuffer = (
845+
self._auto_gen_kv_pairs_buf: AutoGeneratedKeyValuePairsBuffer = (
852846
AutoGeneratedKeyValuePairsBuffer()
853847
)
854848

@@ -878,90 +872,95 @@ def emit(self, record: logging.LogRecord) -> None:
878872
except Exception:
879873
self.handleError(record)
880874

881-
# Added to `logging.StreamHandler` in Python 3.7
882875
# override
883876
def setStream(self, stream: IO[bytes]) -> Optional[IO[bytes]]:
877+
"""
878+
Sets the instance’s stream to the specified value, if it is different. The old stream is
879+
flushed before the new stream is set.
880+
NOTE: The old stream will be closed by calling this method.
881+
882+
:param stream: A writable byte output stream to which the handler will write the serialized
883+
IR byte sequences.
884+
:return: The old stream if the stream was changed, or `None` if it wasn't.
885+
"""
886+
887+
# NOTE: This function is implemented by mirroring CPython's implementation.
888+
884889
if stream is self._ostream:
885890
return None
891+
886892
old_stream: IO[bytes] = self._ostream
887-
self._ostream = stream
888-
# TODO: The following call will close the old stream. However, `logging.StreamHandler`'s
889-
# implementation will only flush the stream but leave it opened. To support this behaviour,
890-
# we need `clp_ffi_py.ir.Serializer` to allow closing the serializer without closing the
891-
# underlying output stream.
892-
self._init_new_serializer(stream)
893+
with self.lock if self.lock else nullcontext():
894+
# TODO: The following call will close the old stream. However, `logging.StreamHandler`'s
895+
# implementation will only flush the stream but leave it opened. To support this
896+
# behaviour, we need `clp_ffi_py.ir.Serializer` to allow closing the serializer without
897+
# closing the underlying output stream.
898+
self._init_new_serializer(stream)
899+
self._ostream = stream
893900
return old_stream
894901

895902
# override
896903
def close(self) -> None:
897-
if self._serializer is None:
904+
if self._is_closed():
898905
return
899-
if self._loglevel_timeout:
900-
self._loglevel_timeout.timeout()
901-
# NOTE: Closing the serializer will ensure that any buffered results are flushed and the
902-
# underlying output stream is properly closed.
903-
self._serializer.close()
904-
self._serializer = None
906+
self._close_serializer()
905907
super().close()
906908

907909
def _is_closed(self) -> bool:
908910
return self._serializer is None
909911

912+
def _close_serializer(self) -> None:
913+
"""
914+
Closes the current serializer if it has been set.
915+
916+
NOTE: The underlying output stream will also be closed.
917+
"""
918+
if self._is_closed():
919+
return
920+
assert self._serializer is not None
921+
self._serializer.close()
922+
self._serializer = None
923+
910924
def _init_new_serializer(self, stream: IO[bytes]) -> None:
911925
"""
912926
Initializes a new serializer that writes to the given stream.
913927
914928
:param stream: The stream that the underlying serializer will write to.
915929
"""
916-
cctx: ZstdCompressor = ZstdCompressor()
930+
self._close_serializer()
917931
self._serializer = Serializer(
918-
cctx.stream_writer(stream) if self._enable_compression else stream
932+
ZstdCompressor().stream_writer(stream) if self._enable_compression else stream
919933
)
920934

921935
def _write(self, record: logging.LogRecord) -> None:
922936
"""
923937
Writes the log event into the underlying serializer.
924938
925939
:param record: The log event to serialize.
926-
:raise IOError: If the handler has been already closed.
940+
:raise RuntimeError: If the handler has been already closed.
927941
:raise TypeError: If `record.msg` is not a Python dictionary.
928942
"""
929943
if self._is_closed():
930-
raise IOError("Stream already closed.")
944+
raise RuntimeError("Stream already closed.")
931945

932946
if not isinstance(record.msg, dict):
933947
raise TypeError("The log msg must be a valid Python dictionary.")
934948

935-
curr_ts: int = floor(time.time() * 1000)
936-
937-
if self._loglevel_timeout is not None:
938-
self._loglevel_timeout.update(record.levelno, curr_ts, self._log_level_timeout_callback)
939-
940949
self._serialize_kv_pair_log_event(
941-
self._auto_generated_kv_pairs_buf.generate(curr_ts, self._tz, record), record.msg
950+
self._auto_gen_kv_pairs_buf.generate(Timestamp.now(), record), record.msg
942951
)
943952

944953
def _serialize_kv_pair_log_event(
945-
self, auto_generated_kv_pairs: Dict[str, Any], user_generated_kv_pairs: Dict[str, Any]
954+
self, auto_gen_kv_pairs: Dict[str, Any], user_gen_kv_pairs: Dict[str, Any]
946955
) -> None:
947956
"""
948-
:param auto_generated_kv_pairs: A dict of auto generated kv pairs.
949-
:param user_generated_kv_pairs: A dict of user generated kv pairs.
957+
:param auto_gen_kv_pairs: A dict of auto generated kv pairs.
958+
:param user_gen_kv_pairs: A dict of user generated kv pairs.
950959
"""
951-
log_event: Dict[str, Any] = {
952-
AUTO_GENERATED_KV_PAIRS_KEY: auto_generated_kv_pairs,
953-
USER_GENERATED_KV_PAIRS_KEY: user_generated_kv_pairs,
954-
}
960+
if self._is_closed():
961+
raise RuntimeError("Stream already closed.")
955962
assert self._serializer is not None
956-
self._serializer.serialize_log_event_from_msgpack_map(serialize_dict_to_msgpack(log_event))
957-
958-
def _log_level_timeout_callback(self, msg: str) -> None:
959-
"""
960-
Callback for `CLPLogLevelTimeout` to log internal errors.
961-
962-
:param msg: The message sent from `CLPLogLevelTimeout`.`
963-
"""
964-
curr_ts: int = floor(time.time() * 1000)
965-
self._serialize_kv_pair_log_event(
966-
create_loglib_generated_log_event_as_auto_generated_kv_pairs(curr_ts, self._tz, msg), {}
963+
self._serializer.serialize_log_event_from_msgpack_map(
964+
serialize_dict_to_msgpack(auto_gen_kv_pairs),
965+
serialize_dict_to_msgpack(user_gen_kv_pairs),
967966
)

src/clp_logging/utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import time
4+
from math import floor
5+
6+
7+
class Timestamp:
8+
"""
9+
This class represents a Unix timestamp along with the timezone offset from
10+
UTC.
11+
"""
12+
13+
@staticmethod
14+
def now() -> Timestamp:
15+
"""
16+
:return: A `Timestamp` instance representing the current time.
17+
"""
18+
ts: float = time.time()
19+
return Timestamp(
20+
unix_ts=floor(ts * 1000),
21+
utc_offset=time.localtime(ts).tm_isdst,
22+
)
23+
24+
def __init__(self, unix_ts: int, utc_offset: int):
25+
"""
26+
Initializes a `Timestamp` instance with the current time.
27+
28+
:param unix_ts: Unix timestamp in milliseconds.
29+
:param utc_offset: The number of seconds the timezone is ahead of
30+
(positive) or behind (negative) UTC.
31+
"""
32+
self._utc_offset: int = utc_offset
33+
self._unix_ts: int = unix_ts
34+
35+
def get_unix_ts(self) -> int:
36+
"""
37+
:return: The Unix timestamp (milliseconds since the Unix epoch).
38+
"""
39+
return self._unix_ts
40+
41+
def get_utc_offset(self) -> int:
42+
"""
43+
:return: The number of seconds the timezone is ahead of (positive) or behind (negative) UTC.
44+
"""
45+
return self._utc_offset

0 commit comments

Comments
 (0)