summaryrefslogtreecommitdiffstats
path: root/roles/anitya/fedmsg/templates/logging.py.j2
blob: 947a370ef0e3d04bdb5c24ce4327361deddc23df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# Setup fedmsg logging.

# All of these modules are just used by the ContextInjector below.
import inspect
import logging
import os
import socket
import traceback

psutil = None
try:
    import psutil
except (OSError, ImportError):
    # We run into issues when trying to import psutil from inside mod_wsgi on
    # rhel7.  If we hit that here, then just fail quietly.
    # https://github.com/jmflinuxtx/kerneltest-harness/pull/17#issuecomment-48007837
    pass


class ContextInjector(logging.Filter):
    """ Logging filter that adds context to log records.

    Filters are typically used to "filter" log records.  They declare a filter
    method that can return True or False.  Only records with 'True' will
    actually be logged.

    Here, we somewhat abuse the concept of a filter.  We always return true,
    but we use the opportunity to hang important contextual information on the
    log record to later be used by the logging Formatter.  We don't normally
    want to see all this stuff in normal log records, but we *do* want to see
    it when we are emailed error messages.  Seeing an error, but not knowing
    which host it comes from, is not that useful.

    http://docs.python.org/2/howto/logging-cookbook.html#filters-contextual
    """

    def filter(self, record):
        current_process = ContextInjector.get_current_process()
        current_hostname = socket.gethostname()

        record.host = current_hostname
        record.proc = current_process
        record.pid = current_process.pid
        record.proc_name = current_process.name
        record.command_line = current_process.cmdline
        # These are callabls on more modern versions of psutil.
        if callable(record.proc_name):
            record.proc_name = record.proc_name()
        if callable(record.command_line):
            record.command_line = record.command_line()
        record.command_line = " ".join(record.command_line)
        record.callstack = self.format_callstack()
        return True

    @staticmethod
    def format_callstack():
        for i, frame in enumerate(f[0] for f in inspect.stack()):
            if not '__name__' in frame.f_globals:
                continue
            modname = frame.f_globals['__name__'].split('.')[0]
            if modname != "logging":
                break

        def _format_frame(frame):
            return '  File "%s", line %i in %s\n    %s' % (frame)

        stack = traceback.extract_stack()
        stack = stack[:-i]
        return "\n".join([_format_frame(frame) for frame in stack])

    @staticmethod
    def get_current_process():
        mypid = os.getpid()

        if not psutil:
            raise OSError("Could not import psutil for %r" % mypid)

        for proc in psutil.process_iter():
            if proc.pid == mypid:
                return proc

        # This should be impossible.
        raise ValueError("Could not find process %r" % mypid)

    @classmethod
    def __json__(cls):
        """ We need to be jsonifiable for "fedmsg-config" """
        return {'name': 'ContextInjector'}


hefty_format = """Message
-------
[%(asctime)s][%(name)10s %(levelname)7s]
%(message)s

Process Details
---------------
host:     %(host)s
PID:      %(pid)s
name:     %(proc_name)s
command:  %(command_line)s

Callstack that lead to the logging statement
--------------------------------------------
%(callstack)s
"""


# See the following for constraints on this format http://bit.ly/Xn1WDn
config = dict(
    logging=dict(
        version=1,
        formatters=dict(
            bare={
                "datefmt": "%Y-%m-%d %H:%M:%S",
                "format": "[%(asctime)s][%(name)10s %(levelname)7s] %(message)s"
            },
            hefty={
                "datefmt": "%Y-%m-%d %H:%M:%S",
                "format": hefty_format,
            },
        ),
        filters=dict(
            context={
                # This "()" syntax in the stdlib doesn't seem to be documented
                # anywhere.  I had to read
                # /usr/lib64/python2.7/logging/config.py to figure it out.
                "()": ContextInjector,
            },
        ),
        handlers=dict(
            console={
                "class": "logging.StreamHandler",
                "formatter": "bare",
                "level": "INFO",
                "stream": "ext://sys.stdout",
            },
            mailer={
                "class": "logging.handlers.SMTPHandler",
                "formatter": "hefty",
                "filters": ["context"],
                "level": "ERROR",
                "mailhost": "bastion.vpn.fedoraproject.org",
                "fromaddr": "fedmsg@fedoraproject.org",
                "toaddrs": ["sysadmin-datanommer-members@fedoraproject.org"],
                "subject": "fedmsg error log (anitya)",
            },
        ),
        loggers=dict(
            fedmsg={
                "level": "INFO",
                "propagate": False,
                "handlers": ["console", "mailer"],
            },
            moksha={
                "level": "INFO",
                "propagate": False,
                "handlers": ["console", "mailer"],
            },
        ),
    ),
)