]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/api/logging.py
3949fdae1798c44569ef70494d5ce1f0e35f7aa2
[nominatim.git] / nominatim / api / logging.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Functions for specialised logging with HTML output.
9 """
10 from typing import Any, Optional, cast
11 from contextvars import ContextVar
12 import io
13
14 import sqlalchemy as sa
15 from sqlalchemy.ext.asyncio import AsyncConnection
16
17 try:
18     from pygments import highlight
19     from pygments.lexers import PythonLexer, PostgresLexer
20     from pygments.formatters import HtmlFormatter
21     CODE_HIGHLIGHT = True
22 except ModuleNotFoundError:
23     CODE_HIGHLIGHT = False
24
25
26 class BaseLogger:
27     """ Interface for logging function.
28
29         The base implementation does nothing. Overwrite the functions
30         in derived classes which implement logging functionality.
31     """
32     def get_buffer(self) -> str:
33         return ''
34
35     def function(self, func: str, **kwargs: Any) -> None:
36         """ Start a new debug chapter for the given function and its parameters.
37         """
38
39
40     def section(self, heading: str) -> None:
41         """ Start a new section with the given title.
42         """
43
44
45     def comment(self, text: str) -> None:
46         """ Add a simple comment to the debug output.
47         """
48
49
50     def var_dump(self, heading: str, var: Any) -> None:
51         """ Print the content of the variable to the debug output prefixed by
52             the given heading.
53         """
54
55
56     def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
57         """ Print the SQL for the given statement.
58         """
59
60
61 class HTMLLogger(BaseLogger):
62     """ Logger that formats messages in HTML.
63     """
64     def __init__(self):
65         self.buffer = io.StringIO()
66
67
68     def get_buffer(self) -> str:
69         return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
70
71     def function(self, func: str, **kwargs: Any) -> None:
72         """ Start a new debug chapter for the given function and its parameters.
73         """
74         self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
75         for name, value in kwargs.items():
76             self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
77         self._write('</dl></p>')
78
79
80     def section(self, heading: str) -> None:
81         """ Start a new section with the given title.
82         """
83         self._write(f"<h2>{heading}</h2>")
84
85     def comment(self, text: str) -> None:
86         """ Add a simple comment to the debug output.
87         """
88         self._write(f"<p>{text}</p>")
89
90     def var_dump(self, heading: str, var: Any) -> None:
91         """ Print the content of the variable to the debug output prefixed by
92             the given heading.
93         """
94         self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
95
96
97     def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
98         """ Dump the SQL statement to debug output.
99         """
100         sqlstr = str(cast('sa.ClauseElement', statement)
101                       .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
102         if CODE_HIGHLIGHT:
103             sqlstr = highlight(sqlstr, PostgresLexer(),
104                                HtmlFormatter(nowrap=True, lineseparator='<br>'))
105             self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
106         else:
107             self._write(f'<code class="lang-sql">{sqlstr}</code>')
108
109
110     def _python_var(self, var: Any) -> str:
111         if CODE_HIGHLIGHT:
112             fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
113             return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
114
115         return f'<code class="lang-python">{str(var)}</code>'
116
117
118     def _write(self, text: str) -> None:
119         """ Add the raw text to the debug output.
120         """
121         self.buffer.write(text)
122
123
124 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
125
126
127 def set_log_output(fmt: str) -> None:
128     """ Enable collecting debug information.
129     """
130     if fmt == 'html':
131         logger.set(HTMLLogger())
132     elif fmt == 'text':
133         logger.set(TextLogger())
134     else:
135         logger.set(BaseLogger())
136
137
138 def log() -> BaseLogger:
139     """ Return the logger for the current context.
140     """
141     return logger.get()
142
143
144 def get_and_disable() -> str:
145     """ Return the current content of the debug buffer and disable logging.
146     """
147     buf = logger.get().get_buffer()
148     logger.set(BaseLogger())
149     return buf
150
151
152 HTML_HEADER: str = """<!DOCTYPE html>
153 <html>
154 <head>
155   <title>Nominatim - Debug</title>
156   <style>
157 """ + \
158 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
159 """
160     h2 { font-size: x-large }
161
162     dl {
163       padding-left: 10pt;
164       font-family: monospace
165     }
166
167     dt {
168       float: left;
169       font-weight: bold;
170       margin-right: 0.5em
171     }
172
173     dt::after { content: ": "; }
174
175     dd::after {
176       clear: left;
177       display: block
178     }
179
180     .lang-sql {
181       color: #555;
182       font-size: small
183     }
184
185     h5 {
186         border: solid lightgrey 0.1pt;
187         margin-bottom: 0;
188         background-color: #f7f7f7
189     }
190
191     h5 + .highlight {
192         padding: 3pt;
193         border: solid lightgrey 0.1pt
194     }
195   </style>
196 </head>
197 <body>
198 """
199
200 HTML_FOOTER: str = "</body></html>"