-
Notifications
You must be signed in to change notification settings - Fork 6
feat: support batching #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,28 +1,36 @@ | ||||||||||
| # -*- coding: utf-8 -*- | ||||||||||
|
|
||||||||||
| import logging | ||||||||||
| import warnings | ||||||||||
| from logging.handlers import QueueHandler | ||||||||||
| from logging.handlers import MemoryHandler, QueueHandler | ||||||||||
| from logging.handlers import QueueListener | ||||||||||
| import os | ||||||||||
| from queue import Queue | ||||||||||
| from typing import Dict | ||||||||||
| from typing import Optional | ||||||||||
| from typing import Type | ||||||||||
| import time | ||||||||||
| from typing import Optional, Union | ||||||||||
|
|
||||||||||
| from logging_loki import const | ||||||||||
| from logging_loki import emitter | ||||||||||
| from logging_loki.emitter import BasicAuth, LokiEmitter | ||||||||||
|
|
||||||||||
| LOKI_MAX_BATCH_BUFFER_SIZE = int(os.environ.get('LOKI_MAX_BATCH_BUFFER_SIZE', 10)) | ||||||||||
|
|
||||||||||
| class LokiQueueHandler(QueueHandler): | ||||||||||
| """This handler automatically creates listener and `LokiHandler` to handle logs queue.""" | ||||||||||
|
|
||||||||||
| def __init__(self, queue: Queue, **kwargs): | ||||||||||
| handler: Union['LokiBatchHandler', 'LokiHandler'] | ||||||||||
|
|
||||||||||
| def __init__(self, queue: Queue, batch_interval: Optional[float] = None, **kwargs): | ||||||||||
| """Create new logger handler with the specified queue and kwargs for the `LokiHandler`.""" | ||||||||||
| super().__init__(queue) | ||||||||||
| self.handler = LokiHandler(**kwargs) # noqa: WPS110 | ||||||||||
|
|
||||||||||
| loki_handler = LokiHandler(**kwargs) # noqa: WPS110 | ||||||||||
| self.handler = LokiBatchHandler(batch_interval, target=loki_handler) if batch_interval else loki_handler | ||||||||||
|
|
||||||||||
| self.listener = QueueListener(self.queue, self.handler) | ||||||||||
| self.listener.start() | ||||||||||
|
|
||||||||||
| def flush(self) -> None: | ||||||||||
| super().flush() | ||||||||||
| self.handler.flush() | ||||||||||
|
|
||||||||||
| def __del__(self): | ||||||||||
| self.listener.stop() | ||||||||||
|
|
||||||||||
|
|
@@ -33,20 +41,16 @@ class LokiHandler(logging.Handler): | |||||||||
| `Loki API <https://github.com/grafana/loki/blob/master/docs/api.md>`_ | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| emitters: Dict[str, Type[emitter.LokiEmitter]] = { | ||||||||||
| "0": emitter.LokiEmitterV0, | ||||||||||
| "1": emitter.LokiEmitterV1, | ||||||||||
| } | ||||||||||
| emitter: LokiEmitter | ||||||||||
|
|
||||||||||
| def __init__( | ||||||||||
| self, | ||||||||||
| url: str, | ||||||||||
| tags: Optional[dict] = None, | ||||||||||
| headers: Optional[dict] = None, | ||||||||||
| auth: Optional[emitter.BasicAuth] = None, | ||||||||||
| version: Optional[str] = None, | ||||||||||
| auth: Optional[BasicAuth] = None, | ||||||||||
| as_json: Optional[bool] = False, | ||||||||||
| props_to_labels: Optional[list[str]] = None | ||||||||||
| props_to_labels: Optional[list[str]] = None, | ||||||||||
| ): | ||||||||||
| """ | ||||||||||
| Create new Loki logging handler. | ||||||||||
|
|
@@ -55,24 +59,13 @@ def __init__( | |||||||||
| url: Endpoint used to send log entries to Loki (e.g. `https://my-loki-instance/loki/api/v1/push`). | ||||||||||
| tags: Default tags added to every log record. | ||||||||||
| auth: Optional tuple with username and password for basic HTTP authentication. | ||||||||||
| version: Version of Loki emitter to use. | ||||||||||
| headers: Optional record with headers that are send with each POST to loki. | ||||||||||
| as_json: Flag to support sending entire JSON record instead of only the message. | ||||||||||
| props_to_labels: List of properties that should be converted to loki labels. | ||||||||||
|
|
||||||||||
| """ | ||||||||||
| super().__init__() | ||||||||||
|
|
||||||||||
| if version is None and const.emitter_ver == "0": | ||||||||||
| msg = ( | ||||||||||
| "Loki /api/prom/push endpoint is in the depreciation process starting from version 0.4.0.", | ||||||||||
| "Explicitly set the emitter version to '0' if you want to use the old endpoint.", | ||||||||||
| "Or specify '1' if you have Loki version> = 0.4.0.", | ||||||||||
| "When the old API is removed from Loki, the handler will use the new version by default.", | ||||||||||
| ) | ||||||||||
| warnings.warn(" ".join(msg), DeprecationWarning) | ||||||||||
|
|
||||||||||
| version = version or const.emitter_ver | ||||||||||
| if version not in self.emitters: | ||||||||||
| raise ValueError("Unknown emitter version: {0}".format(version)) | ||||||||||
| self.emitter = self.emitters[version](url, tags, headers, auth, as_json, props_to_labels) | ||||||||||
| self.emitter = LokiEmitter(url, tags, headers, auth, as_json, props_to_labels) | ||||||||||
|
|
||||||||||
| def handleError(self, record): # noqa: N802 | ||||||||||
| """Close emitter and let default handler take actions on error.""" | ||||||||||
|
|
@@ -86,3 +79,38 @@ def emit(self, record: logging.LogRecord): | |||||||||
| self.emitter(record, self.format(record)) | ||||||||||
| except Exception: | ||||||||||
| self.handleError(record) | ||||||||||
|
|
||||||||||
| def emit_batch(self, records: list[logging.LogRecord]): | ||||||||||
| """Send a batch of log records to Loki.""" | ||||||||||
| # noinspection PyBroadException | ||||||||||
| try: | ||||||||||
| self.emitter.emit_batch([(record, self.format(record)) for record in records]) | ||||||||||
| except Exception: | ||||||||||
| for record in records: | ||||||||||
| self.handleError(record) | ||||||||||
|
|
||||||||||
| class LokiBatchHandler(MemoryHandler): | ||||||||||
| interval: float # The interval at which batched logs are sent in seconds | ||||||||||
| _last_flush_time: float | ||||||||||
| target: LokiHandler | ||||||||||
|
|
||||||||||
| def __init__(self, interval: float, capacity: int = LOKI_MAX_BATCH_BUFFER_SIZE, **kwargs): | ||||||||||
| super().__init__(capacity, **kwargs) | ||||||||||
| self.interval = interval | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that in
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. python-logging-loki/logging_loki/handlers.py Lines 26 to 29 in c200531
In the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that I see this chunky code i'm gonna change it to use a ternary operator
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably too late for this library, but I would (here and elsewhere) only use dependency injection in the You can then still have other static builder/factory methods that create the objects and stitches them together.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would love to add dependency injection as well, but since this repo is a fork, I just want to make minimal changes |
||||||||||
| self._last_flush_time = time.time() | ||||||||||
|
|
||||||||||
| def flush(self) -> None: | ||||||||||
| self.acquire() | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this acquired lock imply that all logs made during the flush are lost because of the in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NVM, it's a lock on another object ... (the emitter, not the handler)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, the handler lock is to prevent the entire buffer of logs to be flushed/sent twice The other lock used in the emitter is to prevent the emitter from creating an infinite loop: |
||||||||||
| try: | ||||||||||
| if self.target and self.buffer: | ||||||||||
| self.target.emit_batch(self.buffer) | ||||||||||
| self.buffer.clear() | ||||||||||
| finally: | ||||||||||
| self._last_flush_time = time.time() | ||||||||||
| self.release() | ||||||||||
|
|
||||||||||
| def shouldFlush(self, record: logging.LogRecord) -> bool: | ||||||||||
| return ( | ||||||||||
| super().shouldFlush(record) or | ||||||||||
| (time.time() - self._last_flush_time >= self.interval) | ||||||||||
| ) | ||||||||||
Uh oh!
There was an error while loading. Please reload this page.