├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pygelf ├── __init__.py ├── gelf.py └── handlers.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── config │ ├── cert.pem │ ├── docker-compose.yml │ ├── gencert.sh │ ├── graylog-setup.sh │ ├── key.pem │ └── requirements.txt ├── helper.py ├── test_common_fields.py ├── test_core_functions.py ├── test_debug_mode.py ├── test_dynamic_fields.py ├── test_handler_specific.py ├── test_queuehandler_support.py └── test_static_fields.py └── tox.ini /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-22.04 8 | strategy: 9 | matrix: 10 | python-version: 11 | - '3.9' 12 | - '3.10' 13 | - '3.11' 14 | - 'pypy3.9' 15 | - 'pypy3.10' 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install Python dependencies 22 | run: | 23 | pip install -r tests/config/requirements.txt 24 | pip install -e . 25 | - name: Set up Graylog 26 | run: | 27 | docker compose -f tests/config/docker-compose.yml up -d 28 | sleep 60 29 | ./tests/config/graylog-setup.sh 30 | - name: Run tests 31 | run: pytest -v --cov=pygelf 32 | - uses: coverallsapp/github-action@v2 33 | with: 34 | flag-name: ${{ matrix.python-version }} 35 | parallel: true 36 | pylint: 37 | runs-on: ubuntu-22.04 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Install Python dependencies 41 | run: pip install -r tests/config/requirements.txt 42 | - name: Run pylint 43 | run: pylint pygelf 44 | coverage: 45 | needs: 46 | - tests 47 | runs-on: ubuntu-22.04 48 | steps: 49 | - uses: coverallsapp/github-action@v2 50 | with: 51 | parallel-finished: true 52 | fail-on-error: false 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | venv/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [DESIGN] 2 | max-args=7 3 | min-public-methods=1 4 | 5 | [BASIC] 6 | good-names=e,_ 7 | 8 | [FORMAT] 9 | max-line-length=120 10 | single-line-if-stmt=no 11 | indent-string=' ' 12 | indent-after-paren=4 13 | expected-line-ending-format=LF 14 | 15 | [MESSAGES CONTROL] 16 | disable=C0111,too-many-arguments,too-many-instance-attributes,deprecated-method 17 | 18 | [REPORTS] 19 | reports=no -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ivan Mukhin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include tox.ini 4 | recursive-include tests *.py 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pygelf 2 | ====== 3 | 4 | .. image:: https://github.com/keeprocking/pygelf/actions/workflows/tests.yml/badge.svg?branch=master 5 | :target: https://github.com/keeprocking/pygelf/actions 6 | .. image:: https://coveralls.io/repos/github/keeprocking/pygelf/badge.svg?branch=master 7 | :target: https://coveralls.io/github/keeprocking/pygelf?branch=master 8 | .. image:: https://badge.fury.io/py/pygelf.svg 9 | :target: https://pypi.python.org/pypi/pygelf 10 | .. image:: https://img.shields.io/pypi/dm/pygelf 11 | :target: https://pypi.python.org/pypi/pygelf 12 | 13 | Python logging handlers with GELF (Graylog Extended Log Format) support. 14 | 15 | Currently TCP, UDP, TLS (encrypted TCP) and HTTP logging handlers are supported. 16 | 17 | Get pygelf 18 | ========== 19 | .. code:: python 20 | 21 | pip install pygelf 22 | 23 | Usage 24 | ===== 25 | 26 | .. code:: python 27 | 28 | from pygelf import GelfTcpHandler, GelfUdpHandler, GelfTlsHandler, GelfHttpHandler, GelfHttpsHandler 29 | import logging 30 | 31 | 32 | logging.basicConfig(level=logging.INFO) 33 | logger = logging.getLogger() 34 | logger.addHandler(GelfTcpHandler(host='127.0.0.1', port=9401)) 35 | logger.addHandler(GelfUdpHandler(host='127.0.0.1', port=9402)) 36 | logger.addHandler(GelfTlsHandler(host='127.0.0.1', port=9403)) 37 | logger.addHandler(GelfHttpHandler(host='127.0.0.1', port=9404)) 38 | logger.addHandler(GelfHttpsHandler(host='127.0.0.1', port=9405)) 39 | 40 | logger.info('hello gelf') 41 | 42 | Message structure 43 | ================= 44 | 45 | According to the GELF spec, each message has the following mandatory fields: 46 | 47 | - **version**: '1.1', can be overridden when creating a logger 48 | - **short_message**: the log message itself 49 | - **timestamp**: current timestamp 50 | - **level**: syslog-compliant_ log level number (e.g. WARNING will be sent as 4) 51 | - **host**: hostname of the machine that sent the message 52 | - **full_message**: this field contains stack trace and is being written **ONLY** when logging an exception, e.g. 53 | 54 | .. code:: python 55 | 56 | try: 57 | 1/0 58 | except ZeroDivisionError as e: 59 | logger.exception(e) 60 | 61 | .. _syslog-compliant: https://en.wikipedia.org/wiki/Syslog#Severity_level 62 | 63 | In debug mode (when handler was created with debug=True option) each message contains some extra fields (which are pretty self-explanatory): 64 | 65 | - **_file** 66 | - **_line** 67 | - **_module** 68 | - **_func** 69 | - **_logger_name** 70 | 71 | Configuration 72 | ============= 73 | 74 | Each handler has the following parameters: 75 | 76 | - **host**: IP address of the GELF input 77 | - **port**: port of the GELF input 78 | - **debug** (False by default): if true, each log message will include debugging info: module name, file name, line number, method name 79 | - **version** ('1.1' by default): GELF protocol version, can be overridden 80 | - **include_extra_fields** (False by default): if true, each log message will include all the extra fields set to LogRecord 81 | - **json_default** (:code:`str` with exception for several :code:`datetime` objects): function that is called for objects that cannot be serialized to JSON natively by python. Default implementation is custom function that returns result of :code:`isoformat()` method for :code:`datetime.datetime`, :code:`datetime.time`, :code:`datetime.date` objects and result of :code:`str(obj)` call for other objects (which is string representation of an object with fallback to :code:`repr`) 82 | 83 | Also, there are some handler-specific parameters. 84 | 85 | UDP: 86 | 87 | - **chunk\_size** (1300 by default) - maximum length of the message. If log length exceeds this value, it splits into multiple chunks (see https://www.graylog.org/resources/gelf/ section "chunked GELF") with the length equals to this value. This parameter must be less than the MTU_. If the logs don't seem to be delivered, try to reduce this value. 88 | - **compress** (True by default) - if true, compress log messages before sending them to the server 89 | 90 | .. _MTU: https://en.wikipedia.org/wiki/Maximum_transmission_unit 91 | 92 | TLS: 93 | 94 | - **validate** (False by default) - if true, validate server certificate. If server provides a certificate that doesn't exist in **ca_certs**, you won't be able to send logs over TLS 95 | - **ca_certs** (None by default) - path to CA bundle file. This parameter is required if **validate** is true. 96 | - **certfile** (None by default) - path to certificate file that will be used to identify ourselves to the remote endpoint. This is necessary when the remote server has client authentication required. If **certfile** contains the private key, it should be placed before the certificate. 97 | - **keyfile** (None by default) - path to the private key. If the private key is stored in **certfile** this parameter can be None. 98 | 99 | HTTP: 100 | 101 | - **compress** (True by default) - if true, compress log messages before sending them to the server 102 | - **path** ('/gelf' by default) - path of the HTTP input (http://docs.graylog.org/en/latest/pages/sending_data.html#gelf-via-http) 103 | - **timeout** (5 by default) - amount of seconds that HTTP client should wait before it discards the request if the server doesn't respond 104 | 105 | HTTPS: 106 | 107 | - **compress** (True by default) - if true, compress log messages before sending them to the server 108 | - **path** ('/gelf' by default) - path of the HTTP input (http://docs.graylog.org/en/latest/pages/sending_data.html#gelf-via-http) 109 | - **timeout** (5 by default) - amount of seconds that HTTP client should wait before it discards the request if the server doesn't respond 110 | - **validate** - whether or not to validate the input's certificate 111 | - **ca_certs** - path to the CA certificate file that signed the certificate the input is using 112 | - **certfile** - not yet used 113 | - **keyfile** - not yet used 114 | - **keyfile_password** - not yet used 115 | 116 | Static fields 117 | ============= 118 | 119 | If you need to include some static fields into your logs, simply pass them to the handler constructor. Each additional field should start with underscore. You can't add field '\_id'. 120 | 121 | Example: 122 | 123 | .. code:: python 124 | 125 | handler = GelfUdpHandler(host='127.0.0.1', port=9402, _app_name='pygelf', _something=11) 126 | logger.addHandler(handler) 127 | 128 | Dynamic fields 129 | ============== 130 | 131 | If you need to include some dynamic fields into your logs, add them to record by using LoggingAdapter or logging.Filter and create handler with include_extra_fields set to True. 132 | All the non-trivial fields of the record will be sent to graylog2 with '\_' added before the name 133 | 134 | Example: 135 | 136 | .. code:: python 137 | 138 | class ContextFilter(logging.Filter): 139 | 140 | def filter(self, record): 141 | record.job_id = threading.local().process_id 142 | return True 143 | 144 | logger.addFilter(ContextFilter()) 145 | handler = GelfUdpHandler(host='127.0.0.1', port=9402, include_extra_fields=True) 146 | logger.addHandler(handler) 147 | 148 | Defining fields from environment 149 | ================================ 150 | 151 | If you need to include some fields from the environment into your logs, add them to record by using `additional_env_fields`. 152 | 153 | The following example will add an `env` field to the logs, taking its value from the environment variable `FLASK_ENV`. 154 | 155 | .. code:: python 156 | 157 | handler = GelfTcpHandler(host='127.0.0.1', port=9402, include_extra_fields=True, additional_env_fields={'env': 'FLASK_ENV'}) 158 | logger.addHandler(handler) 159 | 160 | The following can also be used in defining logging from configuration files (yaml/ini): 161 | 162 | .. code:: ini 163 | 164 | [formatters] 165 | keys=standard 166 | 167 | [formatter_standard] 168 | class=logging.Formatter 169 | format=%(message)s 170 | 171 | [handlers] 172 | keys=graylog 173 | 174 | [handler_graylog] 175 | class=pygelf.GelfTcpHandler 176 | formatter=standard 177 | args=('127.0.0.1', '12201') 178 | kwargs={'include_extra_fields': True, 'debug': True, 'additional_env_fields': {'env': 'FLASK_ENV'}} 179 | 180 | [loggers] 181 | keys=root 182 | 183 | [logger_root] 184 | level=WARN 185 | handlers=graylog 186 | -------------------------------------------------------------------------------- /pygelf/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import GelfTcpHandler, GelfUdpHandler, GelfTlsHandler, GelfHttpHandler, GelfHttpsHandler 2 | -------------------------------------------------------------------------------- /pygelf/gelf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import json 4 | import zlib 5 | import os 6 | import struct 7 | import traceback 8 | 9 | 10 | LEVELS = { 11 | logging.DEBUG: 7, 12 | logging.INFO: 6, 13 | logging.WARNING: 4, 14 | logging.ERROR: 3, 15 | logging.CRITICAL: 2 16 | } 17 | 18 | 19 | # skip_list is used to filter additional fields in a log message. 20 | # It contains all attributes listed in 21 | # http://docs.python.org/library/logging.html#logrecord-attributes 22 | # plus exc_text, which is only found in the logging module source, 23 | # and id, which is prohibited by the GELF format. 24 | SKIP_LIST = ( 25 | 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename', 26 | 'funcName', 'id', 'levelname', 'levelno', 'lineno', 'module', 27 | 'msecs', 'message', 'msg', 'name', 'pathname', 'process', 28 | 'processName', 'relativeCreated', 'thread', 'threadName' 29 | ) 30 | 31 | 32 | def make(record, domain, debug, version, additional_fields, additional_env_fields, include_extra_fields=False): 33 | gelf = { 34 | 'version': version, 35 | 'short_message': record.getMessage(), 36 | 'timestamp': record.created, 37 | 'level': LEVELS[record.levelno], 38 | 'host': domain 39 | } 40 | 41 | if record.exc_info: 42 | gelf['full_message'] = '\n'.join(traceback.format_exception(*record.exc_info)) 43 | elif record.exc_text is not None: 44 | # QueueHandler, if used, formats the record, so that exc_info will always be empty: 45 | # https://docs.python.org/3/library/logging.handlers.html#logging.handlers.QueueHandler 46 | gelf['full_message'] = record.exc_text 47 | 48 | if debug: 49 | gelf['_file'] = record.filename 50 | gelf['_line'] = record.lineno 51 | gelf['_module'] = record.module 52 | gelf['_func'] = record.funcName 53 | gelf['_logger_name'] = record.name 54 | 55 | if additional_fields is not None: 56 | gelf.update(additional_fields) 57 | 58 | if additional_env_fields is not None: 59 | appended = {} 60 | for name, env in additional_env_fields.items(): 61 | if env in os.environ: 62 | appended["_" + name] = os.environ.get(env) 63 | 64 | gelf.update(appended) 65 | 66 | if include_extra_fields: 67 | add_extra_fields(gelf, record) 68 | 69 | return gelf 70 | 71 | 72 | def add_extra_fields(gelf, record): 73 | for key, value in record.__dict__.items(): 74 | if key not in SKIP_LIST and not key.startswith('_'): 75 | gelf[f'_{key}'] = value 76 | 77 | 78 | def object_to_json(obj): 79 | """Convert object that cannot be natively serialized by python to JSON representation.""" 80 | if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)): 81 | return obj.isoformat() 82 | return str(obj) 83 | 84 | 85 | def pack(gelf, compress, default): 86 | packed = json.dumps(gelf, separators=(',', ':'), default=default).encode('utf-8') 87 | return zlib.compress(packed) if compress else packed 88 | 89 | 90 | def split(gelf, chunk_size): 91 | header = b'\x1e\x0f' 92 | message_id = os.urandom(8) 93 | chunks = [gelf[pos:pos + chunk_size] for pos in range(0, len(gelf), chunk_size)] 94 | number_of_chunks = len(chunks) 95 | 96 | for chunk_index, chunk in enumerate(chunks): 97 | yield b''.join(( 98 | header, 99 | message_id, 100 | struct.pack('b', chunk_index), 101 | struct.pack('b', number_of_chunks), 102 | chunk 103 | )) 104 | -------------------------------------------------------------------------------- /pygelf/handlers.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import socket 3 | 4 | try: 5 | import httplib 6 | except ImportError: 7 | import http.client as httplib 8 | 9 | from logging.handlers import SocketHandler, DatagramHandler 10 | from logging import Handler as LoggingHandler 11 | from pygelf import gelf 12 | 13 | 14 | class BaseHandler: 15 | def __init__(self, debug=False, version='1.1', include_extra_fields=False, compress=False, 16 | static_fields=None, json_default=gelf.object_to_json, additional_env_fields=None, **kwargs): 17 | """ 18 | Logging handler that transforms each record into GELF (graylog extended log format) and sends it over TCP. 19 | 20 | :param debug: include debug fields, e.g. line number, or not 21 | :param include_extra_fields: include non-default fields from record to message, or not 22 | :param json_default: function that is called for objects that cannot be serialized to JSON natively by python 23 | :param kwargs: additional fields that will be included in the log message, e.g. application name. 24 | Each additional field should start with underscore, e.g. _app_name 25 | """ 26 | 27 | self.additional_env_fields = additional_env_fields 28 | if self.additional_env_fields is None: 29 | self.additional_env_fields = {} 30 | 31 | self.debug = debug 32 | self.version = version 33 | self.additional_fields = static_fields if static_fields else kwargs 34 | self.include_extra_fields = include_extra_fields 35 | self.additional_fields.pop('_id', None) 36 | self.domain = socket.gethostname() 37 | self.compress = compress 38 | self.json_default = json_default 39 | 40 | def convert_record_to_gelf(self, record): 41 | return gelf.pack( 42 | gelf.make(record, self.domain, self.debug, self.version, self.additional_fields, 43 | self.additional_env_fields, self.include_extra_fields), 44 | self.compress, self.json_default 45 | ) 46 | 47 | 48 | class GelfTcpHandler(BaseHandler, SocketHandler): 49 | 50 | def __init__(self, host, port, **kwargs): 51 | """ 52 | Logging handler that transforms each record into GELF (graylog extended log format) and sends it over TCP. 53 | 54 | :param host: GELF TCP input host 55 | :param port: GELF TCP input port 56 | """ 57 | 58 | SocketHandler.__init__(self, host, port) 59 | BaseHandler.__init__(self, **kwargs) 60 | 61 | def makePickle(self, record): 62 | """ if you send the message over tcp, it should always be null terminated or the input will reject it """ 63 | return self.convert_record_to_gelf(record) + b'\x00' 64 | 65 | 66 | class GelfUdpHandler(BaseHandler, DatagramHandler): 67 | 68 | def __init__(self, host, port, compress=True, chunk_size=1300, **kwargs): 69 | """ 70 | Logging handler that transforms each record into GELF (graylog extended log format) and sends it over UDP. 71 | If message length exceeds chunk_size, the message splits into multiple chunks. 72 | The number of chunks must be less than 128. 73 | 74 | :param host: GELF UDP input host 75 | :param port: GELF UDP input port 76 | :param compress: compress message before sending it to the server or not 77 | :param chunk_size: length of a chunk, should be less than the MTU (maximum transmission unit) 78 | """ 79 | 80 | DatagramHandler.__init__(self, host, port) 81 | BaseHandler.__init__(self, compress=compress, **kwargs) 82 | 83 | self.chunk_size = chunk_size 84 | 85 | def send(self, s): 86 | if len(s) <= self.chunk_size: 87 | DatagramHandler.send(self, s) 88 | return 89 | 90 | chunks = gelf.split(s, self.chunk_size) 91 | for chunk in chunks: 92 | DatagramHandler.send(self, chunk) 93 | 94 | def makePickle(self, record): 95 | return self.convert_record_to_gelf(record) 96 | 97 | 98 | class GelfTlsHandler(GelfTcpHandler): 99 | 100 | def __init__(self, validate=False, ca_certs=None, certfile=None, keyfile=None, **kwargs): 101 | """ 102 | TCP GELF logging handler with TLS support 103 | 104 | :param validate: if true, validate server certificate. In that case ca_certs are required 105 | :param ca_certs: path to CA bundle file. For instance, on CentOS it would be '/etc/pki/tls/certs/ca-bundle.crt' 106 | :param certfile: path to the certificate file that is used to identify ourselves to the server 107 | :param keyfile: path to the private key. If the private key is stored with the certificate, 108 | this parameter can be ignored 109 | """ 110 | 111 | if validate and ca_certs is None: 112 | raise ValueError('CA bundle file path must be specified') 113 | 114 | if keyfile is not None and certfile is None: 115 | raise ValueError('certfile must be specified') 116 | 117 | GelfTcpHandler.__init__(self, **kwargs) 118 | 119 | self.ca_certs = ca_certs 120 | self.reqs = ssl.CERT_REQUIRED if validate else ssl.CERT_NONE 121 | self.certfile = certfile 122 | self.keyfile = keyfile if keyfile else certfile 123 | 124 | def makeSocket(self, timeout=1): 125 | plain_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 126 | 127 | if hasattr(plain_socket, 'settimeout'): 128 | plain_socket.settimeout(timeout) 129 | 130 | wrapped_socket = ssl.wrap_socket(plain_socket, ca_certs=self.ca_certs, cert_reqs=self.reqs, 131 | keyfile=self.keyfile, certfile=self.certfile) 132 | wrapped_socket.connect((self.host, self.port)) 133 | 134 | return wrapped_socket 135 | 136 | 137 | class GelfHttpHandler(BaseHandler, LoggingHandler): 138 | 139 | def __init__(self, host, port, compress=True, path='/gelf', timeout=5, **kwargs): 140 | """ 141 | Logging handler that transforms each record into GELF (graylog extended log format) and sends it over HTTP. 142 | 143 | :param host: GELF HTTP input host 144 | :param port: GELF HTTP input port 145 | :param compress: compress message before sending it to the server or not 146 | :param path: path of the HTTP input (http://docs.graylog.org/en/latest/pages/sending_data.html#gelf-via-http) 147 | :param timeout: amount of seconds that HTTP client should wait before it discards the request 148 | if the server doesn't respond 149 | """ 150 | 151 | LoggingHandler.__init__(self) 152 | BaseHandler.__init__(self, compress=compress, **kwargs) 153 | 154 | self.host = host 155 | self.port = port 156 | self.path = path 157 | self.timeout = timeout 158 | self.headers = {} 159 | 160 | if compress: 161 | self.headers['Content-Encoding'] = 'gzip,deflate' 162 | 163 | def emit(self, record): 164 | data = self.convert_record_to_gelf(record) 165 | connection = httplib.HTTPConnection(host=self.host, port=self.port, timeout=self.timeout) 166 | connection.request('POST', self.path, data, self.headers) 167 | 168 | 169 | class GelfHttpsHandler(BaseHandler, LoggingHandler): 170 | 171 | def __init__(self, host, port, compress=True, path='/gelf', timeout=5, validate=False, 172 | ca_certs=None, certfile=None, keyfile=None, keyfile_password=None, **kwargs): 173 | """ 174 | Logging handler that transforms each record into GELF (graylog extended log format) and sends it over HTTP. 175 | 176 | :param host: GELF HTTP input host 177 | :param port: GELF HTTP input port 178 | :param compress: compress message before sending it to the server or not 179 | :param path: path of the HTTP input (http://docs.graylog.org/en/latest/pages/sending_data.html#gelf-via-http) 180 | :param timeout: amount of seconds that HTTP client should wait before it discards the request 181 | if the server doesn't respond 182 | :param validate: whether or not to validate the input's certificate 183 | :param ca_certs: path to the CA certificate file that signed the certificate the input is using 184 | :param certfile: not yet used 185 | :param keyfile: not yet used 186 | :param keyfile_password: not yet used 187 | """ 188 | 189 | LoggingHandler.__init__(self) 190 | BaseHandler.__init__(self, compress=compress, **kwargs) 191 | 192 | self.host = host 193 | self.port = port 194 | self.path = path 195 | self.timeout = timeout 196 | self.headers = {} 197 | self.ca_certs = ca_certs 198 | self.keyfile = keyfile 199 | self.certfile = certfile 200 | self.keyfile_password = keyfile_password 201 | 202 | # Set up context: https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection 203 | # create_default_context returns an SSLContext object 204 | self.ctx = ssl.create_default_context() 205 | 206 | if validate and ca_certs is None: 207 | raise ValueError('CA bundle file path must be specified') 208 | 209 | if not validate: 210 | self.ctx.check_hostname = False 211 | self.ctx.verify_mode = ssl.CERT_NONE 212 | else: 213 | # Load our CA file 214 | self.ctx.load_verify_locations(cafile=self.ca_certs) 215 | 216 | if compress: 217 | self.headers['Content-Encoding'] = 'gzip,deflate' 218 | 219 | def emit(self, record): 220 | data = self.convert_record_to_gelf(record) 221 | connection = httplib.HTTPSConnection(host=self.host, port=self.port, context=self.ctx, timeout=self.timeout) 222 | connection.request('POST', self.path, data, self.headers) 223 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max-line-length=120 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='pygelf', 6 | version='0.4.2', 7 | packages=['pygelf'], 8 | description='Logging handlers with GELF support', 9 | keywords='logging udp tcp ssl tls graylog2 graylog gelf', 10 | author='Ivan Mukhin', 11 | author_email='muhin.ivan@gmail.com', 12 | url='https://github.com/keeprocking/pygelf', 13 | long_description=open('README.rst').read(), 14 | license='MIT', 15 | classifiers=[ 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python', 20 | 'Programming Language :: Python :: 3.9', 21 | 'Programming Language :: Python :: 3.10', 22 | 'Programming Language :: Python :: 3.11', 23 | 'Programming Language :: Python :: Implementation :: CPython', 24 | 'Programming Language :: Python :: Implementation :: PyPy', 25 | 'Topic :: System :: Logging', 26 | 'Topic :: Software Development :: Libraries :: Python Modules' 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keeprocking/pygelf/11a2b5d27c50372692d33d1f72045835578731b2/tests/__init__.py -------------------------------------------------------------------------------- /tests/config/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID5zCCAs+gAwIBAgIUTc9rrPVUCfREv9XgpEXPjzJI1eAwDQYJKoZIhvcNAQEL 3 | BQAwgYIxCzAJBgNVBAYTAi0tMQswCQYDVQQIDAJDTzEQMA4GA1UEBwwHQm91bGRl 4 | cjEPMA0GA1UECgwGcHlnZWxmMQ8wDQYDVQQLDAZweWdlbGYxEjAQBgNVBAMMCWxv 5 | Y2FsaG9zdDEeMBwGCSqGSIb3DQEJARYPaGVsbG9Ad29ybGQuY29tMB4XDTI0MDcw 6 | OTExMDg1M1oXDTM0MDcwNzExMDg1M1owgYIxCzAJBgNVBAYTAi0tMQswCQYDVQQI 7 | DAJDTzEQMA4GA1UEBwwHQm91bGRlcjEPMA0GA1UECgwGcHlnZWxmMQ8wDQYDVQQL 8 | DAZweWdlbGYxEjAQBgNVBAMMCWxvY2FsaG9zdDEeMBwGCSqGSIb3DQEJARYPaGVs 9 | bG9Ad29ybGQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA24eV 10 | veQMALAQei1ogFnxIz7dLUcDdzsbTRuu07cLtk/D0ODr49QTCEmkNuLZmpzNtrVK 11 | V6wX6xeci4nrEzJJ0DMQ2f+HI6IS7EiVDxivpu7RU82BknCY3zh+e/iu7CMfjZm4 12 | +PWVc4uPJGayn1SwXN66o8vB8xXa9VEgliG0HwWJ21a61vfxJuieYUeRHQds81Xq 13 | nJlLj6mZp917/GzJnERp8suYaiQuijbZqMfrGR0RdfGHuzkqMsbFOjAUQn9yMc3e 14 | oBoioxzhBQ06oX7p09tN8RhQGq5kpvieCu7vtoZTBc89Otvx23MBFJiYxjaryFJq 15 | 8fJmATGl08WK1CbjlwIDAQABo1MwUTAdBgNVHQ4EFgQU+3i3H9+nU/JJ0AhYcAj2 16 | e4l19V4wHwYDVR0jBBgwFoAU+3i3H9+nU/JJ0AhYcAj2e4l19V4wDwYDVR0TAQH/ 17 | BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAZKBxcIVQT2P5idtcasXKiae4bimD 18 | XfpxoFYvreJ631z+lSUWPoOmQOf2mseAn0J7uC7PfUz6xScDG1I4xTfUHxlhxWBz 19 | eLVjyO5rpUfqJqXTvws1QrmMQZo9XH/4c8OAnz8IebmyCAlhmxGel6TtHWbrOcyM 20 | XlrbzrtWrBjXQPJuvtyidzix6WlwmbxOccwtGiRJGkLUr8N/Qyb5I+/+qQYsH0/6 21 | V5PCIa5ez+ZksaamEMfxyLftnZQI5n7M01k/meQW5m5SPAvdkG/4DnkhQS5dBBeY 22 | DMvL9dYAO5gBLHPryxMi96QprtxPpZfhm6SoEPz6Uj8TQa4Wv9dFPuZUTA== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /tests/config/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mongo: 3 | image: "mongo:3.3" 4 | elasticsearch: 5 | image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.2 6 | environment: 7 | - http.host=0.0.0.0 8 | - transport.host=localhost 9 | - network.host=0.0.0.0 10 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 11 | graylog: 12 | image: "graylog/graylog:3.3" 13 | environment: 14 | - GRAYLOG_PASSWORD_SECRET=CVanHILkuYhsxE50BrNR6FFt75rS3h0V2uUlHxAshGB90guZznEoDxN7zhPx6Bcn61mfhY2T5r0PRkZVwowsTkHU2rBZnv0d 15 | - GRAYLOG_ROOT_PASSWORD_SHA2=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 16 | - GRAYLOG_HTTP_EXTERNAL_URI=http://127.0.0.1:9000/ 17 | volumes: 18 | - ./cert.pem:/usr/share/graylog/data/cert.pem 19 | - ./key.pem:/usr/share/graylog/data/key.pem 20 | links: 21 | - mongo 22 | - elasticsearch 23 | depends_on: 24 | - mongo 25 | - elasticsearch 26 | ports: 27 | - "9000:9000" 28 | - "12201:12201/tcp" 29 | - "12202:12202/udp" 30 | - "12203:12203" 31 | - "12204:12204/tcp" 32 | - "12205:12205/tcp" 33 | -------------------------------------------------------------------------------- /tests/config/gencert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | umask 077 4 | 5 | answers() { 6 | echo -- 7 | echo CO 8 | echo Boulder 9 | echo pygelf 10 | echo pygelf 11 | echo localhost 12 | echo hello@world.com 13 | } 14 | 15 | PEM1=`/bin/mktemp /tmp/openssl.XXXXXX` 16 | PEM2=`/bin/mktemp /tmp/openssl.XXXXXX` 17 | trap "rm -f $PEM1 $PEM2" SIGINT 18 | answers | openssl req -newkey rsa:2048 -keyout $PEM1 -nodes -x509 -days 3650 -out $PEM2 2> /dev/null 19 | cat $PEM1 > key.pem 20 | cat $PEM2 > cert.pem 21 | rm -f $PEM1 $PEM2 22 | -------------------------------------------------------------------------------- /tests/config/graylog-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | API_URL="http://localhost:9000/api" 4 | INPUTS_URL="$API_URL/system/inputs" 5 | 6 | TCP_INPUT='{ 7 | "title": "tcp", 8 | "configuration": { 9 | "bind_address": "0.0.0.0", 10 | "port": 12201 11 | }, 12 | "type": "org.graylog2.inputs.gelf.tcp.GELFTCPInput", 13 | "global": true 14 | }' 15 | 16 | UDP_INPUT='{ 17 | "title": "udp", 18 | "configuration": { 19 | "bind_address": "0.0.0.0", 20 | "port": 12202 21 | }, 22 | "type": "org.graylog2.inputs.gelf.udp.GELFUDPInput", 23 | "global": true 24 | }' 25 | 26 | HTTP_INPUT='{ 27 | "title": "http", 28 | "configuration": { 29 | "bind_address": "0.0.0.0", 30 | "port": 12203 31 | }, 32 | "type": "org.graylog2.inputs.gelf.http.GELFHttpInput", 33 | "global": true 34 | }' 35 | 36 | TLS_INPUT='{ 37 | "title": "tls", 38 | "configuration": { 39 | "bind_address": "0.0.0.0", 40 | "port": 12204, 41 | "tls_enable": true, 42 | "tls_cert_file": "/usr/share/graylog/data/cert.pem", 43 | "tls_key_file": "/usr/share/graylog/data/key.pem" 44 | }, 45 | "type": "org.graylog2.inputs.gelf.tcp.GELFTCPInput", 46 | "global": true 47 | }' 48 | 49 | HTTPS_INPUT='{ 50 | "title": "https", 51 | "configuration": { 52 | "bind_address": "0.0.0.0", 53 | "port": 12205, 54 | "tls_cert_file": "/usr/share/graylog/data/cert.pem", 55 | "tls_client_auth": "disabled", 56 | "tls_client_auth_cert_file": "", 57 | "tls_enable": true, 58 | "tls_key_file": "/usr/share/graylog/data/key.pem" 59 | }, 60 | "type": "org.graylog2.inputs.gelf.http.GELFHttpInput", 61 | "global": true 62 | }' 63 | 64 | curl -u admin:admin "$API_URL/search/universal/relative?query=test&range=5&fields=message" > /dev/null 65 | 66 | curl -u admin:admin -X "POST" -H "Content-Type: application/json" -H "X-Requested-By: cli" -d "${TCP_INPUT}" "$INPUTS_URL" 67 | curl -u admin:admin -X "POST" -H "Content-Type: application/json" -H "X-Requested-By: cli" -d "${UDP_INPUT}" "$INPUTS_URL" 68 | curl -u admin:admin -X "POST" -H "Content-Type: application/json" -H "X-Requested-By: cli" -d "${HTTP_INPUT}" "$INPUTS_URL" 69 | curl -u admin:admin -X "POST" -H "Content-Type: application/json" -H "X-Requested-By: cli" -d "${TLS_INPUT}" "$INPUTS_URL" 70 | curl -u admin:admin -X "POST" -H "Content-Type: application/json" -H "X-Requested-By: cli" -d "${HTTPS_INPUT}" "$INPUTS_URL" 71 | 72 | sleep 10 73 | 74 | # Due to some reason graylog seems to just ignore a couple of first incoming messages. 75 | # Without this workaround one or two tests from the whole test suite will always fail. 76 | for _ in {1..5}; do 77 | curl -X "POST" -H "Content-Type: application/json" "http://localhost:12203/gelf" -p0 -d '{"short_message": "warm-up", "host": "localhost"}' 78 | sleep 1 79 | curl -k -X "POST" -H "Content-Type: application/json" "https://localhost:12205/gelf" -p0 -d '{"short_message": "warm-up", "host": "localhost"}' 80 | sleep 1 81 | done 82 | 83 | docker exec -u 0 $(docker ps |grep graylog | awk '{print $1}') chown graylog data/key.pem 84 | docker exec -u 0 $(docker ps |grep graylog | awk '{print $1}') chmod 0600 data/key.pem -------------------------------------------------------------------------------- /tests/config/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDbh5W95AwAsBB6 3 | LWiAWfEjPt0tRwN3OxtNG67Ttwu2T8PQ4Ovj1BMISaQ24tmanM22tUpXrBfrF5yL 4 | iesTMknQMxDZ/4cjohLsSJUPGK+m7tFTzYGScJjfOH57+K7sIx+Nmbj49ZVzi48k 5 | ZrKfVLBc3rqjy8HzFdr1USCWIbQfBYnbVrrW9/Em6J5hR5EdB2zzVeqcmUuPqZmn 6 | 3Xv8bMmcRGnyy5hqJC6KNtmox+sZHRF18Ye7OSoyxsU6MBRCf3Ixzd6gGiKjHOEF 7 | DTqhfunT203xGFAarmSm+J4K7u+2hlMFzz062/HbcwEUmJjGNqvIUmrx8mYBMaXT 8 | xYrUJuOXAgMBAAECggEAEHLfyusf5RtMGMN3PYq/sSrgKqjT/yaMHKJy3oUsGPxw 9 | CSIZOqhkxq2x4rTrphSSq1p5GAmafzB1N9VdKkKN2k5BOHxXdVmK/m5P0OJCIwlo 10 | 4NUYnM7X/X2/qG1ROftFvXfjrq/EA3TVBF63io2vaz37ERPO0/ZywgfSn70lwF8n 11 | x+Fnq8svmpnvLJeFYYxrAVFu6tMicQ5qwpNYDBnR4iltJPMRbXyGmuTDPTyiwvNy 12 | MMUO72YJhSF+jNbZ1rBggu5sWizeVng7x1ZomZ8VGNf4inxDtP1gIro6OgCgyziw 13 | 9oJXG1VxKwUBvZZnJpMtQbcYpzL6UFxtROXGdMKNiQKBgQD7LXxZPvGCTMXRTyMm 14 | plfvi4TVVNU64TVYJ3v7Tutxbzspk47guq1bs8GLTjGHMdrpvWU/dAsXywvmPgUq 15 | 0/Xu85oxD4pydW2y6RzQADsv5GT39V4U7I6CpLNp1FSpL+Ax0sFizw/r2spbTTwX 16 | BFxaSoZvAudKjzlfsTvJGZJXGQKBgQDfvo1TYJAYlTAUpftWGhuBzGkyC13sIEtX 17 | RHlzkbL53jHSOJhtGyXNyPZu2+Jf28c+aeRgcauCMjA9TXl3n3qeQgl8yfl3Ozw2 18 | p8Ak+nsh6zxlVrxv0kmijrK8+QPNvcEmsGR+FYpB4SlFy/EMRrkk8v/3v2mzwrzV 19 | 2WSX6orWLwKBgFbENLNjoevf92nBi6P7TF8sc/t5rZsEVvgX06VRctWhle9b0Lxi 20 | 4CXQZ6hmn4dTOosJ6OLNhFN4yaiEfiZ4R/l+XyJZevrlA7sM+e4EVm3J2PMq6JAT 21 | 03rfyDZjqcc0CZ9MbY9jdd2Em+iEOqC82eY63kuU1i8UYL5krAFg+wNpAoGBAMr+ 22 | c/QYYMOc3wFs0cn9Z8Vscmhv/aeUaSZkvpfGsY2XNLZYmJosjjDUyhgsMIbtvCf5 23 | JLUHjCZUaIXFu5V3QGVC3p60FDxDps6jhWVHR92vMZ1zgwUk0Z/FPY7LkdLg/NOg 24 | J5xo6IX6xVpIvIp5w9ItEWRx6nGoFXEjiet2ZL4LAoGAUeCSnPSqw0CTenJvB4mB 25 | ETgc2MOAEqi4ipUhZNKutbA0XK+FTGUY3EMgZ5sEd/v6kbba7Sc7kdYn0nYV0nt7 26 | 8QAOr4FpfooEuSUvxhAQ9LOM71DNoQd007OvZGjZcY3NQVf9AjZVPf4WxGMFj/0d 27 | GZDmB/1bIM6+W9gxrCl7yT8= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/config/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | mock 3 | pytest-cov 4 | coveralls 5 | pylint 6 | -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import time 3 | import logging 4 | import pytest 5 | import requests 6 | 7 | 8 | @pytest.fixture 9 | def logger(handler): 10 | logger = logging.getLogger('test') 11 | logger.addHandler(handler) 12 | yield logger 13 | logger.removeHandler(handler) 14 | 15 | 16 | def log_warning(logger, message, args=None, fields=None): 17 | args = args if args else [] 18 | fields = fields if fields else [] 19 | logger.warning(message, *args) 20 | api_response = _get_api_response(message % args, fields) 21 | return _parse_api_response(api_response) 22 | 23 | 24 | def log_exception(logger, message, exception, fields=None): 25 | fields = fields if fields else [] 26 | logger.exception(exception) 27 | api_response = _get_api_response(message, fields) 28 | return _parse_api_response(api_response) 29 | 30 | 31 | def get_unique_message(): 32 | return str(uuid.uuid4()) 33 | 34 | 35 | DEFAULT_FIELDS = [ 36 | 'message', 'full_message', 'source', 'level', 37 | 'func', 'file', 'line', 'module', 'logger_name', 38 | ] 39 | BASE_API_URL = 'http://127.0.0.1:9000/api/search/universal/relative?query={0}&range=5&fields=' 40 | def _build_api_string(message, fields): 41 | return BASE_API_URL.format(message) + '%2C'.join(set(DEFAULT_FIELDS + fields)) 42 | 43 | 44 | def _get_api_response(message, fields): 45 | time.sleep(3) 46 | url = _build_api_string(message, fields) 47 | api_response = requests.get(url, auth=('admin', 'admin'), headers={'accept': 'application/json'}) 48 | return api_response 49 | 50 | 51 | def _parse_api_response(api_response): 52 | assert api_response.status_code == 200 53 | 54 | messages = api_response.json()['messages'] 55 | assert len(messages) == 1 56 | 57 | return messages[0]['message'] 58 | -------------------------------------------------------------------------------- /tests/test_common_fields.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import pytest 3 | import mock 4 | from pygelf import GelfTcpHandler, GelfUdpHandler, GelfHttpHandler, GelfTlsHandler, GelfHttpsHandler 5 | from tests.helper import logger, get_unique_message, log_warning, log_exception 6 | 7 | 8 | SYSLOG_LEVEL_ERROR = 3 9 | SYSLOG_LEVEL_WARNING = 4 10 | 11 | 12 | @pytest.fixture(params=[ 13 | GelfTcpHandler(host='127.0.0.1', port=12201), 14 | GelfUdpHandler(host='127.0.0.1', port=12202), 15 | GelfUdpHandler(host='127.0.0.1', port=12202, compress=False), 16 | GelfHttpHandler(host='127.0.0.1', port=12203), 17 | GelfHttpHandler(host='127.0.0.1', port=12203, compress=False), 18 | GelfTlsHandler(host='127.0.0.1', port=12204), 19 | GelfHttpsHandler(host='127.0.0.1', port=12205, validate=False), 20 | GelfHttpsHandler(host='localhost', port=12205, validate=True, ca_certs='tests/config/cert.pem'), 21 | GelfTlsHandler(host='127.0.0.1', port=12204, validate=True, ca_certs='tests/config/cert.pem'), 22 | ]) 23 | def handler(request): 24 | return request.param 25 | 26 | 27 | def test_simple_message(logger): 28 | message = get_unique_message() 29 | graylog_response = log_warning(logger, message) 30 | assert graylog_response['message'] == message 31 | assert graylog_response['level'] == SYSLOG_LEVEL_WARNING 32 | assert 'full_message' not in graylog_response 33 | assert 'file' not in graylog_response 34 | assert 'module' not in graylog_response 35 | assert 'func' not in graylog_response 36 | assert 'logger_name' not in graylog_response 37 | assert 'line' not in graylog_response 38 | 39 | 40 | def test_formatted_message(logger): 41 | message = get_unique_message() 42 | template = message + '_%s_%s' 43 | graylog_response = log_warning(logger, template, args=('hello', 'gelf')) 44 | assert graylog_response['message'] == message + '_hello_gelf' 45 | assert graylog_response['level'] == SYSLOG_LEVEL_WARNING 46 | assert 'full_message' not in graylog_response 47 | 48 | 49 | def test_full_message(logger): 50 | message = get_unique_message() 51 | 52 | try: 53 | raise ValueError(message) 54 | except ValueError as e: 55 | graylog_response = log_exception(logger, message, e) 56 | assert graylog_response['message'] == message 57 | assert graylog_response['level'] == SYSLOG_LEVEL_ERROR 58 | assert message in graylog_response['full_message'] 59 | assert 'Traceback (most recent call last)' in graylog_response['full_message'] 60 | assert 'ValueError: ' in graylog_response['full_message'] 61 | assert 'file' not in graylog_response 62 | assert 'module' not in graylog_response 63 | assert 'func' not in graylog_response 64 | assert 'logger_name' not in graylog_response 65 | assert 'line' not in graylog_response 66 | 67 | 68 | def test_source(logger): 69 | original_source = socket.gethostname() 70 | with mock.patch('socket.gethostname', return_value='different_domain'): 71 | message = get_unique_message() 72 | graylog_response = log_warning(logger, message) 73 | assert graylog_response['source'] == original_source 74 | -------------------------------------------------------------------------------- /tests/test_core_functions.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import struct 4 | import zlib 5 | 6 | import pytest 7 | 8 | from pygelf import gelf 9 | 10 | 11 | class _ObjWithStr: 12 | def __str__(self): 13 | return 'str' 14 | 15 | 16 | class _ObjWithRepr: 17 | def __repr__(self): 18 | return 'repr' 19 | 20 | 21 | _now = datetime.datetime.now() 22 | 23 | 24 | @pytest.mark.parametrize( 25 | ('obj', 'expected'), 26 | [ 27 | (_ObjWithStr(), 'str'), 28 | (_ObjWithRepr(), 'repr'), 29 | (_now, _now.isoformat()), 30 | (_now.time(), _now.time().isoformat()), 31 | (_now.date(), _now.date().isoformat()), 32 | ] 33 | ) 34 | def test_object_to_json(obj, expected): 35 | result = gelf.object_to_json(obj) 36 | assert result == expected 37 | 38 | 39 | @pytest.mark.parametrize('compress', [True, False]) 40 | def test_pack(compress): 41 | message = {'version': '1.1', 'short_message': 'test pack', 'foo': _ObjWithStr()} 42 | expected = json.loads(json.dumps(message, default=str)) 43 | packed_message = gelf.pack(message, compress, default=str) 44 | unpacked_message = zlib.decompress(packed_message) if compress else packed_message 45 | unpacked_message = json.loads(unpacked_message.decode('utf-8')) 46 | assert expected == unpacked_message 47 | 48 | 49 | def test_split(): 50 | message = b'12345' 51 | header = b'\x1e\x0f' 52 | chunks = list(gelf.split(message, 2)) 53 | expected = [ 54 | (struct.pack('b', 0), struct.pack('b', 3), b'12'), 55 | (struct.pack('b', 1), struct.pack('b', 3), b'34'), 56 | (struct.pack('b', 2), struct.pack('b', 3), b'5') 57 | ] 58 | 59 | assert len(chunks) == len(expected) 60 | 61 | for index, chunk in enumerate(chunks): 62 | expected_index, expected_chunks_count, expected_chunk = expected[index] 63 | assert chunk[:2] == header 64 | assert chunk[10:11] == expected_index 65 | assert chunk[11:12] == expected_chunks_count 66 | assert chunk[12:] == expected_chunk 67 | -------------------------------------------------------------------------------- /tests/test_debug_mode.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pygelf import GelfTcpHandler, GelfUdpHandler, GelfHttpHandler, GelfTlsHandler, GelfHttpsHandler 3 | from tests.helper import logger, get_unique_message, log_warning 4 | 5 | 6 | @pytest.fixture(params=[ 7 | GelfTcpHandler(host='127.0.0.1', port=12201, debug=True), 8 | GelfUdpHandler(host='127.0.0.1', port=12202, debug=True), 9 | GelfUdpHandler(host='127.0.0.1', port=12202, compress=False, debug=True), 10 | GelfHttpHandler(host='127.0.0.1', port=12203, debug=True), 11 | GelfHttpHandler(host='127.0.0.1', port=12203, compress=False, debug=True), 12 | GelfTlsHandler(host='127.0.0.1', port=12204, debug=True), 13 | GelfTlsHandler(host='127.0.0.1', port=12204, debug=True, validate=True, ca_certs='tests/config/cert.pem'), 14 | GelfHttpsHandler(host='localhost', port=12205, debug=True, validate=True, ca_certs='tests/config/cert.pem') 15 | 16 | ]) 17 | def handler(request): 18 | return request.param 19 | 20 | 21 | def test_debug_mode(logger): 22 | message = get_unique_message() 23 | graylog_response = log_warning(logger, message) 24 | assert graylog_response['message'] == message 25 | assert graylog_response['file'] == 'helper.py' 26 | assert graylog_response['module'] == 'helper' 27 | assert graylog_response['func'] == 'log_warning' 28 | assert graylog_response['logger_name'] == 'test' 29 | assert 'line' in graylog_response 30 | -------------------------------------------------------------------------------- /tests/test_dynamic_fields.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pytest 3 | from pygelf import GelfTcpHandler, GelfUdpHandler, GelfHttpHandler, GelfTlsHandler, GelfHttpsHandler 4 | from tests.helper import get_unique_message, log_warning 5 | 6 | 7 | class DummyFilter(logging.Filter): 8 | def filter(self, record): 9 | record.ozzy = 'diary of a madman' 10 | record.van_halen = 1984 11 | record.id = 42 12 | return True 13 | 14 | 15 | @pytest.fixture(params=[ 16 | GelfTcpHandler(host='127.0.0.1', port=12201, include_extra_fields=True), 17 | GelfUdpHandler(host='127.0.0.1', port=12202, include_extra_fields=True), 18 | GelfUdpHandler(host='127.0.0.1', port=12202, compress=False, include_extra_fields=True), 19 | GelfHttpHandler(host='127.0.0.1', port=12203, include_extra_fields=True), 20 | GelfHttpHandler(host='127.0.0.1', port=12203, compress=False, include_extra_fields=True), 21 | GelfTlsHandler(host='127.0.0.1', port=12204, include_extra_fields=True), 22 | GelfHttpsHandler(host='127.0.0.1', port=12205, validate=False, include_extra_fields=True), 23 | GelfTlsHandler(host='127.0.0.1', port=12204, validate=True, ca_certs='tests/config/cert.pem', include_extra_fields=True), 24 | ]) 25 | def handler(request): 26 | return request.param 27 | 28 | 29 | @pytest.fixture 30 | def logger(handler): 31 | logger = logging.getLogger('test') 32 | dummy_filter = DummyFilter() 33 | logger.addFilter(dummy_filter) 34 | logger.addHandler(handler) 35 | yield logger 36 | logger.removeHandler(handler) 37 | logger.removeFilter(dummy_filter) 38 | 39 | 40 | def test_dynamic_fields(logger): 41 | message = get_unique_message() 42 | graylog_response = log_warning(logger, message, fields=['ozzy', 'van_halen']) 43 | assert graylog_response['message'] == message 44 | assert graylog_response['ozzy'] == 'diary of a madman' 45 | assert graylog_response['van_halen'] == 1984 46 | assert graylog_response['_id'] != 42 47 | assert 'id' not in graylog_response 48 | -------------------------------------------------------------------------------- /tests/test_handler_specific.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pygelf import GelfTlsHandler, GelfHttpsHandler 3 | 4 | 5 | def test_tls_handler_init(): 6 | with pytest.raises(ValueError): 7 | GelfTlsHandler(host='127.0.0.1', port=12204, validate=True) 8 | 9 | with pytest.raises(ValueError): 10 | GelfTlsHandler(host='127.0.0.1', port=12204, keyfile='/dev/null') 11 | 12 | 13 | def test_https_handler_init(): 14 | with pytest.raises(ValueError): 15 | GelfHttpsHandler(host='127.0.0.1', port=12205, validate=True) 16 | -------------------------------------------------------------------------------- /tests/test_queuehandler_support.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pytest 3 | import mock 4 | from pygelf import GelfTcpHandler, GelfUdpHandler, GelfHttpHandler, GelfTlsHandler, GelfHttpsHandler 5 | from tests.helper import logger, get_unique_message, log_exception 6 | 7 | 8 | @pytest.fixture(params=[ 9 | GelfTcpHandler(host='127.0.0.1', port=12201), 10 | GelfUdpHandler(host='127.0.0.1', port=12202), 11 | GelfUdpHandler(host='127.0.0.1', port=12202, compress=False), 12 | GelfHttpHandler(host='127.0.0.1', port=12203), 13 | GelfHttpHandler(host='127.0.0.1', port=12203, compress=False), 14 | GelfTlsHandler(host='127.0.0.1', port=12204), 15 | GelfHttpsHandler(host='127.0.0.1', port=12205, validate=False), 16 | GelfTlsHandler(host='127.0.0.1', port=12204, validate=True, ca_certs='tests/config/cert.pem'), 17 | GelfHttpsHandler(host='localhost', port=12205, validate=True, ca_certs='tests/config/cert.pem'), 18 | ]) 19 | def handler(request): 20 | return request.param 21 | 22 | 23 | def fake_handle(self, record): 24 | self.format(record) 25 | record.exc_info = None 26 | self.emit(record) 27 | 28 | 29 | def test_full_message(logger): 30 | message = get_unique_message() 31 | 32 | with mock.patch.object(logging.Handler, 'handle', new=fake_handle): 33 | try: 34 | raise ValueError(message) 35 | except ValueError as e: 36 | graylog_response = log_exception(logger, message, e) 37 | assert message in graylog_response['full_message'] 38 | assert 'Traceback (most recent call last)' in graylog_response['full_message'] 39 | assert 'ValueError: ' in graylog_response['full_message'] 40 | -------------------------------------------------------------------------------- /tests/test_static_fields.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pygelf import GelfTcpHandler, GelfUdpHandler, GelfHttpHandler, GelfTlsHandler, GelfHttpsHandler, GelfTlsHandler 3 | from tests.helper import logger, get_unique_message, log_warning 4 | 5 | 6 | STATIC_FIELDS = { 7 | '_ozzy': 'diary of a madman', 8 | '_van_halen': 1984, 9 | '_id': 42 10 | } 11 | 12 | 13 | @pytest.fixture(params=[ 14 | GelfTcpHandler(host='127.0.0.1', port=12201, **STATIC_FIELDS), 15 | GelfUdpHandler(host='127.0.0.1', port=12202, **STATIC_FIELDS), 16 | GelfUdpHandler(host='127.0.0.1', port=12202, compress=False, **STATIC_FIELDS), 17 | GelfHttpHandler(host='127.0.0.1', port=12203, **STATIC_FIELDS), 18 | GelfHttpHandler(host='127.0.0.1', port=12203, compress=False, **STATIC_FIELDS), 19 | GelfTlsHandler(host='127.0.0.1', port=12204, **STATIC_FIELDS), 20 | GelfTlsHandler(host='127.0.0.1', port=12204, validate=True, ca_certs='tests/config/cert.pem', **STATIC_FIELDS), 21 | GelfTcpHandler(host='127.0.0.1', port=12201, static_fields=STATIC_FIELDS, _ozzy='billie jean'), 22 | GelfUdpHandler(host='127.0.0.1', port=12202, static_fields=STATIC_FIELDS, _ozzy='billie jean'), 23 | GelfUdpHandler(host='127.0.0.1', port=12202, compress=False, static_fields=STATIC_FIELDS, _ozzy='billie jean'), 24 | GelfHttpHandler(host='127.0.0.1', port=12203, static_fields=STATIC_FIELDS, _ozzy='billie jean'), 25 | GelfHttpHandler(host='127.0.0.1', port=12203, compress=False, static_fields=STATIC_FIELDS, _ozzy='billie jean'), 26 | GelfTlsHandler(host='127.0.0.1', port=12204, static_fields=STATIC_FIELDS), 27 | GelfHttpsHandler(host='127.0.0.1', port=12205, validate=False, static_fields=STATIC_FIELDS, _ozzy='billie jean'), 28 | GelfTlsHandler(host='127.0.0.1', port=12204, validate=True, ca_certs='tests/config/cert.pem', static_fields=STATIC_FIELDS, _ozzy='billie jean'), 29 | ]) 30 | def handler(request): 31 | return request.param 32 | 33 | 34 | def test_static_fields(logger): 35 | message = get_unique_message() 36 | graylog_response = log_warning(logger, message, fields=['ozzy', 'van_halen']) 37 | assert graylog_response['message'] == message 38 | assert graylog_response['ozzy'] == 'diary of a madman' 39 | assert graylog_response['van_halen'] == 1984 40 | assert graylog_response['_id'] != 42 41 | assert 'id' not in graylog_response 42 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv] 2 | commands = py.test -v -s 3 | deps = 4 | pytest 5 | mock 6 | requests 7 | --------------------------------------------------------------------------------