From fd4e9409c8c967b4835e0ef0146c7dd91530e0b2 Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 01:14:57 -0600 Subject: [PATCH 01/12] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 732c190..195ec50 100644 --- a/.gitignore +++ b/.gitignore @@ -157,5 +157,5 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ .vscode \ No newline at end of file From 0a20e6d47e4e4e3a2530b2069060e3f64bcb7fcf Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 01:16:22 -0600 Subject: [PATCH 02/12] Upgrade 'requests' dependency version --- requirements-test.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 15e0d8b..99ae2e0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,2 @@ -requests==2.28.2 +requests==2.32.3 pytest \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bf0d9d4..ef487e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests==2.28.2 \ No newline at end of file +requests==2.32.3 \ No newline at end of file From 2e820a868502a2905b646617fe02d33ba047a0eb Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 01:17:41 -0600 Subject: [PATCH 03/12] Added fallback to the current time in nanoseconds if the timestamp is missing or invalid in the stream's append_value() function. --- loki_logger_handler/stream.py | 70 ++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/loki_logger_handler/stream.py b/loki_logger_handler/stream.py index 55307a9..f291453 100644 --- a/loki_logger_handler/stream.py +++ b/loki_logger_handler/stream.py @@ -1,30 +1,72 @@ import json +import time -class _StreamtEncoder(json.JSONEncoder): +class _StreamEncoder(json.JSONEncoder): + """ + A custom JSON encoder for the Stream class. + This is an internal class used to handle the serialization + of Stream objects into JSON format. + """ def default(self, obj): if isinstance(obj, Stream): return obj.__dict__ - return json.JSONEncoder.default(self, obj) + return super().default(obj) class Stream(object): + """ + A class representing a data stream with associated labels and values. + + Attributes: + stream (dict): A dictionary containing the labels for the stream. + values (list): A list of timestamped values associated with the stream. + """ def __init__(self, labels=None): - if labels is None: - labels = {} - self.stream = labels + """ + Initialize a Stream object with optional labels. + + Args: + labels (dict, optional): A dictionary of labels for the stream. Defaults to an empty dictionary. + """ + self.stream = labels if labels is not None else {} self.values = [] - def addLabel(self, key, value): + def add_label(self, key, value): + """ + Add a label to the stream. + + Args: + key (str): The label's key. + value (str): The label's value. + """ self.stream[key] = value - def appendValue(self, value): - self.values.append( - [ - str(int(value.get("timestamp") * 1e9)), - json.dumps(value, ensure_ascii=False), - ] - ) + def append_value(self, value): + """ + Append a value to the stream with a timestamp. + + Args: + value (dict): A dictionary representing the value to be appended. + It should contain a 'timestamp' key. + """ + try: + # Convert the timestamp to nanoseconds and ensure it's a string + timestamp = str(int(value.get("timestamp") * 1e9)) + except (TypeError, ValueError): + # Fallback to the current time in nanoseconds if the timestamp is missing or invalid + timestamp = str(time.time_ns()) + + self.values.append([ + timestamp, + json.dumps(value, ensure_ascii=False) + ]) def serialize(self): - return json.dumps(self, cls=_StreamtEncoder) + """ + Serialize the Stream object to a JSON string. + + Returns: + str: The JSON string representation of the Stream object. + """ + return json.dumps(self, cls=_StreamEncoder) From e4b018723587f7588a7388032d47b3c1d0d9164d Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 01:18:28 -0600 Subject: [PATCH 04/12] Reorganize the sequence of arguments in the LokiLoggerHandler constructor (useful change when loading logging configuration from files using `logging.config.fileConfig()`) --- loki_logger_handler/loki_logger_handler.py | 132 +++++++++++++++++---- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/loki_logger_handler/loki_logger_handler.py b/loki_logger_handler/loki_logger_handler.py index bb164fc..80baa12 100644 --- a/loki_logger_handler/loki_logger_handler.py +++ b/loki_logger_handler/loki_logger_handler.py @@ -11,46 +11,94 @@ class LokiLoggerHandler(logging.Handler): + """ + A custom logging handler that sends logs to a Loki server. + + Attributes: + labels (dict): Default labels for the logs. + label_keys (dict): Specific log record keys to extract as labels. + timeout (int): Timeout interval for flushing logs. + logger_formatter (logging.Formatter): Formatter for log records. + request (LokiRequest): Loki request object for sending logs. + buffer (queue.Queue): Buffer to store log records before sending. + flush_thread (threading.Thread): Thread for periodically flushing logs. + """ + def __init__( self, url, labels, - labelKeys=None, + label_keys=None, + additional_headers=None, timeout=10, compressed=True, - defaultFormatter=LoggerFormatter(), - additional_headers=dict() + loguru=False, + default_formatter=None ): + """ + Initialize the LokiLoggerHandler object. + + Args: + url (str): The URL of the Loki server. + labels (dict): Default labels for the logs. + label_keys (dict, optional): Specific log record keys to extract as labels. Defaults to an empty dictionary. + additional_headers (dict, optional): Additional headers for the Loki request. Defaults to an empty dict. + timeout (int, optional): Timeout interval for flushing logs. Defaults to 10 seconds. + compressed (bool, optional): Whether to compress the logs using gzip. Defaults to True. + loguru (bool, optional): Whether to use LoguruFormatter. Defaults to False. + default_formatter (logging.Formatter, optional): Formatter for the log records. If not provided, + LoggerFormatter or LoguruFormatter will be used. + """ super().__init__() - if labelKeys is None: - labelKeys = {} - self.labels = labels - self.labelKeys = labelKeys + self.label_keys = label_keys if label_keys is not None else {} self.timeout = timeout - self.logger_formatter = defaultFormatter - self.request = LokiRequest(url=url, compressed=compressed, additional_headers=additional_headers) + self.logger_formatter = default_formatter if default_formatter is not None else ( + LoguruFormatter() if loguru else LoggerFormatter() + ) + self.request = LokiRequest(url=url, compressed=compressed, additional_headers=additional_headers or {}) self.buffer = queue.Queue() self.flush_thread = threading.Thread(target=self._flush, daemon=True) self.flush_thread.start() def emit(self, record): - self._put(self.logger_formatter.format(record)) - + """ + Emit a log record. + + Args: + record (logging.LogRecord): The log record to be emitted. + """ + # noinspection PyBroadException + try: + formatted_record = self.logger_formatter.format(record) + self._put(formatted_record) + except Exception: + # Silently ignore any exceptions + pass + + # noinspection PyBroadException def _flush(self): + """ + Flush the buffer by sending the logs to the Loki server. + This function runs in a separate thread and periodically sends logs. + """ atexit.register(self._send) - flushing = False while True: - if not flushing and not self.buffer.empty(): - flushing = True - self._send() - flushing = False + if not self.buffer.empty(): + try: + self._send() + except Exception: + # Silently ignore any exceptions + pass else: time.sleep(self.timeout) def _send(self): + """ + Send the buffered logs to the Loki server. + """ temp_streams = {} while not self.buffer.empty(): @@ -59,31 +107,69 @@ def _send(self): stream = Stream(log.labels) temp_streams[log.key] = stream - temp_streams[log.key].appendValue(log.line) + temp_streams[log.key].append_value(log.line) if temp_streams: - streams = Streams(temp_streams.values()) + streams = Streams(list(temp_streams.values())) self.request.send(streams.serialize()) def write(self, message): + """ + Write a message to the log. + + Args: + message (str): The message to be logged. + """ self.emit(message.record) def _put(self, log_record): - labels = self.labels + """ + Put a log record into the buffer. + + Args: + log_record (dict): The formatted log record. + """ + labels = self.labels.copy() - for key in self.labelKeys: - if key in log_record.keys(): + for key in self.label_keys: + if key in log_record: labels[key] = log_record[key] self.buffer.put(LogLine(labels, log_record)) class LogLine: + """ + Represents a single log line with associated labels. + + Attributes: + labels (dict): Labels associated with the log line. + key (str): A unique key generated from the labels. + line (str): The actual log line content. + """ + def __init__(self, labels, line): + """ + Initialize a LogLine object. + + Args: + labels (dict): Labels associated with the log line. + line (str): The actual log line content. + """ self.labels = labels - self.key = self.key_from_lables(labels) + self.key = self._key_from_labels(labels) self.line = line - def key_from_lables(self, labels): + @staticmethod + def _key_from_labels(labels): + """ + Generate a unique key from the labels. + + Args: + labels (dict): Labels to generate the key from. + + Returns: + str: A unique key generated from the labels. + """ key_list = sorted(labels.keys()) return "_".join(key_list) From 6fee936945c81192469e5492aabda0c196041661 Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 01:19:30 -0600 Subject: [PATCH 05/12] Improved docstrings and comments for better readability and maintenance. General functions improvements. --- .../formatters/logger_formatter.py | 20 ++++++-- .../formatters/loguru_formatter.py | 26 +++++++--- loki_logger_handler/loki_request.py | 42 +++++++++++++++-- loki_logger_handler/streams.py | 47 ++++++++++++++++--- 4 files changed, 114 insertions(+), 21 deletions(-) diff --git a/loki_logger_handler/formatters/logger_formatter.py b/loki_logger_handler/formatters/logger_formatter.py index 08698b7..112cf65 100644 --- a/loki_logger_handler/formatters/logger_formatter.py +++ b/loki_logger_handler/formatters/logger_formatter.py @@ -2,6 +2,9 @@ class LoggerFormatter: + """ + A custom formatter for log records that formats the log record into a structured dictionary. + """ LOG_RECORD_FIELDS = { "msg", "levelname", @@ -29,6 +32,15 @@ def __init__(self): pass def format(self, record): + """ + Format a log record into a structured dictionary. + + Args: + record (logging.LogRecord): The log record to format. + + Returns: + dict: A dictionary representation of the log record. + """ formatted = { "message": record.getMessage(), "timestamp": record.created, @@ -40,13 +52,15 @@ def format(self, record): "level": record.levelname, } + # Capture any custom fields added to the log record record_keys = set(record.__dict__.keys()) - missing_fields = record_keys - self.LOG_RECORD_FIELDS + custom_fields = record_keys - self.LOG_RECORD_FIELDS - for key in missing_fields: + for key in custom_fields: formatted[key] = getattr(record, key) - if record.levelname == "ERROR": + # Check if the log level indicates an error (case-insensitive and can be partial) + if record.levelname.upper().startswith("ER"): formatted["file"] = record.filename formatted["path"] = record.pathname formatted["line"] = record.lineno diff --git a/loki_logger_handler/formatters/loguru_formatter.py b/loki_logger_handler/formatters/loguru_formatter.py index 24ce62f..ac98b6e 100644 --- a/loki_logger_handler/formatters/loguru_formatter.py +++ b/loki_logger_handler/formatters/loguru_formatter.py @@ -2,10 +2,22 @@ class LoguruFormatter: + """ + A custom formatter for log records generated by Loguru, formatting the record into a structured dictionary. + """ def __init__(self): pass def format(self, record): + """ + Format a Loguru log record into a structured dictionary. + + Args: + record (dict): The Loguru log record to format. + + Returns: + dict: A dictionary representation of the log record. + """ formatted = { "message": record.get("message"), "timestamp": record.get("time").timestamp(), @@ -14,16 +26,16 @@ def format(self, record): "function": record.get("function"), "module": record.get("module"), "name": record.get("name"), - "level": record.get("level").name, + "level": record.get("level").name.upper(), } - if record.get("extra"): - if record.get("extra").get("extra"): - formatted.update(record.get("extra").get("extra")) - else: - formatted.update(record.get("extra")) + # Update with extra fields if available + extra = record.get("extra", {}) + if isinstance(extra, dict): + formatted.update(extra.get("extra", extra)) - if record.get("level").name == "ERROR": + # Check if the log level indicates an error (case-insensitive and can be partial) + if formatted["level"].startswith("ER"): formatted["file"] = record.get("file").name formatted["path"] = record.get("file").path formatted["line"] = record.get("line") diff --git a/loki_logger_handler/loki_request.py b/loki_logger_handler/loki_request.py index eb67cb9..dc5e9cd 100644 --- a/loki_logger_handler/loki_request.py +++ b/loki_logger_handler/loki_request.py @@ -1,27 +1,59 @@ import requests import gzip +import sys class LokiRequest: - def __init__(self, url, compressed=False, additional_headers=dict()): + """ + A class to send logs to a Loki server, with optional compression and custom headers. + + Attributes: + url (str): The URL of the Loki server. + compressed (bool): Whether to compress the logs using gzip. + headers (dict): Additional headers to include in the request. + session (requests.Session): The session used for making HTTP requests. + """ + def __init__(self, url, compressed=False, additional_headers=None): + """ + Initialize the LokiRequest object with the server URL, compression option, and additional headers. + + Args: url (str): The URL of the Loki server. compressed (bool, optional): Whether to compress the logs using + gzip. Defaults to False. additional_headers (dict, optional): Additional headers to include in the request. + Defaults to an empty dictionary. + """ self.url = url self.compressed = compressed - self.headers = additional_headers - self.headers["Content-type"] = "application/json" + self.headers = additional_headers if additional_headers is not None else {} + self.headers["Content-Type"] = "application/json" self.session = requests.Session() def send(self, data): + """ + Send the log data to the Loki server. + + Args: + data (str): The log data to be sent. + + Raises: + requests.RequestException: If the request fails. + """ response = None try: if self.compressed: self.headers["Content-Encoding"] = "gzip" - data = gzip.compress(bytes(data, "utf-8")) + data = gzip.compress(data.encode("utf-8")) response = self.session.post(self.url, data=data, headers=self.headers) response.raise_for_status() except requests.RequestException as e: - print(f"Error while sending logs: {e}") + sys.stderr.write(f"Error while sending logs: {e}\n") + if response is not None: + sys.stderr.write( + f"Response status code: {response.status_code}, " + f"response text: {response.text}, " + f"post request URL: {response.request.url}\n" + ) finally: if response: diff --git a/loki_logger_handler/streams.py b/loki_logger_handler/streams.py index 4a39eae..eaeeb14 100644 --- a/loki_logger_handler/streams.py +++ b/loki_logger_handler/streams.py @@ -2,23 +2,58 @@ class _LokiRequestEncoder(json.JSONEncoder): + """ + A custom JSON encoder for the Streams class. + This internal class is used to handle the serialization + of Streams objects into the JSON format expected by Loki. + """ def default(self, obj): if isinstance(obj, Streams): + # Convert the Streams object to a dictionary format suitable for Loki return {"streams": [stream.__dict__ for stream in obj.streams]} - return json.JSONEncoder.default(self, obj) + # Use the default serialization method for other objects + return super().default(obj) class Streams: + """ + A class representing a collection of Stream objects. + + Attributes: + streams (list): A list of Stream objects. + """ def __init__(self, streams=None): - if streams is None: - streams = [] - self.streams = streams + """ + Initialize a Streams object with an optional list of Stream objects. + + Args: + streams (list, optional): A list of Stream objects. Defaults to an empty list. + """ + self.streams = streams if streams is not None else [] - def addStream(self, stream): + def add_stream(self, stream): + """ + Add a single Stream object to the streams list. + + Args: + stream (Stream): The Stream object to be added. + """ self.streams.append(stream) - def addStreams(self, streams): + def set_streams(self, streams): + """ + Set the streams list to a new list of Stream objects. + + Args: + streams (list): A list of Stream objects to replace the current streams. + """ self.streams = streams def serialize(self): + """ + Serialize the Streams object to a JSON string. + + Returns: + str: The JSON string representation of the Streams object. + """ return json.dumps(self, cls=_LokiRequestEncoder) From af117a4dce59a9c3e485b5db55d163d68e5f4f2e Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 01:19:48 -0600 Subject: [PATCH 06/12] Added tests for custom formatters, empty buffer handling. Fixed test cases to ensure proper exception handling and correct formatter behavior. --- tests/test_loki_logger_handler.py | 127 +++++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 21 deletions(-) diff --git a/tests/test_loki_logger_handler.py b/tests/test_loki_logger_handler.py index f1e3395..6bf4678 100644 --- a/tests/test_loki_logger_handler.py +++ b/tests/test_loki_logger_handler.py @@ -1,3 +1,4 @@ +import logging import unittest import pytest from unittest.mock import patch, Mock, MagicMock, call @@ -9,6 +10,11 @@ from tests.helper import LevelObject, RecordValueMock, TimeObject +class CustomFormatter(logging.Formatter): + def format(self, record): + return f"Custom formatted: {record.getMessage()}" + + class TestLokiLoggerHandler(unittest.TestCase): @patch("loki_logger_handler.loki_logger_handler.threading.Thread") @patch.object(LokiLoggerHandler, "_put") @@ -28,9 +34,9 @@ def test_emit(self, mock_formatter, mock_put, mock_thread): } handler = LokiLoggerHandler( url="your_url", - labels={"application": "Test", "envornment": "Develop"}, - labelKeys={}, - defaultFormatter=mock_formatter, + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, + default_formatter=mock_formatter, ) # Act handler.emit(record) @@ -44,8 +50,8 @@ def test_emit_no_record(self, mock_thread): # Arrange loki = LokiLoggerHandler( url="your_url", - labels={"application": "Test", "envornment": "Develop"}, - labelKeys={}, + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, ) # Act/Assert @@ -85,9 +91,9 @@ def test_write(self, mock_formatter, mock_emit, mock_put, mock_thread): } handler = LokiLoggerHandler( url="your_url", - labels={"application": "Test", "envornment": "Develop"}, - labelKeys={}, - defaultFormatter=mock_formatter, + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, + default_formatter=mock_formatter, ) # Act @@ -115,8 +121,8 @@ def test_put(self, mock_logline, mock_thread): } handler = LokiLoggerHandler( url="your_url", - labels={"application": "Test", "envornment": "Develop"}, - labelKeys={}, + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, ) mock_queue = Mock() @@ -124,12 +130,12 @@ def test_put(self, mock_logline, mock_thread): # Act handler._put(mock_message.record) - expected_labels = {"application": "Test", "envornment": "Develop"} + expected_labels = {"application": "Test", "environment": "Develop"} mock_logline.assert_called_with(expected_labels, mock_message.record) @patch("loki_logger_handler.loki_logger_handler.threading.Thread") @patch("loki_logger_handler.loki_logger_handler.LogLine") - def test_put_lable_key(self, mock_logline, mock_thread): + def test_put_label_key(self, mock_logline, mock_thread): # Arrange mock_message = Mock() @@ -146,8 +152,8 @@ def test_put_lable_key(self, mock_logline, mock_thread): } handler = LokiLoggerHandler( url="your_url", - labels={"application": "Test", "envornment": "Develop"}, - labelKeys={"function"}, + labels={"application": "Test", "environment": "Develop"}, + label_keys={"function"}, ) mock_queue = Mock() @@ -157,7 +163,7 @@ def test_put_lable_key(self, mock_logline, mock_thread): expected_labels = { "application": "Test", - "envornment": "Develop", + "environment": "Develop", "function": "sample_function", } mock_logline.assert_called_with(expected_labels, mock_message.record) @@ -168,7 +174,7 @@ def test_flush_happy_path(self, mock_sleep, mock_thread): handler = LokiLoggerHandler( "http://test_url", labels={"label1": "value1"}, - defaultFormatter=LoguruFormatter(), + default_formatter=LoguruFormatter(), ) handler.buffer.put("test_log") # Add an item to the buffer handler._send = Mock() # Mock the _send method @@ -197,7 +203,7 @@ def test_send_diff_labels( handler = LokiLoggerHandler( "http://test_url", labels={"label1": "value1"}, - defaultFormatter=LoguruFormatter(), + default_formatter=LoguruFormatter(), ) record = { @@ -237,9 +243,9 @@ def test_send_diff_labels( mock_stream.assert_has_calls( [ call(log1.labels), - call().appendValue(log1.line), + call().append_value(log1.line), call(log2.labels), - call().appendValue(log2.line), + call().append_value(log2.line), ] ) @@ -253,7 +259,7 @@ def test_send_same_labels( handler = LokiLoggerHandler( "http://test_url", labels={"label1": "value1"}, - defaultFormatter=LoguruFormatter(), + default_formatter=LoguruFormatter(), ) record = { @@ -294,7 +300,7 @@ def test_send_empty_buffer(self, mock_streams, mock_thread): handler = LokiLoggerHandler( "http://test_url", labels={"label1": "value1"}, - defaultFormatter=LoguruFormatter(), + default_formatter=LoguruFormatter(), ) mock_queue = Mock() @@ -304,6 +310,85 @@ def test_send_empty_buffer(self, mock_streams, mock_thread): mock_streams.assert_not_called() + @patch("loki_logger_handler.loki_logger_handler.threading.Thread") + def test_custom_formatter(self, mock_thread): + # Arrange + custom_formatter = CustomFormatter() + handler = LokiLoggerHandler( + url="your_url", + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, + default_formatter=custom_formatter, + ) + + record = Mock() + record.getMessage.return_value = "Test message" + + # Act + handler.emit(record) + + # Assert + formatted_message = handler.logger_formatter.format(record) + self.assertEqual(formatted_message, "Custom formatted: Test message") + + @patch("loki_logger_handler.loki_logger_handler.threading.Thread") + def test_empty_buffer(self, mock_thread): + handler = LokiLoggerHandler( + "http://test_url", + labels={"label1": "value1"}, + default_formatter=Mock(), # Mock the formatter to avoid actual formatting + ) + + handler._send = Mock() # Mock the _send method + handler.buffer.empty = Mock(return_value=True) # Buffer is empty + + # Call _flush directly and then immediately break out of the loop + with patch("loki_logger_handler.loki_logger_handler.time.sleep", side_effect=Exception("StopIteration")): + try: + handler._flush() + except Exception as e: + self.assertEqual(str(e), "StopIteration") + + handler._send.assert_not_called() # _send should not be called + + @patch("loki_logger_handler.loki_logger_handler.threading.Thread") + @patch.object(LokiLoggerHandler, "_put") + def test_formatter_exception(self, mock_put, mock_thread): + handler = LokiLoggerHandler( + "http://test_url", + labels={"label1": "value1"}, + default_formatter=LoguruFormatter(), + ) + + record = MagicMock() + handler.logger_formatter.format = Mock(side_effect=Exception("Formatter Error")) + + handler.emit(record) + + mock_put.assert_not_called() # The log should not be added to the buffer + + @patch("loki_logger_handler.loki_logger_handler.threading.Thread") + @patch.object(LokiLoggerHandler, "_put") + @patch("loki_logger_handler.formatters.logger_formatter.LoggerFormatter") + def test_emit_with_custom_formatter(self, mock_formatter, mock_put, mock_thread): + # Arrange + custom_formatter = CustomFormatter() + handler = LokiLoggerHandler( + url="your_url", + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, + default_formatter=custom_formatter, + ) + + record = Mock() + record.getMessage.return_value = "Test message" + + # Act + handler.emit(record) + + # Assert + mock_put.assert_called_with("Custom formatted: Test message") + if __name__ == "__main__": unittest.main() From 8bf731f1fb1964bdb2f307cd565d30bb8c72eab9 Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 02:23:58 -0600 Subject: [PATCH 07/12] Added argument to disable JSON format for message (Default to JSON format always) --- loki_logger_handler/loki_logger_handler.py | 12 ++++++++---- loki_logger_handler/stream.py | 13 +++++++------ tests/test_loki_logger_handler.py | 10 +++++++--- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/loki_logger_handler/loki_logger_handler.py b/loki_logger_handler/loki_logger_handler.py index 80baa12..078f222 100644 --- a/loki_logger_handler/loki_logger_handler.py +++ b/loki_logger_handler/loki_logger_handler.py @@ -22,6 +22,7 @@ class LokiLoggerHandler(logging.Handler): request (LokiRequest): Loki request object for sending logs. buffer (queue.Queue): Buffer to store log records before sending. flush_thread (threading.Thread): Thread for periodically flushing logs. + message_in_json_format (bool): Whether to format log values as JSON. """ def __init__( @@ -30,6 +31,7 @@ def __init__( labels, label_keys=None, additional_headers=None, + message_in_json_format=True, timeout=10, compressed=True, loguru=False, @@ -41,13 +43,14 @@ def __init__( Args: url (str): The URL of the Loki server. labels (dict): Default labels for the logs. - label_keys (dict, optional): Specific log record keys to extract as labels. Defaults to an empty dictionary. - additional_headers (dict, optional): Additional headers for the Loki request. Defaults to an empty dict. + label_keys (dict, optional): Specific log record keys to extract as labels. Defaults to None. + additional_headers (dict, optional): Additional headers for the Loki request. Defaults to None. + message_in_json_format (bool): Whether to format log values as JSON. timeout (int, optional): Timeout interval for flushing logs. Defaults to 10 seconds. compressed (bool, optional): Whether to compress the logs using gzip. Defaults to True. loguru (bool, optional): Whether to use LoguruFormatter. Defaults to False. default_formatter (logging.Formatter, optional): Formatter for the log records. If not provided, - LoggerFormatter or LoguruFormatter will be used. + LoggerFormatter or LoguruFormatter will be used. """ super().__init__() @@ -61,6 +64,7 @@ def __init__( self.buffer = queue.Queue() self.flush_thread = threading.Thread(target=self._flush, daemon=True) self.flush_thread.start() + self.message_in_json_format = message_in_json_format def emit(self, record): """ @@ -104,7 +108,7 @@ def _send(self): while not self.buffer.empty(): log = self.buffer.get() if log.key not in temp_streams: - stream = Stream(log.labels) + stream = Stream(log.labels, self.message_in_json_format) temp_streams[log.key] = stream temp_streams[log.key].append_value(log.line) diff --git a/loki_logger_handler/stream.py b/loki_logger_handler/stream.py index f291453..d97e34a 100644 --- a/loki_logger_handler/stream.py +++ b/loki_logger_handler/stream.py @@ -21,8 +21,9 @@ class Stream(object): Attributes: stream (dict): A dictionary containing the labels for the stream. values (list): A list of timestamped values associated with the stream. + message_in_json_format (bool): Whether to format log values as JSON. """ - def __init__(self, labels=None): + def __init__(self, labels=None, message_in_json_format=True): """ Initialize a Stream object with optional labels. @@ -31,6 +32,7 @@ def __init__(self, labels=None): """ self.stream = labels if labels is not None else {} self.values = [] + self.message_in_json_format = message_in_json_format def add_label(self, key, value): """ @@ -56,11 +58,10 @@ def append_value(self, value): except (TypeError, ValueError): # Fallback to the current time in nanoseconds if the timestamp is missing or invalid timestamp = str(time.time_ns()) - - self.values.append([ - timestamp, - json.dumps(value, ensure_ascii=False) - ]) + + formatted_value = json.dumps(value, ensure_ascii=False) if self.message_in_json_format else value + + self.values.append([timestamp, formatted_value]) def serialize(self): """ diff --git a/tests/test_loki_logger_handler.py b/tests/test_loki_logger_handler.py index 6bf4678..3b13cb7 100644 --- a/tests/test_loki_logger_handler.py +++ b/tests/test_loki_logger_handler.py @@ -200,10 +200,12 @@ def test_flush_happy_path(self, mock_sleep, mock_thread): def test_send_diff_labels( self, mock_lokirequest, mock_stream, mock_streams, mock_thread ): + message_in_json_format = True handler = LokiLoggerHandler( "http://test_url", labels={"label1": "value1"}, default_formatter=LoguruFormatter(), + message_in_json_format=message_in_json_format ) record = { @@ -242,9 +244,9 @@ def test_send_diff_labels( mock_stream.assert_has_calls( [ - call(log1.labels), + call(log1.labels, message_in_json_format), call().append_value(log1.line), - call(log2.labels), + call(log2.labels, message_in_json_format), call().append_value(log2.line), ] ) @@ -256,10 +258,12 @@ def test_send_diff_labels( def test_send_same_labels( self, mock_lokirequest, mock_stream, mock_streams, mock_thread ): + message_in_json_format = True handler = LokiLoggerHandler( "http://test_url", labels={"label1": "value1"}, default_formatter=LoguruFormatter(), + message_in_json_format=message_in_json_format ) record = { @@ -292,7 +296,7 @@ def test_send_same_labels( actual_streams = list(mock_streams.call_args[0][0]) self.assertEqual(expected_streams, actual_streams) - mock_stream.assert_has_calls([call(log1.labels)]) + mock_stream.assert_has_calls([call(log1.labels, message_in_json_format)]) @patch("loki_logger_handler.loki_logger_handler.threading.Thread") @patch("loki_logger_handler.loki_logger_handler.Streams") From d44db0fdb3d75252b08ac65f59f1049a62ba045d Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Sun, 18 Aug 2024 02:24:28 -0600 Subject: [PATCH 08/12] Update README --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0fbe7ee..c7433ad 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,27 @@ A logging handler that sends log messages to Loki in JSON format ## Features -* Logs pushed in JSON format +* Logs pushed in JSON format by default * Custom labels definition * Allows defining loguru and logger extra keys as labels * Logger extra keys added automatically as keys into pushed JSON * Publish in batch of Streams -* Publis logs compressed +* Publish logs compressed ## Args * url (str): The URL of the Loki server. * labels (dict): A dictionary of labels to attach to each log message. -* labelKeys (dict, optional): A dictionary of keys to extract from each log message and use as labels. Defaults to None. -* timeout (int, optional): The time in seconds to wait before flushing the buffer. Defaults to 10. +* label_keys (dict, optional): A dictionary of keys to extract from each log message and use as labels. Defaults to None. +* additional_headers (dict, optional): Additional headers for the Loki request. Defaults to None. +* timeout (int, optional): Timeout interval in seconds to wait before flushing the buffer. Defaults to 10. * compressed (bool, optional): Whether to compress the log messages before sending them to Loki. Defaults to True. -* defaultFormatter (logging.Formatter, optional): The formatter to use for log messages. Defaults to LoggerFormatter(). +* loguru (bool, optional): Whether to use `LoguruFormatter`. Defaults to False. +* default_formatter (logging.Formatter, optional): Formatter for the log records. If not provided,`LoggerFormatter` or `LoguruFormatter` will be used. ## Formatters -* LoggerFormatter: Formater for default python logging implementation -* LoguruFormatter: Formater for Loguru python library +* LoggerFormatter: Formatter for default python logging implementation +* LoguruFormatter: Formatter for Loguru python library ## How to use @@ -39,16 +41,14 @@ logger.setLevel(logging.DEBUG) # Create an instance of the custom handler custom_handler = LokiLoggerHandler( url=os.environ["LOKI_URL"], - labels={"application": "Test", "envornment": "Develop"}, - labelKeys={}, + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, timeout=10, ) # Create an instance of the custom handler logger.addHandler(custom_handler) logger.debug("Debug message", extra={'custom_field': 'custom_value'}) - - ``` @@ -63,10 +63,10 @@ os.environ["LOKI_URL"]="https://USER:PASSWORD@logs-prod-eu-west-0.grafana.net/lo custom_handler = LokiLoggerHandler( url=os.environ["LOKI_URL"], - labels={"application": "Test", "envornment": "Develop"}, - labelKeys={}, + labels={"application": "Test", "environment": "Develop"}, + label_keys={}, timeout=10, - defaultFormatter=LoguruFormatter(), + default_formatter=LoguruFormatter(), ) logger.configure(handlers=[{"sink": custom_handler, "serialize": True}]) @@ -130,18 +130,18 @@ logger.info( Loki query sample : ``` - {envornment="Develop"} |= `` | json + {environment="Develop"} |= `` | json ``` Filter by level: ``` -{envornment="Develop", level="INFO"} |= `` | json +{environment="Develop", level="INFO"} |= `` | json ``` Filter by extra: ``` -{envornment="Develop", level="INFO"} |= `` | json | code=`200` +{environment="Develop", level="INFO"} |= `` | json | code=`200` ``` ## License From 2b041bca6219e33995839bf51687e0134e500d83 Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Mon, 26 Aug 2024 15:35:17 -0600 Subject: [PATCH 09/12] Extending compatibility to Python 2.7 and any Python 3.x --- .github/workflows/publish.yml | 27 +++++++++------- .github/workflows/test.yml | 17 ++++++---- README.md | 14 ++++++--- .../formatters/logger_formatter.py | 18 ++++++++++- .../formatters/loguru_formatter.py | 31 ++++++++++++++++--- loki_logger_handler/loki_logger_handler.py | 25 +++++++++------ loki_logger_handler/loki_request.py | 28 ++++++++++++----- loki_logger_handler/stream.py | 12 +++++-- loki_logger_handler/streams.py | 4 +-- setup.py | 5 +-- tests/helper.py | 8 ++--- tests/test_loki_logger_handler.py | 9 ++++-- 12 files changed, 141 insertions(+), 57 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98c1ec5..912e1f2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,30 +11,35 @@ jobs: deploy: runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/loki-logger-handler - permissions: - id-token: write - + strategy: + matrix: + python-version: ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + steps: - uses: actions/checkout@v3 - - name: Set up Python + + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: - python-version: '3.x' + python-version: ${{ matrix.python-version }} + - name: Install Build Dependencies - run: pip install build twine + run: | + pip install --upgrade pip + pip install build twine - name: Extract Tag Version id: extract_tag - run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Set Package Version - run: sed -i "s/version=.*/version='${{ steps.extract_tag.outputs.tag }}',/" setup.py + run: | + sed -i "s/version=.*/version='${{ env.tag }}',/" setup.py - name: Build Distribution run: python -m build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d07587..8a8ffc4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,18 +8,23 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 with: - python-version: 3.x + python-version: ${{ matrix.python-version }} - name: Install Dependencies - run: pip install -r requirements-test.txt + run: | + pip install --upgrade pip + pip install -r requirements-test.txt - name: Run Tests - run: python -m unittest discover tests/ \ No newline at end of file + run: python -m unittest discover tests/ diff --git a/README.md b/README.md index c7433ad..f759dfd 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # loki_logger_handler -A logging handler that sends log messages to Loki in JSON format +[![PyPI](https://img.shields.io/pypi/v/loki_logger_handler?color=blue&label=pypi%20version)]() +[![PyPI](https://img.shields.io/pypi/pyversions/loki_logger_handler.svg)]() +[![Downloads](https://pepy.tech/badge/loki_logger_handler)](https://pepy.tech/project/loki_logger_handler) + +A logging handler that sends log messages to **(Grafana) Loki** in JSON format. ## Features * Logs pushed in JSON format by default * Custom labels definition -* Allows defining loguru and logger extra keys as labels +* Allows defining *loguru* and *logger* extra keys as labels * Logger extra keys added automatically as keys into pushed JSON * Publish in batch of Streams * Publish logs compressed @@ -17,14 +21,14 @@ A logging handler that sends log messages to Loki in JSON format * labels (dict): A dictionary of labels to attach to each log message. * label_keys (dict, optional): A dictionary of keys to extract from each log message and use as labels. Defaults to None. * additional_headers (dict, optional): Additional headers for the Loki request. Defaults to None. -* timeout (int, optional): Timeout interval in seconds to wait before flushing the buffer. Defaults to 10. +* timeout (int, optional): Timeout interval in seconds to wait before flushing the buffer. Defaults to 10 seconds. * compressed (bool, optional): Whether to compress the log messages before sending them to Loki. Defaults to True. * loguru (bool, optional): Whether to use `LoguruFormatter`. Defaults to False. * default_formatter (logging.Formatter, optional): Formatter for the log records. If not provided,`LoggerFormatter` or `LoguruFormatter` will be used. ## Formatters -* LoggerFormatter: Formatter for default python logging implementation -* LoguruFormatter: Formatter for Loguru python library +* **LoggerFormatter**: Formatter for default python logging implementation +* **LoguruFormatter**: Formatter for Loguru python library ## How to use diff --git a/loki_logger_handler/formatters/logger_formatter.py b/loki_logger_handler/formatters/logger_formatter.py index 112cf65..4be34e0 100644 --- a/loki_logger_handler/formatters/logger_formatter.py +++ b/loki_logger_handler/formatters/logger_formatter.py @@ -1,4 +1,5 @@ import traceback +import logging class LoggerFormatter: @@ -64,6 +65,21 @@ def format(self, record): formatted["file"] = record.filename formatted["path"] = record.pathname formatted["line"] = record.lineno - formatted["stacktrace"] = traceback.format_exc() + formatted["stacktrace"] = self._format_stacktrace(record.exc_info) return formatted + + @staticmethod + def _format_stacktrace(exc_info): + """ + Format the stacktrace if exc_info is present. + + Args: + exc_info (tuple or None): Exception info tuple as returned by sys.exc_info(). + + Returns: + str or None: Formatted stacktrace as a string, or None if exc_info is not provided. + """ + if exc_info: + return "".join(traceback.format_exception(*exc_info)) + return None diff --git a/loki_logger_handler/formatters/loguru_formatter.py b/loki_logger_handler/formatters/loguru_formatter.py index ac98b6e..0316a93 100644 --- a/loki_logger_handler/formatters/loguru_formatter.py +++ b/loki_logger_handler/formatters/loguru_formatter.py @@ -1,4 +1,5 @@ import traceback +import sys class LoguruFormatter: @@ -18,9 +19,18 @@ def format(self, record): Returns: dict: A dictionary representation of the log record. """ + # Convert timestamp to a standard format across Python versions + timestamp = record.get("time") + if hasattr(timestamp, "timestamp"): + # Python 3.x + timestamp = timestamp.timestamp() + else: + # Python 2.7: Convert datetime to a Unix timestamp + timestamp = (timestamp - timestamp.utcoffset()).total_seconds() + formatted = { "message": record.get("message"), - "timestamp": record.get("time").timestamp(), + "timestamp": timestamp, "process": record.get("process").id, "thread": record.get("thread").id, "function": record.get("function"), @@ -32,7 +42,11 @@ def format(self, record): # Update with extra fields if available extra = record.get("extra", {}) if isinstance(extra, dict): - formatted.update(extra.get("extra", extra)) + # Handle the nested "extra" key correctly + if "extra" in extra and isinstance(extra["extra"], dict): + formatted.update(extra["extra"]) + else: + formatted.update(extra) # Check if the log level indicates an error (case-insensitive and can be partial) if formatted["level"].startswith("ER"): @@ -42,9 +56,16 @@ def format(self, record): if record.get("exception"): exc_type, exc_value, exc_traceback = record.get("exception") - formatted_traceback = traceback.format_exception( - exc_type, exc_value, exc_traceback - ) + if sys.version_info[0] == 2: + # Python 2.7: Use the older method for formatting exceptions + formatted_traceback = traceback.format_exception( + exc_type, exc_value, exc_traceback + ) + else: + # Python 3.x: This is the same + formatted_traceback = traceback.format_exception( + exc_type, exc_value, exc_traceback + ) formatted["stacktrace"] = "".join(formatted_traceback) return formatted diff --git a/loki_logger_handler/loki_logger_handler.py b/loki_logger_handler/loki_logger_handler.py index 078f222..deb27ba 100644 --- a/loki_logger_handler/loki_logger_handler.py +++ b/loki_logger_handler/loki_logger_handler.py @@ -1,4 +1,11 @@ -import queue +import sys + +# Compatibility for Python 2 and 3 queue module +try: + import queue # Python 3.x +except ImportError: + import Queue as queue # Python 2.7 + import threading import time import logging @@ -52,7 +59,7 @@ def __init__( default_formatter (logging.Formatter, optional): Formatter for the log records. If not provided, LoggerFormatter or LoguruFormatter will be used. """ - super().__init__() + super(LokiLoggerHandler, self).__init__() self.labels = labels self.label_keys = label_keys if label_keys is not None else {} @@ -62,8 +69,12 @@ def __init__( ) self.request = LokiRequest(url=url, compressed=compressed, additional_headers=additional_headers or {}) self.buffer = queue.Queue() - self.flush_thread = threading.Thread(target=self._flush, daemon=True) + self.flush_thread = threading.Thread(target=self._flush) + + # Set daemon for Python 2 and 3 compatibility + self.flush_thread.daemon = True self.flush_thread.start() + self.message_in_json_format = message_in_json_format def emit(self, record): @@ -73,15 +84,12 @@ def emit(self, record): Args: record (logging.LogRecord): The log record to be emitted. """ - # noinspection PyBroadException try: formatted_record = self.logger_formatter.format(record) self._put(formatted_record) except Exception: - # Silently ignore any exceptions - pass + pass # Silently ignore any exceptions - # noinspection PyBroadException def _flush(self): """ Flush the buffer by sending the logs to the Loki server. @@ -94,8 +102,7 @@ def _flush(self): try: self._send() except Exception: - # Silently ignore any exceptions - pass + pass # Silently ignore any exceptions else: time.sleep(self.timeout) diff --git a/loki_logger_handler/loki_request.py b/loki_logger_handler/loki_request.py index dc5e9cd..eb178a9 100644 --- a/loki_logger_handler/loki_request.py +++ b/loki_logger_handler/loki_request.py @@ -2,6 +2,11 @@ import gzip import sys +try: + from io import BytesIO as IO # For Python 3 +except ImportError: + from StringIO import StringIO as IO # For Python 2 + class LokiRequest: """ @@ -17,9 +22,11 @@ def __init__(self, url, compressed=False, additional_headers=None): """ Initialize the LokiRequest object with the server URL, compression option, and additional headers. - Args: url (str): The URL of the Loki server. compressed (bool, optional): Whether to compress the logs using - gzip. Defaults to False. additional_headers (dict, optional): Additional headers to include in the request. - Defaults to an empty dictionary. + Args: + url (str): The URL of the Loki server. + compressed (bool, optional): Whether to compress the logs using gzip. Defaults to False. + additional_headers (dict, optional): Additional headers to include in the request. + Defaults to an empty dictionary. """ self.url = url self.compressed = compressed @@ -41,18 +48,23 @@ def send(self, data): try: if self.compressed: self.headers["Content-Encoding"] = "gzip" - data = gzip.compress(data.encode("utf-8")) + buf = IO() + with gzip.GzipFile(fileobj=buf, mode='wb') as f: + f.write(data.encode("utf-8")) + data = buf.getvalue() response = self.session.post(self.url, data=data, headers=self.headers) response.raise_for_status() except requests.RequestException as e: - sys.stderr.write(f"Error while sending logs: {e}\n") + sys.stderr.write("Error while sending logs: {}\n".format(e)) if response is not None: sys.stderr.write( - f"Response status code: {response.status_code}, " - f"response text: {response.text}, " - f"post request URL: {response.request.url}\n" + "Response status code: {}, " + "response text: {}, " + "post request URL: {}\n".format( + response.status_code, response.text, response.request.url + ) ) finally: diff --git a/loki_logger_handler/stream.py b/loki_logger_handler/stream.py index d97e34a..95d26b3 100644 --- a/loki_logger_handler/stream.py +++ b/loki_logger_handler/stream.py @@ -1,6 +1,14 @@ import json import time +# Compatibility for Python 2 and 3 +try: + from time import time_ns # Python 3.7+ +except ImportError: + import datetime + def time_ns(): + return int((time.time() + datetime.datetime.now().microsecond / 1e6) * 1e9) + class _StreamEncoder(json.JSONEncoder): """ @@ -11,7 +19,7 @@ class _StreamEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Stream): return obj.__dict__ - return super().default(obj) + return super(_StreamEncoder, self).default(obj) class Stream(object): @@ -57,7 +65,7 @@ def append_value(self, value): timestamp = str(int(value.get("timestamp") * 1e9)) except (TypeError, ValueError): # Fallback to the current time in nanoseconds if the timestamp is missing or invalid - timestamp = str(time.time_ns()) + timestamp = str(time_ns()) formatted_value = json.dumps(value, ensure_ascii=False) if self.message_in_json_format else value diff --git a/loki_logger_handler/streams.py b/loki_logger_handler/streams.py index eaeeb14..7c28ebc 100644 --- a/loki_logger_handler/streams.py +++ b/loki_logger_handler/streams.py @@ -12,10 +12,10 @@ def default(self, obj): # Convert the Streams object to a dictionary format suitable for Loki return {"streams": [stream.__dict__ for stream in obj.streams]} # Use the default serialization method for other objects - return super().default(obj) + return super(_LokiRequestEncoder, self).default(obj) -class Streams: +class Streams(object): # Explicitly inherit from object for Python 2 compatibility """ A class representing a collection of Stream objects. diff --git a/setup.py b/setup.py index 2bb315f..071580c 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="loki-logger-handler", - version="0.0.2", + version="0.0.3", author="Xente", description="Handler designed for transmitting logs to Grafana Loki in JSON format.", long_description=long_description, @@ -17,6 +17,7 @@ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", @@ -27,5 +28,5 @@ install_requires=[], test_suite="tests", license="MIT", - python_requires=">=3.6", + python_requires=">=2.7", ) diff --git a/tests/helper.py b/tests/helper.py index 5682c1c..d8b505f 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,15 +1,15 @@ -class RecordValueMock: +class RecordValueMock(object): # Explicitly inherit from object for Python 2.7 compatibility def __init__(self, id, name): self.id = id self.name = name -class LevelObject: +class LevelObject(object): # Explicitly inherit from object for Python 2.7 compatibility def __init__(self, name): self.name = name -class TimeObject: +class TimeObject(object): # Explicitly inherit from object for Python 2.7 compatibility def __init__(self, timestamp): self._timestamp = timestamp @@ -17,7 +17,7 @@ def timestamp(self): return self._timestamp -class FileObject: +class FileObject(object): # Explicitly inherit from object for Python 2.7 compatibility def __init__(self, name, path): self.name = name self.path = path diff --git a/tests/test_loki_logger_handler.py b/tests/test_loki_logger_handler.py index 3b13cb7..c2d969d 100644 --- a/tests/test_loki_logger_handler.py +++ b/tests/test_loki_logger_handler.py @@ -1,7 +1,12 @@ import logging import unittest import pytest -from unittest.mock import patch, Mock, MagicMock, call + +try: + from unittest.mock import patch, Mock, MagicMock, call # Python 3.x +except ImportError: + from mock import patch, Mock, MagicMock, call # Python 2.7 + from loki_logger_handler.loki_logger_handler import LogLine, LokiLoggerHandler from loki_logger_handler.stream import Stream from loki_logger_handler.formatters.logger_formatter import LoggerFormatter @@ -12,7 +17,7 @@ class CustomFormatter(logging.Formatter): def format(self, record): - return f"Custom formatted: {record.getMessage()}" + return "Custom formatted: {}".format(record.getMessage()) class TestLokiLoggerHandler(unittest.TestCase): From 6af41afea5e57d6b29e7d39ed6da6ee9a773b27d Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Mon, 26 Aug 2024 15:38:23 -0600 Subject: [PATCH 10/12] Remove introduced setting in workflow in the last commit --- .github/workflows/publish.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 912e1f2..e329e96 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,5 +41,3 @@ jobs: - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} From 3417a8c9cebcc5b762d96e95098c1058d0d19884 Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Mon, 26 Aug 2024 20:40:51 -0600 Subject: [PATCH 11/12] Upating verison with the latest released number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 071580c..faae0ad 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="loki-logger-handler", - version="0.0.3", + version="0.1.4", author="Xente", description="Handler designed for transmitting logs to Grafana Loki in JSON format.", long_description=long_description, From e34d16474004550aac7216a9992117b36c9bd329 Mon Sep 17 00:00:00 2001 From: Norman Valerio Date: Mon, 26 Aug 2024 20:55:17 -0600 Subject: [PATCH 12/12] Adjusting build details --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- setup.py | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e329e96..ccb458e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a8ffc4..4a21ecc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["2.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout Code diff --git a/setup.py b/setup.py index faae0ad..edbb00a 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,11 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], - keywords="loki, loguru, logger, handler", + keywords="loki, loguru, logging, logger, handler", install_requires=[], test_suite="tests", license="MIT",