Repository: napalm-automation/napalm-logs (language: python)

Issue:
Release 0.10.0

Add model for process ceiling error logs on Juniper
On Juniper, when the system has too many open processes, it will breach a low watermark, and log errors:
```
jlaunchd: System reaching processes ceiling critical watermark: Contact to system administrator to clean up unnecessary processes or increase maxproc ceiling. Further non-suid process fork request will be denied.
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml
new file mode 100644
index 00000000..43b517a5
--- /dev/null
+++ b/.github/workflows/code.yml
@@ -0,0 +1,36 @@
+name: Test code
+
+on:
+  push:
+    branches:
+    - develop
+    - master
+  pull_request:
+    branches:
+    - develop
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [3.6, 3.7]
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v1
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        apt-get update && apt-get install -y jq python3-dev gcc
+        python -m pip install --upgrade pip
+        pip install -r requirements-dev.txt
+    - name: Tox
+      run: |
+        tox
+    - name: Results
+      run: |
+        cat /tmp/report.json | jq .
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 77e24295..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-language: python
-python:
-- 2.7
-- 3.5
-- 3.6
-- 3.7
-install:
-  - pip install tox-travis
-  - pip install coveralls
-deploy:
-  - provider: pypi
-    user: mirucha
-    password:
-      secure: "p971DB8HxHvIEfSg5l3UjicCxAmBqFV0tnj8Em8FRIGvOZEHgbCuLpSNSWdr5X8KFpLq+5W0uUJhzLj6lTQFXZXfqclDq8SLT7CdJCT62xjM5NvIWV0znS3A6TBBJsY/rTpEnxK3PuFbyn4WLRE624IG8xe8J2M8AYMlRrs4RFRh3DNA/mbgd6pi7uV/cYFu6OLp0Asu0bSV1BqU3aJFX//FJ3RYjknwCyV8JAly1eiDTgYprNP13oCDsNFkBa6Dn0BaPn /vYZei7D/ismooLxdlLY0NUAjbpqppj6Fuyb+/aBa2Dp7udxReS4/9rX0fIXl7q+qqMdkp5N6+W9jILa4Ykr0a37348IDx2GpD1+PdYzZTdC1m7JqKfAZ/YV+f7RNuZoUQz1fumx7993QYk2n4ONUkTIFbjakzZf9Ylh0ISo7ym461RHl/mIJh94DvFifkqkNVgWeBX1wyS1UCa+wqTnYEg50Mdgvhj0ZcQxKv5ZMUh6fiK3bS/fBO084Lkyd0dggEdjB42tqCd4s0sL2SI 5/2M5D+yYqq6NZX15i/GjpQmeee/Gwv5YWHB/GOA8+jFEpGa8Em630b8jpoxN/o5GYJTAoVcqe3mvEjXeb0Ga7Oeo1TNlrcBYsZHrN/mL4aS+c3XSm4HATKdY1vsntP5R9hevtHObXRjTjcXik="
-    on:
-      tags: true
-      branch: master
-script:
-- tox
-after_success:
-- coveralls
-- if [ $TRAVIS_TAG ]; then curl -X POST https://readthedocs.org/build/napalm-logs; fi
diff --git a/Dockerfile b/Dockerfile
index 5b9268e5..11d7c9f3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,9 +7,9 @@ COPY ./ /var/cache/napalm-logs/
 RUN apk add --no-cache \
     libffi \
     libffi-dev \
-    python-dev \
+    python3-dev \
     build-base \
-    && pip --no-cache-dir install cffi /var/cache/napalm-logs/ \
+    && pip --no-cache-dir install cffi pyzmq==19.0.2 /var/cache/napalm-logs/ \
     && rm -rf /var/cache/napalm-logs/
 
 CMD napalm-logs --config-file /etc/napalm/logs
diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py
index 495d5ea6..0dcf53b7 100644
--- a/docs/_themes/flask_theme_support.py
+++ b/docs/_themes/flask_theme_support.py
@@ -1,7 +1,19 @@
 # flasky extensions.  flasky pygments style based on tango style
 from pygments.style import Style
-from pygments.token import Keyword, Name, Comment, String, Error, \
-     Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
+from pygments.token import (
+    Keyword,
+    Name,
+    Comment,
+    String,
+    Error,
+    Number,
+    Operator,
+    Generic,
+    Whitespace,
+    Punctuation,
+    Other,
+    Literal,
+)
 
 
 class FlaskyStyle(Style):
@@ -11,76 +23,67 @@ class FlaskyStyle(Style):
     styles = {
         # No corresponding class for the following:
         # Text:                     "", # class:  ''
-        Whitespace:                "underline #f8f8f8",      # class: 'w'
-        Error:                     "#a40000 border:#ef2929",  # class: 'err'
-        Other:                     "#000000",                # class 'x'
-
-        Comment:                   "italic #8f5902",  # class: 'c'
-        Comment.Preproc:           "noitalic",       # class: 'cp'
-
-        Keyword:                   "bold #004461",   # class: 'k'
-        Keyword.Constant:          "bold #004461",   # class: 'kc'
-        Keyword.Declaration:       "bold #004461",   # class: 'kd'
-        Keyword.Namespace:         "bold #004461",   # class: 'kn'
-        Keyword.Pseudo:            "bold #004461",   # class: 'kp'
-        Keyword.Reserved:          "bold #004461",   # class: 'kr'
-        Keyword.Type:              "bold #004461",   # class: 'kt'
-
-        Operator:                  "#582800",   # class: 'o'
-        Operator.Word:             "bold #004461",   # class: 'ow' - like keywords
-
-        Punctuation:               "bold #000000",   # class: 'p'
-
+        Whitespace: "underline #f8f8f8",  # class: 'w'
+        Error: "#a40000 border:#ef2929",  # class: 'err'
+        Other: "#000000",  # class 'x'
+        Comment: "italic #8f5902",  # class: 'c'
+        Comment.Preproc: "noitalic",  # class: 'cp'
+        Keyword: "bold #004461",  # class: 'k'
+        Keyword.Constant: "bold #004461",  # class: 'kc'
+        Keyword.Declaration: "bold #004461",  # class: 'kd'
+        Keyword.Namespace: "bold #004461",  # class: 'kn'
+        Keyword.Pseudo: "bold #004461",  # class: 'kp'
+        Keyword.Reserved: "bold #004461",  # class: 'kr'
+        Keyword.Type: "bold #004461",  # class: 'kt'
+        Operator: "#582800",  # class: 'o'
+        Operator.Word: "bold #004461",  # class: 'ow' - like keywords
+        Punctuation: "bold #000000",  # class: 'p'
         # because special names such as Name.Class, Name.Function, etc.
         # are not recognized as such later in the parsing, we choose them
         # to look the same as ordinary variables.
-        Name:                      "#000000",        # class: 'n'
-        Name.Attribute:            "#c4a000",        # class: 'na' - to be revised
-        Name.Builtin:              "#004461",        # class: 'nb'
-        Name.Builtin.Pseudo:       "#3465a4",        # class: 'bp'
-        Name.Class:                "#000000",        # class: 'nc' - to be revised
-        Name.Constant:             "#000000",        # class: 'no' - to be revised
-        Name.Decorator:            "#888",           # class: 'nd' - to be revised
-        Name.Entity:               "#ce5c00",        # class: 'ni'
-        Name.Exception:            "bold #cc0000",   # class: 'ne'
-        Name.Function:             "#000000",        # class: 'nf'
-        Name.Property:             "#000000",        # class: 'py'
-        Name.Label:                "#f57900",        # class: 'nl'
-        Name.Namespace:            "#000000",        # class: 'nn' - to be revised
-        Name.Other:                "#000000",        # class: 'nx'
-        Name.Tag:                  "bold #004461",   # class: 'nt' - like a keyword
-        Name.Variable:             "#000000",        # class: 'nv' - to be revised
-        Name.Variable.Class:       "#000000",        # class: 'vc' - to be revised
-        Name.Variable.Global:      "#000000",        # class: 'vg' - to be revised
-        Name.Variable.Instance:    "#000000",        # class: 'vi' - to be revised
-
-        Number:                    "#990000",        # class: 'm'
-
-        Literal:                   "#000000",        # class: 'l'
-        Literal.Date:              "#000000",        # class: 'ld'
-
-        String:                    "#4e9a06",        # class: 's'
-        String.Backtick:           "#4e9a06",        # class: 'sb'
-        String.Char:               "#4e9a06",        # class: 'sc'
-        String.Doc:                "italic #8f5902",  # class: 'sd' - like a comment
-        String.Double:             "#4e9a06",        # class: 's2'
-        String.Escape:             "#4e9a06",        # class: 'se'
-        String.Heredoc:            "#4e9a06",        # class: 'sh'
-        String.Interpol:           "#4e9a06",        # class: 'si'
-        String.Other:              "#4e9a06",        # class: 'sx'
-        String.Regex:              "#4e9a06",        # class: 'sr'
-        String.Single:             "#4e9a06",        # class: 's1'
-        String.Symbol:             "#4e9a06",        # class: 'ss'
-
-        Generic:                   "#000000",        # class: 'g'
-        Generic.Deleted:           "#a40000",        # class: 'gd'
-        Generic.Emph:              "italic #000000",  # class: 'ge'
-        Generic.Error:             "#ef2929",        # class: 'gr'
-        Generic.Heading:           "bold #000080",   # class: 'gh'
-        Generic.Inserted:          "#00A000",        # class: 'gi'
-        Generic.Output:            "#888",           # class: 'go'
-        Generic.Prompt:            "#745334",        # class: 'gp'
-        Generic.Strong:            "bold #000000",   # class: 'gs'
-        Generic.Subheading:        "bold #800080",   # class: 'gu'
-        Generic.Traceback:         "bold #a40000",   # class: 'gt'
+        Name: "#000000",  # class: 'n'
+        Name.Attribute: "#c4a000",  # class: 'na' - to be revised
+        Name.Builtin: "#004461",  # class: 'nb'
+        Name.Builtin.Pseudo: "#3465a4",  # class: 'bp'
+        Name.Class: "#000000",  # class: 'nc' - to be revised
+        Name.Constant: "#000000",  # class: 'no' - to be revised
+        Name.Decorator: "#888",  # class: 'nd' - to be revised
+        Name.Entity: "#ce5c00",  # class: 'ni'
+        Name.Exception: "bold #cc0000",  # class: 'ne'
+        Name.Function: "#000000",  # class: 'nf'
+        Name.Property: "#000000",  # class: 'py'
+        Name.Label: "#f57900",  # class: 'nl'
+        Name.Namespace: "#000000",  # class: 'nn' - to be revised
+        Name.Other: "#000000",  # class: 'nx'
+        Name.Tag: "bold #004461",  # class: 'nt' - like a keyword
+        Name.Variable: "#000000",  # class: 'nv' - to be revised
+        Name.Variable.Class: "#000000",  # class: 'vc' - to be revised
+        Name.Variable.Global: "#000000",  # class: 'vg' - to be revised
+        Name.Variable.Instance: "#000000",  # class: 'vi' - to be revised
+        Number: "#990000",  # class: 'm'
+        Literal: "#000000",  # class: 'l'
+        Literal.Date: "#000000",  # class: 'ld'
+        String: "#4e9a06",  # class: 's'
+        String.Backtick: "#4e9a06",  # class: 'sb'
+        String.Char: "#4e9a06",  # class: 'sc'
+        String.Doc: "italic #8f5902",  # class: 'sd' - like a comment
+        String.Double: "#4e9a06",  # class: 's2'
+        String.Escape: "#4e9a06",  # class: 'se'
+        String.Heredoc: "#4e9a06",  # class: 'sh'
+        String.Interpol: "#4e9a06",  # class: 'si'
+        String.Other: "#4e9a06",  # class: 'sx'
+        String.Regex: "#4e9a06",  # class: 'sr'
+        String.Single: "#4e9a06",  # class: 's1'
+        String.Symbol: "#4e9a06",  # class: 'ss'
+        Generic: "#000000",  # class: 'g'
+        Generic.Deleted: "#a40000",  # class: 'gd'
+        Generic.Emph: "italic #000000",  # class: 'ge'
+        Generic.Error: "#ef2929",  # class: 'gr'
+        Generic.Heading: "bold #000080",  # class: 'gh'
+        Generic.Inserted: "#00A000",  # class: 'gi'
+        Generic.Output: "#888",  # class: 'go'
+        Generic.Prompt: "#745334",  # class: 'gp'
+        Generic.Strong: "bold #000000",  # class: 'gs'
+        Generic.Subheading: "bold #800080",  # class: 'gu'
+        Generic.Traceback: "bold #a40000",  # class: 'gt'
     }
diff --git a/docs/conf.py b/docs/conf.py
index 7b340237..f301b9a4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -47,7 +47,7 @@
     'sphinx.ext.intersphinx',
     'sphinx.ext.coverage',
     'sphinx.ext.viewcode',
-    'sphinx.ext.githubpages'
+    'sphinx.ext.githubpages',
 ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -110,7 +110,7 @@
     'github_user': 'napalm-automation',
     'github_repo': 'napalm-logs',
     'github_banner': True,
-    'show_related': False
+    'show_related': False,
 }
 
 # Add any paths that contain custom static files (such as style sheets) here,
@@ -155,15 +155,12 @@
     # The paper size ('letterpaper' or 'a4paper').
     #
     # 'papersize': 'letterpaper',
-
     # The font size ('10pt', '11pt' or '12pt').
     #
     # 'pointsize': '10pt',
-
     # Additional stuff for the LaTeX preamble.
     #
     # 'preamble': '',
-
     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
@@ -173,8 +170,13 @@
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, 'napalm-logs.tex', u'napalm-logs Documentation',
-     u'Mircea Ulinic', 'manual'),
+    (
+        master_doc,
+        'napalm-logs.tex',
+        u'napalm-logs Documentation',
+        u'Mircea Ulinic',
+        'manual',
+    ),
 ]
 
 
@@ -182,10 +184,7 @@
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, 'napalm-logs', u'napalm-logs Documentation',
-     [author], 1)
-]
+man_pages = [(master_doc, 'napalm-logs', u'napalm-logs Documentation', [author], 1)]
 
 
 # -- Options for Texinfo output -------------------------------------------
@@ -194,11 +193,18 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'napalm-logs', u'napalm-logs Documentation',
-     author, 'napalm-logs',
-     ('napalm-logs is a Python library that listens to syslog messages from network devices and returns strucuted data'
-      'following the OpenConfig or IETF YANG models'),
-     'Miscellaneous'),
+    (
+        master_doc,
+        'napalm-logs',
+        u'napalm-logs Documentation',
+        author,
+        'napalm-logs',
+        (
+            'napalm-logs is a Python library that listens to syslog messages from network devices and returns strucuted data'
+            'following the OpenConfig or IETF YANG models'
+        ),
+        'Miscellaneous',
+    ),
 ]
 
 # -- Options for Epub output ----------------------------------------------
@@ -247,13 +253,10 @@ def gen_messages_rst():
         for message in os_cfg['messages']:
             error_name = message['error']
             if error_name not in defined_errors:
-                defined_errors[error_name] = {
-                    'doc': '',
-                    'os': [],
-                    'model': ''
-                }
-            if not defined_errors[error_name]['doc'] or\
-               len(defined_errors[error_name]['doc']) < len(message['__doc__']):
+                defined_errors[error_name] = {'doc': '', 'os': [], 'model': ''}
+            if not defined_errors[error_name]['doc'] or len(
+                defined_errors[error_name]['doc']
+            ) < len(message['__doc__']):
                 defined_errors[error_name]['doc'] = message['__doc__']
             if not defined_errors[error_name]['model']:
                 defined_errors[error_name]['model'] = message['model']
@@ -265,8 +268,14 @@ def gen_messages_rst():
     for error_name, error_details in defined_errors.items():
         os_name = error_details['os'][0]  # Picking up the first OS in the list.
         error_path = os.path.join(test_root_path, os_name, error_name)
-        test_cases = [name for name in os.listdir(error_path) if os.path.isdir(os.path.join(error_path, name))]
-        test_case_name = 'default' if 'default' in test_cases else test_cases[0]  # Picking up a test case.
+        test_cases = [
+            name
+            for name in os.listdir(error_path)
+            if os.path.isdir(os.path.join(error_path, name))
+        ]
+        test_case_name = (
+            'default' if 'default' in test_cases else test_cases[0]
+        )  # Picking up a test case.
         test_case_path = os.path.join(error_path, test_case_name)
         raw_message_filepath = os.path.join(test_case_path, 'syslog.msg')
         log.debug('Looking for %s', raw_message_filepath)
@@ -283,16 +292,20 @@ def gen_messages_rst():
         log.debug('Read YANG text:')
         log.debug(yang_message)
         struct_yang_message = json.loads(yang_message)
-        indented_yang_message = json.dumps(struct_yang_message, indent=4, sort_keys=True)
+        indented_yang_message = json.dumps(
+            struct_yang_message, indent=4, sort_keys=True
+        )
         log.debug('Struct YANG message:')
         log.debug(struct_yang_message)
         msg_template = env.get_template('message_template.jinja')
-        rendered_template = msg_template.render(error_name=error_name,
-                                                error_doc=error_details['doc'],
-                                                error_yang=error_details['model'],
-                                                error_os_list=list(set(error_details['os'])),
-                                                error_txt_example=raw_message.strip(),
-                                                error_json_example=indented_yang_message.replace('\n}', '\n  }'))
+        rendered_template = msg_template.render(
+            error_name=error_name,
+            error_doc=error_details['doc'],
+            error_yang=error_details['model'],
+            error_os_list=list(set(error_details['os'])),
+            error_txt_example=raw_message.strip(),
+            error_json_example=indented_yang_message.replace('\n}', '\n  }'),
+        )
         message_rst_path = 'messages/{error_name}.rst'.format(error_name=error_name)
         with open(message_rst_path, 'w') as rst_fh:
             rst_fh.write(rendered_template)
diff --git a/docs/developers/index.rst b/docs/developers/index.rst
index 560ed366..7f85944c 100644
--- a/docs/developers/index.rst
+++ b/docs/developers/index.rst
@@ -10,3 +10,4 @@ Here we you will find out how to add new functionality to ``napalm-logs``.
    architecture
    pluggable_modules
    device_profiles
+   local_testing
diff --git a/docs/listener/tcp.rst b/docs/listener/tcp.rst
index ad352549..53ad8890 100644
--- a/docs/listener/tcp.rst
+++ b/docs/listener/tcp.rst
@@ -72,3 +72,45 @@ Example:
   listener:
     tcp:
       max_clients: 100
+
+.. _listener-tcp-framing:
+
+``framing``: ``traditional``
+----------------------------
+
+.. versionadded:: 0.10.0
+
+Framing mode used when receiving messages. Available options: ``traditional`` 
+or ``octet-counted``.
+
+In protocol engineering, *framing* means how multiple messages over the same
+connection are separated. Usually, this is transparent to users. Unfortunately,
+the early syslog protocol evolved and so there are cases where users need to
+specify the framing. The ``traditional`` framing is nontransparent. With it,
+messages end when an LF (i.e., line break / return) is encountered, and the next
+message starts immediately after the LF. If multi-line messages are received,
+these are essentially broken up into multiple message, usually with all but the
+first message segment being incorrectly formatted. The ``octet-counted`` framing
+solves this issue. With it, each message is prefixed with the actual message
+length, so that a receiver knows exactly where the message ends. Multi-line
+messages cause no problem here. This mode is very close to the method described
+in RFC5425 for TLS-enabled syslog. Unfortunately, only few syslogd
+implementations support ``octet-counted`` framing. As such, the ``traditional``
+framing is set as default, even though it has defects. If it is known that the
+transmitter supports ``octet-counted`` framing, it is suggested to use that
+framing mode.
+
+(Detailed documentation notes based on `<rsyslog 
+omfwd> https://www.rsyslog.com/doc/v8-stable/configuration/modules/omfwd.html#tcp-framing`__)
+
+.. _listener-tcp-frame-delimiter:
+
+``frame_delimiter``: ``\n``
+---------------------------
+
+.. versionadded:: 0.10.0
+
+Sets a custom frame delimiter for TCP transmission when ``framing`` is
+configured in ``traditional`` mode. The delimiter has to be a number between
+0 and 255 (representing the ASCII-code of said character). The default value for
+this parameter is 10, representing a ``\n``.
diff --git a/docs/metrics/index.rst b/docs/metrics/index.rst
index 596199c3..28e5a9fe 100644
--- a/docs/metrics/index.rst
+++ b/docs/metrics/index.rst
@@ -29,11 +29,11 @@ Listener Process(es)
 --------------------
 
 napalm_logs_listener_logs_ingested
-  Count of ingested log messages. Labels are used to seperate metrics for each Listener process.
+  Count of ingested log messages. Labels are used to separate metrics for each Listener process.
 
 napalm_logs_listener_messages_published
   Count of published messages. These are messages published to the message queue for processing by the Server Process.
-  Labels are used to seperate metrics for each Listener process.
+  Labels are used to separate metrics for each Listener process.
 
 Server Process
 --------------
@@ -42,53 +42,62 @@ napalm_logs_server_messages_received
   Count of messages received from Listener processes.
 
 napalm_logs_server_messages_with_identified_os
-  Count of messages with positive OS identification. Labels are used to seperate metrics for each Device OS.
+  Count of messages with positive OS identification. Labels are used to separate metrics for each Device OS.
 
 napalm_logs_server_messages_without_identified_os
   Count of messages which fail OS identification.
 
 napalm_logs_server_messages_failed_device_queuing
   Count of messages per device OS that fail to be queued to a proper Device process. Note these are messages that
-  pass OS identification and we know how to route them but fail to be queued. Labels are used to seperate metrics
+  pass OS identification and we know how to route them but fail to be queued. Labels are used to separate metrics
   for each Device OS.
 
 napalm_logs_server_messages_device_queued
-  Count of messages sucessfully queued to Device processes. Labels are used to seperate metrics for each Device OS process.
+  Count of messages successfully queued to Device processes. Labels are used to separate metrics for each Device OS process.
 
 napalm_logs_server_messages_unknown_queued
-  Count of messages which fail OS indentification and thus we don't know how to route them, but the user has instructed
+  Count of messages which fail OS identification and thus we don't know how to route them, but the user has instructed
   the system to queue them "as-is."
 
 Device Process(es)
 ------------------
 
 napalm_logs_device_messages_received
-  Count of messages received from the Server process. Labels are used to seperate metrics for each Device OS process.
+  Count of messages received from the Server process. Labels are used to separate metrics for each Device OS process.
 
 napalm_logs_device_raw_published_messages
   Count of raw type published messages. In this case, the message did not match a configured message type but the
-  user has instructed the system to publish the message in a raw format. Labels are used to seperate metrics for
+  user has instructed the system to publish the message in a raw format. Labels are used to separate metrics for
   each Device OS process.
 
 napalm_logs_device_published_messages
-  Count of published messages. These are messages which are sucessfully converted to an OpenConfig format. Labels
-  are used to seperate metrics for each Device OS process.
+  Count of published messages. These are messages which are successfully converted to an OpenConfig format. Labels
+  are used to separate metrics for each Device OS process.
 
-napalm_logs_device_oc_object_failed 
+napalm_logs_device_oc_object_failed
   Counter of failed OpenConfig object generations. These are messages for which the system attempts to map to a
-  known OpenConfig object model but fails. Labels are used to seperate metrics for each Device OS process.
+  known OpenConfig object model but fails. Labels are used to separate metrics for each Device OS process.
+
+napalm_logs_device_published_messages_attrs
+  Count of published messages. This metrics supersedes 
+  ``napalm_logs_device_published_messages`` as it provides a more granular 
+  selection, using two additional labels (besides ``device_os``): ``error`` and 
+  ``host`` for the *napalm-logs* error / message type and the host, 
+  respectively. As this metric has a potential to generate a large amount of 
+  metrics, you are able to disable it by configuring 
+  ``metrics_include_attrbutes: false`` in the napalm-logs configuration file.
 
 Publisher Process(es)
 ---------------------
 
 napalm_logs_publisher_received_messages
-  Count of messages received by the Publisher from Device Process(es). Labels are used to seperate metrics for
+  Count of messages received by the Publisher from Device Process(es). Labels are used to separate metrics for
   each Publisher process.
 
 napalm_logs_publisher_whitelist_blacklist_check_fail
-  Count of messages which fail the whitelist/blacklist check. Labels are used to seperate metrics for each
+  Count of messages which fail the whitelist/blacklist check. Labels are used to separate metrics for each
   Publisher process.
 
 napalm_logs_publisher_messages_published
   Count of published messages. These are messages which are published for clients to receive (i.e. output of the
-  system). Labels are used to seperate metrics for each Publisher process.
+  system). Labels are used to separate metrics for each Publisher process.
diff --git a/docs/options/index.rst b/docs/options/index.rst
index 00a30b8b..5f982df1 100644
--- a/docs/options/index.rst
+++ b/docs/options/index.rst
@@ -171,6 +171,23 @@ Configuration file example:
 
   metrics_dir: /tmp/a_new_dir_for_metrics
 
+.. _configuration-options-metrics-attrs:
+
+``metrics_include_attributes``
+------------------------------
+
+.. versionadded:: 0.10.0
+
+Disable detailed metrics with attributes per published device OS, hostname, and 
+napalm-logs error type. Default: ``True`` (the metrics will include detailed 
+attributes).
+
+Configuration file example:
+
+.. code-block:: yaml
+
+  metrics_include_attributes: false
+
 .. _configuration-options-certificate:
 
 ``certificate``
diff --git a/docs/publisher/alerta.rst b/docs/publisher/alerta.rst
index 0417c32e..753d9cc0 100644
--- a/docs/publisher/alerta.rst
+++ b/docs/publisher/alerta.rst
@@ -50,8 +50,29 @@ Configuration examples:
 Available options
 ^^^^^^^^^^^^^^^^^
 
-The options are generally inherited from the :ref:`publisher-http` Publisher, 
-with the following notes:
+.. _publisher-alerta-pairs:
+
+``pairs``
+---------
+
+.. versionadded:: 0.10.0
+
+Hash that defines the remapping of a specific *napalm-logs* notification to a
+pair notification that will close the previous alert. For example, 
+an ``INTERFACE_UP`` alert would close an existing ``INTERFACE_DOWN`` alert, 
+instead of creating an alert for ``INTERFACE_UP``, and so on.
+
+``pair`` defaults to:
+
+.. code-block:: yaml
+
+    pairs:
+      INTERFACE_UP: INTERFACE_DOWN
+      OSPF_NEIGHBOR_UP: OSPF_NEIGHBOR_DOWN
+      ISIS_NEIGHBOR_UP: ISIS_NEIGHBOR_DOWN
+
+The next options are generally inherited from the :ref:`publisher-http`
+Publisher, with the following notes:
 
 .. _publisher-alerta-address:
 
diff --git a/docs/publisher/index.rst b/docs/publisher/index.rst
index f0764010..72ca41c1 100644
--- a/docs/publisher/index.rst
+++ b/docs/publisher/index.rst
@@ -74,6 +74,7 @@ Available publishers and their options
    kafka
    log
    zmq
+   prometheus
 
 Globally available options
 --------------------------
diff --git a/docs/publisher/prometheus.rst b/docs/publisher/prometheus.rst
new file mode 100644
index 00000000..b5d702e7
--- /dev/null
+++ b/docs/publisher/prometheus.rst
@@ -0,0 +1,59 @@
+.. _publisher-prometheus:
+
+==========
+Prometheus
+==========
+
+.. versionadded:: 0.10.0
+
+Expose *napalm-logs* notifications as Prometheus metrics.
+
+.. important::
+
+    In order to make use of this Publisher, you'll need to enable the 
+    :ref:`configuration-options-enable-metrics` option (and eventually 
+    customise the other related settings).
+
+The metrics start with ``napalm_logs``, plus the *napalm-logs* notification 
+name. For example, the metric corresponding to :ref:`ISIS_NEIGHBOR_DOWN` is 
+named ``napalm_logs_isis_neighbor_down``.
+
+All the metrics have at least the ``host`` label. Some of them have additional
+labels, whenever it makes sense to include additional information that can be 
+used for selection and/or alerting.
+
+By default, it will expose metrics for all kinds of messages, so you might want
+to narrow down the selection using the :ref:`publisher-opts-error-whitelist` or
+:ref:`publisher-opts-error-blacklist` options.
+
+CLI usage example:
+
+.. code-block:: bash
+
+  $ sudo napalm-logs --publisher prometheus --enable-metrics
+
+Starting through the command above, you'll find the metrics at 
+http://localhost:9443/metrics.
+
+Metrics examples:
+
+.. code-block:: text
+
+  # HELP napalm_logs_interface_up_total Multiprocess metric
+  # TYPE napalm_logs_interface_up_total counter
+  napalm_logs_interface_up_total{host="veos01",interface="Ethernet28"} 1.0
+  # HELP napalm_logs_interface_down_total Multiprocess metric
+  # TYPE napalm_logs_interface_down_total counter
+  napalm_logs_interface_down_total{host="veos01",interface="Ethernet28"} 1.0
+  # HELP napalm_logs_isis_neighbor_down_total Multiprocess metric
+  # TYPE napalm_logs_isis_neighbor_down_total counter
+  napalm_logs_isis_neighbor_down_total{host="HOSTNAME",interface="et7",level="L1",neighbor="1920.0000.2006"} 1.0
+  # HELP napalm_logs_bgp_neighbor_state_changed_total Multiprocess metric
+  # TYPE napalm_logs_bgp_neighbor_state_changed_total counter
+  napalm_logs_bgp_neighbor_state_changed_total{current_state="IDLE",host="HOSTNAME",neighbor="192.0.2.2",peer_as="12345",previous_state="ESTABLISHED"} 1.0
+  # HELP napalm_logs_agent_initialized_total Multiprocess metric
+  # TYPE napalm_logs_agent_initialized_total counter
+  napalm_logs_agent_initialized_total{host="edge01.bru01"} 1.0
+  # HELP napalm_logs_interface_mac_limit_reached Multiprocess metric
+  # TYPE napalm_logs_interface_mac_limit_reached gauge
+  napalm_logs_interface_mac_limit_reached{host="vmx01",interface="ge-1/0/23.0",pid="15711"} 3.0
diff --git a/docs/releases/0.10.0.rst b/docs/releases/0.10.0.rst
new file mode 100644
index 00000000..de5fa501
--- /dev/null
+++ b/docs/releases/0.10.0.rst
@@ -0,0 +1,114 @@
+.. _release-0.10.0:
+
+==================================
+Release 0.10.0 - Codename Nokogiri
+==================================
+
+New Publisher Interface: Prometheus
+-----------------------------------
+
+Expose napalm-logs notifications as Prometheus metrics. In order to enable this 
+Publisher, besides listing it under the ``publisher`` configuration (or 
+``--publisher prometheus`` CLI), you will also need to enable the metrics by 
+setting the :ref:`configuration-options-enable-metrics` option to `true`. See 
+also: :ref:`metrics`.
+
+Once the metrics and the Prometheus Publish are enabled, you'll be able to 
+scrape metrics such as:
+
+.. code-block:: text
+
+  # HELP napalm_logs_interface_down_total Multiprocess metric
+  # TYPE napalm_logs_interface_down_total counter
+  napalm_logs_interface_down_total{host="test-switch1",interface="FastEthernet0/2"} 1.0
+  napalm_logs_interface_down_total{host="NetAuto_CSRv-03",interface="GigabitEthernet2"} 2.0
+  napalm_logs_interface_down_total{host="router1",interface="GigabitEthernet2"} 1.0
+  # HELP napalm_logs_user_exit_config_mode_total Multiprocess metric
+  # TYPE napalm_logs_user_exit_config_mode_total counter
+  napalm_logs_user_exit_config_mode_total{host="test-ztp",user="admin"} 2.0
+  napalm_logs_user_exit_config_mode_total{host="test-ztp",user="console"} 1.0
+  # HELP napalm_logs_forward_traffic_total Multiprocess metric
+  # TYPE napalm_logs_forward_traffic_total counter
+  napalm_logs_forward_traffic_total{host="fw01"} 1.0
+  # HELP napalm_logs_local_traffic_total Multiprocess metric
+  # TYPE napalm_logs_local_traffic_total counter
+  napalm_logs_local_traffic_total{host="fw01"} 1.0
+  # HELP napalm_logs_user_write_config_total Multiprocess metric
+  # TYPE napalm_logs_user_write_config_total counter
+  napalm_logs_user_write_config_total{host="GeorgeSwitch",user="admin"} 1.0
+  # HELP napalm_logs_bfd_state_change_total Multiprocess metric
+  # TYPE napalm_logs_bfd_state_change_total counter
+  napalm_logs_bfd_state_change_total{host="HOSTNAME",interface="Ethernet50/1",session_state="DOWN"} 1.0
+  napalm_logs_bfd_state_change_total{host="HOSTNAME",interface="Ethernet50/1",session_state="UP"} 2.0
+  # HELP napalm_logs_isis_neighbor_down_total Multiprocess metric
+  # TYPE napalm_logs_isis_neighbor_down_total counter
+  napalm_logs_isis_neighbor_down_total{host="HOSTNAME",interface="et7",level="L1",neighbor="1920.0000.2006"} 1.0
+  # HELP napalm_logs_isis_neighbor_up_total Multiprocess metric
+  # TYPE napalm_logs_isis_neighbor_up_total counter
+  napalm_logs_isis_neighbor_up_total{host="HOSTNAME",interface="et7",level="L1",neighbor="1920.0000.2006"} 1.0
+  # HELP napalm_logs_user_login_total Multiprocess metric
+  # TYPE napalm_logs_user_login_total counter
+  napalm_logs_user_login_total{host="sw01.pdx01",user="luke"} 1.0
+  # HELP napalm_logs_interface_duplex_mode_total Multiprocess metric
+  # TYPE napalm_logs_interface_duplex_mode_total counter
+  napalm_logs_interface_duplex_mode_total{duplex_mode="FULL",host="nexus-switch",interface="Ethernet1/33"} 0.0
+  # HELP napalm_logs_stp_block_bpduguard_total Multiprocess metric
+  # TYPE napalm_logs_stp_block_bpduguard_total counter
+  napalm_logs_stp_block_bpduguard_total{host="switch01"} 1.0
+  # HELP napalm_logs_listener_logs_ingested_total Multiprocess metric
+  # TYPE napalm_logs_listener_logs_ingested_total counter
+  # HELP napalm_logs_bgp_neighbor_state_changed_total Multiprocess metric
+  # TYPE napalm_logs_bgp_neighbor_state_changed_total counter
+  napalm_logs_bgp_neighbor_state_changed_total{host="HOSTNAME",neighbor="192.0.2.2",peer_as="12345"} 2.0
+  napalm_logs_bgp_neighbor_state_changed_total{host="some-switch",neighbor="192.0.2.2",peer_as="12345"} 1.0
+  # HELP napalm_logs_bgp_prefix_limit_exceeded_total Multiprocess metric
+  # TYPE napalm_logs_bgp_prefix_limit_exceeded_total counter
+  napalm_logs_bgp_prefix_limit_exceeded_total{host="edge01",neighbor="194.53.172.97",peer_as="2611"} 1.0
+
+For more details, see :ref:`publisher-prometheus`.
+
+Improvements
+------------
+
+The :ref:`listener-tcp` now accepts two additional parameters, 
+:ref:`listener-tcp-framing`, and :ref:`listener-tcp-frame-delimiter`. Using 
+these two arguments you can optimise the communication between the syslog 
+server and napalm-logs, by using increased sized windows and sending multiple 
+syslog messages at once (see :ref:`listener-tcp-buffer-size`).
+
+ISO8601 date-time format is now supported by the Junos and Arista parsers.
+
+Improved global date-time parsing: `ec59dd8 
+<https://github.com/napalm-automation/napalm-logs/commit/ec59dd84bede1a5a8656f6949637dace2fb93211>`_ 
+and `e2785c9 
+<https://github.com/napalm-automation/napalm-logs/commit/e2785c93e67ba378b84eb3fbf63caf9f9933749a>`_ 
+(obsoleted by `47605f9 
+<https://github.com/napalm-automation/napalm-logs/commit/47605f9>`_). With 
+these changes, `dateutil` library is now a dependency.
+
+The :ref:`publisher-alerta` added in the previous release, 
+:ref:`release-0.9.0`, now provides a new option, :ref:`publisher-alerta-pairs`.
+
+New Structured Messages
+-----------------------
+
+There are several new structured messages added for Junos:
+
+- :ref:`BGP_INCORRECT_AS_NUMBER`
+- :ref:`BGP_CONNECTION_REJECTED`
+- :ref:`BGP_CONNECTION_RESET`
+- :ref:`BGP_CEASE_PREFIX_LIMIT_EXCEEDED`
+- :ref:`PROCESS_CEILING_WATERMARK_REACHED`
+- :ref:`LACP_INTERFACE_DOWN`
+- :ref:`SYSTEM_ALARM_CLEARED` (as a counter-part of the existing 
+  :ref:`SYSTEM_ALARM`)
+- :ref:`DDOS_PROTOCOL_VIOLATION_SET`
+- :ref:`MAJOR_ALARM_SET` and :ref:`MAJOR_ALARM_CLEARED`, as well as 
+  :ref:`MINOR_ALARM_SET` and :ref:`MINOR_ALARM_CLEARED` notification pairs.
+
+Internal Changes and Documentation
+----------------------------------
+
+The CI has been switched to using GitHub Actions, and this together with the 
+new documentation for :ref:`local_testing` added by `@Ichabond 
+<https://github.com/Ichabond>`_ should improve the contributing experience.
diff --git a/docs/releases/index.rst b/docs/releases/index.rst
index 7798f355..6dbe21af 100644
--- a/docs/releases/index.rst
+++ b/docs/releases/index.rst
@@ -8,11 +8,12 @@ Release Notes
 Latest Release
 ^^^^^^^^^^^^^^
 
-- :ref:`release-0.9.0`
+- :ref:`release-0.10.0`
 
 Previous Releases
 ^^^^^^^^^^^^^^^^^
 
+- :ref:`release-0.9.0`
 - :ref:`release-0.8.0`
 - :ref:`release-0.7.0`
 - :ref:`release-0.6.0`
diff --git a/docs/requirements.txt b/docs/requirements.txt
index a34c2a94..0d249e73 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,9 +1,6 @@
+-r ../requirements.txt
 jinja2
-pyzmq
-pyyaml
-pynacl
 u-msgpack-python
 sphinx
 sphinx-rtd-theme
 sphinxcontrib-napoleon
-prometheus_client
diff --git a/examples/client_auth.py b/examples/client_auth.py
index 93e203f3..5ee4d4c3 100644
--- a/examples/client_auth.py
+++ b/examples/client_auth.py
@@ -18,22 +18,21 @@
 import napalm_logs.utils
 
 server_address = '127.0.0.1'  # --publish-address
-server_port = 49017           # --publish-port
-auth_address = '127.0.0.1'    # --auth-address
-auth_port = 49018             # --auth-port
+server_port = 49017  # --publish-port
+auth_address = '127.0.0.1'  # --auth-address
+auth_port = 49018  # --auth-port
 
 certificate = '/var/cache/napalm-logs.crt'  # --certificate
 
 # Using zmq
 context = zmq.Context()
 socket = context.socket(zmq.SUB)
-socket.connect('tcp://{address}:{port}'.format(address=server_address,
-                                               port=server_port))
+socket.connect(
+    'tcp://{address}:{port}'.format(address=server_address, port=server_port)
+)
 socket.setsockopt(zmq.SUBSCRIBE, '')
 
-auth = napalm_logs.utils.ClientAuth(certificate,
-                                    address=auth_address,
-                                    port=auth_port)
+auth = napalm_logs.utils.ClientAuth(certificate, address=auth_address, port=auth_port)
 
 while True:
     raw_object = socket.recv()
diff --git a/examples/client_no_auth.py b/examples/client_no_auth.py
index 690d4e49..f17cbf48 100644
--- a/examples/client_no_auth.py
+++ b/examples/client_no_auth.py
@@ -16,13 +16,14 @@
 import json
 
 server_address = '127.0.0.1'  # --publish-address
-server_port = 49017           # --publish-port
+server_port = 49017  # --publish-port
 
 # Using zmq
 context = zmq.Context()
 socket = context.socket(zmq.SUB)
-socket.connect('tcp://{address}:{port}'.format(address=server_address,
-                                               port=server_port))
+socket.connect(
+    'tcp://{address}:{port}'.format(address=server_address, port=server_port)
+)
 socket.setsockopt(zmq.SUBSCRIBE, b'')
 
 while True:
diff --git a/logo.png b/logo.png
index 8ab18553..1097ac39 100644
Binary files a/logo.png and b/logo.png differ
diff --git a/napalm_logs/auth.py b/napalm_logs/auth.py
index 557e7064..db334f54 100644
--- a/napalm_logs/auth.py
+++ b/napalm_logs/auth.py
@@ -24,6 +24,7 @@
 from napalm_logs.config import AUTH_KEEP_ALIVE_ACK
 from napalm_logs.exceptions import BindException
 from napalm_logs.exceptions import SSLMismatchException
+
 # exceptions
 from napalm_logs.exceptions import NapalmLogsExit
 
@@ -54,13 +55,16 @@ class NapalmLogsAuthProc(NapalmLogsProc):
         |                               |
         | <------------ ACK ----------- |
     '''
-    def __init__(self,
-                 certificate,
-                 keyfile,
-                 private_key,
-                 signature_hex,
-                 auth_address=AUTH_ADDRESS,
-                 auth_port=AUTH_PORT):
+
+    def __init__(
+        self,
+        certificate,
+        keyfile,
+        private_key,
+        signature_hex,
+        auth_address=AUTH_ADDRESS,
+        auth_port=AUTH_PORT,
+    ):
         self.certificate = certificate
         self.keyfile = keyfile
         self.__key = private_key
@@ -124,10 +128,13 @@ def verify_cert(self):
         '''
         Checks that the provided cert and key are valid and usable
         '''
-        log.debug('Verifying the %s certificate, keyfile: %s',
-                  self.certificate, self.keyfile)
+        log.debug(
+            'Verifying the %s certificate, keyfile: %s', self.certificate, self.keyfile
+        )
         try:
-            ssl.create_default_context().load_cert_chain(self.certificate, keyfile=self.keyfile)
+            ssl.create_default_context().load_cert_chain(
+                self.certificate, keyfile=self.keyfile
+            )
         except ssl.SSLError:
             error_string = 'SSL certificate and key do not match'
             log.error(error_string)
@@ -149,7 +156,9 @@ def _create_skt(self):
         try:
             self.socket.bind((self.auth_address, self.auth_port))
         except socket.error as msg:
-            error_string = 'Unable to bind (auth) to port {} on {}: {}'.format(self.auth_port, self.auth_address, msg)
+            error_string = 'Unable to bind (auth) to port {} on {}: {}'.format(
+                self.auth_port, self.auth_address, msg
+            )
             log.error(error_string, exc_info=True)
             raise BindException(error_string)
 
@@ -162,19 +171,26 @@ def start(self):
         log.debug('Starting the auth process')
         self.verify_cert()
         self._create_skt()
-        log.debug('The auth process can receive at most %d parallel connections', AUTH_MAX_CONN)
+        log.debug(
+            'The auth process can receive at most %d parallel connections',
+            AUTH_MAX_CONN,
+        )
         self.socket.listen(AUTH_MAX_CONN)
-        thread = threading.Thread(target=self._suicide_when_without_parent, args=(os.getppid(),))
+        thread = threading.Thread(
+            target=self._suicide_when_without_parent, args=(os.getppid(),)
+        )
         thread.start()
         signal.signal(signal.SIGTERM, self._exit_gracefully)
         self.__up = True
         while self.__up:
             try:
                 (clientsocket, address) = self.socket.accept()
-                wrapped_auth_skt = ssl.wrap_socket(clientsocket,
-                                                   server_side=True,
-                                                   certfile=self.certificate,
-                                                   keyfile=self.keyfile)
+                wrapped_auth_skt = ssl.wrap_socket(
+                    clientsocket,
+                    server_side=True,
+                    certfile=self.certificate,
+                    keyfile=self.keyfile,
+                )
             except ssl.SSLError:
                 log.exception('SSL error', exc_info=True)
                 continue
@@ -187,8 +203,9 @@ def start(self):
                     raise NapalmLogsExit(msg)
             log.info('%s connected', address)
             log.debug('Starting the handshake')
-            client_thread = threading.Thread(target=self._handshake,
-                                             args=(wrapped_auth_skt, address))
+            client_thread = threading.Thread(
+                target=self._handshake, args=(wrapped_auth_skt, address)
+            )
             client_thread.start()
 
     def stop(self):
diff --git a/napalm_logs/base.py b/napalm_logs/base.py
index 101348a7..7cffff29 100644
--- a/napalm_logs/base.py
+++ b/napalm_logs/base.py
@@ -27,6 +27,7 @@
 import napalm_logs.utils
 import napalm_logs.config as CONFIG
 import napalm_logs.buffer
+
 # processes
 from napalm_logs.auth import NapalmLogsAuthProc
 from napalm_logs.device import NapalmLogsDeviceProc
@@ -34,6 +35,7 @@
 from napalm_logs.publisher import NapalmLogsPublisherProc
 from napalm_logs.listener_proc import NapalmLogsListenerProc
 from napalm_logs.pub_proxy import NapalmLogsPublisherProxy
+
 # exceptions
 from napalm_logs.exceptions import ConfigurationException
 
@@ -41,34 +43,37 @@
 
 
 class NapalmLogs:
-    def __init__(self,
-                 address='0.0.0.0',
-                 port=514,
-                 listener='udp',
-                 publisher='zmq',
-                 publish_address='0.0.0.0',
-                 publish_port=49017,
-                 auth_address='0.0.0.0',
-                 auth_port=49018,
-                 metrics_enabled=False,
-                 metrics_address='0.0.0.0',
-                 metrics_port='9215',
-                 metrics_dir='/tmp/napalm_logs_metrics',
-                 certificate=None,
-                 keyfile=None,
-                 disable_security=False,
-                 config_path=None,
-                 config_dict=None,
-                 extension_config_path=None,
-                 extension_config_dict=None,
-                 log_level='warning',
-                 log_format='%(asctime)s,%(msecs)03.0f [%(name)-17s][%(levelname)-8s] %(message)s',
-                 device_blacklist=[],
-                 device_whitelist=[],
-                 hwm=None,
-                 device_worker_processes=1,
-                 serializer='msgpack',
-                 buffer=None):
+    def __init__(
+        self,
+        address='0.0.0.0',
+        port=514,
+        listener='udp',
+        publisher='zmq',
+        publish_address='0.0.0.0',
+        publish_port=49017,
+        auth_address='0.0.0.0',
+        auth_port=49018,
+        metrics_enabled=False,
+        metrics_address='0.0.0.0',
+        metrics_port='9215',
+        metrics_dir='/tmp/napalm_logs_metrics',
+        certificate=None,
+        keyfile=None,
+        disable_security=False,
+        config_path=None,
+        config_dict=None,
+        extension_config_path=None,
+        extension_config_dict=None,
+        log_level='warning',
+        log_format='%(asctime)s,%(msecs)03.0f [%(name)-17s][%(levelname)-8s] %(message)s',
+        device_blacklist=[],
+        device_whitelist=[],
+        hwm=None,
+        device_worker_processes=1,
+        serializer='msgpack',
+        buffer=None,
+        opts=None,
+    ):
         '''
         Init the napalm-logs engine.
 
@@ -107,7 +112,7 @@ def __init__(self,
         self.hwm = hwm
         self._buffer_cfg = buffer
         self._buffer = None
-        self.opts = {}
+        self.opts = opts if opts else {}
         # Setup the environment
         self._setup_log()
         self._build_config()
@@ -171,9 +176,7 @@ def _setup_metrics(self):
             registry = CollectorRegistry()
             multiprocess.MultiProcessCollector(registry)
             start_http_server(
-                port=self.metrics_port,
-                addr=self.metrics_address,
-                registry=registry
+                port=self.metrics_port, addr=self.metrics_address, registry=registry
             )
 
     def _setup_log(self):
@@ -181,8 +184,7 @@ def _setup_log(self):
         Setup the log object.
         '''
         logging_level = CONFIG.LOGGING_LEVEL.get(self.log_level.lower())
-        logging.basicConfig(format=self.log_format,
-                            level=logging_level)
+        logging.basicConfig(format=self.log_format, level=logging_level)
 
     def _post_preparation(self):
         '''
@@ -216,7 +218,9 @@ def _post_preparation(self):
                 error_blacklist.remove('UNKNOWN')
             if 'send_raw' in pub_opts and 'RAW' in error_blacklist:
                 error_blacklist.remove('RAW')
-            self.opts['_server_send_unknown'] |= 'UNKNOWN' in error_whitelist or 'UNKNOWN' not in error_blacklist
+            self.opts['_server_send_unknown'] |= (
+                'UNKNOWN' in error_whitelist or 'UNKNOWN' not in error_blacklist
+            )
             pub[pub_name]['error_whitelist'] = error_whitelist
             pub[pub_name]['error_blacklist'] = error_blacklist
 
@@ -226,9 +230,9 @@ def _whitelist_blacklist(self, os_name):
         depending on the whitelist-blacklist logic
         configured by the user.
         '''
-        return napalm_logs.ext.check_whitelist_blacklist(os_name,
-                                                         whitelist=self.device_whitelist,
-                                                         blacklist=self.device_blacklist)
+        return napalm_logs.ext.check_whitelist_blacklist(
+            os_name, whitelist=self.device_whitelist, blacklist=self.device_blacklist
+        )
 
     @staticmethod
     def _extract_yaml_docstring(stream):
@@ -244,9 +248,7 @@ def _extract_yaml_docstring(stream):
             if not line_strip:
                 continue
             if line_strip.startswith('#'):
-                comment_lines.append(
-                    line_strip.replace('#', '', 1).strip()
-                )
+                comment_lines.append(line_strip.replace('#', '', 1).strip())
             else:
                 break
         return ' '.join(comment_lines)
@@ -260,8 +262,7 @@ def _load_config(self, path):
         log.debug('Reading configuration from %s', path)
         if not os.path.isdir(path):
             msg = (
-                'Unable to read from {path}: '
-                'the directory does not exist!'
+                'Unable to read from {path}: ' 'the directory does not exist!'
             ).format(path=path)
             log.error(msg)
             raise IOError(msg)
@@ -287,13 +288,15 @@ def _load_config(self, path):
                 log.debug('Ignoring %s', os_name)
                 continue
             if not self._whitelist_blacklist(os_name):
-                log.debug('Not building config for %s (whitelist-blacklist logic)', os_name)
+                log.debug(
+                    'Not building config for %s (whitelist-blacklist logic)', os_name
+                )
                 # Ignore devices that are not in the whitelist (if defined),
                 #   or those operating systems that are on the blacklist.
                 # This way we can prevent starting unwanted sub-processes.
                 continue
             log.debug('Building config for %s:', os_name)
-            log.debug('='*40)
+            log.debug('=' * 40)
             if os_name not in config:
                 config[os_name] = {}
             files = os.listdir(os_dir)
@@ -308,7 +311,7 @@ def _load_config(self, path):
                     try:
                         log.debug('Loading %s as YAML', file_)
                         with open(filepath, 'r') as fstream:
-                            cfg = yaml.load(fstream)
+                            cfg = yaml.load(fstream, Loader=yaml.FullLoader)
                             # Reposition at the top and read the comments.
                             if file_name not in CONFIG.OS_INIT_FILENAMES:
                                 # If the file name is not a profile init.
@@ -335,34 +338,51 @@ def _load_config(self, path):
                         # Sample init file:
                         # def extract(message):
                         #     return {'tag': 'A_TAG', 'host': 'hostname'}
-                        if hasattr(mod, CONFIG.INIT_RUN_FUN) and\
-                           hasattr(getattr(mod, CONFIG.INIT_RUN_FUN), '__call__'):
+                        if hasattr(mod, CONFIG.INIT_RUN_FUN) and hasattr(
+                            getattr(mod, CONFIG.INIT_RUN_FUN), '__call__'
+                        ):
                             # if extract is defined and is callable
                             if 'prefixes' not in config[os_name]:
                                 config[os_name]['prefixes'] = []
-                            config[os_name]['prefixes'].append({
-                                'values': {'tag': ''},
-                                'line': '',
-                                '__python_fun__': getattr(mod, CONFIG.INIT_RUN_FUN),
-                                '__python_mod__': filepath  # Will be used for debugging
-                            })
-                            log.info('Adding the prefix function defined under %s to %s',
-                                     filepath, os_name)
+                            config[os_name]['prefixes'].append(
+                                {
+                                    'values': {'tag': ''},
+                                    'line': '',
+                                    '__python_fun__': getattr(mod, CONFIG.INIT_RUN_FUN),
+                                    '__python_mod__': filepath,  # Will be used for debugging
+                                }
+                            )
+                            log.info(
+                                'Adding the prefix function defined under %s to %s',
+                                filepath,
+                                os_name,
+                            )
                         elif file_name != '__init__':
                             # If __init__.py does not have the extractor function, no problem.
-                            log.warning('%s does not have the "%s" function defined. Ignoring.',
-                                        filepath, CONFIG.INIT_RUN_FUN)
+                            log.warning(
+                                '%s does not have the "%s" function defined. Ignoring.',
+                                filepath,
+                                CONFIG.INIT_RUN_FUN,
+                            )
                     else:
                         # Other python files require the `emit` function.
                         if hasattr(mod, '__tag__'):
                             mod_tag = getattr(mod, '__tag__')
                         else:
-                            log.info('%s does not have __tag__, defaulting the tag to %s', filepath, file_name)
+                            log.info(
+                                '%s does not have __tag__, defaulting the tag to %s',
+                                filepath,
+                                file_name,
+                            )
                             mod_tag = file_name
                         if hasattr(mod, '__error__'):
                             mod_err = getattr(mod, '__error__')
                         else:
-                            log.info('%s does not have __error__, defaulting the error to %s', filepath, file_name)
+                            log.info(
+                                '%s does not have __error__, defaulting the error to %s',
+                                filepath,
+                                file_name,
+                            )
                             mod_err = file_name
                         if hasattr(mod, '__match_on__'):
                             err_match = getattr(mod, '__match_on__')
@@ -372,33 +392,48 @@ def _load_config(self, path):
                         if hasattr(mod, '__yang_model__'):
                             model = getattr(mod, '__yang_model__')
                         log.debug('Mathing on %s', err_match)
-                        if hasattr(mod, CONFIG.CONFIG_RUN_FUN) and\
-                           hasattr(getattr(mod, CONFIG.CONFIG_RUN_FUN), '__call__'):
-                            log.debug('Adding %s with tag:%s, error:%s, matching on:%s',
-                                      file_, mod_tag, mod_err, err_match)
+                        if hasattr(mod, CONFIG.CONFIG_RUN_FUN) and hasattr(
+                            getattr(mod, CONFIG.CONFIG_RUN_FUN), '__call__'
+                        ):
+                            log.debug(
+                                'Adding %s with tag:%s, error:%s, matching on:%s',
+                                file_,
+                                mod_tag,
+                                mod_err,
+                                err_match,
+                            )
                             # the structure below must correspond to the VALID_CONFIG structure enforcement
                             if 'messages' not in config[os_name]:
                                 config[os_name]['messages'] = []
-                            config[os_name]['messages'].append({
-                                'tag': mod_tag,
-                                'error': mod_err,
-                                'match_on': err_match,
-                                '__doc__': mod.__doc__,
-                                '__python_fun__': getattr(mod, CONFIG.CONFIG_RUN_FUN),
-                                '__python_mod__': filepath,  # Will be used for debugging
-                                'line': '',
-                                'model': model,
-                                'values': {},
-                                'mapping': {'variables': {}, 'static': {}}
-                            })
+                            config[os_name]['messages'].append(
+                                {
+                                    'tag': mod_tag,
+                                    'error': mod_err,
+                                    'match_on': err_match,
+                                    '__doc__': mod.__doc__,
+                                    '__python_fun__': getattr(
+                                        mod, CONFIG.CONFIG_RUN_FUN
+                                    ),
+                                    '__python_mod__': filepath,  # Will be used for debugging
+                                    'line': '',
+                                    'model': model,
+                                    'values': {},
+                                    'mapping': {'variables': {}, 'static': {}},
+                                }
+                            )
                         else:
-                            log.warning('%s does not have the "%s" function defined. Ignoring.',
-                                        filepath, CONFIG.CONFIG_RUN_FUN)
+                            log.warning(
+                                '%s does not have the "%s" function defined. Ignoring.',
+                                filepath,
+                                CONFIG.CONFIG_RUN_FUN,
+                            )
                 else:
                     log.info('Ignoring %s (extension not allowed)', filepath)
-            log.debug('-'*40)
+            log.debug('-' * 40)
         if not config:
-            msg = 'Could not find proper configuration files under {path}'.format(path=path)
+            msg = 'Could not find proper configuration files under {path}'.format(
+                path=path
+            )
             log.error(msg)
             raise IOError(msg)
         log.debug('Complete config:')
@@ -412,23 +447,22 @@ def _raise_config_exception(error_string):
         raise ConfigurationException(error_string)
 
     def _compare_values(self, value, config, dev_os, key_path):
-        if 'line' not in value or\
-           'values' not in value or\
-           '__python_fun__' not in value:  # Check looks good when using a Python-defined profile.
+        if (
+            'line' not in value
+            or 'values' not in value
+            or '__python_fun__' not in value
+        ):  # Check looks good when using a Python-defined profile.
             return
         from_line = re.findall(r'\{(\w+)\}', config['line'])
         if set(from_line) == set(config['values']):
             return
         if config.get('error'):
             error = 'The "values" do not match variables in "line" for {}:{} in {}'.format(
-                ':'.join(key_path),
-                config.get('error'),
-                dev_os
+                ':'.join(key_path), config.get('error'), dev_os
             )
         else:
             error = 'The "values" do not match variables in "line" for {} in {}'.format(
-                ':'.join(key_path),
-                dev_os
+                ':'.join(key_path), dev_os
             )
         self._raise_config_exception(error)
 
@@ -436,15 +470,22 @@ def _verify_config_key(self, key, value, valid, config, dev_os, key_path):
         key_path.append(key)
         if config.get(key, False) is False:
             self._raise_config_exception(
-                'Unable to find key "{}" for {}'.format(':'.join(key_path), dev_os))
+                'Unable to find key "{}" for {}'.format(':'.join(key_path), dev_os)
+            )
         if isinstance(value, type):
             if not isinstance(config[key], value):
                 self._raise_config_exception(
-                    'Key "{}" for {} should be {}'.format(':'.join(key_path), dev_os, value))
+                    'Key "{}" for {} should be {}'.format(
+                        ':'.join(key_path), dev_os, value
+                    )
+                )
         elif isinstance(value, dict):
             if not isinstance(config[key], dict):
                 self._raise_config_exception(
-                    'Key "{}" for {} should be of type <dict>'.format(':'.join(key_path), dev_os))
+                    'Key "{}" for {} should be of type <dict>'.format(
+                        ':'.join(key_path), dev_os
+                    )
+                )
             self._verify_config_dict(value, config[key], dev_os, key_path)
             # As we have already checked that the config below this point is correct, we know that "line" and "values"
             # exists in the config if they are present in the valid config
@@ -452,7 +493,10 @@ def _verify_config_key(self, key, value, valid, config, dev_os, key_path):
         elif isinstance(value, list):
             if not isinstance(config[key], list):
                 self._raise_config_exception(
-                    'Key "{}" for {} should be of type <list>'.format(':'.join(key_path), dev_os))
+                    'Key "{}" for {} should be of type <list>'.format(
+                        ':'.join(key_path), dev_os
+                    )
+                )
             for item in config[key]:
                 self._verify_config_dict(value[0], item, dev_os, key_path)
                 self._compare_values(value[0], item, dev_os, key_path)
@@ -491,20 +535,26 @@ def _build_config(self):
                 # No custom config path requested
                 # Read the native config files
                 self.config_path = os.path.join(
-                    os.path.dirname(os.path.realpath(__file__)),
-                    'config'
+                    os.path.dirname(os.path.realpath(__file__)), 'config'
                 )
             log.info('Reading the configuration from %s', self.config_path)
             self.config_dict = self._load_config(self.config_path)
-        if not self.extension_config_dict and\
-           self.extension_config_path and\
-           os.path.normpath(self.extension_config_path) != os.path.normpath(self.config_path):  # same path?
+        if (
+            not self.extension_config_dict
+            and self.extension_config_path
+            and os.path.normpath(self.extension_config_path)
+            != os.path.normpath(self.config_path)
+        ):  # same path?
             # When extension config is not sent as dict
             # But `extension_config_path` is specified
-            log.info('Reading extension configuration from %s', self.extension_config_path)
+            log.info(
+                'Reading extension configuration from %s', self.extension_config_path
+            )
             self.extension_config_dict = self._load_config(self.extension_config_path)
         if self.extension_config_dict:
-            napalm_logs.utils.dictupdate(self.config_dict, self.extension_config_dict)  # deep merge
+            napalm_logs.utils.dictupdate(
+                self.config_dict, self.extension_config_dict
+            )  # deep merge
 
     def _start_auth_proc(self):
         '''
@@ -514,46 +564,46 @@ def _start_auth_proc(self):
         verify_key = self.__signing_key.verify_key
         sgn_verify_hex = verify_key.encode(encoder=nacl.encoding.HexEncoder)
         log.debug('Starting the authenticator subprocess')
-        auth = NapalmLogsAuthProc(self.certificate,
-                                  self.keyfile,
-                                  self.__priv_key,
-                                  sgn_verify_hex,
-                                  self.auth_address,
-                                  self.auth_port)
+        auth = NapalmLogsAuthProc(
+            self.certificate,
+            self.keyfile,
+            self.__priv_key,
+            sgn_verify_hex,
+            self.auth_address,
+            self.auth_port,
+        )
         proc = Process(target=auth.start)
         proc.start()
         proc.description = 'Auth process'
         log.debug('Started auth process as %s with PID %s', proc._name, proc.pid)
         return proc
 
-    def _start_lst_proc(self,
-                        listener_type,
-                        listener_opts):
+    def _start_lst_proc(self, listener_type, listener_opts):
         '''
         Start the listener process.
         '''
         log.debug('Starting the listener process for %s', listener_type)
-        listener = NapalmLogsListenerProc(self.opts,
-                                          self.address,
-                                          self.port,
-                                          listener_type,
-                                          listener_opts=listener_opts)
+        listener = NapalmLogsListenerProc(
+            self.opts,
+            self.address,
+            self.port,
+            listener_type,
+            listener_opts=listener_opts,
+        )
         proc = Process(target=listener.start)
         proc.start()
         proc.description = 'Listener process'
         log.debug('Started listener process as %s with PID %s', proc._name, proc.pid)
         return proc
 
-    def _start_srv_proc(self,
-                        started_os_proc):
+    def _start_srv_proc(self, started_os_proc):
         '''
         Start the server process.
         '''
         log.debug('Starting the server process')
-        server = NapalmLogsServerProc(self.opts,
-                                      self.config_dict,
-                                      started_os_proc,
-                                      buffer=self._buffer)
+        server = NapalmLogsServerProc(
+            self.opts, self.config_dict, started_os_proc, buffer=self._buffer
+        )
         proc = Process(target=server.start)
         proc.start()
         proc.description = 'Server process'
@@ -570,44 +620,44 @@ def _start_pub_px_proc(self):
         log.debug('Started pub proxy as %s with PID %s', proc._name, proc.pid)
         return proc
 
-    def _start_pub_proc(self,
-                        publisher_type,
-                        publisher_opts,
-                        pub_id):
+    def _start_pub_proc(self, publisher_type, publisher_opts, pub_id):
         '''
         Start the publisher process.
         '''
         log.debug('Starting the publisher process for %s', publisher_type)
-        publisher = NapalmLogsPublisherProc(self.opts,
-                                            self.publish_address,
-                                            self.publish_port,
-                                            publisher_type,
-                                            self.serializer,
-                                            self.__priv_key,
-                                            self.__signing_key,
-                                            publisher_opts,
-                                            disable_security=self.disable_security,
-                                            pub_id=pub_id)
+        publisher = NapalmLogsPublisherProc(
+            self.opts,
+            self.publish_address,
+            self.publish_port,
+            publisher_type,
+            self.serializer,
+            self.__priv_key,
+            self.__signing_key,
+            publisher_opts,
+            disable_security=self.disable_security,
+            pub_id=pub_id,
+        )
         proc = Process(target=publisher.start)
         proc.start()
         proc.description = 'Publisher process'
         log.debug('Started publisher process as %s with PID %s', proc._name, proc.pid)
         return proc
 
-    def _start_dev_proc(self,
-                        device_os,
-                        device_config):
+    def _start_dev_proc(self, device_os, device_config):
         '''
         Start the device worker process.
         '''
         log.info('Starting the child process for %s', device_os)
-        dos = NapalmLogsDeviceProc(device_os,
-                                   self.opts,
-                                   device_config)
+        dos = NapalmLogsDeviceProc(device_os, self.opts, device_config)
         os_proc = Process(target=dos.start)
         os_proc.start()
         os_proc.description = '%s device process' % device_os
-        log.debug('Started process %s for %s, having PID %s', os_proc._name, device_os, os_proc.pid)
+        log.debug(
+            'Started process %s for %s, having PID %s',
+            os_proc._name,
+            device_os,
+            os_proc.pid,
+        )
         return os_proc
 
     def start_engine(self):
@@ -615,7 +665,9 @@ def start_engine(self):
         Start the child processes (one per device OS)
         '''
         if self.disable_security is True:
-            log.warning('***Not starting the authenticator process due to disable_security being set to True***')
+            log.warning(
+                '***Not starting the authenticator process due to disable_security being set to True***'
+            )
         else:
             log.debug('Generating the private key')
             self.__priv_key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
@@ -630,9 +682,7 @@ def start_engine(self):
         pub_id = 0
         for pub in self.publisher:
             publisher_type, publisher_opts = list(pub.items())[0]
-            proc = self._start_pub_proc(publisher_type,
-                                        publisher_opts,
-                                        pub_id)
+            proc = self._start_pub_proc(publisher_type, publisher_opts, pub_id)
             self._processes.append(proc)
             pub_id += 1
         # device process start
@@ -640,23 +690,27 @@ def start_engine(self):
         started_os_proc = []
         for device_os, device_config in self.config_dict.items():
             if not self._whitelist_blacklist(device_os):
-                log.debug('Not starting process for %s (whitelist-blacklist logic)', device_os)
+                log.debug(
+                    'Not starting process for %s (whitelist-blacklist logic)', device_os
+                )
                 # Ignore devices that are not in the whitelist (if defined),
                 #   or those operating systems that are on the blacklist.
                 # This way we can prevent starting unwanted sub-processes.
                 continue
-            log.debug('Will start %d worker process(es) for %s', self.device_worker_processes, device_os)
+            log.debug(
+                'Will start %d worker process(es) for %s',
+                self.device_worker_processes,
+                device_os,
+            )
             for proc_index in range(self.device_worker_processes):
-                self._processes.append(self._start_dev_proc(device_os,
-                                                            device_config))
+                self._processes.append(self._start_dev_proc(device_os, device_config))
             started_os_proc.append(device_os)
         # start the server process
         self._processes.append(self._start_srv_proc(started_os_proc))
         # start listener process
         for lst in self.listener:
             listener_type, listener_opts = list(lst.items())[0]
-            proc = self._start_lst_proc(listener_type,
-                                        listener_opts)
+            proc = self._start_lst_proc(listener_type, listener_opts)
             self._processes.append(proc)
         thread = threading.Thread(target=self._check_children)
         thread.start()
@@ -670,7 +724,9 @@ def _check_children(self):
             for process in self._processes:
                 if process.is_alive() is True:
                     continue
-                log.debug('%s is dead. Stopping the napalm-logs engine.', process.description)
+                log.debug(
+                    '%s is dead. Stopping the napalm-logs engine.', process.description
+                )
                 self.stop_engine()
 
     def stop_engine(self):
diff --git a/napalm_logs/buffer/__init__.py b/napalm_logs/buffer/__init__.py
index 9b511370..953cfbe6 100644
--- a/napalm_logs/buffer/__init__.py
+++ b/napalm_logs/buffer/__init__.py
@@ -21,7 +21,7 @@
     'mem': MemoryBuffer,
     'memory': MemoryBuffer,
     'cache': MemoryBuffer,
-    'redis': RedisBuffer
+    'redis': RedisBuffer,
 }
 
 
@@ -38,6 +38,4 @@ def get_interface(name):
         raise InvalidBufferException(msg)
 
 
-__all__ = (
-    'get_interface',
-)
+__all__ = ('get_interface',)
diff --git a/napalm_logs/buffer/memory.py b/napalm_logs/buffer/memory.py
index ac0c24b0..4d646923 100644
--- a/napalm_logs/buffer/memory.py
+++ b/napalm_logs/buffer/memory.py
@@ -16,6 +16,7 @@ class MemoryBuffer(object):
     '''
     Memory buffer class.
     '''
+
     def __init__(self, expire_time, **kwargs):
         self.expire_time = expire_time
         self.expire_time_delta = datetime.timedelta(0, expire_time, 0)
@@ -23,10 +24,7 @@ def __init__(self, expire_time, **kwargs):
         self._cache = {}
 
     def __setitem__(self, key, val):
-        self._cache[key] = {
-            'data': val,
-            'timestamp': datetime.datetime.utcnow()
-        }
+        self._cache[key] = {'data': val, 'timestamp': datetime.datetime.utcnow()}
 
     def __contains__(self, key):
         return True if key in self._cache else False
diff --git a/napalm_logs/buffer/redisbuf.py b/napalm_logs/buffer/redisbuf.py
index 4647bc42..024d1f10 100644
--- a/napalm_logs/buffer/redisbuf.py
+++ b/napalm_logs/buffer/redisbuf.py
@@ -10,6 +10,7 @@
 
 try:
     import redis
+
     HAS_REDIS = True
 except ImportError:
     HAS_REDIS = False
@@ -22,6 +23,7 @@ class RedisBuffer(object):
     '''
     Memory buffer class.
     '''
+
     def __init__(self, expire_time, **kwargs):
         self.expire_time = expire_time
         # expire_time is assumed to be in seconds
diff --git a/napalm_logs/config/__init__.py b/napalm_logs/config/__init__.py
index 7adf36bb..6be10002 100644
--- a/napalm_logs/config/__init__.py
+++ b/napalm_logs/config/__init__.py
@@ -36,11 +36,7 @@
 BUFFER_EXPIRE_TIME = 5
 
 # Allowed names for the init files.
-OS_INIT_FILENAMES = (
-    '__init__',
-    'init',
-    'index'
-)
+OS_INIT_FILENAMES = ('__init__', 'init', 'index')
 # The name of the function to be invoked when extracting the parts from the
 #   raw syslog message.
 INIT_RUN_FUN = 'extract'
@@ -50,36 +46,22 @@
 
 UNKNOWN_DEVICE_NAME = 'unknown'
 
-LISTENER_OPTS = {
-}
+LISTENER_OPTS = {}
 
-LOGGER_OPTS = {
-    'send_raw': False,
-    'send_unknown': False
-}
+LOGGER_OPTS = {'send_raw': False, 'send_unknown': False}
 
-PUBLISHER_OPTS = {
-    'send_raw': False,
-    'send_unknown': False
-}
+PUBLISHER_OPTS = {'send_raw': False, 'send_unknown': False}
 
 LOGGING_LEVEL = {
     'debug': logging.DEBUG,
     'info': logging.INFO,
     'warning': logging.WARNING,
     'error': logging.ERROR,
-    'critical': logging.CRITICAL
+    'critical': logging.CRITICAL,
 }
 
 VALID_CONFIG = {
-    'prefixes': [
-        {
-            'values': {
-                'tag': six.string_type
-            },
-            'line': six.string_type
-        }
-    ],
+    'prefixes': [{'values': {'tag': six.string_type}, 'line': six.string_type}],
     'messages': [
         {
             # 'error' should be unique and vendor agnostic.
@@ -90,12 +72,9 @@
             'values': dict,
             'line': six.string_type,
             'model': six.string_type,
-            'mapping': {
-                'variables': dict,
-                'static': dict
-            }
+            'mapping': {'variables': dict, 'static': dict},
         }
-    ]
+    ],
 }
 
 # listener
diff --git a/napalm_logs/config/eos/init.yml b/napalm_logs/config/eos/init.yml
index 5e8dbee0..5c1e9b6f 100644
--- a/napalm_logs/config/eos/init.yml
+++ b/napalm_logs/config/eos/init.yml
@@ -12,4 +12,11 @@ prefixes:
       processName: (\w+)
       tag: ([\w-]+)
     line: '{date} {time} {host} {processName}: %{tag}'
-
+  # ISO8601 date-time format
+  - values:
+      date: (\d{4}-\d{2}-\d{2})
+      time: (\d{2}:\d{2}:\d{2}[\.\d{3}]?[\+|-]\d{2}:\d{2})
+      host: ([^ ]+)
+      processName: (\w+)
+      tag: ([\w-]+)
+    line: '{date}T{time} {host} {processName}: %{tag}'
diff --git a/napalm_logs/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED.yml b/napalm_logs/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED.yml
new file mode 100644
index 00000000..701e3dce
--- /dev/null
+++ b/napalm_logs/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED.yml
@@ -0,0 +1,39 @@
+messages:
+  - error: BGP_CEASE_PREFIX_LIMIT_EXCEEDED
+    tag: BGP_CEASE_PREFIX_LIMIT_EXCEEDED
+    values:
+      peer: (\d+\.\d+\.\d+\.\d+)
+      asn: (\d+)
+      limit|int: (\d+)
+      current|int: (\d+)
+      table: (\w+)
+      type: (\w+)
+      peeringType: (\w+)
+      vrf: (\w+)
+    line: '{peer} ({peeringType} AS {asn}): Shutting down peer due to exceeding configured maximum prefix-limit({limit}) for {table}-{type} nlri: {current} (instance {vrf})'
+    model: openconfig-network-instance
+    mapping:
+      variables:
+        network-instances//network-instance//{vrf}//protocols//protocol//bgp//neighbors//neighbor//{peer}//state//peer_as: asn
+        network-instances//network-instance//{vrf}//protocols//protocol//bgp//neighbors//neighbor//{peer}//afi_safis//afi_safi//{table}//state//prefixes//received: current
+        network-instances//network-instance//{vrf}//protocols//protocol//bgp//neighbors//neighbor//{peer}//afi_safis//afi_safi//{table}//ipv4_{type}//prefix_limit//state//max_prefixes: limit
+      static: {}
+  - error: BGP_CEASE_PREFIX_LIMIT_EXCEEDED
+    tag: BGP_CEASE_PREFIX_LIMIT_EXCEEDED
+    values:
+      peer: ([\w\d:]+)
+      asn: (\d+)
+      limit|int: (\d+)
+      current|int: (\d+)
+      table: (\w+)
+      type: (\w+)
+      peeringType: (\w+)
+      vrf: (\w+)
+    line: '{peer} ({peeringType} AS {asn}): Shutting down peer due to exceeding configured maximum prefix-limit({limit}) for {table}-{type} nlri: {current} (instance {vrf})'
+    model: openconfig-network-instance
+    mapping:
+      variables:
+        network-instances//network-instance//{vrf}//protocols//protocol//bgp//neighbors//neighbor//{peer}//state//peer_as: asn
+        network-instances//network-instance//{vrf}//protocols//protocol//bgp//neighbors//neighbor//{peer}//afi_safis//afi_safi//{table}//state//prefixes//received: current
+        network-instances//network-instance//{vrf}//protocols//protocol//bgp//neighbors//neighbor//{peer}//afi_safis//afi_safi//{table}//ipv6_{type}//prefix_limit//state//max_prefixes: limit
+      static: {}
diff --git a/napalm_logs/config/junos/BGP_CONNECTION_REJECTED.yml b/napalm_logs/config/junos/BGP_CONNECTION_REJECTED.yml
new file mode 100644
index 00000000..151ef348
--- /dev/null
+++ b/napalm_logs/config/junos/BGP_CONNECTION_REJECTED.yml
@@ -0,0 +1,15 @@
+messages:
+  - error: BGP_CONNECTION_REJECTED
+    tag: bgp_handle_notify
+    values:
+      peer: ([\w\d:\.]+)
+      asn: (\d+)
+      peeringType: (\w+)
+      number: (\d+)
+    line: '{number}: NOTIFICATION received from {peer} ({peeringType} AS {asn}): code 6 (Cease) subcode 5 (Connection Rejected)'
+    model: openconfig-bgp
+    mapping:
+      variables:
+        bgp//neighbors//neighbor//{peer}//state//peer_as: asn
+      static:
+        bgp//neighbors//neighbor//{peer}//state//session_state: ACTIVE
diff --git a/napalm_logs/config/junos/BGP_CONNECTION_RESET.yml b/napalm_logs/config/junos/BGP_CONNECTION_RESET.yml
new file mode 100644
index 00000000..8e1df1e4
--- /dev/null
+++ b/napalm_logs/config/junos/BGP_CONNECTION_RESET.yml
@@ -0,0 +1,14 @@
+messages:
+  - error: BGP_CONNECTION_RESET
+    tag: bgp_recv
+    values:
+      peer: ([\w\d:\.]+)
+      asn: (\d+)
+      peeringType: (\w+)
+    line: 'read from peer {peer} ({peeringType} AS {asn}) failed: Connection reset by peer'
+    model: openconfig-bgp
+    mapping:
+      variables:
+        bgp//neighbors//neighbor//{peer}//state//peer_as: asn
+      static:
+        bgp//neighbors//neighbor//{peer}//state//session_state: ACTIVE
diff --git a/napalm_logs/config/junos/BGP_INCORRECT_AS_NUMBER.yml b/napalm_logs/config/junos/BGP_INCORRECT_AS_NUMBER.yml
new file mode 100644
index 00000000..be1757ed
--- /dev/null
+++ b/napalm_logs/config/junos/BGP_INCORRECT_AS_NUMBER.yml
@@ -0,0 +1,16 @@
+messages:
+  - error: BGP_INCORRECT_AS_NUMBER
+    tag: bgp_pp_recv
+    values:
+      peer: ([\w\d:\.]+)
+      asn: (\d+)
+      number: (\d+)
+      port: (\d+)
+      interface: (.+)
+    line: '{number}: NOTIFICATION sent to {peer}+{port} (proto): code 2 (Open Message Error) subcode 2 (bad peer AS number), Reason: no group for {peer}+{port} (proto) from AS {asn} found (peer as mismatch) in master({interface}), dropping him'
+    model: openconfig-bgp
+    mapping:
+      variables:
+        bgp//neighbors//neighbor//{peer}//state//peer_as: asn
+      static:
+        bgp//neighbors//neighbor//{peer}//state//session_state: OPENSENT
diff --git a/napalm_logs/config/junos/BGP_MD5_INCORRECT.yml b/napalm_logs/config/junos/BGP_MD5_INCORRECT.yml
index 6ab621a2..a25bd586 100644
--- a/napalm_logs/config/junos/BGP_MD5_INCORRECT.yml
+++ b/napalm_logs/config/junos/BGP_MD5_INCORRECT.yml
@@ -6,7 +6,7 @@ messages:
   - error: BGP_MD5_INCORRECT
     tag: tcp_auth_ok
     values:
-      peer: (\d+\.\d+\.\d+\.\d+)
+      peer: ([\w\d:\.]+)
       port: (\d+)
     line: 'Packet from {peer}:{port} wrong MD5 digest'
     model: openconfig-bgp
@@ -14,3 +14,14 @@ messages:
       variables: {}
       static:
         bgp//neighbors//neighbor//{peer}//state//session_state: CONNECT
+  - error: BGP_MD5_INCORRECT
+    tag: tcp_auth_ok
+    values:
+      peer: ([\w\d:\.]+)
+      port: (\d+)
+    line: 'Packet from {peer}:{port} missing MD5 digest'
+    model: openconfig-bgp
+    mapping:
+      variables: {}
+      static:
+        bgp//neighbors//neighbor//{peer}//state//session_state: ACTIVE
diff --git a/napalm_logs/config/junos/CONFIGURATION_COMMIT_REQUESTED.yml b/napalm_logs/config/junos/CONFIGURATION_COMMIT_REQUESTED.yml
index 66ce5f75..bc91866e 100644
--- a/napalm_logs/config/junos/CONFIGURATION_COMMIT_REQUESTED.yml
+++ b/napalm_logs/config/junos/CONFIGURATION_COMMIT_REQUESTED.yml
@@ -7,7 +7,7 @@ messages:
     tag: UI_COMMIT
     values:
       user: (\w+)
-      comment: (\w+)
+      comment: (.*)
     line: "User '{user}' requested 'commit' operation (comment: {comment})"
     model: NO_MODEL
     mapping:
diff --git a/napalm_logs/config/junos/DDOS_PROTOCOL_VIOLATION_SET.yml b/napalm_logs/config/junos/DDOS_PROTOCOL_VIOLATION_SET.yml
new file mode 100644
index 00000000..a55212e1
--- /dev/null
+++ b/napalm_logs/config/junos/DDOS_PROTOCOL_VIOLATION_SET.yml
@@ -0,0 +1,17 @@
+messages:
+  - error: DDOS_PROTOCOL_VIOLATION_SET
+    tag: DDOS_PROTOCOL_VIOLATION_SET
+    values:
+      entityType: ([\w\-]+)
+      additionalText: (.*)
+      startTime: ([\w\s\-\:]+)
+      times: (\d+)
+    line: 'Warning: Host-bound traffic for protocol/exception  {entityType}:{additionalText} for {times} times, started at {startTime}'
+    model: ietf-alarm
+    mapping:
+      variables:
+        alarms//alarm//entity-type: entityType
+        alarms//alarm//additional-text: additionalText
+        alarms//alarm//alarm-time: startTime
+      static:
+        alarms//alarm//event-type: 10  # security-service-or-mechanism-violation
diff --git a/napalm_logs/config/junos/LACP_INTERFACE_DOWN.yml b/napalm_logs/config/junos/LACP_INTERFACE_DOWN.yml
new file mode 100644
index 00000000..6e0a61a5
--- /dev/null
+++ b/napalm_logs/config/junos/LACP_INTERFACE_DOWN.yml
@@ -0,0 +1,12 @@
+messages:
+  - error: LACP_INTERFACE_DOWN
+    tag: LACP_INTF_DOWN
+    values:
+      interface: ([\w\-\/\:]+)
+      member: ([\w\-\/\:]+)
+    line: '{interface}: Interface marked down due to lacp timeout on member {member}'
+    model: openconfig-lacp
+    mapping:
+      variables:
+        lacp//interfaces//interface//{interface}//members//member//{member}//state//interface: member
+      static: {}
diff --git a/napalm_logs/config/junos/MAJOR_ALARM_CLEARED.yml b/napalm_logs/config/junos/MAJOR_ALARM_CLEARED.yml
new file mode 100644
index 00000000..0fd3c0b2
--- /dev/null
+++ b/napalm_logs/config/junos/MAJOR_ALARM_CLEARED.yml
@@ -0,0 +1,11 @@
+messages:
+  - error: MAJOR_ALARM_CLEARED
+    tag: craftd
+    values:
+      reason: (.*)
+    line: 'Major alarm cleared, {reason}'
+    model: ietf-alarm
+    mapping:
+      variables:
+        alarms//alarm//additional-text: reason
+      static: {}
diff --git a/napalm_logs/config/junos/MAJOR_ALARM_SET.yml b/napalm_logs/config/junos/MAJOR_ALARM_SET.yml
new file mode 100644
index 00000000..fdd2019b
--- /dev/null
+++ b/napalm_logs/config/junos/MAJOR_ALARM_SET.yml
@@ -0,0 +1,17 @@
+# This message is dedicated to the major alarms, as a specific subset of the
+# SYSTEM_ALARM. While providing fewer details than the SYSTEM_ALARM notifications
+# it is a more reliable source of the alarm status, as SYSTEM_ALARM through the
+# color scheme YELLOW / RED doesn't accurately provide the severity of the alarm
+# (i.e., some major alarms are marked as YELLOW, while some minor alarms can be
+# marked as YELLOW as well - and vice-versa).
+messages:
+  - error: MAJOR_ALARM_SET
+    tag: craftd
+    values:
+      reason: (.*)
+    line: 'Major alarm set, {reason}'
+    model: ietf-alarm
+    mapping:
+      variables:
+        alarms//alarm//additional-text: reason
+      static: {}
diff --git a/napalm_logs/config/junos/MINOR_ALARM_CLEARED.yml b/napalm_logs/config/junos/MINOR_ALARM_CLEARED.yml
new file mode 100644
index 00000000..663bb9a0
--- /dev/null
+++ b/napalm_logs/config/junos/MINOR_ALARM_CLEARED.yml
@@ -0,0 +1,11 @@
+messages:
+  - error: MINOR_ALARM_CLEARED
+    tag: craftd
+    values:
+      reason: (.*)
+    line: 'Minor alarm cleared, {reason}'
+    model: ietf-alarm
+    mapping:
+      variables:
+        alarms//alarm//additional-text: reason
+      static: {}
diff --git a/napalm_logs/config/junos/MINOR_ALARM_SET.yml b/napalm_logs/config/junos/MINOR_ALARM_SET.yml
new file mode 100644
index 00000000..178d8374
--- /dev/null
+++ b/napalm_logs/config/junos/MINOR_ALARM_SET.yml
@@ -0,0 +1,17 @@
+# This message is dedicated to the major alarms, as a specific subset of the
+# SYSTEM_ALARM. While providing fewer details than the SYSTEM_ALARM notifications
+# it is a more reliable source of the alarm status, as SYSTEM_ALARM through the
+# color scheme YELLOW / RED doesn't accurately provide the severity of the alarm
+# (i.e., some major alarms are marked as YELLOW, while some minor alarms can be
+# marked as YELLOW as well - and vice-versa).
+messages:
+  - error: MINOR_ALARM_SET
+    tag: craftd
+    values:
+      reason: (.*)
+    line: 'Minor alarm set, {reason}'
+    model: ietf-alarm
+    mapping:
+      variables:
+        alarms//alarm//additional-text: reason
+      static: {}
diff --git a/napalm_logs/config/junos/PROCESS_CEILING_WATERMARK_REACHED.yml b/napalm_logs/config/junos/PROCESS_CEILING_WATERMARK_REACHED.yml
new file mode 100644
index 00000000..ffe2ca5b
--- /dev/null
+++ b/napalm_logs/config/junos/PROCESS_CEILING_WATERMARK_REACHED.yml
@@ -0,0 +1,13 @@
+messages:
+  # 'error' should be unique and vendor agnostic. Currently we are using the JUNOS syslog message name as the canonical name.
+  # This may change if we are able to find a more well defined naming system.
+  - error: PROCESS_CEILING_WATERMARK_REACHED
+    tag: jlaunchd
+    values:
+      severity: (\w+)
+    line: 'System reaching processes ceiling {severity} watermark: Contact to system administrator to clean up unnecessary processes or increase maxproc ceiling.'
+    model: NO_MODEL
+    mapping:
+      variables:
+        process//ceiling//watermark: severity
+      static: {}
diff --git a/napalm_logs/config/junos/SYSTEM_ALARM.yml b/napalm_logs/config/junos/SYSTEM_ALARM.yml
index de074614..2a249e72 100644
--- a/napalm_logs/config/junos/SYSTEM_ALARM.yml
+++ b/napalm_logs/config/junos/SYSTEM_ALARM.yml
@@ -11,7 +11,7 @@ messages:
     values:
       component: (FPC \d)
       color|color_to_severity: (\w+)
-      reason: ([\w ]+)
+      reason: (.*)
     line: 'FPC color={color}, class=CHASSIS, reason={component} {reason}'
     model: ietf-hardware
     mapping:
@@ -29,7 +29,7 @@ messages:
       component: (\w+)
       color|color_to_severity: (\w+)
       class: (\w+)
-      reason: ([\w ]+)
+      reason: (.*)
     line: '{component} color={color}, class={class}, reason={reason}'
     model: ietf-hardware
     mapping:
diff --git a/napalm_logs/config/junos/SYSTEM_ALARM_CLEARED.yml b/napalm_logs/config/junos/SYSTEM_ALARM_CLEARED.yml
new file mode 100644
index 00000000..5d0160bb
--- /dev/null
+++ b/napalm_logs/config/junos/SYSTEM_ALARM_CLEARED.yml
@@ -0,0 +1,34 @@
+messages:
+  - error: SYSTEM_ALARM_CLEARED
+    tag: Alarm cleared
+    values:
+      component: (FPC \d)
+      color|color_to_severity: (\w+)
+      reason: (.*)
+    line: 'FPC color={color}, class=CHASSIS, reason={component} {reason}'
+    model: ietf-hardware
+    mapping:
+      variables:
+        hardware-state//component//{component}//name: component
+        hardware-state//component//{component}//state//alarm-state: color
+        hardware-state//component//{component}//state//alarm-reason: reason
+      static:
+        hardware-state//component//{component}//class: CHASSIS
+# The following is a general catch all for system alarms, if you would like to
+# add more specif matches please do so above this comment
+  - error: SYSTEM_ALARM_CLEARED
+    tag: Alarm cleared
+    values:
+      component: (\w+)
+      color|color_to_severity: (\w+)
+      class: (\w+)
+      reason: (.*)
+    line: '{component} color={color}, class={class}, reason={reason}'
+    model: ietf-hardware
+    mapping:
+      variables:
+        hardware-state//component//{component}//name: component
+        hardware-state//component//{component}//class: class
+        hardware-state//component//{component}//state//alarm-state: color
+        hardware-state//component//{component}//state//alarm-reason: reason
+      static: {}
diff --git a/napalm_logs/config/junos/init.yml b/napalm_logs/config/junos/init.yml
index 8b5c19bb..bb2aa24a 100644
--- a/napalm_logs/config/junos/init.yml
+++ b/napalm_logs/config/junos/init.yml
@@ -4,7 +4,16 @@
 # Matching white space in `values`
 #
 prefixes:
+  # jlaunchd doesn't have proper tags. Override the match to use jlaunchd as the tag instead.
   - time_format: "%b %d %H:%M:%S"
+    values:
+      date: (\w+ +\d+)
+      time: (\d\d:\d\d:\d\d)
+      hostPrefix: (re\d.)?
+      host: ([^ ]+)
+      tag: (jlaunchd)
+    line: '{date} {time} {hostPrefix}{host} {tag}: '
+  - time_format: '%b %d %H:%M:%S'
     values:
       date: (\w+\s+\d+)
       time: (\d\d:\d\d:\d\d)
@@ -49,3 +58,59 @@ prefixes:
       # Some logs have data which can be inside brackets or parenthesis
       additionalData: (?:(?:\[|\()(.+)(?:\]|\)))?
     line: '{date} {time} {hostPrefix}{host} fpc{fpcId} fpc{fpcId2} dcpfe: {tag}{additionalData}:'
+
+  # Same structures as above, but to match the ISO8601 date-time format.
+    # jlaunchd doesn't have proper tags. Override the match to use jlaunchd as the tag instead.
+  - time_format: "%b %d %H:%M:%S"
+    values:
+      date: (\d{4}-\d{2}-\d{2})
+      time: (\d{2}:\d{2}:\d{2}[\.\d{3}]?[\+|-]\d{2}:\d{2})
+      hostPrefix: (re\d.)?
+      host: ([^ ]+)
+      tag: (jlaunchd)
+    line: '{date}T{time} {hostPrefix}{host} {tag}: '
+  - time_format: '%b %d %H:%M:%S'
+    values:
+      date: (\d{4}-\d{2}-\d{2})
+      time: (\d{2}:\d{2}:\d{2}[\.\d{3}]?[\+|-]\d{2}:\d{2})
+      hostPrefix: (re\d.)?
+      host: ([^ ]+)
+      processName: /?(\w+)
+      # Most log lines have a process ID, however some do not
+      processId: \[?(\d+)?\]?
+      tag: ([\w\s]+)
+    line: '{date}T{time} {hostPrefix}{host} {processName}{processId}: {tag}:'
+  - time_format: "%b %d %H:%M:%S"
+    values:
+      date: (\d{4}-\d{2}-\d{2})
+      time: (\d{2}:\d{2}:\d{2}[\.\d{3}]?[\+|-]\d{2}:\d{2})
+      hostPrefix: (re\d.)?
+      host: ([^ ]+)
+      tag: (\w+)
+      # Most log lines have a process ID, however some do not
+      processId: (\d+)
+    line: '{date}T{time} {hostPrefix}{host} {tag}[{processId}]:'
+  - time_format: "%b %d %H:%M:%S"
+    values:
+      date: (\d{4}-\d{2}-\d{2})
+      time: (\d{2}:\d{2}:\d{2}[\.\d{3}]?[\+|-]\d{2}:\d{2})
+      hostPrefix: (re\d.)?
+      host: ([^ ]+)
+      fpcId: (\d+)
+      tag: (\w+)
+      # Some logs have data which can be inside brackets or parenthesis
+      additionalData: (?:(?:\[|\()(.+)(?:\]|\)))?
+    line: '{date}T{time} {hostPrefix}{host} fpc{fpcId} {tag}{additionalData}:'
+  # The following matches dcpfe specific logs
+  - time_format: "%b %d %H:%M:%S"
+    values:
+      date: (\d{4}-\d{2}-\d{2})
+      time: (\d{2}:\d{2}:\d{2}[\.\d{3}]?[\+|-]\d{2}:\d{2})
+      hostPrefix: (re\d.)?
+      host: ([^ ]+)
+      fpcId: (\d+)
+      fpcId2: (\d+)
+      tag: (\w+)
+      # Some logs have data which can be inside brackets or parenthesis
+      additionalData: (?:(?:\[|\()(.+)(?:\]|\)))?
+    line: '{date}T{time} {hostPrefix}{host} fpc{fpcId} fpc{fpcId2} dcpfe: {tag}{additionalData}:'
diff --git a/napalm_logs/config/nxos/USER_LOGIN.py b/napalm_logs/config/nxos/USER_LOGIN.py
index 04b7b19c..17495036 100644
--- a/napalm_logs/config/nxos/USER_LOGIN.py
+++ b/napalm_logs/config/nxos/USER_LOGIN.py
@@ -40,17 +40,15 @@
 
 log = logging.getLogger(__file__)
 
-_RGX_PARTS = [
-    ('user', r'(\w+)'),
-    ('uid', r'(\d+)'),
-    ('sshPid', r'(\d+)')
-]
+_RGX_PARTS = [('user', r'(\w+)'), ('uid', r'(\d+)'), ('sshPid', r'(\d+)')]
 _RGX_PARTS = OrderedDict(_RGX_PARTS)
 
 _RGX = (
     r'pam_unix\(dcos_sshd:session\): session opened for user '
     r'{0[user]} by \(uid={0[uid]}\) - dcos_sshd\[{0[sshPid]}\]'
-).format(_RGX_PARTS)  # ATTENTION to escape the parans
+).format(
+    _RGX_PARTS
+)  # ATTENTION to escape the parans
 
 
 def emit(msg_dict):
diff --git a/napalm_logs/config/nxos/__init__.py b/napalm_logs/config/nxos/__init__.py
index 9cdade9a..8e35e33e 100644
--- a/napalm_logs/config/nxos/__init__.py
+++ b/napalm_logs/config/nxos/__init__.py
@@ -16,10 +16,12 @@
     ('time', r'(\d\d:\d\d:\d\d)'),
     ('timeZone', r'(\w\w\w)'),
     ('tag', r'([\w\d-]+)'),
-    ('message', r'(.*)')
+    ('message', r'(.*)'),
 ]
 _RGX_PARTS = OrderedDict(_RGX_PARTS)
-_RGX = r'\<{0[pri]}\>{0[host]}: {0[date]} {0[time]} {0[timeZone]}: %{0[tag]}: {0[message]}'.format(_RGX_PARTS)
+_RGX = r'\<{0[pri]}\>{0[host]}: {0[date]} {0[time]} {0[timeZone]}: %{0[tag]}: {0[message]}'.format(
+    _RGX_PARTS
+)
 
 _ALT_RGX_PARTS = [
     ('pri', r'(\d+)'),
@@ -27,15 +29,18 @@
     ('time', r'(\d\d:\d\d:\d\d)'),
     ('host', r'([^ ]+)'),
     ('tag', r'([\w\d-]+)'),
-    ('message', r'(.*)')
+    ('message', r'(.*)'),
 ]
 _ALT_RGX_PARTS = OrderedDict(_ALT_RGX_PARTS)
-_ALT_RGX = r'\<{0[pri]}\>{0[date]} {0[time]} {0[host]} %{0[tag]}: {0[message]}'.format(_ALT_RGX_PARTS)
+_ALT_RGX = r'\<{0[pri]}\>{0[date]} {0[time]} {0[host]} %{0[tag]}: {0[message]}'.format(
+    _ALT_RGX_PARTS
+)
 
 _TIME_FORMAT = ('{date} {time} {timeZone}', '%Y %b %d %H:%M:%S %Z')
 _ALT_TIME_FORMAT = ('{date} {time}', '%Y %b %d %H:%M:%S')
 
 
 def extract(msg):
-    return napalm_logs.utils.extract(_RGX, msg, _RGX_PARTS, _TIME_FORMAT) or\
-           napalm_logs.utils.extract(_ALT_RGX, msg, _ALT_RGX_PARTS, _ALT_TIME_FORMAT)
+    return napalm_logs.utils.extract(
+        _RGX, msg, _RGX_PARTS, _TIME_FORMAT
+    ) or napalm_logs.utils.extract(_ALT_RGX, msg, _ALT_RGX_PARTS, _ALT_TIME_FORMAT)
diff --git a/napalm_logs/device.py b/napalm_logs/device.py
index 5dfeccad..9b91832b 100644
--- a/napalm_logs/device.py
+++ b/napalm_logs/device.py
@@ -9,12 +9,15 @@
 import re
 import signal
 import logging
+import calendar
+import datetime
+import dateutil
 import threading
-from datetime import datetime, timedelta
 
 # Import thrid party libs
 import zmq
 import umsgpack
+import dateparser
 from prometheus_client import Counter
 
 # Import napalm-logs pkgs
@@ -23,6 +26,7 @@
 from napalm_logs.proc import NapalmLogsProc
 from napalm_logs.config import PUB_PX_IPC_URL
 from napalm_logs.config import DEV_IPC_URL
+
 # exceptions
 from napalm_logs.exceptions import NapalmLogsExit
 
@@ -33,10 +37,8 @@ class NapalmLogsDeviceProc(NapalmLogsProc):
     '''
     Device sub-process class.
     '''
-    def __init__(self,
-                 name,
-                 opts,
-                 config):
+
+    def __init__(self, name, opts, config):
         self._name = name
         log.debug('Starting process for %s', self._name)
         self._config = config
@@ -94,13 +96,15 @@ def _compile_messages(self):
             model = message_dict['model']
             match_on = message_dict.get('match_on', 'tag')
             if '__python_fun__' in message_dict:
-                self.compiled_messages.append({
-                    'error': error,
-                    'tag': tag,
-                    'match_on': match_on,
-                    'model': model,
-                    '__python_fun__': message_dict['__python_fun__']
-                })
+                self.compiled_messages.append(
+                    {
+                        'error': error,
+                        'tag': tag,
+                        'match_on': match_on,
+                        'model': model,
+                        '__python_fun__': message_dict['__python_fun__'],
+                    }
+                )
                 continue
             values = message_dict['values']
             line = message_dict['line']
@@ -131,7 +135,7 @@ def _compile_messages(self):
                     'values': values,
                     'replace': replace,
                     'model': model,
-                    'mapping': mapping
+                    'mapping': mapping,
                 }
             )
         log.debug('Compiled messages:')
@@ -158,7 +162,7 @@ def _parse(self, msg_dict):
                 return {
                     'model': message['model'],
                     'error': message['error'],
-                    '__python_fun__': message['__python_fun__']
+                    '__python_fun__': message['__python_fun__'],
                 }
             error_present = True
             match = message['line'].search(msg_dict['message'])
@@ -170,20 +174,30 @@ def _parse(self, msg_dict):
                 'model': message['model'],
                 'mapping': message['mapping'],
                 'replace': message['replace'],
-                'error': message['error']
+                'error': message['error'],
             }
             for key in values.keys():
                 # Check if the value needs to be replaced
                 if key in message['replace']:
-                    result = napalm_logs.utils.cast(match.group(positions.get(key)), message['replace'][key])
+                    result = napalm_logs.utils.cast(
+                        match.group(positions.get(key)), message['replace'][key]
+                    )
                 else:
                     result = match.group(positions.get(key))
                 ret[key] = result
             return ret
         if error_present is True:
-            log.info('Configured regex did not match for os: %s tag %s', self._name, msg_dict.get('tag', ''))
+            log.info(
+                'Configured regex did not match for os: %s tag %s',
+                self._name,
+                msg_dict.get('tag', ''),
+            )
         else:
-            log.info('Syslog message not configured for os: %s tag %s', self._name, msg_dict.get('tag', ''))
+            log.info(
+                'Syslog message not configured for os: %s tag %s',
+                self._name,
+                msg_dict.get('tag', ''),
+            )
 
     def _emit(self, **kwargs):
         '''
@@ -193,9 +207,13 @@ def _emit(self, **kwargs):
         oc_dict = {}
         for mapping, result_key in kwargs['mapping']['variables'].items():
             result = kwargs[result_key]
-            oc_dict = napalm_logs.utils.setval(mapping.format(**kwargs), result, oc_dict)
+            oc_dict = napalm_logs.utils.setval(
+                mapping.format(**kwargs), result, oc_dict
+            )
         for mapping, result in kwargs['mapping']['static'].items():
-            oc_dict = napalm_logs.utils.setval(mapping.format(**kwargs), result, oc_dict)
+            oc_dict = napalm_logs.utils.setval(
+                mapping.format(**kwargs), result, oc_dict
+            )
 
         return oc_dict
 
@@ -207,36 +225,13 @@ def _publish(self, obj):
         self.pub.send(bin_obj)
 
     def _format_time(self, time, date, timezone, prefix_id):
-        # TODO can we work out the time format from the regex? Probably but this is a task for another day
-        time_format = self._config['prefixes'][prefix_id].get('time_format', '')
-        if not time or not date or not time_format:
-            return int(datetime.now().strftime('%s'))
-        # Most syslog do not include the year, so we will add the current year if we are not supplied with one
-        if '%y' in time_format or '%Y' in time_format:
-            parsed_time = datetime.strptime('{} {}'.format(date, time), time_format)
-        else:
-            year = datetime.now().year
-            try:
-                parsed_time = datetime.strptime('{} {} {}'.format(year, date, time), '%Y {}'.format(time_format))
-                # If the timestamp is in the future then it is likely that the year
-                # is wrong. We subtract 1 day from the parsed time to eleminate any
-                # difference between clocks.
-                if parsed_time - timedelta(days=1) > datetime.now():
-                    parsed_time = datetime.strptime(
-                        '{} {} {}'.format(year - 1, date, time),
-                        '%Y {}'.format(time_format)
-                    )
-            except ValueError:
-                # It is rare but by appending the year from the server, we could produce
-                # an invalid date such as February 29, 2018 (2018 is not a leap year). This
-                # is caused by the device emitting the syslog having an incorrect local date set.
-                # In such cases, we fall back to the full date from the server and log this action.
-                parsed_time = datetime.now().strftime(time_format)
-                log.info(
-                    "Invalid date produced while formatting syslog date. Falling back to server date [%s]",
-                    self._name
-                )
-        return int((parsed_time - datetime(1970, 1, 1)).total_seconds())
+        date_time = None
+        if time and date:
+            date_time = dateparser.parse('{} {}'.format(date, time))
+        if not date_time:
+            tz = dateutil.tz.gettz(timezone)
+            date_time = datetime.datetime.now(tz)
+        return int(calendar.timegm(date_time.utctimetuple()))
 
     def start(self):
         '''
@@ -246,27 +241,34 @@ def start(self):
         napalm_logs_device_messages_received = Counter(
             'napalm_logs_device_messages_received',
             "Count of messages received by the device process",
-            ['device_os']
+            ['device_os'],
         )
         napalm_logs_device_raw_published_messages = Counter(
             'napalm_logs_device_raw_published_messages',
             "Count of raw type published messages",
-            ['device_os']
+            ['device_os'],
         )
         napalm_logs_device_published_messages = Counter(
             'napalm_logs_device_published_messages',
             "Count of published messages",
-            ['device_os']
+            ['device_os'],
         )
         napalm_logs_device_oc_object_failed = Counter(
             'napalm_logs_device_oc_object_failed',
             "Counter of failed OpenConfig object generations",
-            ['device_os']
+            ['device_os'],
         )
-
+        if self.opts.get('metrics_include_attributes', True):
+            napalm_logs_device_published_messages_attrs = Counter(
+                'napalm_logs_device_published_messages_attrs',
+                "Counter of published messages, with more granular selection",
+                ['device_os', 'host', 'error'],
+            )
         self._setup_ipc()
         # Start suicide polling thread
-        thread = threading.Thread(target=self._suicide_when_without_parent, args=(os.getppid(),))
+        thread = threading.Thread(
+            target=self._suicide_when_without_parent, args=(os.getppid(),)
+        )
         thread.start()
         signal.signal(signal.SIGTERM, self._exit_gracefully)
         self.__up = True
@@ -282,17 +284,21 @@ def start(self):
                     return
                 else:
                     raise NapalmLogsExit(error)
-            log.debug('%s: dequeued %s, received from %s', self._name, msg_dict, address)
+            log.debug(
+                '%s: dequeued %s, received from %s', self._name, msg_dict, address
+            )
             napalm_logs_device_messages_received.labels(device_os=self._name).inc()
             host = msg_dict.get('host')
             prefix_id = msg_dict.pop('__prefix_id__')
             if 'timestamp' in msg_dict:
                 timestamp = msg_dict.pop('timestamp')
             else:
-                timestamp = self._format_time(msg_dict.get('time', ''),
-                                              msg_dict.get('date', ''),
-                                              msg_dict.get('timeZone', 'UTC'),
-                                              prefix_id)
+                timestamp = self._format_time(
+                    msg_dict.get('time', ''),
+                    msg_dict.get('date', ''),
+                    msg_dict.get('timeZone', 'UTC'),
+                    prefix_id,
+                )
             facility = msg_dict.get('facility')
             severity = msg_dict.get('severity')
 
@@ -309,22 +315,28 @@ def start(self):
                     'error': 'RAW',
                     'model_name': 'raw',
                     'facility': facility,
-                    'severity': severity
+                    'severity': severity,
                 }
                 log.debug('Queueing to be published:')
                 log.debug(to_publish)
                 # self.pub_pipe.send(to_publish)
                 self.pub.send(umsgpack.packb(to_publish))
-                napalm_logs_device_raw_published_messages.labels(device_os=self._name).inc()
+                napalm_logs_device_raw_published_messages.labels(
+                    device_os=self._name
+                ).inc()
                 continue
             try:
                 if '__python_fun__' in kwargs:
-                    log.debug('Using the Python parser to determine the YANG-equivalent object')
+                    log.debug(
+                        'Using the Python parser to determine the YANG-equivalent object'
+                    )
                     yang_obj = kwargs['__python_fun__'](msg_dict)
                 else:
                     yang_obj = self._emit(**kwargs)
             except Exception:
-                log.exception('Unexpected error when generating the OC object.', exc_info=True)
+                log.exception(
+                    'Unexpected error when generating the OC object.', exc_info=True
+                )
                 napalm_logs_device_oc_object_failed.labels(device_os=self._name).inc()
                 continue
             log.debug('Generated OC object:')
@@ -341,7 +353,7 @@ def start(self):
                 'yang_model': model_name,
                 'os': self._name,
                 'facility': facility,
-                'severity': severity
+                'severity': severity,
             }
             log.debug('Queueing to be published:')
             log.debug(to_publish)
@@ -349,6 +361,12 @@ def start(self):
             self.pub.send(umsgpack.packb(to_publish))
             # self._publish(to_publish)
             napalm_logs_device_published_messages.labels(device_os=self._name).inc()
+            if self.opts.get('metrics_include_attributes', True):
+                napalm_logs_device_published_messages_attrs.labels(
+                    device_os=self._name,
+                    error=to_publish['error'],
+                    host=to_publish['host'],
+                ).inc()
 
     def stop(self):
         '''
diff --git a/napalm_logs/exceptions.py b/napalm_logs/exceptions.py
index 84395473..3a75d215 100644
--- a/napalm_logs/exceptions.py
+++ b/napalm_logs/exceptions.py
@@ -9,6 +9,7 @@ class NapalmLogsException(Exception):
     '''
     Base exception class; all napalm-logs exceptions should inherit this.
     '''
+
     def __init__(self, msg=''):
         super(NapalmLogsException, self).__init__(msg)
         self.strerror = msg
@@ -20,6 +21,7 @@ class BindException(NapalmLogsException):
     address / port. Either the values are not correct, either another processs
     is already using them.
     '''
+
     pass
 
 
@@ -27,6 +29,7 @@ class TransportException(NapalmLogsException):
     '''
     Exception raised when encounering an error in a transport process.
     '''
+
     pass
 
 
@@ -34,6 +37,7 @@ class InvalidTransportException(TransportException):
     '''
     Raised when the user selects a transport that does not exist.
     '''
+
     pass
 
 
@@ -41,6 +45,7 @@ class ListenerException(NapalmLogsException):
     '''
     Exception raised when encountering an exception in a listener process.
     '''
+
     pass
 
 
@@ -48,6 +53,7 @@ class InvalidListenerException(ListenerException):
     '''
     Raised when the user selets a listener that does not exist.
     '''
+
     pass
 
 
@@ -55,6 +61,7 @@ class SerializerException(NapalmLogsException):
     '''
     Raised in case of serializer-related errors.
     '''
+
     pass
 
 
@@ -62,6 +69,7 @@ class InvalidSerializerException(SerializerException):
     '''
     Raised when the user selects a serializer not available.
     '''
+
     pass
 
 
@@ -69,6 +77,7 @@ class BufferException(NapalmLogsException):
     '''
     Raised in case of buffer errors.
     '''
+
     pass
 
 
@@ -76,6 +85,7 @@ class InvalidBufferException(BufferException):
     '''
     Raised when the user selects a buffer interface that is not available.
     '''
+
     pass
 
 
@@ -83,6 +93,7 @@ class ConfigurationException(NapalmLogsException):
     '''
     Exception thrown when the user configuration is not correct.
     '''
+
     pass
 
 
@@ -90,6 +101,7 @@ class OpenConfigPathException(NapalmLogsException):
     '''
     Unable to set the open config path specified.
     '''
+
     pass
 
 
@@ -97,6 +109,7 @@ class NapalmLogsExit(NapalmLogsException):
     '''
     Raised on unexpected exit.
     '''
+
     pass
 
 
@@ -104,6 +117,7 @@ class CryptoException(NapalmLogsException):
     '''
     Raised when unable to decrypt.
     '''
+
     pass
 
 
@@ -111,6 +125,7 @@ class BadSignatureException(NapalmLogsException):
     '''
     Raised when the signature was forged or corrupted.
     '''
+
     pass
 
 
@@ -118,6 +133,7 @@ class SSLMismatchException(NapalmLogsException):
     '''
     Raised when the SSL certificate and key do not match
     '''
+
     pass
 
 
@@ -125,4 +141,5 @@ class ClientConnectException(NapalmLogsException):
     '''
     Raised when the client is unable to connect.
     '''
+
     pass
diff --git a/napalm_logs/listener/__init__.py b/napalm_logs/listener/__init__.py
index 0e89b784..dab785f4 100644
--- a/napalm_logs/listener/__init__.py
+++ b/napalm_logs/listener/__init__.py
@@ -11,6 +11,7 @@
 # Import napalm-logs pkgs
 # Exceptions
 from napalm_logs.exceptions import InvalidListenerException
+
 # Listener classes
 from napalm_logs.listener.tcp import TCPListener
 from napalm_logs.listener.udp import UDPListener
@@ -25,7 +26,7 @@
 LISTENER_LOOKUP = {
     'tcp': TCPListener,
     'udp': UDPListener,
-    '*': UDPListener  # default listener
+    '*': UDPListener,  # default listener
 }
 
 if HAS_KAFKA:
@@ -46,11 +47,11 @@ def get_listener(name):
         log.debug('Using %s as listener', name)
         return LISTENER_LOOKUP[name]
     except KeyError:
-        msg = 'Listener {} is not available. Are the dependencies installed?'.format(name)
+        msg = 'Listener {} is not available. Are the dependencies installed?'.format(
+            name
+        )
         log.error(msg, exc_info=True)
         raise InvalidListenerException(msg)
 
 
-__all__ = (
-    'get_listener',
-)
+__all__ = ('get_listener',)
diff --git a/napalm_logs/listener/base.py b/napalm_logs/listener/base.py
index 1bd8edc8..d999a332 100644
--- a/napalm_logs/listener/base.py
+++ b/napalm_logs/listener/base.py
@@ -8,6 +8,7 @@ class ListenerBase:
     '''
     The base class for the listener.
     '''
+
     def __init__(self, address, port, **kwargs):
         pass
 
diff --git a/napalm_logs/listener/kafka.py b/napalm_logs/listener/kafka.py
index fcd81cbe..9a9991bb 100644
--- a/napalm_logs/listener/kafka.py
+++ b/napalm_logs/listener/kafka.py
@@ -13,6 +13,7 @@
 # Import third party libs
 try:
     import kafka
+
     HAS_KAFKA = True
 except ImportError:
     HAS_KAFKA = False
@@ -28,13 +29,15 @@ class KafkaListener(ListenerBase):
     '''
     Kafka listener class.
     '''
+
     def __init__(self, address, port, **kwargs):
         if kwargs.get('address'):
             address = kwargs['address']
         if kwargs.get('port'):
             port = kwargs['port']
-        self.bootstrap_servers = kwargs.get('bootstrap_servers',
-                                            '{}:{}'.format(address, port))
+        self.bootstrap_servers = kwargs.get(
+            'bootstrap_servers', '{}:{}'.format(address, port)
+        )
         self.group_id = kwargs.get('group_id', 'napalm-logs')
         self.topic = kwargs.get('topic', 'syslog.net')
 
@@ -42,12 +45,15 @@ def start(self):
         '''
         Startup the kafka consumer.
         '''
-        log.debug('Creating the consumer using the bootstrap servers: %s and the group ID: %s',
-                  self.bootstrap_servers,
-                  self.group_id)
+        log.debug(
+            'Creating the consumer using the bootstrap servers: %s and the group ID: %s',
+            self.bootstrap_servers,
+            self.group_id,
+        )
         try:
-            self.consumer = kafka.KafkaConsumer(bootstrap_servers=self.bootstrap_servers,
-                                                group_id=self.group_id)
+            self.consumer = kafka.KafkaConsumer(
+                bootstrap_servers=self.bootstrap_servers, group_id=self.group_id
+            )
         except kafka.errors.NoBrokersAvailable as err:
             log.error(err, exc_info=True)
             raise ListenerException(err)
diff --git a/napalm_logs/listener/tcp.py b/napalm_logs/listener/tcp.py
index 02bc2f6f..6c70a5de 100644
--- a/napalm_logs/listener/tcp.py
+++ b/napalm_logs/listener/tcp.py
@@ -6,16 +6,18 @@
 from __future__ import unicode_literals
 
 # Import pythond stdlib
-try:
-    import Queue as queue
-except ImportError:
-    import queue
+import re
 import time
 import random
 import socket
 import logging
 import threading
 
+try:
+    import Queue as queue
+except ImportError:
+    import queue
+
 # Import third party libs
 
 # Import napalm-logs pkgs
@@ -24,17 +26,21 @@
 from napalm_logs.config import REUSE_PORT
 from napalm_logs.config import MAX_TCP_CLIENTS
 from napalm_logs.listener.base import ListenerBase
+
 # exceptions
 from napalm_logs.exceptions import BindException
 from napalm_logs.exceptions import ListenerException
 
 log = logging.getLogger(__name__)
 
+OCTET_FRAMING_RGX = r'\d+\s(<\d+>)'
+
 
 class TCPListener(ListenerBase):
     '''
     TCP syslog listener class
     '''
+
     def __init__(self, address, port, **kwargs):
         if kwargs.get('address'):
             self.address = kwargs['address']
@@ -48,6 +54,8 @@ def __init__(self, address, port, **kwargs):
         self.reuse_port = kwargs.get('reuse_port', REUSE_PORT)
         self.socket_timeout = kwargs.get('socket_timeout', TIMEOUT)
         self.max_clients = kwargs.get('max_clients', MAX_TCP_CLIENTS)
+        self.framing = kwargs.get('framing', 'traditional')
+        self.frame_delimiter = kwargs.get('frame_delimiter', '\n')
         self.buffer = queue.Queue()
 
     def _client_connection(self, conn, addr):
@@ -57,19 +65,39 @@ def _client_connection(self, conn, addr):
         log.debug('Established connection with %s:%d', addr[0], addr[1])
         conn.settimeout(self.socket_timeout)
         try:
+            prev_msg = ''
             while self.__up:
                 msg = conn.recv(self.buffer_size)
                 if not msg:
                     # log.debug('Received empty message from %s', addr)
                     # disabled ^ as it was too noisy
                     continue
-                log.debug('[%s] Received %s from %s. Adding in the queue', time.time(), msg, addr)
-                self.buffer.put((msg, '{}:{}'.format(addr[0], addr[1])))
+                log.debug('[%s] Received %s from %s', time.time(), msg, addr)
+                messages = []
+                if isinstance(msg, bytes):
+                    msg = msg.decode('utf-8')
+                if self.framing == 'traditional':
+                    msg = prev_msg + msg
+                    msg_lines = msg.split(self.frame_delimiter)
+                    if len(msg_lines) > 1:
+                        for line in msg_lines[:-1]:
+                            messages.append(line)
+                        prev_msg = msg_lines[-1]
+                    else:
+                        messages = [msg]
+                elif self.framing == 'octet-counted':
+                    msg_chunks = re.split(OCTET_FRAMING_RGX, msg)
+                    messages = [
+                        '{}{}'.format(pri, body).strip()
+                        for pri, body in zip(msg_chunks[1::2], msg_chunks[2::2])
+                    ]
+                for message in messages:
+                    log.debug('[%s] Queueing %s', time.time(), message)
+                    self.buffer.put((message, '{}:{}'.format(addr[0], addr[1])))
         except socket.timeout:
             if not self.__up:
                 return
-            log.debug('Connection %s:%d timed out', addr[1], addr[0])
-            raise ListenerException('Connection %s:%d timed out' % addr)
+            log.info('Connection %s:%d timed out', addr[0], addr[1])
         finally:
             log.debug('Closing connection with %s', addr)
             conn.close()
@@ -90,7 +118,9 @@ def _serve_clients(self):
                 msg = 'Received listener socket error: {}'.format(error)
                 log.error(msg, exc_info=True)
                 raise ListenerException(msg)
-            client_thread = threading.Thread(target=self._client_connection, args=(conn, addr,))
+            client_thread = threading.Thread(
+                target=self._client_connection, args=(conn, addr,)
+            )
             client_thread.start()
 
     def start(self):
@@ -111,7 +141,9 @@ def start(self):
         try:
             self.skt.bind((self.address, int(self.port)))
         except socket.error as msg:
-            error_string = 'Unable to bind to port {} on {}: {}'.format(self.port, self.address, msg)
+            error_string = 'Unable to bind to port {} on {}: {}'.format(
+                self.port, self.address, msg
+            )
             log.error(error_string, exc_info=True)
             raise BindException(error_string)
         log.debug('Accepting max %d parallel connections', self.max_clients)
diff --git a/napalm_logs/listener/udp.py b/napalm_logs/listener/udp.py
index fc4e03f4..5c16e6d7 100644
--- a/napalm_logs/listener/udp.py
+++ b/napalm_logs/listener/udp.py
@@ -16,6 +16,7 @@
 from napalm_logs.config import BUFFER_SIZE
 from napalm_logs.config import REUSE_PORT
 from napalm_logs.listener.base import ListenerBase
+
 # exceptions
 from napalm_logs.exceptions import BindException
 from napalm_logs.exceptions import ListenerException
@@ -27,6 +28,7 @@ class UDPListener(ListenerBase):
     '''
     UDP syslog listener class.
     '''
+
     def __init__(self, address, port, **kwargs):
         if kwargs.get('address'):
             self.address = kwargs['address']
@@ -57,7 +59,9 @@ def start(self):
         try:
             self.skt.bind((self.address, int(self.port)))
         except socket.error as msg:
-            error_string = 'Unable to bind to port {} on {}: {}'.format(self.port, self.address, msg)
+            error_string = 'Unable to bind to port {} on {}: {}'.format(
+                self.port, self.address, msg
+            )
             log.error(error_string, exc_info=True)
             raise BindException(error_string)
 
diff --git a/napalm_logs/listener/zeromq.py b/napalm_logs/listener/zeromq.py
index ddb7cb53..df1160a7 100644
--- a/napalm_logs/listener/zeromq.py
+++ b/napalm_logs/listener/zeromq.py
@@ -12,6 +12,7 @@
 # Import third party libs
 try:
     import zmq
+
     HAS_ZMQ = True
 except ImportError:
     HAS_ZMQ = False
@@ -27,6 +28,7 @@ class ZMQListener(ListenerBase):
     '''
     ZMQ listener class.
     '''
+
     def __init__(self, address, port, **kwargs):
         if kwargs.get('address'):
             address = kwargs['address']
@@ -46,15 +48,15 @@ def start(self):
         '''
         Startup the zmq consumer.
         '''
-        zmq_uri = '{protocol}://{address}:{port}'.format(
-                       protocol=self.protocol,
-                       address=self.address,
-                       port=self.port
-                   ) if self.port else\
-                   '{protocol}://{address}'.format(  # noqa
-                       protocol=self.protocol,
-                       address=self.address
-                   )
+        zmq_uri = (
+            '{protocol}://{address}:{port}'.format(
+                protocol=self.protocol, address=self.address, port=self.port
+            )
+            if self.port
+            else '{protocol}://{address}'.format(  # noqa
+                protocol=self.protocol, address=self.address
+            )
+        )
         log.debug('ZMQ URI: %s', zmq_uri)
         self.ctx = zmq.Context()
         if hasattr(zmq, self.type):
diff --git a/napalm_logs/listener_proc.py b/napalm_logs/listener_proc.py
index 9020edec..b2289cb4 100644
--- a/napalm_logs/listener_proc.py
+++ b/napalm_logs/listener_proc.py
@@ -29,13 +29,16 @@ class NapalmLogsListenerProc(NapalmLogsProc):
     '''
     publisher sub-process class.
     '''
-    def __init__(self,
-                 opts,
-                 address,
-                 port,
-                 listener_type,
-                 # pipe,
-                 listener_opts=None):
+
+    def __init__(
+        self,
+        opts,
+        address,
+        port,
+        listener_type,
+        # pipe,
+        listener_opts=None,
+    ):
         self.__up = False
         self.opts = opts
         self.address = address
@@ -55,9 +58,7 @@ def _setup_listener(self):
         listener_class = get_listener(self._listener_type)
         self.address = self.listener_opts.pop('address', self.address)
         self.port = self.listener_opts.pop('port', self.port)
-        self.listener = listener_class(self.address,
-                                       self.port,
-                                       **self.listener_opts)
+        self.listener = listener_class(self.address, self.port, **self.listener_opts)
 
     def _setup_ipc(self):
         '''
@@ -95,7 +96,9 @@ def start(self):
         self._setup_listener()
         self.listener.start()
         # Start suicide polling thread
-        thread = threading.Thread(target=self._suicide_when_without_parent, args=(os.getppid(),))
+        thread = threading.Thread(
+            target=self._suicide_when_without_parent, args=(os.getppid(),)
+        )
         thread.start()
         signal.signal(signal.SIGTERM, self._exit_gracefully)
         self.__up = True
@@ -109,13 +112,22 @@ def start(self):
                 else:
                     log.error(lerr, exc_info=True)
                     raise NapalmLogsExit(lerr)
-            log.debug('Received %s from %s. Queueing to the server.', log_message, log_source)
+            log.debug(
+                'Received %s from %s. Queueing to the server.', log_message, log_source
+            )
             if not log_message:
-                log.info('Empty message received from %s. Not queueing to the server.', log_source)
+                log.info(
+                    'Empty message received from %s. Not queueing to the server.',
+                    log_source,
+                )
                 continue
-            c_logs_ingested.labels(listener_type=self._listener_type, address=self.address, port=self.port).inc()
+            c_logs_ingested.labels(
+                listener_type=self._listener_type, address=self.address, port=self.port
+            ).inc()
             self.pub.send(umsgpack.packb((log_message, log_source)))
-            c_messages_published.labels(listener_type=self._listener_type, address=self.address, port=self.port).inc()
+            c_messages_published.labels(
+                listener_type=self._listener_type, address=self.address, port=self.port
+            ).inc()
 
     def stop(self):
         log.info('Stopping the listener process')
diff --git a/napalm_logs/proc.py b/napalm_logs/proc.py
index cb500021..3709daff 100644
--- a/napalm_logs/proc.py
+++ b/napalm_logs/proc.py
@@ -17,6 +17,7 @@ class NapalmLogsProc:
     '''
     Sub-process base class.
     '''
+
     def _suicide_when_without_parent(self, parent_pid):
         '''
         Kill this process when the parent died.
diff --git a/napalm_logs/pub_proxy.py b/napalm_logs/pub_proxy.py
index 87652158..91913a73 100644
--- a/napalm_logs/pub_proxy.py
+++ b/napalm_logs/pub_proxy.py
@@ -17,6 +17,7 @@
 from napalm_logs.config import PUB_IPC_URL
 from napalm_logs.config import PUB_PX_IPC_URL
 from napalm_logs.proc import NapalmLogsProc
+
 # exceptions
 from napalm_logs.exceptions import NapalmLogsExit
 
@@ -27,6 +28,7 @@ class NapalmLogsPublisherProxy(NapalmLogsProc):
     '''
     Internal IPC proxy sub-process class.
     '''
+
     def __init__(self, hwm):
         self.hwm = hwm
         self.__up = False
@@ -69,7 +71,9 @@ def start(self):
         '''
         self._setup_ipc()
         # Start suicide polling thread
-        thread = threading.Thread(target=self._suicide_when_without_parent, args=(os.getppid(),))
+        thread = threading.Thread(
+            target=self._suicide_when_without_parent, args=(os.getppid(),)
+        )
         thread.start()
         signal.signal(signal.SIGTERM, self._exit_gracefully)
         try:
diff --git a/napalm_logs/publisher.py b/napalm_logs/publisher.py
index 576f6879..e77ea12b 100644
--- a/napalm_logs/publisher.py
+++ b/napalm_logs/publisher.py
@@ -24,6 +24,7 @@
 from napalm_logs.proc import NapalmLogsProc
 from napalm_logs.transport import get_transport
 from napalm_logs.serializer import get_serializer
+
 # exceptions
 from napalm_logs.exceptions import NapalmLogsExit
 
@@ -34,17 +35,20 @@ class NapalmLogsPublisherProc(NapalmLogsProc):
     '''
     publisher sub-process class.
     '''
-    def __init__(self,
-                 opts,
-                 address,
-                 port,
-                 transport_type,
-                 serializer,
-                 private_key,
-                 signing_key,
-                 publisher_opts,
-                 disable_security=False,
-                 pub_id=None):
+
+    def __init__(
+        self,
+        opts,
+        address,
+        port,
+        transport_type,
+        serializer,
+        private_key,
+        signing_key,
+        publisher_opts,
+        disable_security=False,
+        pub_id=None,
+    ):
         self.__up = False
         self.opts = opts
         self.pub_id = pub_id
@@ -75,7 +79,11 @@ def _setup_ipc(self):
         on the right transport.
         '''
         self.ctx = zmq.Context()
-        log.debug('Setting up the %s publisher subscriber #%d', self._transport_type, self.pub_id)
+        log.debug(
+            'Setting up the %s publisher subscriber #%d',
+            self._transport_type,
+            self.pub_id,
+        )
         self.sub = self.ctx.socket(zmq.SUB)
         self.sub.connect(PUB_IPC_URL)
         self.sub.setsockopt(zmq.SUBSCRIBE, b'')
@@ -91,20 +99,28 @@ def _setup_transport(self):
         Setup the transport.
         '''
         if 'RAW' in self.error_whitelist:
-            log.info('%s %d will publish partially parsed messages', self._transport_type, self.pub_id)
+            log.info(
+                '%s %d will publish partially parsed messages',
+                self._transport_type,
+                self.pub_id,
+            )
         if 'UNKNOWN' in self.error_whitelist:
-            log.info('%s %d will publish unknown messages', self._transport_type, self.pub_id)
+            log.info(
+                '%s %d will publish unknown messages', self._transport_type, self.pub_id
+            )
         transport_class = get_transport(self._transport_type)
-        log.debug('Serializing the object for %s using %s',
-                  self._transport_type,
-                  self.serializer)
+        log.debug(
+            'Serializing the object for %s using %s',
+            self._transport_type,
+            self.serializer,
+        )
         self.serializer_fun = get_serializer(self.serializer)
-        self.transport = transport_class(self.address,
-                                         self.port,
-                                         **self.publisher_opts)
+        self.transport = transport_class(self.address, self.port, **self.publisher_opts)
         self.__transport_encrypt = True
-        if hasattr(self.transport, 'NO_ENCRYPT') and\
-           getattr(self.transport, 'NO_ENCRYPT') is True:
+        if (
+            hasattr(self.transport, 'NO_ENCRYPT')
+            and getattr(self.transport, 'NO_ENCRYPT') is True
+        ):
             self.__transport_encrypt = False
 
     def _prepare(self, serialized_obj):
@@ -137,21 +153,23 @@ def start(self):
         napalm_logs_publisher_received_messages = Counter(
             'napalm_logs_publisher_received_messages',
             "Count of messages received by the publisher",
-            ['publisher_type', 'address', 'port']
+            ['publisher_type', 'address', 'port'],
         )
         napalm_logs_publisher_whitelist_blacklist_check_fail = Counter(
             'napalm_logs_publisher_whitelist_blacklist_check_fail',
             "Count of messages which fail the whitelist/blacklist check",
-            ['publisher_type', 'address', 'port']
+            ['publisher_type', 'address', 'port'],
         )
         napalm_logs_publisher_messages_published = Counter(
             'napalm_logs_publisher_messages_published',
             "Count of published messages",
-            ['publisher_type', 'address', 'port']
+            ['publisher_type', 'address', 'port'],
         )
         self._setup_ipc()
         # Start suicide polling thread
-        thread = threading.Thread(target=self._suicide_when_without_parent, args=(os.getppid(),))
+        thread = threading.Thread(
+            target=self._suicide_when_without_parent, args=(os.getppid(),)
+        )
         thread.start()
         signal.signal(signal.SIGTERM, self._exit_gracefully)
         self.transport.start()
@@ -173,21 +191,25 @@ def start(self):
             napalm_logs_publisher_received_messages.labels(
                 publisher_type=self._transport_type,
                 address=self.address,
-                port=self.port
+                port=self.port,
             ).inc()
-            if not napalm_logs.ext.check_whitelist_blacklist(obj['error'],
-                                                             whitelist=self.error_whitelist,
-                                                             blacklist=self.error_blacklist):
+            if not napalm_logs.ext.check_whitelist_blacklist(
+                obj['error'],
+                whitelist=self.error_whitelist,
+                blacklist=self.error_blacklist,
+            ):
                 # Apply the whitelist / blacklist logic
                 # If it doesn't match, jump over.
-                log.debug('This error type is %s. Skipping for %s #%d',
-                          obj['error'],
-                          self._transport_type,
-                          self.pub_id)
+                log.debug(
+                    'This error type is %s. Skipping for %s #%d',
+                    obj['error'],
+                    self._transport_type,
+                    self.pub_id,
+                )
                 napalm_logs_publisher_whitelist_blacklist_check_fail.labels(
                     publisher_type=self._transport_type,
                     address=self.address,
-                    port=self.port
+                    port=self.port,
                 ).inc()
                 continue
             serialized_obj = self._serialize(obj, bin_obj)
@@ -199,11 +221,15 @@ def start(self):
             napalm_logs_publisher_messages_published.labels(
                 publisher_type=self._transport_type,
                 address=self.address,
-                port=self.port
+                port=self.port,
             ).inc()
 
     def stop(self):
-        log.info('Stopping publisher process %s (publisher #%d)', self._transport_type, self.pub_id)
+        log.info(
+            'Stopping publisher process %s (publisher #%d)',
+            self._transport_type,
+            self.pub_id,
+        )
         self.__up = False
         self.sub.close()
         self.ctx.term()
diff --git a/napalm_logs/scripts/cli.py b/napalm_logs/scripts/cli.py
index 2d283c0d..ccfa8f9b 100644
--- a/napalm_logs/scripts/cli.py
+++ b/napalm_logs/scripts/cli.py
@@ -52,7 +52,7 @@ def __init__(self, *args, **kwargs):
             '-v',
             action='store_true',
             dest='version',
-            help='Show version number and exit.'
+            help='Show version number and exit.',
         )
 
     def add_option_group(self, *args, **kwargs):
@@ -65,9 +65,7 @@ def parse_args(self, args=None, values=None):
         if 'args_stdin' in options.__dict__ and options.args_stdin is True:
             new_inargs = sys.stdin.readlines()
             new_inargs = [arg.rstrip('\r\n') for arg in new_inargs]
-            new_options, new_args = optparse.OptionParser.parse_args(
-                self,
-                new_inargs)
+            new_options, new_args = optparse.OptionParser.parse_args(self, new_inargs)
             options.__dict__.update(new_options.__dict__)
             args.extend(new_args)
         self.options, self.args = options, args
@@ -79,123 +77,142 @@ def print_version(self):
 class NLOptionParser(OptionParser, object):
     def prepare(self):
         self.add_option(
-            '-c', '--config-file',
+            '-c',
+            '--config-file',
             dest='config_file',
-            help=('Config file absolute path. Default: {0}'.format(defaults.CONFIG_FILE))
+            help=(
+                'Config file absolute path. Default: {0}'.format(defaults.CONFIG_FILE)
+            ),
         )
         self.add_option(
-            '-a', '--address',
+            '-a',
+            '--address',
             dest='address',
-            help=('Listener address. Default: {0}'.format(defaults.ADDRESS))
+            help=('Listener address. Default: {0}'.format(defaults.ADDRESS)),
         )
         self.add_option(
-            '--config-path',
-            dest='config_path',
-            help=('Device config path.')
+            '--config-path', dest='config_path', help=('Device config path.')
         )
         self.add_option(
             '--extension-config-path',
             dest='extension_config_path',
-            help=('Extension config path.')
+            help=('Extension config path.'),
         )
         self.add_option(
-            '-p', '--port',
+            '-p',
+            '--port',
             dest='port',
             type=int,
-            help=('Listener bind port. Default: {0}'.format(defaults.PORT))
+            help=('Listener bind port. Default: {0}'.format(defaults.PORT)),
         )
         self.add_option(
             '--listener',
             dest='listener',
-            help=('Listener type. Default: {0}'.format(defaults.LISTENER))
+            help=('Listener type. Default: {0}'.format(defaults.LISTENER)),
         )
         self.add_option(
-            '-s', '--serializer',
+            '-s',
+            '--serializer',
             dest='serializer',
-            help=('Serializer type. Default: {0}'.format(defaults.SERIALIZER))
+            help=('Serializer type. Default: {0}'.format(defaults.SERIALIZER)),
         )
         self.add_option(
             '--publisher',
             dest='publisher',
-            help=('Publish transport. Default: {0}'.format(defaults.PUBLISHER))
+            help=('Publish transport. Default: {0}'.format(defaults.PUBLISHER)),
         )
         self.add_option(
             '--publish-address',
             dest='publish_address',
-            help=('Publisher bind address. Default: {0}'.format(defaults.PUBLISH_ADDRESS))
+            help=(
+                'Publisher bind address. Default: {0}'.format(defaults.PUBLISH_ADDRESS)
+            ),
         )
         self.add_option(
             '--publish-port',
             dest='publish_port',
             type=int,
-            help=('Publisher bind port. Default: {0}'.format(defaults.PUBLISH_PORT))
+            help=('Publisher bind port. Default: {0}'.format(defaults.PUBLISH_PORT)),
         )
         self.add_option(
             '--auth-address',
             dest='auth_address',
-            help=('Authenticator bind address. Default: {0}'.format(defaults.AUTH_ADDRESS))
+            help=(
+                'Authenticator bind address. Default: {0}'.format(defaults.AUTH_ADDRESS)
+            ),
         )
         self.add_option(
             '--auth-port',
             dest='auth_port',
             type=int,
-            help=('Authenticator bind port. Default: {0}'.format(defaults.AUTH_PORT))
+            help=('Authenticator bind port. Default: {0}'.format(defaults.AUTH_PORT)),
         )
         self.add_option(
             '--enable-metrics',
             dest='metrics_enabled',
             action="store_true",
             default=False,
-            help=('Enable metrics collection and exporting (Prometheus metrics).')
+            help=('Enable metrics collection and exporting (Prometheus metrics).'),
         )
         self.add_option(
             '--metrics-address',
             dest='metrics_address',
-            help=('Prometheus metrics HTTP server listener address. Default: {0}'.format(defaults.METRICS_ADDRESS))
+            help=(
+                'Prometheus metrics HTTP server listener address. Default: {0}'.format(
+                    defaults.METRICS_ADDRESS
+                )
+            ),
         )
         self.add_option(
             '--metrics-port',
             dest='metrics_port',
             type=int,
-            help=('Prometheus metrics HTTP server listener bind port. Default: {0}'.format(defaults.METRICS_PORT))
+            help=(
+                'Prometheus metrics HTTP server listener bind port. Default: {0}'.format(
+                    defaults.METRICS_PORT
+                )
+            ),
         )
         self.add_option(
             '--metrics-dir',
             dest='metrics_dir',
-            help=('Directory to store metrics in. Must be writable by the processes. '
-                  'Default: {0}'.format(defaults.METRICS_DIR))
+            help=(
+                'Directory to store metrics in. Must be writable by the processes. '
+                'Default: {0}'.format(defaults.METRICS_DIR)
+            ),
         )
         self.add_option(
             '--certificate',
             dest='certificate',
-            help=('Absolute path to the SSL certificate used for client authentication.')
+            help=(
+                'Absolute path to the SSL certificate used for client authentication.'
+            ),
         )
         self.add_option(
-            '--keyfile',
-            dest='keyfile',
-            help=('Absolute path to the SSL keyfile')
+            '--keyfile', dest='keyfile', help=('Absolute path to the SSL keyfile')
         )
         self.add_option(
             '--disable-security',
             dest='disable_security',
             action="store_true",
             default=False,
-            help=('Disable encryption and data signing when publishing.')
+            help=('Disable encryption and data signing when publishing.'),
         )
         self.add_option(
-            '-l', '--log-level',
+            '-l',
+            '--log-level',
             dest='log_level',
-            help=('Logging level. Default: {0}'.format(defaults.LOG_LEVEL))
+            help=('Logging level. Default: {0}'.format(defaults.LOG_LEVEL)),
         )
         self.add_option(
             '--log-file',
             dest='log_file',
-            help=('Logging file. Default: {0}'.format(defaults.LOG_FILE))
+            help=('Logging file. Default: {0}'.format(defaults.LOG_FILE)),
         )
         self.add_option(
             '--log-format',
             dest='log_format',
-            help=('Logging format. Default: {0}'.format(defaults.LOG_FORMAT))
+            help=('Logging format. Default: {0}'.format(defaults.LOG_FORMAT)),
         )
         self.add_option(
             '--hwm',
@@ -207,14 +224,16 @@ def prepare(self):
                 'and it tunes the capacity of the napalm-logs engine. '
                 'For high performance, this number can be increased, but implies'
                 'higher memory consumption. '
-                'Default: {0}'.format(defaults.ZMQ_INTERNAL_HWM))
+                'Default: {0}'.format(defaults.ZMQ_INTERNAL_HWM)
+            ),
         )
         self.add_option(
-            '-w', '--device-worker-processes',
+            '-w',
+            '--device-worker-processes',
             dest='device_worker_processes',
             type=int,
             help='Number of worker processes per device. Default: 1.',
-            default=1
+            default=1,
         )
 
     def convert_env_dict(self, d):
@@ -223,7 +242,10 @@ def convert_env_dict(self, d):
                 if not v.startswith('${') or not v.endswith('}'):
                     continue
                 if not os.environ.get(v[2:-1]):
-                    log.error('No env variable found for %s, please check your config file', v[2:-1])
+                    log.error(
+                        'No env variable found for %s, please check your config file',
+                        v[2:-1],
+                    )
                     sys.exit(1)
                 d[k] = os.environ[v[2:-1]]
             if isinstance(v, dict):
@@ -231,25 +253,28 @@ def convert_env_dict(self, d):
             if isinstance(v, list):
                 self.convert_env_list(v)
 
-    def convert_env_list(self, l):
-        for n, v in enumerate(l):
-            if isinstance(v, six.string_type):
-                if not v.startswith('${') or not v.endswith('}'):
+    def convert_env_list(self, lst):
+        for name, value in enumerate(lst):
+            if isinstance(value, six.string_type):
+                if not value.startswith('${') or not value.endswith('}'):
                     continue
-                if not os.environ.get(v[2:-1]):
-                    log.error('No env variable found for %s, please check your config file', v[2:-1])
+                if not os.environ.get(value[2:-1]):
+                    log.error(
+                        'No env variable found for %s, please check your config file',
+                        value[2:-1],
+                    )
                     sys.exit(1)
-                l[n] = os.environ[v[2:-1]]
-            if isinstance(v, dict):
-                self.convert_env_dict(v)
-            if isinstance(v, list):
-                self.convert_env_list(v)
+                lst[name] = os.environ[value[2:-1]]
+            if isinstance(value, dict):
+                self.convert_env_dict(value)
+            if isinstance(value, list):
+                self.convert_env_list(value)
 
     def read_config_file(self, filepath):
         config = {}
         try:
             with open(filepath, 'r') as fstream:
-                config = yaml.load(fstream)
+                config = yaml.load(fstream, Loader=yaml.FullLoader)
         except (IOError, yaml.YAMLError):
             log.info('Unable to read from %s', filepath)
         # Convert any env variables
@@ -264,9 +289,15 @@ def parse(self, log, screen_handler):
             sys.exit(1)
         config_file_path = self.options.config_file or defaults.CONFIG_FILE
         file_cfg = self.read_config_file(config_file_path)
-        log_file = self.options.log_file or file_cfg.get('log_file') or defaults.LOG_FILE
-        log_lvl = self.options.log_level or file_cfg.get('log_level') or defaults.LOG_LEVEL
-        log_fmt = self.options.log_format or file_cfg.get('log_format') or defaults.LOG_FORMAT
+        log_file = (
+            self.options.log_file or file_cfg.get('log_file') or defaults.LOG_FILE
+        )
+        log_lvl = (
+            self.options.log_level or file_cfg.get('log_level') or defaults.LOG_LEVEL
+        )
+        log_fmt = (
+            self.options.log_format or file_cfg.get('log_format') or defaults.LOG_FORMAT
+        )
         if log_file.lower() not in defaults.LOG_FILE_CLI_OPTIONS:
             log_file_dir = os.path.dirname(log_file)
             if not os.path.isdir(log_file_dir):
@@ -277,12 +308,18 @@ def parse(self, log, screen_handler):
                     log.error('Unable to create %s', log_file_dir, exc_info=True)
                     sys.exit(0)
             log.removeHandler(screen_handler)  # remove printing to the screen
-            logging.basicConfig(filename=log_file,
-                                level=defaults.LOGGING_LEVEL.get(log_lvl.lower(), 'warning'),
-                                format=log_fmt)  # log to filecm
+            logging.basicConfig(
+                filename=log_file,
+                level=defaults.LOGGING_LEVEL.get(log_lvl.lower(), 'warning'),
+                format=log_fmt,
+            )  # log to filecm
         cert = self.options.certificate or file_cfg.get('certificate')
-        disable_security = self.options.disable_security or file_cfg.get('disable_security', False)
-        metrics_enabled = self.options.metrics_enabled or file_cfg.get('metrics_enabled', False)
+        disable_security = self.options.disable_security or file_cfg.get(
+            'disable_security', False
+        )
+        metrics_enabled = self.options.metrics_enabled or file_cfg.get(
+            'metrics_enabled', False
+        )
         if not cert and disable_security is False:
             log.error('certfile must be specified for server-side operations')
             raise ValueError('Please specify a valid SSL certificate.')
@@ -339,41 +376,57 @@ def parse(self, log, screen_handler):
             hwm = file_cfg['hwm']
 
         cfg = {
-            'address': self.options.address or file_cfg.get('address') or defaults.ADDRESS,
+            'address': self.options.address
+            or file_cfg.get('address')
+            or defaults.ADDRESS,
             'port': self.options.port or file_cfg.get('port') or defaults.PORT,
             'listener': listener,
             'publisher': publisher,
-            'publish_address': self.options.publish_address or file_cfg.get('publish_address') or
-                               defaults.PUBLISH_ADDRESS,  # noqa
-            'publish_port': self.options.publish_port or file_cfg.get('publish_port') or
-                            defaults.PUBLISH_PORT,  # noqa
-            'auth_address': self.options.auth_address or file_cfg.get('auth_address') or
-                            defaults.AUTH_ADDRESS,  # noqa
-            'auth_port': self.options.auth_port or file_cfg.get('auth_port') or
-                         defaults.AUTH_PORT,
+            'publish_address': self.options.publish_address
+            or file_cfg.get('publish_address')
+            or defaults.PUBLISH_ADDRESS,  # noqa
+            'publish_port': self.options.publish_port
+            or file_cfg.get('publish_port')
+            or defaults.PUBLISH_PORT,  # noqa
+            'auth_address': self.options.auth_address
+            or file_cfg.get('auth_address')
+            or defaults.AUTH_ADDRESS,  # noqa
+            'auth_port': self.options.auth_port
+            or file_cfg.get('auth_port')
+            or defaults.AUTH_PORT,
             'metrics_enabled': metrics_enabled,
-            'metrics_address': self.options.metrics_address or file_cfg.get('metrics_address') or
-                               defaults.METRICS_ADDRESS,
-            'metrics_port': self.options.metrics_port or file_cfg.get('metrics_port') or
-                               defaults.METRICS_PORT,
-            'metrics_dir': self.options.metrics_dir or file_cfg.get('metrics_dir') or
-                               defaults.METRICS_DIR,
+            'metrics_address': self.options.metrics_address
+            or file_cfg.get('metrics_address')
+            or defaults.METRICS_ADDRESS,
+            'metrics_port': self.options.metrics_port
+            or file_cfg.get('metrics_port')
+            or defaults.METRICS_PORT,
+            'metrics_dir': self.options.metrics_dir
+            or file_cfg.get('metrics_dir')
+            or defaults.METRICS_DIR,
             'certificate': cert,
             'keyfile': self.options.keyfile or file_cfg.get('keyfile'),
             'disable_security': disable_security,
             'config_path': self.options.config_path or file_cfg.get('config_path'),
-            'extension_config_path': self.options.extension_config_path or file_cfg.get('extension_config_path'),
+            'extension_config_path': self.options.extension_config_path
+            or file_cfg.get('extension_config_path'),
             'log_level': log_lvl,
             'log_format': log_fmt,
             'device_whitelist': device_whitelist,
             'device_blacklist': device_blacklist,
             'hwm': hwm,
-            'device_worker_processes': self.options.device_worker_processes or\
-                                       file_cfg.get('device_worker_processes') or 1,
-            'serializer': self.options.serializer or file_cfg.get('serializer') or
-                          defaults.SERIALIZER,
-            'buffer': buffer_cfg
+            'device_worker_processes': self.options.device_worker_processes
+            or file_cfg.get('device_worker_processes')
+            or 1,
+            'serializer': self.options.serializer
+            or file_cfg.get('serializer')
+            or defaults.SERIALIZER,
+            'buffer': buffer_cfg,
+            'opts': {},
         }
+        for opt, val in file_cfg.items():
+            if opt not in cfg:
+                cfg['opts'][opt] = val
         return cfg
 
 
diff --git a/napalm_logs/serializer/__init__.py b/napalm_logs/serializer/__init__.py
index 3e977ec3..3b66d32d 100644
--- a/napalm_logs/serializer/__init__.py
+++ b/napalm_logs/serializer/__init__.py
@@ -27,7 +27,7 @@
     'str': str,
     'yaml': yaml.safe_dump,
     'pprint': pprint.pformat,
-    '*': umsgpack.packb  # default serializer
+    '*': umsgpack.packb,  # default serializer
 }
 
 
@@ -44,6 +44,4 @@ def get_serializer(name):
         raise InvalidSerializerException(msg)
 
 
-__all__ = (
-    'get_listener',
-)
+__all__ = ('get_listener',)
diff --git a/napalm_logs/server.py b/napalm_logs/server.py
index 911a3e5c..53ed89d0 100644
--- a/napalm_logs/server.py
+++ b/napalm_logs/server.py
@@ -25,6 +25,7 @@
 from napalm_logs.config import PUB_PX_IPC_URL
 from napalm_logs.config import UNKNOWN_DEVICE_NAME
 from napalm_logs.proc import NapalmLogsProc
+
 # exceptions
 from napalm_logs.exceptions import NapalmLogsExit
 
@@ -35,11 +36,8 @@ class NapalmLogsServerProc(NapalmLogsProc):
     '''
     Server sub-process class.
     '''
-    def __init__(self,
-                 opts,
-                 config,
-                 started_os_proc,
-                 buffer=None):
+
+    def __init__(self, opts, config, started_os_proc, buffer=None):
         self.opts = opts
         self.config = config
         self.started_os_proc = started_os_proc
@@ -116,10 +114,12 @@ def _compile_prefixes(self):
                 values = prefix.get('values', {})
                 line = prefix.get('line', '')
                 if prefix.get('__python_fun__'):
-                    self.compiled_prefixes[dev_os].append({
-                        '__python_fun__': prefix['__python_fun__'],
-                        '__python_mod__': prefix['__python_mod__']
-                    })
+                    self.compiled_prefixes[dev_os].append(
+                        {
+                            '__python_fun__': prefix['__python_fun__'],
+                            '__python_mod__': prefix['__python_mod__'],
+                        }
+                    )
                     continue  # if python profiler defined for this prefix,
                     # no need to go further, but jump to the next prefix
                 # Add 'pri' and 'message' to the line, and values
@@ -138,12 +138,14 @@ def _compile_prefixes(self):
                 escaped = re.escape(line).replace(r'\{', '{').replace(r'\}', '}')
                 # Replace a whitespace with \s+
                 escaped = escaped.replace(r'\ ', r'\s+')
-                self.compiled_prefixes[dev_os].append({
-                    'prefix': re.compile(escaped.format(**values)),
-                    'prefix_positions': sorted_position,
-                    'raw_prefix': escaped.format(**values),
-                    'values': values
-                })
+                self.compiled_prefixes[dev_os].append(
+                    {
+                        'prefix': re.compile(escaped.format(**values)),
+                        'prefix_positions': sorted_position,
+                        'raw_prefix': escaped.format(**values),
+                        'values': values,
+                    }
+                )
         # log.debug('Compiled prefixes')
         # log.debug(self.compiled_prefixes)
 
@@ -158,12 +160,19 @@ def _identify_prefix(self, msg, data):
             prefix_id += 1
             match = None
             if '__python_fun__' in prefix:
-                log.debug('Trying to match using the %s custom python profiler', prefix['__python_mod__'])
+                log.debug(
+                    'Trying to match using the %s custom python profiler',
+                    prefix['__python_mod__'],
+                )
                 try:
                     match = prefix['__python_fun__'](msg)
                 except Exception:
-                    log.error('Exception while parsing %s with the %s python profiler',
-                              msg, prefix['__python_mod__'], exc_info=True)
+                    log.error(
+                        'Exception while parsing %s with the %s python profiler',
+                        msg,
+                        prefix['__python_mod__'],
+                        exc_info=True,
+                    )
             else:
                 log.debug('Matching using YAML-defined profiler:')
                 log.debug(prefix['raw_prefix'])
@@ -172,7 +181,11 @@ def _identify_prefix(self, msg, data):
                 log.debug('Match not found')
                 continue
             if '__python_fun__' in prefix:
-                log.debug('%s matched using the custom python profiler %s', msg, prefix['__python_mod__'])
+                log.debug(
+                    '%s matched using the custom python profiler %s',
+                    msg,
+                    prefix['__python_mod__'],
+                )
                 msg_dict = match  # the output as-is from the custom function
             else:
                 positions = prefix.get('prefix_positions', {})
@@ -187,7 +200,9 @@ def _identify_prefix(self, msg, data):
             # The pri has to be an int as it is retrived using regex '\<(\d+)\>'
             if 'pri' in msg_dict:
                 msg_dict['facility'] = int(int(msg_dict['pri']) / 8)
-                msg_dict['severity'] = int(int(msg_dict['pri']) - (msg_dict['facility'] * 8))
+                msg_dict['severity'] = int(
+                    int(msg_dict['pri']) - (msg_dict['facility'] * 8)
+                )
             return msg_dict
 
     def _identify_os(self, msg):
@@ -223,41 +238,49 @@ def start(self):
         # metric counters
         napalm_logs_server_messages_received = Counter(
             "napalm_logs_server_messages_received",
-            "Count of messages received from listener processes"
+            "Count of messages received from listener processes",
         )
         napalm_logs_server_skipped_buffered_messages = Counter(
             'napalm_logs_server_skipped_buffered_messages',
             'Count of messages skipped as they were already buffered',
-            ['device_os']
+            ['device_os'],
         )
         napalm_logs_server_messages_with_identified_os = Counter(
             "napalm_logs_server_messages_with_identified_os",
             "Count of messages with positive os identification",
-            ['device_os']
+            ['device_os'],
         )
         napalm_logs_server_messages_without_identified_os = Counter(
             "napalm_logs_server_messages_without_identified_os",
-            "Count of messages with negative os identification"
+            "Count of messages with negative os identification",
         )
         napalm_logs_server_messages_failed_device_queuing = Counter(
             "napalm_logs_server_messages_failed_device_queuing",
             "Count of messages per device os that fail to be queued to a device process",
-            ['device_os']
+            ['device_os'],
         )
         napalm_logs_server_messages_device_queued = Counter(
             "napalm_logs_server_messages_device_queued",
             "Count of messages queued to device processes",
-            ['device_os']
+            ['device_os'],
         )
         napalm_logs_server_messages_unknown_queued = Counter(
             "napalm_logs_server_messages_unknown_queued",
-            "Count of messages queued as unknown"
+            "Count of messages queued as unknown",
         )
+        if self.opts.get('metrics_include_attributes', True):
+            napalm_logs_server_messages_attrs = Counter(
+                "napalm_logs_server_messages_attrs",
+                "Count of messages from the server process with their details",
+                ['device_os', 'host', 'tag'],
+            )
         self._setup_ipc()
         # Start suicide polling thread
         cleanup = threading.Thread(target=self._cleanup_buffer)
         cleanup.start()
-        thread = threading.Thread(target=self._suicide_when_without_parent, args=(os.getppid(),))
+        thread = threading.Thread(
+            target=self._suicide_when_without_parent, args=(os.getppid(),)
+        )
         thread.start()
         signal.signal(signal.SIGTERM, self._exit_gracefully)
         self.__up = True
@@ -273,10 +296,11 @@ def start(self):
                 else:
                     log.error(error, exc_info=True)
                     raise NapalmLogsExit(error)
-            if six.PY3:
-                msg = str(msg, 'utf-8')
-            else:
-                msg = msg.encode('utf-8')
+            if isinstance(msg, bytes):
+                if six.PY3:
+                    msg = str(msg, 'utf-8')
+                else:
+                    msg = msg.encode('utf-8')
             log.debug('[%s] Dequeued message from %s: %s', address, msg, time.time())
             napalm_logs_server_messages_received.inc()
             os_list = self._identify_os(msg)
@@ -289,32 +313,65 @@ def start(self):
                     log.debug('Queueing message to %s', dev_os)
                     if six.PY3:
                         dev_os = bytes(dev_os, 'utf-8')
+                    napalm_logs_server_messages_with_identified_os.labels(
+                        device_os=dev_os.decode()
+                    ).inc()
                     if self._buffer:
-                        message = '{dev_os}/{host}/{msg}'.format(dev_os=dev_os,
-                                                                 host=msg_dict['host'],
-                                                                 msg=msg_dict['message'])
-                        message_key = base64.b64encode(message)
+                        message = '{dev_os}/{host}/{msg}'.format(
+                            dev_os=dev_os.decode(),
+                            host=msg_dict['host'],
+                            msg=msg_dict['message'],
+                        )
+                        if six.PY3:
+                            message_key = base64.b64encode(
+                                bytes(message, 'utf-8')
+                            ).decode()
+                        else:
+                            message_key = base64.b64encode(message)
                         if self._buffer[message_key]:
-                            log.info('"%s" seems to be already buffered, skipping', msg_dict['message'])
-                            napalm_logs_server_skipped_buffered_messages.labels(device_os=dev_os.decode()).inc()
+                            log.info(
+                                '"%s" seems to be already buffered, skipping',
+                                msg_dict['message'],
+                            )
+                            napalm_logs_server_skipped_buffered_messages.labels(
+                                device_os=dev_os.decode()
+                            ).inc()
                             continue
-                        log.debug('"%s" is not buffered yet, added', msg_dict['message'])
+                        log.debug(
+                            '"%s" is not buffered yet, added', msg_dict['message']
+                        )
                         self._buffer[message_key] = 1
-                    self.pub.send_multipart([dev_os,
-                                             umsgpack.packb((msg_dict, address))])
-                    # self.os_pipes[dev_os].send((msg_dict, address))
-                    napalm_logs_server_messages_with_identified_os.labels(device_os=dev_os.decode()).inc()
-                    napalm_logs_server_messages_device_queued.labels(device_os=dev_os.decode()).inc()
+                    self.pub.send_multipart(
+                        [dev_os, umsgpack.packb((msg_dict, address))]
+                    )
+                    napalm_logs_server_messages_device_queued.labels(
+                        device_os=dev_os.decode()
+                    ).inc()
+                    if self.opts.get('metrics_server_include_attributes', True):
+                        napalm_logs_server_messages_attrs.labels(
+                            device_os=dev_os.decode(),
+                            host=msg_dict['host'],
+                            tag=msg_dict['tag'],
+                        ).inc()
 
                 elif dev_os and dev_os not in self.started_os_proc:
                     # Identified the OS, but the corresponding process does not seem to be started.
-                    log.info('Unable to queue the message to %s. Is the sub-process started?', dev_os)
-                    napalm_logs_server_messages_with_identified_os.labels(device_os=dev_os.decode()).inc()
-                    napalm_logs_server_messages_failed_device_queuing.labels(device_os=dev_os.decode()).inc()
+                    log.info(
+                        'Unable to queue the message to %s. Is the sub-process started?',
+                        dev_os,
+                    )
+                    napalm_logs_server_messages_with_identified_os.labels(
+                        device_os=dev_os.decode()
+                    ).inc()
+                    napalm_logs_server_messages_failed_device_queuing.labels(
+                        device_os=dev_os.decode()
+                    ).inc()
 
                 elif not dev_os and self.opts['_server_send_unknown']:
                     # OS not identified, but the user requested to publish the message as-is
-                    log.debug('Unable to identify the OS, sending directly to the publishers')
+                    log.debug(
+                        'Unable to identify the OS, sending directly to the publishers'
+                    )
                     to_publish = {
                         'ip': address,
                         'host': 'unknown',
@@ -322,7 +379,7 @@ def start(self):
                         'message_details': msg_dict,
                         'os': UNKNOWN_DEVICE_NAME,
                         'error': 'UNKNOWN',
-                        'model_name': 'unknown'
+                        'model_name': 'unknown',
                     }
                     self.publisher_pub.send(umsgpack.packb(to_publish))
                     napalm_logs_server_messages_unknown_queued.inc()
diff --git a/napalm_logs/transport/__init__.py b/napalm_logs/transport/__init__.py
index 52bee647..84fd25cf 100644
--- a/napalm_logs/transport/__init__.py
+++ b/napalm_logs/transport/__init__.py
@@ -11,21 +11,28 @@
 # Import napalm-logs pkgs
 # Exceptions
 from napalm_logs.exceptions import InvalidTransportException
+
 # Transport classes
 from napalm_logs.transport.cli import CLITransport
 from napalm_logs.transport.log import LogTransport
 from napalm_logs.transport.zeromq import ZMQTransport
+
 # extras: require additional underlying libraries
 # ~~~ Kafka ~~~
 from napalm_logs.transport.kafka import HAS_KAFKA
 from napalm_logs.transport.kafka import KafkaTransport
+
 # ~~~ HTTP ~~~
 from napalm_logs.transport.http import HAS_TORNADO
 from napalm_logs.transport.http import HAS_REQUESTS
 from napalm_logs.transport.http import HTTPTransport
+
 # ~~~Alerta~~~
 from napalm_logs.transport.alerta import AlertaTransport
+
 # from napalm_logs.transport.rabbitmq import RabbitMQTransport
+# ~~~Prometheus~~~
+from napalm_logs.transport.prometheus import PrometheusTransport
 
 log = logging.getLogger(__file__)
 
@@ -36,13 +43,16 @@
     'print': CLITransport,
     'console': CLITransport,
     'log': LogTransport,
+    'prometheus': PrometheusTransport,
     # 'rmq': RabbitMQransport,
     # 'rabbitmq': RabbitMQransport,
-    '*': ZMQTransport
+    '*': ZMQTransport,
 }
 
 if HAS_KAFKA:
-    log.info('Kafka dependency seems to be installed, making kafka transport available.')
+    log.info(
+        'Kafka dependency seems to be installed, making kafka transport available.'
+    )
     TRANSPORT_LOOKUP['kafka'] = KafkaTransport
 
 if HAS_REQUESTS or HAS_TORNADO:
@@ -60,11 +70,11 @@ def get_transport(name):
         log.debug('Using %s as transport', name)
         return TRANSPORT_LOOKUP[name]
     except KeyError:
-        msg = 'Transport {} is not available. Are the dependencies installed?'.format(name)
+        msg = 'Transport {} is not available. Are the dependencies installed?'.format(
+            name
+        )
         log.error(msg, exc_info=True)
         raise InvalidTransportException(msg)
 
 
-__all__ = (
-    'get_transport',
-)
+__all__ = ('get_transport',)
diff --git a/napalm_logs/transport/alerta.py b/napalm_logs/transport/alerta.py
index eef762fd..eac30f9e 100644
--- a/napalm_logs/transport/alerta.py
+++ b/napalm_logs/transport/alerta.py
@@ -21,7 +21,7 @@
     7: 'trace',
     8: 'indeterminate',
     9: 'normal',
-    10: 'unknown'
+    10: 'unknown',
 }
 
 
@@ -29,6 +29,7 @@ class AlertaTransport(HTTPTransport):
     '''
     Alerta publisher class.
     '''
+
     def __init__(self, address, port, **kwargs):
         super().__init__(address, port, **kwargs)
         if not self.address.endswith('/alert') and not self.address.endswith('/alert/'):
@@ -42,30 +43,45 @@ def __init__(self, address, port, **kwargs):
         if token and 'Authorization' not in self.headers:
             self.headers.update({'Authorization': 'Bearer {}'.format(token)})
         self.environment = kwargs.get('environment')
+        self.pairs = kwargs.get('pairs')
+        if not self.pairs:
+            self.pairs = {
+                'INTERFACE_UP': 'INTERFACE_DOWN',
+                'OSPF_NEIGHBOR_UP': 'OSPF_NEIGHBOR_DOWN',
+                'ISIS_NEIGHBOR_UP': 'ISIS_NEIGHBOR_DOWN',
+            }
 
     def publish(self, obj):
         data = napalm_logs.utils.unserialize(obj)
+        error = data['error']
+        status = 'open'
+        if error in self.pairs:
+            error = self.pairs[error]
+            status = 'closed'
         alerta_data = {
-            'resource': '{host}::{msg}'.format(host=data['host'], msg=data['error']),
+            'resource': '{host}::{msg}'.format(host=data['host'], msg=error),
             'event': data['error'],
             'service': ['napalm-logs'],
             'text': data['message_details']['message'].strip(),
             'attributes': data,
+            'status': status,
         }
         if self.environment:
             alerta_data['environment'] = self.environment
         alerta_data['severity'] = ALERTA_SEVERITY.get(data['severity'], 'unknown')
         if self.backend == 'tornado':
-            self.tornado_client.fetch(self.address,
-                                      callback=self._handle_tornado_response,
-                                      method=self.method,
-                                      headers=self.headers,
-                                      auth_username=self.username,
-                                      auth_password=self.password,
-                                      body=str(alerta_data),
-                                      validate_cert=self.verify_ssl,
-                                      allow_nonstandard_methods=True,
-                                      decompress_response=False)
+            self.tornado_client.fetch(
+                self.address,
+                callback=self._handle_tornado_response,
+                method=self.method,
+                headers=self.headers,
+                auth_username=self.username,
+                auth_password=self.password,
+                body=str(alerta_data),
+                validate_cert=self.verify_ssl,
+                allow_nonstandard_methods=True,
+                decompress_response=False,
+            )
         elif self.backend == 'requests':
             # Queue the publish object async
             self._publish_queue.put_nowait(alerta_data)
diff --git a/napalm_logs/transport/base.py b/napalm_logs/transport/base.py
index 48cc140b..80bcd6e1 100644
--- a/napalm_logs/transport/base.py
+++ b/napalm_logs/transport/base.py
@@ -8,6 +8,7 @@ class TransportBase:
     '''
     The base class for the transport.
     '''
+
     def __init__(self, address, port, **kwargs):
         pass
 
diff --git a/napalm_logs/transport/cli.py b/napalm_logs/transport/cli.py
index 82918baa..f6d45808 100644
--- a/napalm_logs/transport/cli.py
+++ b/napalm_logs/transport/cli.py
@@ -15,6 +15,7 @@ class CLITransport(TransportBase):
     '''
     CLI transport class.
     '''
+
     NO_ENCRYPT = True
     # This tells the publisher to not encrypt the messages
     #   published over this channel.
diff --git a/napalm_logs/transport/http.py b/napalm_logs/transport/http.py
index 021aa6a3..f87489f4 100644
--- a/napalm_logs/transport/http.py
+++ b/napalm_logs/transport/http.py
@@ -9,6 +9,7 @@
 import logging
 import threading
 import json
+
 try:
     import Queue as queue
 except ImportError:
@@ -17,6 +18,7 @@
 # Import third party libs
 try:
     import requests
+
     HAS_REQUESTS = True
 except ImportError:
     HAS_REQUESTS = False
@@ -24,6 +26,7 @@
 try:
     import tornado
     import tornado.httpclient
+
     HAS_TORNADO = True
 except ImportError:
     HAS_TORNADO = False
@@ -40,6 +43,7 @@ class HTTPTransport(TransportBase):
     '''
     HTTP transport class.
     '''
+
     NO_ENCRYPT = True
     # This tells the publisher to not encrypt the messages
     #   published over this channel.
@@ -76,12 +80,18 @@ def start(self):
         if self.backend not in ('requests', 'tornado'):
             raise TransportException('Invalid HTTP backend: %s', self.backend)
         if self.backend == 'requests' and not HAS_REQUESTS:
-            raise TransportException('Trying to use Requests as backend, but it is not installed')
+            raise TransportException(
+                'Trying to use Requests as backend, but it is not installed'
+            )
         if self.backend == 'tornado' and not HAS_TORNADO:
-            raise TransportException('Trying to use Tornado as backend, but it is not installed')
+            raise TransportException(
+                'Trying to use Tornado as backend, but it is not installed'
+            )
         # Prepare the tornado backend
         if self.backend == 'tornado':
-            self.tornado_client = tornado.httpclient.AsyncHTTPClient(max_clients=self.max_clients)
+            self.tornado_client = tornado.httpclient.AsyncHTTPClient(
+                max_clients=self.max_clients
+            )
         elif self.backend == 'requests':
             # When using requests, we start a threaded pool
             #   with the size specified using max_clients.
@@ -103,16 +113,18 @@ def start(self):
     def publish(self, obj):
         data = napalm_logs.utils.unserialize(obj)
         if self.backend == 'tornado':
-            self.tornado_client.fetch(self.address,
-                                      callback=self._handle_tornado_response,
-                                      method=self.method,
-                                      headers=self.headers,
-                                      auth_username=self.username,
-                                      auth_password=self.password,
-                                      body=str(data),
-                                      validate_cert=self.verify_ssl,
-                                      allow_nonstandard_methods=True,
-                                      decompress_response=False)
+            self.tornado_client.fetch(
+                self.address,
+                callback=self._handle_tornado_response,
+                method=self.method,
+                headers=self.headers,
+                auth_username=self.username,
+                auth_password=self.password,
+                body=str(data),
+                validate_cert=self.verify_ssl,
+                allow_nonstandard_methods=True,
+                decompress_response=False,
+            )
         elif self.backend == 'requests':
             # Queue the publish object async
             self._publish_queue.put_nowait(data)
@@ -136,10 +148,7 @@ def _publish_requests(self):
             session.verify = self.verify_ssl
             try:
                 result = session.request(
-                    self.method,
-                    self.address,
-                    params=self.params,
-                    data=json.dumps(data)
+                    self.method, self.address, params=self.params, data=json.dumps(data)
                 )
                 if not result.ok:
                     log.error('Unable to publish to %s', self.address)
diff --git a/napalm_logs/transport/kafka.py b/napalm_logs/transport/kafka.py
index f9614ecd..f0fa34ba 100644
--- a/napalm_logs/transport/kafka.py
+++ b/napalm_logs/transport/kafka.py
@@ -11,6 +11,7 @@
 # Import third party libs
 try:
     import kafka
+
     HAS_KAFKA = True
 except ImportError:
     HAS_KAFKA = False
@@ -26,6 +27,7 @@ class KafkaTransport(TransportBase):
     '''
     Kafka transport class.
     '''
+
     def __init__(self, address, port, **kwargs):
         if kwargs.get('address'):
             address = kwargs['address']
@@ -41,7 +43,9 @@ def __init__(self, address, port, **kwargs):
 
     def start(self):
         try:
-            self.producer = kafka.KafkaProducer(bootstrap_servers=self.bootstrap_servers)
+            self.producer = kafka.KafkaProducer(
+                bootstrap_servers=self.bootstrap_servers
+            )
         except kafka.errors.NoBrokersAvailable as err:
             log.error(err, exc_info=True)
             raise NapalmLogsException(err)
diff --git a/napalm_logs/transport/log.py b/napalm_logs/transport/log.py
index 9da7c4ff..786ef460 100644
--- a/napalm_logs/transport/log.py
+++ b/napalm_logs/transport/log.py
@@ -19,6 +19,7 @@ class LogTransport(TransportBase):
     '''
     Log transport class.
     '''
+
     def __init__(self, address, port, **kwargs):
         if kwargs.get('address'):
             self.address = kwargs['address']
diff --git a/napalm_logs/transport/prometheus.py b/napalm_logs/transport/prometheus.py
new file mode 100644
index 00000000..71719a8b
--- /dev/null
+++ b/napalm_logs/transport/prometheus.py
@@ -0,0 +1,606 @@
+# -*- coding: utf-8 -*-
+'''
+Export napalm-logs notifications as Prometheus metrics.
+'''
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+# Import stdlib
+import logging
+
+# Import third party libs
+from prometheus_client import Counter, Gauge
+
+# Import napalm-logs pkgs
+import napalm_logs.utils
+from napalm_logs.transport.base import TransportBase
+
+log = logging.getLogger(__name__)
+
+
+class PrometheusTransport(TransportBase):
+    '''
+    Prom transport class.
+    '''
+
+    def __init__(self, address, port, **kwargs):
+        self.metrics = {}
+
+    def __parse_without_details(self, msg):
+        '''
+        Helper to generate Counter metrics that only provide the host label
+        from the structured message.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host'],
+            )
+        self.metrics[error].labels(host=msg['host']).inc()
+
+    def __parse_user_action(self, msg):
+        '''
+        Helper to generate Counter metrics that provide the host label, together
+        with the username under a YANG structure users > user > [USER].
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'user'],
+            )
+        self.metrics[error].labels(
+            host=msg['host'], user=list(msg['yang_message']['users']['user'].keys())[0]
+        ).inc()
+
+    def __parse_interface_basic(self, msg):
+        '''
+        Helper to generate Counter metrics for interface notifications.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'interface'],
+            )
+        if 'interface_state' not in self.metrics:
+            self.metrics['interface_state'] = Gauge(
+                'napalm_logs_interface_state',
+                'State of this interface. 0=DOWN, 1=UP',
+                ['host', 'interface'],
+            )
+        labels = {
+            'host': msg['host'],
+            'interface': list(msg['yang_message']['interfaces']['interface'].keys())[0],
+        }
+        self.metrics[error].labels(**labels).inc()
+        state = 1 if error == 'INTERFACE_UP' else 0
+        self.metrics['interface_state'].labels(**labels).set(state)
+
+    def __parse_lacp(self, msg):
+        '''
+        Helper to generate Counter metrics for LACP notifications.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'interface', 'member'],
+            )
+        lacp_dict = msg['yang_message']['lacp']['interfaces']['interface']
+        if_name = list(lacp_dict.keys())[0]
+        members_dict = lacp_dict[if_name]['members']['member']
+        member_name = list(members_dict.keys())[0]
+        self.metrics[error].labels(
+            host=msg['host'], interface=if_name, member=member_name
+        ).inc()
+
+    def __parse_bgp_basic(self, msg):
+        '''
+        Helper to generate Counter metrics for simple BGP notifications,
+        providing the neighbor address and peer AS number.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'neighbor', 'peer_as'],
+            )
+        neigh_dict = msg['yang_message']['bgp']['neighbors']['neighbor']
+        neighbor = list(neigh_dict.keys())[0]
+        self.metrics[error].labels(
+            host=msg['host'],
+            neighbor=neighbor,
+            peer_as=neigh_dict[neighbor]['state']['peer_as'],
+        ).inc()
+
+    def __parse_network_instance_bgp(self, msg):
+        '''
+        Helper to generate Counter metrics for simple BGP notifications, nested
+        under the network-instance OpenConfig model, providing the neighbor
+        address, instance name, and peer AS number.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'routing_instance', 'neighbor', 'peer_as'],
+            )
+        instance_name = list(
+            msg['yang_message']['network-instances']['network-instance'].keys()
+        )[0]
+        instance_dict = msg['yang_message']['network-instances']['network-instance'][
+            instance_name
+        ]
+        neigh_dict = instance_dict['protocols']['protocol']['bgp']['neighbors'][
+            'neighbor'
+        ]
+        neighbor = list(neigh_dict.keys())[0]
+        self.metrics[error].labels(
+            host=msg['host'],
+            routing_instance=instance_name,
+            neighbor=neighbor,
+            peer_as=neigh_dict[neighbor]['state']['peer_as'],
+        ).inc()
+
+    def __parse_bgp_no_asn(self, msg):
+        '''
+        Helper to generate Counter metrics for simple BGP notifications - but
+        without the AS number, so only the neighbor IP address.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'neighbor'],
+            )
+        neigh_dict = msg['yang_message']['bgp']['neighbors']['neighbor']
+        neighbor = list(neigh_dict.keys())[0]
+        self.metrics[error].labels(host=msg['host'], neighbor=neighbor,).inc()
+
+    def __parse_ospf_neighbor(self, msg):
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'area', 'neighbor', 'interface'],
+            )
+        if 'ospf_neighbor' not in self.metrics:
+            self.metrics['ospf_neighbor'] = Gauge(
+                'napalm_logs_ospf_neighbor_state',
+                'State of the OSPF neighbor. 0=DOWN, 1=UP',
+                ['host', 'area', 'neighbor', 'interface'],
+            )
+        area_dict = msg['yang_message']['network-instances']['network-instance'][
+            'global'
+        ]['protocols']['protocol']['ospf']['ospfv2']['areas']['area']
+        area_id = list(area_dict.keys())[0]
+        iface_dict = area_dict[area_id]['interfaces']['interface']
+        iface_name = list(iface_dict.keys())[0]
+        neighbor = list(iface_dict[iface_name]['neighbors']['neighbor'].keys())[0]
+        labels = {
+            'host': msg['host'],
+            'area': area_id,
+            'neighbor': neighbor,
+            'interface': iface_name,
+        }
+        self.metrics[error].labels(**labels).inc()
+        state = 1 if error == 'OSPF_NEIGHBOR_UP' else 0
+        self.metrics['ospf_neighbor'].labels(**labels).set(state)
+
+    def __parse_isis_neighbor(self, msg):
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'interface', 'level', 'neighbor'],
+            )
+        if 'isis_neighbor' not in self.metrics:
+            self.metrics['isis_neighbor'] = Gauge(
+                'napalm_logs_isis_neighbor_state',
+                'State of the ISIS neighbor. 0=DOWN, 1=UP',
+                ['host', 'interface', 'level', 'neighbor'],
+            )
+        iface_dict = msg['yang_message']['network-instances']['network-instance'][
+            'global'
+        ]['protocols']['protocol']['isis']['interfaces']['interface']
+        iface_name = list(iface_dict.keys())[0]
+        level_dict = iface_dict[iface_name]['levels']['level']
+        level = list(level_dict.keys())[0]
+        neighbor = list(level_dict[level]['adjacencies']['adjacency'].keys())[0]
+        labels = {
+            'host': msg['host'],
+            'interface': iface_name,
+            'level': level,
+            'neighbor': neighbor,
+        }
+        self.metrics[error].labels(**labels).inc()
+        state = 1 if error == 'ISIS_NEIGHBOR_UP' else 0
+        self.metrics['isis_neighbor'].labels(**labels).set(state)
+
+    def __parse_nat_session(self, msg):
+        error = msg['error']
+        labels = [
+            'service_name',
+            'source_address',
+            'source_port',
+            'destination_address',
+            'destination_port',
+            'nat_destination_address',
+            'nat_destination_port',
+            'nat_source_address',
+            'nat_source_port',
+        ]
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host'] + labels,
+            )
+        event = list(msg['yang_message']['security']['flow'].keys())[0]
+        label_values = {'host': msg['host']}
+        for label in labels:
+            label_values[label] = msg['yang_message']['security']['flow'][event][label]
+        self.metrics[error].labels(**label_values).inc()
+
+    def _parse_interface_down(self, msg):
+        '''
+        Build metrics from INTERFACE_DOWN notifications.
+        '''
+        self.__parse_interface_basic(msg)
+
+    def _parse_interface_up(self, msg):
+        '''
+        Build metrics from INTERFACE_UP notifications.
+        '''
+        self.__parse_interface_basic(msg)
+
+    def _parse_interface_duplex_mode(self, msg):
+        '''
+        Build metrics from INTERFACE_DUPLEX_MODE notifications.
+        '''
+        if 'INTERFACE_DUPLEX_MODE' not in self.metrics:
+            self.metrics['INTERFACE_DUPLEX_MODE'] = Counter(
+                'napalm_logs_interface_duplex_mode',
+                'Counter for INTERFACE_DUPLEX_MODE notifications',
+                ['host', 'interface', 'duplex_mode'],
+            )
+        iface_dict = msg['yang_message']['interfaces']['interface']
+        iface_name = list(iface_dict.keys())[0]
+        self.metrics['INTERFACE_DUPLEX_MODE'].labels(
+            host=msg['host'],
+            interface=iface_name,
+            duplex_mode=iface_dict[iface_name]['ethernet']['state']['duplex_mode'],
+        )
+
+    def _parse_interface_mac_limit_reached(self, msg):
+        '''
+        Build metrics from INTERFACE_MAC_LIMIT_REACHED notifications.
+        '''
+        if 'INTERFACE_MAC_LIMIT_REACHED' not in self.metrics:
+            self.metrics['INTERFACE_MAC_LIMIT_REACHED'] = Gauge(
+                'napalm_logs_interface_mac_limit_reached',
+                'Counter for INTERFACE_MAC_LIMIT_REACHED notifications',
+                ['host', 'interface'],
+            )
+        iface_dict = msg['yang_message']['interfaces']['interface']
+        iface_name = list(iface_dict.keys())[0]
+        self.metrics['INTERFACE_MAC_LIMIT_REACHED'].labels(
+            host=msg['host'], interface=iface_name
+        ).set(iface_dict[iface_name]['ethernet']['state']['learned-mac-addresses'])
+
+    def _parse_lacp_interface_down(self, msg):
+        '''
+        Build metrics for LACP_INTERFACE_DOWN messages.
+        '''
+        self.__parse_lacp(msg)
+
+    def _parse_bfd_state_change(self, msg):
+        '''
+        Build metrics from BFD_STATE_CHANGE.
+        '''
+        if 'BFD_STATE_CHANGE' not in self.metrics:
+            self.metrics['BFD_STATE_CHANGE'] = Counter(
+                'napalm_logs_bfd_state_change',
+                'Counter for BFD_STATE_CHANGE notifications',
+                ['host', 'interface', 'session_state'],
+            )
+        iface_dict = msg['yang_message']['bfd']['interfaces']['interface']
+        self.metrics['BFD_STATE_CHANGE'].labels(
+            host=msg['host'],
+            interface=iface_dict['id'],
+            session_state=iface_dict['peers']['peer']['state']['session-state'],
+        ).inc()
+
+    def _parse_ntp_server_unreachable(self, msg):
+        '''
+        Build metrics from NTP_SERVER_UNREACHABLE notifications.
+        '''
+        if 'NTP_SERVER_UNREACHABLE' not in self.metrics:
+            self.metrics['NTP_SERVER_UNREACHABLE'] = Counter(
+                'napalm_logs_ntp_server_unreachable',
+                'Counter for NTP_SERVER_UNREACHABLE notifications',
+                ['host', 'ntp_server'],
+            )
+        self.metrics['NTP_SERVER_UNREACHABLE'].labels(
+            host=msg['host'],
+            ntp_server=list(
+                msg['yang_message']['system']['ntp']['servers']['server'].keys()
+            )[0],
+        ).inc()
+
+    def _parse_bgp_prefix_limit_exceeded(self, msg):
+        '''
+        Build metrics form BGP_PREFIX_LIMIT_EXCEEDED notifications.
+        '''
+        self.__parse_bgp_basic(msg)
+
+    def _parse_bgp_prefix_thresh_exceeded(self, msg):
+        '''
+        Build metrics from BGP_PREFIX_THRESH_EXCEEDED notifications.
+        '''
+        self.__parse_bgp_basic(msg)
+
+    def _parse_bgp_peer_not_configured(self, msg):
+        '''
+        Build metrics from BGP_PEER_NOT_CONFIGURED notifications.
+        '''
+        self.__parse_bgp_basic(msg)
+
+    def _parse_bgp_session_not_configured(self, msg):
+        '''
+        Build metrics from BGP_SESSION_NOT_CONFIGURED notifications.
+        '''
+        self.__parse_bgp_no_asn(msg)
+
+    def _parse_bgp_connection_rejected(self, msg):
+        '''
+        Build metrics from BGP_CONNECTION_REJECTED notifications.
+        '''
+        self.__parse_bgp_basic(msg)
+
+    def _parse_bgp_connection_reset(self, msg):
+        '''
+        Build metrics from BGP_CONNECTION_RESET notifications.
+        '''
+        self.__parse_bgp_basic(msg)
+
+    def _parse_bgp_incorrect_as_number(self, msg):
+        '''
+        Build metrics from BGP_INCORRECT_AS_NUMBER notifications.
+        '''
+        self.__parse_bgp_basic(msg)
+
+    def _parse_bgp_neighbor_state_changed(self, msg):
+        '''
+        Build metrics from BGP_NEIGHBOR_STATE_CHANGED.
+        '''
+        self.__parse_bgp_basic(msg)
+
+    def _parse_bgp_md5_incorrect(self, msg):
+        '''
+        Build metrics from BGP_MD5_INCORRECT.
+        '''
+        self.__parse_bgp_no_asn(msg)
+
+    def _parse_bgp_cease_prefix_limit_exceeded(self, msg):
+        '''
+        Build metrics for BGP_CEASE_PREFIX_LIMIT_EXCEEDED.
+        '''
+        self.__parse_network_instance_bgp(msg)
+
+    def _parse_user_enter_config_mode(self, msg):
+        '''
+        Build metrics for USER_ENTER_CONFIG_MODE.
+        '''
+        self.__parse_user_action(msg)
+
+    def _parse_user_exit_config_mode(self, msg):
+        '''
+        Build metrics for USER_EXIT_CONFIG_MODE.
+        '''
+        self.__parse_user_action(msg)
+
+    def _parse_user_write_config(self, msg):
+        '''
+        Build metrics for USER_WRITE_CONFIG.
+        '''
+        self.__parse_user_action(msg)
+
+    def _parse_user_login(self, msg):
+        '''
+        Build metrics for USER_LOGIN.
+        '''
+        self.__parse_user_action(msg)
+
+    def _parse_user_logout(self, msg):
+        '''
+        Build metrics for USER_LOGOUT.
+        '''
+        self.__parse_user_action(msg)
+
+    def _parse_configuration_commit_requested(self, msg):
+        '''
+        Build metrics for CONFIGURATION_COMMIT_REQUESTED.
+        '''
+        self.__parse_user_action(msg)
+
+    def _parse_configuration_rollback(self, msg):
+        '''
+        Build metrics for CONFIGURATION_ROLLBACK.
+        '''
+        self.__parse_user_action(msg)
+
+    def _parse_system_alarm(self, msg):
+        '''
+        Build metrics for SYSTEM_ALARM.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                [
+                    'host',
+                    'component_name',
+                    'component_class',
+                    'alarm_state',
+                    'alarm_reason',
+                ],
+            )
+        if 'system_alarm_state' not in self.metrics:
+            self.metrics['system_alarm_state'] = Gauge(
+                'napalm_logs_system_alarm_state',
+                'State of the system alarm. 1=SET, 0=CLEARED',
+                [
+                    'host',
+                    'component_name',
+                    'component_class',
+                    'alarm_state',
+                    'alarm_reason',
+                ],
+            )
+        component = msg['yang_message']['hardware-state']['component']
+        component_name = list(component.keys())[0]
+        labels = {
+            'host': msg['host'],
+            'component_name': component_name,
+            'component_class': component[component_name]['class'],
+            'alarm_state': component[component_name]['state']['alarm-state'],
+            'alarm_reason': component[component_name]['state']['alarm-reason'],
+        }
+        self.metrics[error].labels(**labels).inc()
+        state = 1 if error == 'SYSTEM_ALARM' else 0
+        self.metrics['system_alarm_state'].labels(**labels).set(state)
+
+    def _parse_system_alarm_cleared(self, msg):
+        return self._parse_system_alarm(msg)
+
+    def __parse_minor_major_alarm(self, msg):
+        '''
+        Build metrics for MINOR_ALARM_* and MAJOR_ALARM_* notifications.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'alarm_reason'],
+            )
+        severity = error.split('_')[0].lower()
+        alarm_state_metric = '{}_alarm_state'.format(severity)
+        if alarm_state_metric not in self.metrics:
+            self.metrics[alarm_state_metric] = Gauge(
+                'napalm_logs_{}'.format(alarm_state_metric),
+                'State of the {} system alarm. 1=SET, 0=CLEARED'.format(severity),
+                ['host', 'alarm_reason'],
+            )
+        labels = {
+            'host': msg['host'],
+            'alarm_reason': msg['yang_message']['alarms']['alarm']['additional-text'],
+        }
+        self.metrics[error].labels(**labels).inc()
+        state = 1 if error == '{}_ALARM_SET'.format(severity.upper()) else 0
+        self.metrics[alarm_state_metric].labels(**labels).set(state)
+
+    def _parse_major_alarm_set(self, msg):
+        return self.__parse_minor_major_alarm(msg)
+
+    def _parse_major_alarm_cleared(self, msg):
+        return self.__parse_minor_major_alarm(msg)
+
+    def _parse_minor_alarm_set(self, msg):
+        return self.__parse_minor_major_alarm(msg)
+
+    def _parse_minor_alarm_cleared(self, msg):
+        return self.__parse_minor_major_alarm(msg)
+
+    def _parse_ospf_neighbor_up(self, msg):
+        '''
+        Build metrics for OSPF_NEIGHBOR_UP.
+        '''
+        self.__parse_ospf_neighbor(msg)
+
+    def _parse_ospf_neighbor_down(self, msg):
+        '''
+        Build metrics for OSPF_NEIGHBOR_DOWN.
+        '''
+        self.__parse_ospf_neighbor(msg)
+
+    def _parse_isis_neighbor_up(self, msg):
+        '''
+        Build metrics for ISIS_NEIGHBOR_UP.
+        '''
+        self.__parse_isis_neighbor(msg)
+
+    def _parse_isis_neighbor_down(self, msg):
+        '''
+        Build metrics for ISIS_NEIGHBOR_DOWN.
+        '''
+        self.__parse_isis_neighbor(msg)
+
+    def _parse_nat_session_created(self, msg):
+        '''
+        Build metrics for NAT_SESSION_CREATED.
+        '''
+        self.__parse_nat_session(msg)
+
+    def _parse_nat_session_closed(self, msg):
+        '''
+        Build metrics for NAT_SESSION_CLOSED.
+        '''
+        self.__parse_nat_session(msg)
+
+    def _parse_ddos_protocol_violation_set(self, msg):
+        '''
+        Build metrics for DDOS_PROTOCOL_VIOLATION_SET messages.
+        '''
+        error = msg['error']
+        if error not in self.metrics:
+            self.metrics[error] = Counter(
+                'napalm_logs_{error}'.format(error=error.lower()),
+                'Counter for {error} notifications'.format(error=error),
+                ['host', 'event_type', 'entity_type', 'additional_text'],
+            )
+        alarm_dict = msg['yang_message']['alarms']['alarm']
+        labels = {
+            'host': msg['host'],
+            'event_type': alarm_dict['event-type'],
+            'entity_type': alarm_dict['entity-type'],
+            'additional_text': alarm_dict['additional-text'],
+        }
+        self.metrics[error].labels(**labels).inc()
+
+    def start(self):
+        log.debug('Starting the Prometheus publisher')
+
+    def publish(self, obj):
+        data = napalm_logs.utils.unserialize(obj)
+        if data['error'] in ('RAW', 'UNKNOWN'):
+            return
+        fun_name = '_parse_{}'.format(data['error'].lower())
+        if hasattr(self, fun_name):
+            try:
+                getattr(self, fun_name)(data)
+            except Exception:
+                log.error(data, exc_info=True)
+                raise
+        else:
+            # Anything else goes into __parse_without_details which generates
+            # metrics using the napalm-logs error name, and the host as the only
+            # label. If we want something more specific than that, e.g., more
+            # (specific) labels, we'll define a _parse function as above.
+            self.__parse_without_details(data)
+
+    def stop(self):
+        log.debug('Stopping the Prometheus publisher')
diff --git a/napalm_logs/transport/zeromq.py b/napalm_logs/transport/zeromq.py
index dcb5dddc..15faaa7f 100644
--- a/napalm_logs/transport/zeromq.py
+++ b/napalm_logs/transport/zeromq.py
@@ -22,6 +22,7 @@ class ZMQTransport(TransportBase):
     '''
     ZMQ transport class.
     '''
+
     def __init__(self, address, port, **kwargs):
         if kwargs.get('address'):
             self.address = kwargs['address']
@@ -40,9 +41,8 @@ def start(self):
         if ':' in self.address:
             self.socket.ipv6 = True
         try:
-            self.socket.bind('tcp://{addr}:{port}'.format(
-                addr=self.address,
-                port=self.port)
+            self.socket.bind(
+                'tcp://{addr}:{port}'.format(addr=self.address, port=self.port)
             )
         except zmq.error.ZMQError as err:
             log.error(err, exc_info=True)
diff --git a/napalm_logs/utils/__init__.py b/napalm_logs/utils/__init__.py
index 1ae7b109..fd9ce594 100644
--- a/napalm_logs/utils/__init__.py
+++ b/napalm_logs/utils/__init__.py
@@ -39,12 +39,15 @@ class ClientAuth:
     '''
     Client auth class.
     '''
-    def __init__(self,
-                 certificate,
-                 address=defaults.AUTH_ADDRESS,
-                 port=defaults.AUTH_PORT,
-                 timeout=defaults.AUTH_TIMEOUT,
-                 max_try=defaults.AUTH_MAX_TRY):
+
+    def __init__(
+        self,
+        certificate,
+        address=defaults.AUTH_ADDRESS,
+        port=defaults.AUTH_PORT,
+        timeout=defaults.AUTH_TIMEOUT,
+        max_try=defaults.AUTH_MAX_TRY,
+    ):
         self.certificate = certificate
         self.address = address
         self.port = port
@@ -85,8 +88,11 @@ def keep_alive(self):
             msg = self.ssl_skt.recv(len(defaults.AUTH_KEEP_ALIVE_ACK))
             log.debug('Received %s from the keep-alive server', msg)
             if msg != defaults.AUTH_KEEP_ALIVE_ACK:
-                log.error('Received %s instead of %s form the auth keep-alive server',
-                          msg, defaults.AUTH_KEEP_ALIVE_ACK)
+                log.error(
+                    'Received %s instead of %s form the auth keep-alive server',
+                    msg,
+                    defaults.AUTH_KEEP_ALIVE_ACK,
+                )
                 log.error('Re-init the SSL socket.')
                 self.reconnect()
             time.sleep(defaults.AUTH_KEEP_ALIVE_INTERVAL)
@@ -112,16 +118,20 @@ def authenticate(self):
         then do the handshake using the napalm-logs
         auth algorithm.
         '''
-        log.debug('Authenticate to %s:%d, using the certificate %s',
-                  self.address, self.port, self.certificate)
+        log.debug(
+            'Authenticate to %s:%d, using the certificate %s',
+            self.address,
+            self.port,
+            self.certificate,
+        )
         if ':' in self.address:
             skt_ver = socket.AF_INET6
         else:
             skt_ver = socket.AF_INET
         skt = socket.socket(skt_ver, socket.SOCK_STREAM)
-        self.ssl_skt = ssl.wrap_socket(skt,
-                                       ca_certs=self.certificate,
-                                       cert_reqs=ssl.CERT_REQUIRED)
+        self.ssl_skt = ssl.wrap_socket(
+            skt, ca_certs=self.certificate, cert_reqs=ssl.CERT_REQUIRED
+        )
         try:
             self.ssl_skt.connect((self.address, self.port))
             self.auth_try_id = 0
@@ -132,8 +142,12 @@ def authenticate(self):
                 log.error('Trying to authenticate again in %d seconds', self.timeout)
                 time.sleep(self.timeout)
                 self.authenticate()
-            log.critical('Giving up, unable to authenticate to %s:%d using the certificate %s',
-                         self.address, self.port, self.certificate)
+            log.critical(
+                'Giving up, unable to authenticate to %s:%d using the certificate %s',
+                self.address,
+                self.port,
+                self.certificate,
+            )
             raise ClientConnectException(err)
 
         # Explicit INIT
@@ -147,7 +161,9 @@ def authenticate(self):
         # Send back explicit ACK
         self.ssl_skt.write(defaults.MAGIC_ACK)
         self.priv_key = nacl.secret.SecretBox(private_key)
-        self.verify_key = nacl.signing.VerifyKey(verify_key_hex, encoder=nacl.encoding.HexEncoder)
+        self.verify_key = nacl.signing.VerifyKey(
+            verify_key_hex, encoder=nacl.encoding.HexEncoder
+        )
 
     def decrypt(self, binary):
         '''
@@ -182,10 +198,11 @@ def cast(var, function):
         try:
             return locate(function)(var)
         except ValueError:
-            log.error('Unable to use function %s on value %s', function, var, exc_info=True)
+            log.error(
+                'Unable to use function %s on value %s', function, var, exc_info=True
+            )
     # If the function is str function
-    if hasattr(str, function) and\
-            hasattr(getattr(str, function), '__call__'):
+    if hasattr(str, function) and hasattr(getattr(str, function), '__call__'):
         return getattr(str, function)(var)
     glob = globals()
     # If the function is defined in this module
@@ -196,8 +213,7 @@ def cast(var, function):
 
 
 def color_to_severity(var):
-    colour_dict = {'RED': 3,
-                   'YELLOW': 4}
+    colour_dict = {'RED': 3, 'YELLOW': 4}
     return colour_dict.get(var, var)
 
 
@@ -205,10 +221,12 @@ def bgp_state_convert(state):
     """
     Given a matched BGP state, map it to a vendor agnostic version.
     """
-    state_dict = {'OpenSent': 'OPEN_SENT',
-                  'OpenConfirm': 'OPEN_CONFIRM',
-                  'Up': 'ESTABLISHED',
-                  'Down': 'ACTIVE'}
+    state_dict = {
+        'OpenSent': 'OPEN_SENT',
+        'OpenConfirm': 'OPEN_CONFIRM',
+        'Up': 'ESTABLISHED',
+        'Down': 'ACTIVE',
+    }
     return state_dict.get(state, state.upper())
 
 
@@ -246,7 +264,9 @@ def extract(rgx, msg, mapping, time_format=None):
         log.debug(ret)
     if time_format:
         try:
-            parsed_time = datetime.strptime(time_format[0].format(**ret), time_format[1])
+            parsed_time = datetime.strptime(
+                time_format[0].format(**ret), time_format[1]
+            )
         except ValueError as error:
             log.error('Unable to convert date and time into a timestamp: %s', error)
         ret['timestamp'] = int((parsed_time - datetime(1970, 1, 1)).total_seconds())
@@ -324,8 +344,9 @@ def dictupdate(dest, upd):
     Merges upd recursively into dest.
     '''
     recursive_update = True
-    if (not isinstance(dest, collections.Mapping)) \
-            or (not isinstance(upd, collections.Mapping)):
+    if (not isinstance(dest, collections.Mapping)) or (
+        not isinstance(upd, collections.Mapping)
+    ):
         raise TypeError('Cannot update using non-dict types in dictupdate.update()')
     updkeys = list(upd.keys())
     if not set(list(dest.keys())) & set(updkeys):
@@ -337,12 +358,12 @@ def dictupdate(dest, upd):
                 dest_subkey = dest.get(key, None)
             except AttributeError:
                 dest_subkey = None
-            if isinstance(dest_subkey, collections.Mapping) \
-                    and isinstance(val, collections.Mapping):
+            if isinstance(dest_subkey, collections.Mapping) and isinstance(
+                val, collections.Mapping
+            ):
                 ret = dictupdate(dest_subkey, val)
                 dest[key] = ret
-            elif isinstance(dest_subkey, list) \
-                    and isinstance(val, list):
+            elif isinstance(dest_subkey, list) and isinstance(val, list):
                 merged = copy.deepcopy(dest_subkey)
                 merged.extend([x for x in val if x not in merged])
                 dest[key] = merged
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 11e34447..ec240323 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,9 +1,10 @@
 -r requirements.txt
-pytest
-pylama
-jinja2
-coveralls
-pytest-cov
-pytest-json
-pytest-pythonpath
-flake8-import-order
+tox==3.21.4
+black==19.10b0
+pytest==6.2.2
+pylama==7.7.1
+jinja2==2.11.3
+pytest-cov==2.11.1
+pytest-json==0.4.0
+pytest-pythonpath==0.7.3
+flake8-import-order==0.18.1
diff --git a/requirements.txt b/requirements.txt
index aa7474ff..8458cd5e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
 pyzmq
 pyyaml
 pynacl
+dateparser
 u-msgpack-python
 prometheus_client
diff --git a/setup.cfg b/setup.cfg
index 65f1d603..b1b5fa3d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,15 +1,19 @@
+[bdist_wheel]
+universal = 1
+
+[metadata]
+license_file = LICENSE
 
 [pylama]
 linters = mccabe,pep8,pyflakes
 ignore = D203,C901
-skip = build/*,.tox/*
+skip = build/*,.tox/*,.venv/*
 
 [pylama:pep8]
 max_line_length = 120
 
 [tool:pytest]
-addopts = --cov=napalm_logs --cov-report term-missing -vs --pylama
-json_report = report.json
+json_report = /tmp/report.json
 jsonapi = true
 
 [coverage:run]
diff --git a/setup.py b/setup.py
index d402f800..44afd241 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@
 
 setup(
     name='napalm-logs',
-    version='0.9.0',
+    version='0.10.0',
     packages=find_packages(),
     author='Mircea Ulinic',
     author_email='mircea.ulinic@gmail.com',
@@ -35,7 +35,7 @@
         'Programming Language :: Python :: 3.7',
         'Operating System :: POSIX :: Linux',
         'Operating System :: MacOS',
-        'Intended Audience :: Developers'
+        'Intended Audience :: Developers',
     ],
     url='https://github.com/napalm-automation/napalm-logs',
     license="Apache License 2.0",
@@ -43,8 +43,6 @@
     include_package_data=True,
     install_requires=reqs,
     entry_points={
-        'console_scripts': [
-            'napalm-logs=napalm_logs.scripts.cli:napalm_logs_engine'
-        ],
-    }
+        'console_scripts': ['napalm-logs=napalm_logs.scripts.cli:napalm_logs_engine'],
+    },
 )
diff --git a/tox.ini b/tox.ini
index 48158abe..a37d8b82 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,6 @@
 [tox]
-envlist = py{27,35,36,37}
+envlist = py3{6,7},black,pylama
+skip_missing_interpreters = true
 
 [testenv]
 deps =
@@ -7,3 +8,19 @@ deps =
 
 commands=
    py.test -vv
+
+[testenv:black]
+deps =
+    -rrequirements-dev.txt
+
+basepython = python3.6
+commands =
+    black --check --skip-string-normalization .
+
+[testenv:pylama]
+deps =
+    -rrequirements-dev.txt
+
+basepython = python3.6
+commands =
+    pylama napalm_logs/


Test patch:
diff --git a/docs/developers/local_testing.rst b/docs/developers/local_testing.rst
new file mode 100644
index 00000000..93280844
--- /dev/null
+++ b/docs/developers/local_testing.rst
@@ -0,0 +1,32 @@
+.. _local_testing:
+
+=============
+Local Testing
+=============
+
+When adding new functionality to ``napalm-logs``, in particular new parsing templates, it can be helpful to run an end-to-end simulation.
+
+Testing Installation
+++++++++++++++++++++
+
+As referenced in the :ref:`installation <installation>` documentation, it's advisable to use a Python Virtual Environment. This also applies for the testing installation.
+Once you have the environment setup, you can install your local copy (with modifications) of ``napalm-logs`` in it:
+
+.. code-block:: bash
+
+    pip install -e <local_path/url>
+
+Suggested startup options for ``napalm-logs`` once it's installed:
+
+.. code-block:: bash
+
+    napalm-logs -l debug --disable-security --publisher cli  --listener udp --port 5514
+
+This will run ``napalm-logs`` in a ``debug`` log-level, without certificate-based authentication, using the ``cli`` publisher, while listening for ``syslog`` messages on UDP port 5514.
+You can then proceed to send a sample ``syslog`` message to ``napalm-logs`` using ``netcat``:
+
+.. code-block:: bash
+
+    nc -w0 -u 127.0.0.1 5514 <<< "<32>Oct 24 20:21:27  vmx01 jlaunchd: System reaching processes ceiling high watermark: Contact to system administrator to clean up unnecessary processes or increase maxproc ceiling. Further process fork request may be denied."
+
+The debug output will go to ``/var/log/napalm/logs/`` by default, and it will output the yang message on the CLI.
diff --git a/tests/config/eos/BGP_NEIGHBOR_STATE_CHANGED/ISO8601_datetime/syslog.msg b/tests/config/eos/BGP_NEIGHBOR_STATE_CHANGED/ISO8601_datetime/syslog.msg
new file mode 100644
index 00000000..1b008087
--- /dev/null
+++ b/tests/config/eos/BGP_NEIGHBOR_STATE_CHANGED/ISO8601_datetime/syslog.msg
@@ -0,0 +1,1 @@
+<13>2020-03-31T08:41:59+00:00 some-switch Bgp: %BGP-5-ADJCHANGE: peer 192.0.2.2 (VRF default AS 12345) old state Established event AdminReset new state Idle
diff --git a/tests/config/eos/BGP_NEIGHBOR_STATE_CHANGED/ISO8601_datetime/yang.json b/tests/config/eos/BGP_NEIGHBOR_STATE_CHANGED/ISO8601_datetime/yang.json
new file mode 100644
index 00000000..fafa67fe
--- /dev/null
+++ b/tests/config/eos/BGP_NEIGHBOR_STATE_CHANGED/ISO8601_datetime/yang.json
@@ -0,0 +1,37 @@
+{
+  "error": "BGP_NEIGHBOR_STATE_CHANGED", 
+  "message_details": {
+    "severity": 5, 
+    "processName": "Bgp", 
+    "pri": "13", 
+    "time": "08:41:59+00:00", 
+    "tag": "BGP-5-ADJCHANGE", 
+    "date": "2020-03-31", 
+    "facility": 1, 
+    "host": "some-switch", 
+    "message": ": peer 192.0.2.2 (VRF default AS 12345) old state Established event AdminReset new state Idle"
+  }, 
+  "os": "eos", 
+  "timestamp": 1585644119, 
+  "severity": 5, 
+  "yang_message": {
+    "bgp": {
+      "neighbors": {
+        "neighbor": {
+          "192.0.2.2": {
+            "state": {
+              "session-state": "IDLE", 
+              "session-state-old": "ESTABLISHED", 
+              "peer_as": "12345", 
+              "session-state-change-event": "AdminReset"
+            }
+          }
+        }
+      }
+    }
+  }, 
+  "yang_model": "openconfig-bgp", 
+  "ip": "127.0.0.1", 
+  "facility": 1, 
+  "host": "some-switch"
+}
diff --git a/tests/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED/default/syslog.msg b/tests/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED/default/syslog.msg
new file mode 100644
index 00000000..fba1ed35
--- /dev/null
+++ b/tests/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED/default/syslog.msg
@@ -0,0 +1,1 @@
+<172>2020-03-31T08:41:59+00:00 some-router rpd[13037]: BGP_CEASE_PREFIX_LIMIT_EXCEEDED: 172.17.17.1 (External AS 4208010101): Shutting down peer due to exceeding configured maximum prefix-limit(500) for inet-unicast nlri: 501 (instance underlay)
diff --git a/tests/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED/default/yang.json b/tests/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED/default/yang.json
new file mode 100644
index 00000000..8b40ab09
--- /dev/null
+++ b/tests/config/junos/BGP_CEASE_PREFIX_LIMIT_EXCEEDED/default/yang.json
@@ -0,0 +1,64 @@
+{
+  "ip": "127.0.0.1", 
+  "yang_model": "openconfig-network-instance", 
+  "error": "BGP_CEASE_PREFIX_LIMIT_EXCEEDED", 
+  "timestamp": 1585644119, 
+  "os": "junos", 
+  "host": "some-router", 
+  "yang_message": {
+    "network-instances": {
+      "network-instance": {
+        "underlay": {
+          "protocols": {
+            "protocol": {
+              "bgp": {
+                "neighbors": {
+                  "neighbor": {
+                    "172.17.17.1": {
+                      "state": {
+                        "peer_as": "4208010101"
+                      }, 
+                      "afi_safis": {
+                        "afi_safi": {
+                          "inet": {
+                            "state": {
+                              "prefixes": {
+                                "received": 501
+                              }
+                            }, 
+                            "ipv4_unicast": {
+                              "prefix_limit": {
+                                "state": {
+                                  "max_prefixes": 500
+                                }
+                              }
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }, 
+  "message_details": {
+    "message": "172.17.17.1 (External AS 4208010101): Shutting down peer due to exceeding configured maximum prefix-limit(500) for inet-unicast nlri: 501 (instance underlay)", 
+    "date": "2020-03-31", 
+    "processId": "13037", 
+    "tag": "BGP_CEASE_PREFIX_LIMIT_EXCEEDED", 
+    "processName": "rpd", 
+    "hostPrefix": null, 
+    "facility": 21, 
+    "pri": "172", 
+    "host": "some-router", 
+    "time": "08:41:59+00:00", 
+    "severity": 4
+  }, 
+  "facility": 21, 
+  "severity": 4
+}
diff --git a/tests/config/junos/BGP_CONNECTION_REJECTED/default/syslog.msg b/tests/config/junos/BGP_CONNECTION_REJECTED/default/syslog.msg
new file mode 100644
index 00000000..629bd3d9
--- /dev/null
+++ b/tests/config/junos/BGP_CONNECTION_REJECTED/default/syslog.msg
@@ -0,0 +1,1 @@
+<172>2020-03-31T08:41:59+00:00 some-router rpd[13037]: bgp_handle_notify:4236: NOTIFICATION received from 10.0.0.1 (External AS 64512): code 6 (Cease) subcode 5 (Connection Rejected)
diff --git a/tests/config/junos/BGP_CONNECTION_REJECTED/default/yang.json b/tests/config/junos/BGP_CONNECTION_REJECTED/default/yang.json
new file mode 100644
index 00000000..06d09f77
--- /dev/null
+++ b/tests/config/junos/BGP_CONNECTION_REJECTED/default/yang.json
@@ -0,0 +1,37 @@
+{
+  "error": "BGP_CONNECTION_REJECTED",
+  "host": "some-router",
+  "ip": "127.0.0.1",
+  "timestamp": 1585644119,
+  "yang_message": {
+    "bgp": {
+      "neighbors": {
+        "neighbor": {
+          "10.0.0.1": {
+            "state": {
+              "peer_as": "64512",
+              "session_state": "ACTIVE"
+            }
+          }
+        }
+      }
+    }
+  },
+  "message_details": {
+    "date": "2020-03-31",
+    "time": "08:41:59+00:00",
+    "hostPrefix": null,
+    "host": "some-router",
+    "processName": "rpd",
+    "processId": "13037",
+    "tag": "bgp_handle_notify",
+    "pri": "172",
+    "message": "4236: NOTIFICATION received from 10.0.0.1 (External AS 64512): code 6 (Cease) subcode 5 (Connection Rejected)",
+    "facility": 21,
+    "severity": 4
+  },
+  "yang_model": "openconfig-bgp",
+  "os": "junos",
+  "facility": 21,
+  "severity": 4
+}
diff --git a/tests/config/junos/BGP_CONNECTION_RESET/default/syslog.msg b/tests/config/junos/BGP_CONNECTION_RESET/default/syslog.msg
new file mode 100644
index 00000000..ffa79cf9
--- /dev/null
+++ b/tests/config/junos/BGP_CONNECTION_RESET/default/syslog.msg
@@ -0,0 +1,1 @@
+<172>2020-03-31T08:41:59+00:00 some-router rpd[13037]: bgp_recv: read from peer fd00:1065::1 (External AS 65500) failed: Connection reset by peer
diff --git a/tests/config/junos/BGP_CONNECTION_RESET/default/yang.json b/tests/config/junos/BGP_CONNECTION_RESET/default/yang.json
new file mode 100644
index 00000000..9ba5f9bf
--- /dev/null
+++ b/tests/config/junos/BGP_CONNECTION_RESET/default/yang.json
@@ -0,0 +1,37 @@
+{
+  "error": "BGP_CONNECTION_RESET",
+  "host": "some-router",
+  "ip": "127.0.0.1",
+  "timestamp": 1585644119,
+  "yang_message": {
+    "bgp": {
+      "neighbors": {
+        "neighbor": {
+          "fd00:1065::1": {
+            "state": {
+              "peer_as": "65500",
+              "session_state": "ACTIVE"
+            }
+          }
+        }
+      }
+    }
+  },
+  "message_details": {
+    "date": "2020-03-31",
+    "time": "08:41:59+00:00",
+    "hostPrefix": null,
+    "host": "some-router",
+    "processName": "rpd",
+    "processId": "13037",
+    "tag": "bgp_recv",
+    "pri": "172",
+    "message": "read from peer fd00:1065::1 (External AS 65500) failed: Connection reset by peer",
+    "facility": 21,
+    "severity": 4
+  },
+  "yang_model": "openconfig-bgp",
+  "os": "junos",
+  "facility": 21,
+  "severity": 4
+}
diff --git a/tests/config/junos/BGP_INCORRECT_AS_NUMBER/default/syslog.msg b/tests/config/junos/BGP_INCORRECT_AS_NUMBER/default/syslog.msg
new file mode 100644
index 00000000..7dd33873
--- /dev/null
+++ b/tests/config/junos/BGP_INCORRECT_AS_NUMBER/default/syslog.msg
@@ -0,0 +1,1 @@
+<172>2020-03-31T08:41:59+00:00 some-switch rpd[4936]: bgp_pp_recv:4609: NOTIFICATION sent to 10.37.96.6+44759 (proto): code 2 (Open Message Error) subcode 2 (bad peer AS number), Reason: no group for 10.37.96.6+44759 (proto) from AS 4292019902 found (peer as mismatch) in master(ae1.0), dropping him
diff --git a/tests/config/junos/BGP_INCORRECT_AS_NUMBER/default/yang.json b/tests/config/junos/BGP_INCORRECT_AS_NUMBER/default/yang.json
new file mode 100644
index 00000000..4e6d1c27
--- /dev/null
+++ b/tests/config/junos/BGP_INCORRECT_AS_NUMBER/default/yang.json
@@ -0,0 +1,37 @@
+{
+  "ip": "127.0.0.1", 
+  "yang_message": {
+    "bgp": {
+      "neighbors": {
+        "neighbor": {
+          "10.37.96.6": {
+            "state": {
+              "session_state": "OPENSENT",
+              "peer_as": "10.37.96.6"
+            }
+          }
+        }
+      }
+    }
+  }, 
+  "facility": 21, 
+  "timestamp": 1585644119, 
+  "severity": 4, 
+  "message_details": {
+    "hostPrefix": null, 
+    "pri": "172", 
+    "processId": "4936", 
+    "facility": 21, 
+    "severity": 4, 
+    "message": "4609: NOTIFICATION sent to 10.37.96.6+44759 (proto): code 2 (Open Message Error) subcode 2 (bad peer AS number), Reason: no group for 10.37.96.6+44759 (proto) from AS 4292019902 found (peer as mismatch) in master(ae1.0), dropping him", 
+    "date": "2020-03-31", 
+    "tag": "bgp_pp_recv", 
+    "host": "some-switch", 
+    "processName": "rpd", 
+    "time": "08:41:59+00:00"
+  }, 
+  "os": "junos", 
+  "yang_model": "openconfig-bgp", 
+  "host": "some-switch", 
+  "error": "BGP_INCORRECT_AS_NUMBER"
+}
diff --git a/tests/config/junos/BGP_MD5_INCORRECT/ISO8601/syslog.msg b/tests/config/junos/BGP_MD5_INCORRECT/ISO8601/syslog.msg
new file mode 100644
index 00000000..29ef0a38
--- /dev/null
+++ b/tests/config/junos/BGP_MD5_INCORRECT/ISO8601/syslog.msg
@@ -0,0 +1,1 @@
+<4>2020-03-31T08:41:59+00:00 vmx01 /kernel: tcp_auth_ok: Packet from 192.168.140.254:61664 wrong MD5 digest
diff --git a/tests/config/junos/BGP_MD5_INCORRECT/ISO8601/yang.json b/tests/config/junos/BGP_MD5_INCORRECT/ISO8601/yang.json
new file mode 100644
index 00000000..9702d3b5
--- /dev/null
+++ b/tests/config/junos/BGP_MD5_INCORRECT/ISO8601/yang.json
@@ -0,0 +1,36 @@
+{
+  "yang_message": {
+    "bgp": {
+      "neighbors": {
+        "neighbor": {
+          "192.168.140.254": {
+            "state": {
+              "session_state": "CONNECT"
+            }
+          }
+        }
+      }
+    }
+  },
+  "message_details": {
+    "processId": null,
+    "severity": 4,
+    "facility": 0,
+    "hostPrefix": null,
+    "pri": "4",
+    "processName": "kernel",
+    "host": "vmx01",
+    "tag": "tcp_auth_ok",
+    "time": "08:41:59+00:00",
+    "date": "2020-03-31",
+    "message": "Packet from 192.168.140.254:61664 wrong MD5 digest"
+  },
+  "timestamp": 1500585780,
+  "facility": 0,
+  "ip": "127.0.0.1",
+  "host": "vmx01",
+  "yang_model": "openconfig-bgp",
+  "error": "BGP_MD5_INCORRECT",
+  "os": "junos",
+  "severity": 4
+}
diff --git a/tests/config/junos/BGP_MD5_INCORRECT/ipv6/syslog.msg b/tests/config/junos/BGP_MD5_INCORRECT/ipv6/syslog.msg
new file mode 100644
index 00000000..58577386
--- /dev/null
+++ b/tests/config/junos/BGP_MD5_INCORRECT/ipv6/syslog.msg
@@ -0,0 +1,1 @@
+<4>Jul 20 21:23:00 vmx01 /kernel: tcp_auth_ok: Packet from 2001:db8:1::a502:764:1:61664 wrong MD5 digest
diff --git a/tests/config/junos/BGP_MD5_INCORRECT/ipv6/yang.json b/tests/config/junos/BGP_MD5_INCORRECT/ipv6/yang.json
new file mode 100644
index 00000000..7cdd17b7
--- /dev/null
+++ b/tests/config/junos/BGP_MD5_INCORRECT/ipv6/yang.json
@@ -0,0 +1,36 @@
+{
+  "yang_message": {
+    "bgp": {
+      "neighbors": {
+        "neighbor": {
+          "2001:db8:1::a502:764:1": {
+            "state": {
+              "session_state": "CONNECT"
+            }
+          }
+        }
+      }
+    }
+  },
+  "message_details": {
+    "processId": null,
+    "severity": 4,
+    "facility": 0,
+    "hostPrefix": null,
+    "pri": "4",
+    "processName": "kernel",
+    "host": "vmx01",
+    "tag": "tcp_auth_ok",
+    "time": "21:23:00",
+    "date": "Jul 20",
+    "message": "Packet from 2001:db8:1::a502:764:1:61664 wrong MD5 digest"
+  },
+  "timestamp": 1500585780,
+  "facility": 0,
+  "ip": "127.0.0.1",
+  "host": "vmx01",
+  "yang_model": "openconfig-bgp",
+  "error": "BGP_MD5_INCORRECT",
+  "os": "junos",
+  "severity": 4
+}
diff --git a/tests/config/junos/BGP_MD5_INCORRECT/missing/syslog.msg b/tests/config/junos/BGP_MD5_INCORRECT/missing/syslog.msg
new file mode 100644
index 00000000..ff46ac56
--- /dev/null
+++ b/tests/config/junos/BGP_MD5_INCORRECT/missing/syslog.msg
@@ -0,0 +1,1 @@
+<4>Jul 20 21:23:00 vmx01 kernel: tcp_auth_ok: Packet from 192.168.140.254:61664 missing MD5 digest
diff --git a/tests/config/junos/BGP_MD5_INCORRECT/missing/yang.json b/tests/config/junos/BGP_MD5_INCORRECT/missing/yang.json
new file mode 100644
index 00000000..79fca23a
--- /dev/null
+++ b/tests/config/junos/BGP_MD5_INCORRECT/missing/yang.json
@@ -0,0 +1,36 @@
+{
+  "yang_message": {
+    "bgp": {
+      "neighbors": {
+        "neighbor": {
+          "192.168.140.254": {
+            "state": {
+              "session_state": "ACTIVE"
+            }
+          }
+        }
+      }
+    }
+  },
+  "message_details": {
+    "processId": null,
+    "severity": 4,
+    "facility": 0,
+    "hostPrefix": null,
+    "pri": "4",
+    "processName": "kernel",
+    "host": "vmx01",
+    "tag": "tcp_auth_ok",
+    "time": "21:23:00",
+    "date": "Jul 20",
+    "message": "Packet from 192.168.140.254:61664 missing MD5 digest"
+  },
+  "timestamp": 1500585780,
+  "facility": 0,
+  "ip": "127.0.0.1",
+  "host": "vmx01",
+  "yang_model": "openconfig-bgp",
+  "error": "BGP_MD5_INCORRECT",
+  "os": "junos",
+  "severity": 4
+}
diff --git a/tests/config/junos/DDOS_PROTOCOL_VIOLATION_SET/default/syslog.msg b/tests/config/junos/DDOS_PROTOCOL_VIOLATION_SET/default/syslog.msg
new file mode 100644
index 00000000..cfb13907
--- /dev/null
+++ b/tests/config/junos/DDOS_PROTOCOL_VIOLATION_SET/default/syslog.msg
@@ -0,0 +1,1 @@
+<28>Nov  9 01:02:40  some-switch jddosd[4223]: DDOS_PROTOCOL_VIOLATION_SET: Warning: Host-bound traffic for protocol/exception  Redirect:aggregate exceeded its allowed bandwidth at fpc 0 for 6661 times, started at 2020-11-09 01:02:39 UTC
diff --git a/tests/config/junos/DDOS_PROTOCOL_VIOLATION_SET/default/yang.json b/tests/config/junos/DDOS_PROTOCOL_VIOLATION_SET/default/yang.json
new file mode 100644
index 00000000..95027c59
--- /dev/null
+++ b/tests/config/junos/DDOS_PROTOCOL_VIOLATION_SET/default/yang.json
@@ -0,0 +1,33 @@
+{
+  "timestamp": 1604883760, 
+  "severity": 4, 
+  "error": "DDOS_PROTOCOL_VIOLATION_SET", 
+  "os": "junos", 
+  "ip": "127.0.0.1", 
+  "message_details": {
+    "message": "Warning: Host-bound traffic for protocol/exception  Redirect:aggregate exceeded its allowed bandwidth at fpc 0 for 6661 times, started at 2020-11-09 01:02:39 UTC", 
+    "severity": 4, 
+    "tag": "DDOS_PROTOCOL_VIOLATION_SET", 
+    "time": "01:02:40", 
+    "facility": 3, 
+    "hostPrefix": null, 
+    "pri": "28", 
+    "processName": "jddosd", 
+    "host": "some-switch", 
+    "processId": "4223", 
+    "date": "Nov  9"
+  }, 
+  "yang_model": "ietf-alarm", 
+  "yang_message": {
+    "alarms": {
+      "alarm": {
+        "event-type": 10, 
+        "additional-text": "aggregate exceeded its allowed bandwidth at fpc 0", 
+        "entity-type": "Redirect", 
+        "alarm-time": "2020-11-09 01:02:39 UTC"
+      }
+    }
+  }, 
+  "facility": 3, 
+  "host": "some-switch"
+}
diff --git a/tests/config/junos/LACP_INTERFACE_DOWN/default/syslog.msg b/tests/config/junos/LACP_INTERFACE_DOWN/default/syslog.msg
new file mode 100644
index 00000000..751fb256
--- /dev/null
+++ b/tests/config/junos/LACP_INTERFACE_DOWN/default/syslog.msg
@@ -0,0 +1,1 @@
+<175>2020-11-04T07:05:36+00:00 some-switch lacpd[12694]: LACP_INTF_DOWN: ae105: Interface marked down due to lacp timeout on member et-0/0/25:3
diff --git a/tests/config/junos/LACP_INTERFACE_DOWN/default/yang.json b/tests/config/junos/LACP_INTERFACE_DOWN/default/yang.json
new file mode 100644
index 00000000..9a2fa89b
--- /dev/null
+++ b/tests/config/junos/LACP_INTERFACE_DOWN/default/yang.json
@@ -0,0 +1,42 @@
+{
+  "os": "junos", 
+  "yang_message": {
+    "lacp": {
+      "interfaces": {
+        "interface": {
+          "ae105": {
+            "members": {
+              "member": {
+                "et-0/0/25:3": {
+                  "state": {
+                    "interface": "et-0/0/25:3"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }, 
+  "facility": 21, 
+  "error": "LACP_INTERFACE_DOWN", 
+  "yang_model": "openconfig-lacp", 
+  "host": "some-switch", 
+  "severity": 7, 
+  "timestamp": 1604473536, 
+  "ip": "127.0.0.1", 
+  "message_details": {
+    "facility": 21, 
+    "date": "2020-11-04", 
+    "processId": "12694", 
+    "message": "ae105: Interface marked down due to lacp timeout on member et-0/0/25:3", 
+    "host": "some-switch", 
+    "time": "07:05:36+00:00", 
+    "severity": 7, 
+    "hostPrefix": null, 
+    "processName": "lacpd", 
+    "tag": "LACP_INTF_DOWN", 
+    "pri": "175"
+  }
+}
diff --git a/tests/config/junos/MAJOR_ALARM_CLEARED/default/syslog.msg b/tests/config/junos/MAJOR_ALARM_CLEARED/default/syslog.msg
new file mode 100644
index 00000000..830f6aa6
--- /dev/null
+++ b/tests/config/junos/MAJOR_ALARM_CLEARED/default/syslog.msg
@@ -0,0 +1,1 @@
+<28>Jul  8 23:04:13  vmx01 craftd[4817]:  Major alarm cleared, Host 0 me0 : Ethernet Link Down
diff --git a/tests/config/junos/MAJOR_ALARM_CLEARED/default/yang.json b/tests/config/junos/MAJOR_ALARM_CLEARED/default/yang.json
new file mode 100644
index 00000000..84ed9945
--- /dev/null
+++ b/tests/config/junos/MAJOR_ALARM_CLEARED/default/yang.json
@@ -0,0 +1,29 @@
+{
+  "error": "MAJOR_ALARM_CLEARED",
+  "host": "vmx01",
+  "ip": "127.0.0.1",
+  "timestamp": 1594249453,
+  "yang_message": {
+    "alarms": {
+      "alarm": {
+        "additional-text": "Host 0 me0 : Ethernet Link Down"
+      }
+    }
+  },
+  "message_details": {
+    "date": "Jul  8",
+    "time": "23:04:13",
+    "hostPrefix": null,
+    "host": "vmx01",
+    "tag": "craftd",
+    "processId": "4817",
+    "pri": "28",
+    "message": "Major alarm cleared, Host 0 me0 : Ethernet Link Down",
+    "facility": 3,
+    "severity": 4
+  },
+  "yang_model": "ietf-alarm",
+  "os": "junos",
+  "facility": 3,
+  "severity": 4
+}
diff --git a/tests/config/junos/MAJOR_ALARM_SET/default/syslog.msg b/tests/config/junos/MAJOR_ALARM_SET/default/syslog.msg
new file mode 100644
index 00000000..8d793823
--- /dev/null
+++ b/tests/config/junos/MAJOR_ALARM_SET/default/syslog.msg
@@ -0,0 +1,1 @@
+<28>Jul  8 23:04:13  vmx01 craftd[4817]:  Major alarm set, Host 0 me0 : Ethernet Link Down
diff --git a/tests/config/junos/MAJOR_ALARM_SET/default/yang.json b/tests/config/junos/MAJOR_ALARM_SET/default/yang.json
new file mode 100644
index 00000000..4fae5f29
--- /dev/null
+++ b/tests/config/junos/MAJOR_ALARM_SET/default/yang.json
@@ -0,0 +1,29 @@
+{
+  "error": "MAJOR_ALARM_SET",
+  "host": "vmx01",
+  "ip": "127.0.0.1",
+  "timestamp": 1594249453,
+  "yang_message": {
+    "alarms": {
+      "alarm": {
+        "additional-text": "Host 0 me0 : Ethernet Link Down"
+      }
+    }
+  },
+  "message_details": {
+    "date": "Jul  8",
+    "time": "23:04:13",
+    "hostPrefix": null,
+    "host": "vmx01",
+    "tag": "craftd",
+    "processId": "4817",
+    "pri": "28",
+    "message": "Major alarm set, Host 0 me0 : Ethernet Link Down",
+    "facility": 3,
+    "severity": 4
+  },
+  "yang_model": "ietf-alarm",
+  "os": "junos",
+  "facility": 3,
+  "severity": 4
+}
diff --git a/tests/config/junos/MINOR_ALARM_CLEARED/default/syslog.msg b/tests/config/junos/MINOR_ALARM_CLEARED/default/syslog.msg
new file mode 100644
index 00000000..4071ba60
--- /dev/null
+++ b/tests/config/junos/MINOR_ALARM_CLEARED/default/syslog.msg
@@ -0,0 +1,1 @@
+<28>Jul  8 23:04:13  vmx01 craftd[4817]:  Minor alarm cleared, RE 0 /var partition usage is high
diff --git a/tests/config/junos/MINOR_ALARM_CLEARED/default/yang.json b/tests/config/junos/MINOR_ALARM_CLEARED/default/yang.json
new file mode 100644
index 00000000..7dd8aad7
--- /dev/null
+++ b/tests/config/junos/MINOR_ALARM_CLEARED/default/yang.json
@@ -0,0 +1,29 @@
+{
+  "ip": "127.0.0.1", 
+  "yang_message": {
+    "alarms": {
+      "alarm": {
+        "additional-text": "RE 0 /var partition usage is high"
+      }
+    }
+  }, 
+  "host": "vmx01", 
+  "error": "MINOR_ALARM_CLEARED", 
+  "facility": 3, 
+  "yang_model": "ietf-alarm", 
+  "os": "junos", 
+  "severity": 4, 
+  "message_details": {
+    "date": "Jul  8", 
+    "processId": "4817", 
+    "tag": "craftd", 
+    "host": "vmx01", 
+    "hostPrefix": null, 
+    "message": "Minor alarm cleared, RE 0 /var partition usage is high", 
+    "facility": 3, 
+    "severity": 4, 
+    "pri": "28", 
+    "time": "23:04:13"
+  }, 
+  "timestamp": 1594249453
+}
diff --git a/tests/config/junos/MINOR_ALARM_SET/default/syslog.msg b/tests/config/junos/MINOR_ALARM_SET/default/syslog.msg
new file mode 100644
index 00000000..eb00c3cd
--- /dev/null
+++ b/tests/config/junos/MINOR_ALARM_SET/default/syslog.msg
@@ -0,0 +1,1 @@
+<28>Jul  8 23:04:13  vmx01 craftd[4817]:  Minor alarm set, RE 0 /var partition usage is high
diff --git a/tests/config/junos/MINOR_ALARM_SET/default/yang.json b/tests/config/junos/MINOR_ALARM_SET/default/yang.json
new file mode 100644
index 00000000..978d601f
--- /dev/null
+++ b/tests/config/junos/MINOR_ALARM_SET/default/yang.json
@@ -0,0 +1,29 @@
+{
+  "host": "vmx01", 
+  "yang_model": "ietf-alarm", 
+  "ip": "127.0.0.1", 
+  "message_details": {
+    "host": "vmx01", 
+    "pri": "28", 
+    "time": "23:04:13", 
+    "message": "Minor alarm set, RE 0 /var partition usage is high", 
+    "severity": 4, 
+    "tag": "craftd", 
+    "hostPrefix": null, 
+    "processId": "4817", 
+    "facility": 3, 
+    "date": "Jul  8"
+  }, 
+  "yang_message": {
+    "alarms": {
+      "alarm": {
+        "additional-text": "RE 0 /var partition usage is high"
+      }
+    }
+  }, 
+  "severity": 4, 
+  "timestamp": 1594249453, 
+  "error": "MINOR_ALARM_SET", 
+  "os": "junos", 
+  "facility": 3
+}
diff --git a/tests/config/junos/PROCESS_CEILING_WATERMARK_REACHED/default/syslog.msg b/tests/config/junos/PROCESS_CEILING_WATERMARK_REACHED/default/syslog.msg
new file mode 100644
index 00000000..34d65c51
--- /dev/null
+++ b/tests/config/junos/PROCESS_CEILING_WATERMARK_REACHED/default/syslog.msg
@@ -0,0 +1,1 @@
+<32>Oct 24 20:21:27  vmx01 jlaunchd: System reaching processes ceiling high watermark: Contact to system administrator to clean up unnecessary processes or increase maxproc ceiling. Further process fork request may be denied.
diff --git a/tests/config/junos/PROCESS_CEILING_WATERMARK_REACHED/default/yang.json b/tests/config/junos/PROCESS_CEILING_WATERMARK_REACHED/default/yang.json
new file mode 100644
index 00000000..8619f8d8
--- /dev/null
+++ b/tests/config/junos/PROCESS_CEILING_WATERMARK_REACHED/default/yang.json
@@ -0,0 +1,28 @@
+{
+    "host": "vmx01",
+    "yang_message": {
+        "process": {
+            "ceiling": {
+                "watermark": "high"
+            }
+        }
+    },
+    "message_details": {
+        "facility": 4,
+        "hostPrefix": null,
+        "pri": "32",
+        "host": "vmx01",
+        "tag": "jlaunchd",
+        "time": "20:21:27",
+        "date": "Oct 24",
+        "message": "System reaching processes ceiling high watermark: Contact to system administrator to clean up unnecessary processes or increase maxproc ceiling. Further process fork request may be denied.",
+        "severity": 0
+    },
+    "timestamp": 1603570887,
+    "error": "PROCESS_CEILING_WATERMARK_REACHED",
+    "ip": "127.0.0.1",
+    "facility": 4,
+    "os": "junos",
+    "yang_model": "NO_MODEL",
+    "severity": 0
+}
diff --git a/tests/config/junos/SYSTEM_ALARM/reason_nonalpha/syslog.msg b/tests/config/junos/SYSTEM_ALARM/reason_nonalpha/syslog.msg
new file mode 100644
index 00000000..96fc6b06
--- /dev/null
+++ b/tests/config/junos/SYSTEM_ALARM/reason_nonalpha/syslog.msg
@@ -0,0 +1,1 @@
+<28>Jul  8 23:04:13  vmx01 alarmd[2449]: Alarm set: RE color=YELLOW, class=CHASSIS, reason=RE 0 /var partition usage is high
diff --git a/tests/config/junos/SYSTEM_ALARM/reason_nonalpha/yang.json b/tests/config/junos/SYSTEM_ALARM/reason_nonalpha/yang.json
new file mode 100644
index 00000000..e6ef1f1d
--- /dev/null
+++ b/tests/config/junos/SYSTEM_ALARM/reason_nonalpha/yang.json
@@ -0,0 +1,37 @@
+{
+  "severity": 4, 
+  "timestamp": 1594249453, 
+  "os": "junos", 
+  "message_details": {
+    "processName": "alarmd", 
+    "severity": 4, 
+    "tag": "Alarm set", 
+    "hostPrefix": null, 
+    "time": "23:04:13", 
+    "pri": "28", 
+    "message": "RE color=YELLOW, class=CHASSIS, reason=RE 0 /var partition usage is high", 
+    "facility": 3, 
+    "host": "vmx01", 
+    "date": "Jul  8", 
+    "processId": "2449"
+  }, 
+  "yang_model": "ietf-hardware", 
+  "yang_message": {
+    "hardware-state": {
+      "component": {
+        "RE": {
+          "class": "CHASSIS", 
+          "name": "RE", 
+          "state": {
+            "alarm-state": 4, 
+            "alarm-reason": "RE 0 /var partition usage is high"
+          }
+        }
+      }
+    }
+  }, 
+  "error": "SYSTEM_ALARM", 
+  "facility": 3, 
+  "host": "vmx01", 
+  "ip": "127.0.0.1"
+}
diff --git a/tests/config/junos/SYSTEM_ALARM_CLEARED/default/syslog.msg b/tests/config/junos/SYSTEM_ALARM_CLEARED/default/syslog.msg
new file mode 100644
index 00000000..e681e92c
--- /dev/null
+++ b/tests/config/junos/SYSTEM_ALARM_CLEARED/default/syslog.msg
@@ -0,0 +1,1 @@
+<28>Jul  8 23:04:13  vmx01 alarmd[2449]: Alarm cleared: Pwr supply color=YELLOW, class=CHASSIS, reason=PEM 1 Fan Failed
diff --git a/tests/config/junos/SYSTEM_ALARM_CLEARED/default/yang.json b/tests/config/junos/SYSTEM_ALARM_CLEARED/default/yang.json
new file mode 100644
index 00000000..34049718
--- /dev/null
+++ b/tests/config/junos/SYSTEM_ALARM_CLEARED/default/yang.json
@@ -0,0 +1,37 @@
+{
+  "error": "SYSTEM_ALARM_CLEARED",
+  "host": "vmx01",
+  "ip": "127.0.0.1",
+  "timestamp": 1594249453,
+  "yang_message": {
+    "hardware-state": {
+      "component": {
+        "supply": {
+          "name": "supply",
+          "class": "CHASSIS",
+          "state": {
+            "alarm-state": 4,
+            "alarm-reason": "PEM 1 Fan Failed"
+          }
+        }
+      }
+    }
+  },
+  "message_details": {
+    "date": "Jul  8",
+    "time": "23:04:13",
+    "hostPrefix": null,
+    "host": "vmx01",
+    "processName": "alarmd",
+    "processId": "2449",
+    "tag": "Alarm cleared",
+    "pri": "28",
+    "message": "Pwr supply color=YELLOW, class=CHASSIS, reason=PEM 1 Fan Failed",
+    "facility": 3,
+    "severity": 4
+  },
+  "yang_model": "ietf-hardware",
+  "os": "junos",
+  "facility": 3,
+  "severity": 4
+}
diff --git a/tests/config/junos/SYSTEM_ALARM_CLEARED/reason_nonalpha/syslog.msg b/tests/config/junos/SYSTEM_ALARM_CLEARED/reason_nonalpha/syslog.msg
new file mode 100644
index 00000000..36002f21
--- /dev/null
+++ b/tests/config/junos/SYSTEM_ALARM_CLEARED/reason_nonalpha/syslog.msg
@@ -0,0 +1,1 @@
+<28>Jul  8 23:04:13  vmx01 alarmd[2449]: Alarm cleared: RE color=YELLOW, class=CHASSIS, reason=RE 0 /var partition usage is high
diff --git a/tests/config/junos/SYSTEM_ALARM_CLEARED/reason_nonalpha/yang.json b/tests/config/junos/SYSTEM_ALARM_CLEARED/reason_nonalpha/yang.json
new file mode 100644
index 00000000..5efe7b28
--- /dev/null
+++ b/tests/config/junos/SYSTEM_ALARM_CLEARED/reason_nonalpha/yang.json
@@ -0,0 +1,37 @@
+{
+  "severity": 4, 
+  "timestamp": 1594249453, 
+  "os": "junos", 
+  "message_details": {
+    "processName": "alarmd", 
+    "severity": 4, 
+    "tag": "Alarm cleared", 
+    "hostPrefix": null, 
+    "time": "23:04:13", 
+    "pri": "28", 
+    "message": "RE color=YELLOW, class=CHASSIS, reason=RE 0 /var partition usage is high", 
+    "facility": 3, 
+    "host": "vmx01", 
+    "date": "Jul  8", 
+    "processId": "2449"
+  }, 
+  "yang_model": "ietf-hardware", 
+  "yang_message": {
+    "hardware-state": {
+      "component": {
+        "RE": {
+          "class": "CHASSIS", 
+          "name": "RE", 
+          "state": {
+            "alarm-state": 4, 
+            "alarm-reason": "RE 0 /var partition usage is high"
+          }
+        }
+      }
+    }
+  }, 
+  "error": "SYSTEM_ALARM_CLEARED", 
+  "facility": 3, 
+  "host": "vmx01", 
+  "ip": "127.0.0.1"
+}
diff --git a/tests/test_config.py b/tests/test_config.py
index be3449fc..2667a529 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -44,14 +44,16 @@ def startup_proc():
     global NL_BASE
     global NL_PROC
     log.debug('Starting up the napalm-logs process')
-    NL_BASE = NapalmLogs(disable_security=True,
-                         address=NAPALM_LOGS_TEST_ADDR,
-                         port=NAPALM_LOGS_TEST_PORT,
-                         publisher=[{'zmq': {'send_unknown': True}}],
-                         listener=[{'udp': {}}],
-                         publish_address=NAPALM_LOGS_TEST_PUB_ADDR,
-                         publish_port=NAPALM_LOGS_TEST_PUB_PORT,
-                         log_level=NAPALM_LOGS_TEST_LOG_LEVEL)
+    NL_BASE = NapalmLogs(
+        disable_security=True,
+        address=NAPALM_LOGS_TEST_ADDR,
+        port=NAPALM_LOGS_TEST_PORT,
+        publisher=[{'zmq': {'send_unknown': True}}],
+        listener=[{'udp': {}}],
+        publish_address=NAPALM_LOGS_TEST_PUB_ADDR,
+        publish_port=NAPALM_LOGS_TEST_PUB_PORT,
+        log_level=NAPALM_LOGS_TEST_LOG_LEVEL,
+    )
     NL_PROC = Process(target=NL_BASE.start_engine)
     NL_PROC.start()
 
@@ -68,9 +70,10 @@ def startup_local_client():
     global TEST_CLIENT
     context = zmq.Context()
     TEST_CLIENT = context.socket(zmq.SUB)
-    TEST_CLIENT.connect('tcp://{addr}:{port}'.format(
-        addr=NAPALM_LOGS_TEST_PUB_ADDR,
-        port=NAPALM_LOGS_TEST_PUB_PORT)
+    TEST_CLIENT.connect(
+        'tcp://{addr}:{port}'.format(
+            addr=NAPALM_LOGS_TEST_PUB_ADDR, port=NAPALM_LOGS_TEST_PUB_PORT
+        )
     )
     TEST_CLIENT.setsockopt(zmq.SUBSCRIBE, b'')
 
@@ -91,7 +94,11 @@ def generate_tests():
     test_cases = []
     cwd = os.path.dirname(__file__)
     test_path = os.path.join(cwd, 'config')
-    os_dir_list = [name for name in os.listdir(test_path) if os.path.isdir(os.path.join(test_path, name))]
+    os_dir_list = [
+        name
+        for name in os.listdir(test_path)
+        if os.path.isdir(os.path.join(test_path, name))
+    ]
     expected_oss = set(expected_os_errors.keys())
     tested_oss = set(os_dir_list)
     missing_oss = expected_oss - tested_oss
@@ -100,7 +107,11 @@ def generate_tests():
     for os_name in os_dir_list:
         # Subdir is the OS name
         os_path = os.path.join(test_path, os_name)
-        errors = [name for name in os.listdir(os_path) if os.path.isdir(os.path.join(os_path, name))]
+        errors = [
+            name
+            for name in os.listdir(os_path)
+            if os.path.isdir(os.path.join(os_path, name))
+        ]
         expected_errors = set(expected_os_errors[os_name])
         defined_errors = set(errors)
         missing_errors = expected_errors - defined_errors
@@ -108,7 +119,11 @@ def generate_tests():
             test_cases.append((os_name, '__missing__{}'.format(mising_err), ''))
         for error_name in errors:
             error_path = os.path.join(os_path, error_name)
-            cases = [name for name in os.listdir(error_path) if os.path.isdir(os.path.join(error_path, name))]
+            cases = [
+                name
+                for name in os.listdir(error_path)
+                if os.path.isdir(os.path.join(error_path, name))
+            ]
             if not cases:
                 test_cases.append((os_name, error_name, '__missing__'))
             for test_case in cases:
@@ -122,12 +137,22 @@ def generate_tests():
 
 @pytest.mark.parametrize("os_name,error_name,test_case", tests)
 def test_config(os_name, error_name, test_case):
-    assert not os_name.startswith('__missing__'), 'No tests defined for {}'.format(os_name.replace('__missing__', ''))
-    assert not error_name.startswith('__missing__'),\
-        'No tests defined for {}, under {}'.format(error_name.replace('__missing__', ''), os_name)
-    assert test_case != '__missing__', 'No test cases defined for {}, under {}'.format(error_name, os_name)
-    print('Testing {} for {}, under the test case "{}"'.format(
-          error_name, os_name, test_case))
+    assert not os_name.startswith('__missing__'), 'No tests defined for {}'.format(
+        os_name.replace('__missing__', '')
+    )
+    assert not error_name.startswith(
+        '__missing__'
+    ), 'No tests defined for {}, under {}'.format(
+        error_name.replace('__missing__', ''), os_name
+    )
+    assert test_case != '__missing__', 'No test cases defined for {}, under {}'.format(
+        error_name, os_name
+    )
+    print(
+        'Testing {} for {}, under the test case "{}"'.format(
+            error_name, os_name, test_case
+        )
+    )
     cwd = os.path.dirname(__file__)
     test_path = os.path.join(cwd, 'config', os_name, error_name, test_case)
     raw_message_filepath = os.path.join(test_path, 'syslog.msg')
@@ -153,7 +178,10 @@ def test_config(os_name, error_name, test_case):
     log.debug('Struct YANG message:')
     log.debug(struct_yang_message)
     log.debug('Sending the raw message to the napalm-logs daemon')
-    TEST_SKT.sendto(raw_message.strip().encode('utf-8'), (NAPALM_LOGS_TEST_ADDR, NAPALM_LOGS_TEST_PORT))
+    TEST_SKT.sendto(
+        raw_message.strip().encode('utf-8'),
+        (NAPALM_LOGS_TEST_ADDR, NAPALM_LOGS_TEST_PORT),
+    )
     zmq_msg = TEST_CLIENT.recv()
     deserialised_zmq_msg = napalm_logs.utils.unserialize(zmq_msg)
     log.debug('Received from the napalm-logs daemon:')
@@ -168,10 +196,16 @@ def test_config(os_name, error_name, test_case):
     # which means that once a year we will have to update all tests if we
     # check the timestamp.
     # We still expect both to contain a timestamp though.
-    assert struct_yang_message.pop('timestamp', False),\
-        'Yang test file does not contain a timestamp key for {} under {}'.format(error_name, os_name)
-    assert returned_yang.pop('timestamp', False),\
-        'The returned yang does not contain a timestamp key for {} under {}'.format(error_name, os_name)
+    assert struct_yang_message.pop(
+        'timestamp', False
+    ), 'Yang test file does not contain a timestamp key for {} under {}'.format(
+        error_name, os_name
+    )
+    assert returned_yang.pop(
+        'timestamp', False
+    ), 'The returned yang does not contain a timestamp key for {} under {}'.format(
+        error_name, os_name
+    )
     assert struct_yang_message == returned_yang