1
1
# -*- coding: utf-8 -*-
2
2
3
3
import logging
4
- import warnings
5
- from logging .handlers import QueueHandler
4
+ from logging .handlers import MemoryHandler , QueueHandler
6
5
from logging .handlers import QueueListener
6
+ import os
7
7
from queue import Queue
8
- from typing import Dict
9
- from typing import Optional
10
- from typing import Type
8
+ import time
9
+ from typing import Optional , Union
11
10
12
- from logging_loki import const
13
- from logging_loki import emitter
11
+ from logging_loki .emitter import BasicAuth , LokiEmitter
14
12
13
+ LOKI_MAX_BATCH_BUFFER_SIZE = int (os .environ .get ('LOKI_MAX_BATCH_BUFFER_SIZE' , 10 ))
15
14
16
15
class LokiQueueHandler (QueueHandler ):
17
16
"""This handler automatically creates listener and `LokiHandler` to handle logs queue."""
18
17
19
- def __init__ (self , queue : Queue , ** kwargs ):
18
+ handler : Union ['LokiBatchHandler' , 'LokiHandler' ]
19
+
20
+ def __init__ (self , queue : Queue , batch_interval : Optional [float ] = None , ** kwargs ):
20
21
"""Create new logger handler with the specified queue and kwargs for the `LokiHandler`."""
21
22
super ().__init__ (queue )
22
- self .handler = LokiHandler (** kwargs ) # noqa: WPS110
23
+
24
+ loki_handler = LokiHandler (** kwargs ) # noqa: WPS110
25
+ self .handler = LokiBatchHandler (batch_interval , target = loki_handler ) if batch_interval else loki_handler
26
+
23
27
self .listener = QueueListener (self .queue , self .handler )
24
28
self .listener .start ()
25
29
30
+ def flush (self ) -> None :
31
+ super ().flush ()
32
+ self .handler .flush ()
33
+
26
34
def __del__ (self ):
27
35
self .listener .stop ()
28
36
@@ -33,20 +41,16 @@ class LokiHandler(logging.Handler):
33
41
`Loki API <https://github.com/grafana/loki/blob/master/docs/api.md>`_
34
42
"""
35
43
36
- emitters : Dict [str , Type [emitter .LokiEmitter ]] = {
37
- "0" : emitter .LokiEmitterV0 ,
38
- "1" : emitter .LokiEmitterV1 ,
39
- }
44
+ emitter : LokiEmitter
40
45
41
46
def __init__ (
42
47
self ,
43
48
url : str ,
44
49
tags : Optional [dict ] = None ,
45
50
headers : Optional [dict ] = None ,
46
- auth : Optional [emitter .BasicAuth ] = None ,
47
- version : Optional [str ] = None ,
51
+ auth : Optional [BasicAuth ] = None ,
48
52
as_json : Optional [bool ] = False ,
49
- props_to_labels : Optional [list [str ]] = None
53
+ props_to_labels : Optional [list [str ]] = None ,
50
54
):
51
55
"""
52
56
Create new Loki logging handler.
@@ -55,24 +59,13 @@ def __init__(
55
59
url: Endpoint used to send log entries to Loki (e.g. `https://my-loki-instance/loki/api/v1/push`).
56
60
tags: Default tags added to every log record.
57
61
auth: Optional tuple with username and password for basic HTTP authentication.
58
- version: Version of Loki emitter to use.
62
+ headers: Optional record with headers that are send with each POST to loki.
63
+ as_json: Flag to support sending entire JSON record instead of only the message.
64
+ props_to_labels: List of properties that should be converted to loki labels.
59
65
60
66
"""
61
67
super ().__init__ ()
62
-
63
- if version is None and const .emitter_ver == "0" :
64
- msg = (
65
- "Loki /api/prom/push endpoint is in the depreciation process starting from version 0.4.0." ,
66
- "Explicitly set the emitter version to '0' if you want to use the old endpoint." ,
67
- "Or specify '1' if you have Loki version> = 0.4.0." ,
68
- "When the old API is removed from Loki, the handler will use the new version by default." ,
69
- )
70
- warnings .warn (" " .join (msg ), DeprecationWarning )
71
-
72
- version = version or const .emitter_ver
73
- if version not in self .emitters :
74
- raise ValueError ("Unknown emitter version: {0}" .format (version ))
75
- self .emitter = self .emitters [version ](url , tags , headers , auth , as_json , props_to_labels )
68
+ self .emitter = LokiEmitter (url , tags , headers , auth , as_json , props_to_labels )
76
69
77
70
def handleError (self , record ): # noqa: N802
78
71
"""Close emitter and let default handler take actions on error."""
@@ -86,3 +79,38 @@ def emit(self, record: logging.LogRecord):
86
79
self .emitter (record , self .format (record ))
87
80
except Exception :
88
81
self .handleError (record )
82
+
83
+ def emit_batch (self , records : list [logging .LogRecord ]):
84
+ """Send a batch of log records to Loki."""
85
+ # noinspection PyBroadException
86
+ try :
87
+ self .emitter .emit_batch ([(record , self .format (record )) for record in records ])
88
+ except Exception :
89
+ for record in records :
90
+ self .handleError (record )
91
+
92
+ class LokiBatchHandler (MemoryHandler ):
93
+ interval : float # The interval at which batched logs are sent in seconds
94
+ _last_flush_time : float
95
+ target : LokiHandler
96
+
97
+ def __init__ (self , interval : float , capacity : int = LOKI_MAX_BATCH_BUFFER_SIZE , ** kwargs ):
98
+ super ().__init__ (capacity , ** kwargs )
99
+ self .interval = interval
100
+ self ._last_flush_time = time .time ()
101
+
102
+ def flush (self ) -> None :
103
+ self .acquire ()
104
+ try :
105
+ if self .target and self .buffer :
106
+ self .target .emit_batch (self .buffer )
107
+ self .buffer .clear ()
108
+ finally :
109
+ self ._last_flush_time = time .time ()
110
+ self .release ()
111
+
112
+ def shouldFlush (self , record : logging .LogRecord ) -> bool :
113
+ return (
114
+ super ().shouldFlush (record ) or
115
+ (time .time () - self ._last_flush_time >= self .interval )
116
+ )
0 commit comments