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