tcpstates: Add systemd journal logging. (#2058)

* tcpstates: Add systemd journal logging.

Add a -Y/--journal flag to tcpstates.py, which logs events to the
systemd journal.

* tcpstates: Document systemd journal logging.

Update tcpstates_example.txt and tcpstates.8 to include the "-Y" flag.
diff --git a/tools/tcpstates.py b/tools/tcpstates.py
index 381a6d5..1444b31 100755
--- a/tools/tcpstates.py
+++ b/tools/tcpstates.py
@@ -1,4 +1,5 @@
 #!/usr/bin/python
+# -*- coding: utf-8 -*-
 # @lint-avoid-python-3-compatibility-imports
 #
 # tcpstates   Trace the TCP session state changes with durations.
@@ -20,7 +21,8 @@
 from socket import inet_ntop, AF_INET, AF_INET6
 from struct import pack
 import ctypes as ct
-from time import strftime
+from time import strftime, time
+from os import getuid
 
 # arguments
 examples = """examples:
@@ -29,6 +31,7 @@
     ./tcpstates -T        # include time column (HH:MM:SS)
     ./tcpstates -w        # wider colums (fit IPv6)
     ./tcpstates -stT      # csv output, with times & timestamps
+    ./tcpstates -Y        # log events to the systemd journal
     ./tcpstates -L 80     # only trace local port 80
     ./tcpstates -L 80,81  # only trace local ports 80 and 81
     ./tcpstates -D 80     # only trace remote port 80
@@ -51,6 +54,8 @@
     help="comma-separated list of remote ports to trace.")
 parser.add_argument("--ebpf", action="store_true",
     help=argparse.SUPPRESS)
+parser.add_argument("-Y", "--journal", action="store_true",
+    help="log session state changes to the systemd journal")
 args = parser.parse_args()
 debug = 0
 
@@ -237,6 +242,14 @@
     header_string = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s"
     format_string = "%x,%d,%s,%s,%s,%s,%s,%d,%s,%s,%.3f"
 
+if args.journal:
+    try:
+        from systemd import journal
+    except ImportError:
+        print("ERROR: Journal logging requires the systemd.journal module")
+        exit(1)
+
+
 def tcpstate2str(state):
     # from include/net/tcp_states.h:
     tcpstate = {
@@ -259,6 +272,44 @@
     else:
         return str(state)
 
+def journal_fields(event, addr_family):
+    addr_pfx = 'IPV4'
+    if addr_family == AF_INET6:
+        addr_pfx = 'IPV6'
+
+    fields = {
+        # Standard fields described in systemd.journal-fields(7). journal.send
+        # will fill in CODE_LINE, CODE_FILE, and CODE_FUNC for us. If we're
+        # root and specify OBJECT_PID, systemd-journald will add other OBJECT_*
+        # fields for us.
+        'SYSLOG_IDENTIFIER': 'tcpstates',
+        'PRIORITY': 5,
+        '_SOURCE_REALTIME_TIMESTAMP': time() * 1000000,
+        'OBJECT_PID': str(event.pid),
+        'OBJECT_COMM': event.task.decode('utf-8', 'replace'),
+        # Custom fields, aka "stuff we sort of made up".
+        'OBJECT_' + addr_pfx + '_SOURCE_ADDRESS': inet_ntop(addr_family, pack("I", event.saddr)),
+        'OBJECT_TCP_SOURCE_PORT': str(event.ports >> 32),
+        'OBJECT_' + addr_pfx + '_DESTINATION_ADDRESS': inet_ntop(addr_family, pack("I", event.daddr)),
+        'OBJECT_TCP_DESTINATION_PORT': str(event.ports & 0xffffffff),
+        'OBJECT_TCP_OLD_STATE': tcpstate2str(event.oldstate),
+        'OBJECT_TCP_NEW_STATE': tcpstate2str(event.newstate),
+        'OBJECT_TCP_SPAN_TIME': str(event.span_us)
+        }
+
+    msg_format_string = (u"%(OBJECT_COMM)s " +
+        u"%(OBJECT_" + addr_pfx + "_SOURCE_ADDRESS)s " +
+        u"%(OBJECT_TCP_SOURCE_PORT)s → " +
+        u"%(OBJECT_" + addr_pfx + "_DESTINATION_ADDRESS)s " +
+        u"%(OBJECT_TCP_DESTINATION_PORT)s " +
+        u"%(OBJECT_TCP_OLD_STATE)s → %(OBJECT_TCP_NEW_STATE)s")
+    fields['MESSAGE'] = msg_format_string % (fields)
+
+    if getuid() == 0:
+        del fields['OBJECT_COMM'] # Handled by systemd-journald
+
+    return fields
+
 # process event
 def print_ipv4_event(cpu, data, size):
     event = ct.cast(data, ct.POINTER(Data_ipv4)).contents
@@ -282,6 +333,8 @@
         inet_ntop(AF_INET, pack("I", event.daddr)), event.ports & 0xffffffff,
         tcpstate2str(event.oldstate), tcpstate2str(event.newstate),
         float(event.span_us) / 1000))
+    if args.journal:
+        journal.send(**journal_fields(event, AF_INET))
 
 def print_ipv6_event(cpu, data, size):
     event = ct.cast(data, ct.POINTER(Data_ipv6)).contents
@@ -305,6 +358,8 @@
         inet_ntop(AF_INET6, event.daddr), event.ports & 0xffffffff,
         tcpstate2str(event.oldstate), tcpstate2str(event.newstate),
         float(event.span_us) / 1000))
+    if args.journal:
+        journal.send(**journal_fields(event, AF_INET6))
 
 # initialize BPF
 b = BPF(text=bpf_text)