1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Functions for specialised logging with HTML output.
10 from typing import Any, Iterator, Optional, List, Tuple, cast, Union, Mapping, Sequence
11 from contextvars import ContextVar
17 import sqlalchemy as sa
18 from sqlalchemy.ext.asyncio import AsyncConnection
21 from pygments import highlight
22 from pygments.lexers import PythonLexer, PostgresLexer
23 from pygments.formatters import HtmlFormatter
25 except ModuleNotFoundError:
26 CODE_HIGHLIGHT = False
29 def _debug_name(res: Any) -> str:
31 return cast(str, res.names.get('name', next(iter(res.names.values()))))
33 return f"Hnr {res.housenumber}" if res.housenumber is not None else '[NONE]'
37 """ Interface for logging function.
39 The base implementation does nothing. Overwrite the functions
40 in derived classes which implement logging functionality.
42 def get_buffer(self) -> str:
43 """ Return the current content of the log buffer.
47 def function(self, func: str, **kwargs: Any) -> None:
48 """ Start a new debug chapter for the given function and its parameters.
52 def section(self, heading: str) -> None:
53 """ Start a new section with the given title.
57 def comment(self, text: str) -> None:
58 """ Add a simple comment to the debug output.
62 def var_dump(self, heading: str, var: Any) -> None:
63 """ Print the content of the variable to the debug output prefixed by
68 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
69 """ Print the table generated by the generator function.
73 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
74 """ Print a list of search results generated by the generator function.
78 def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
79 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
80 """ Print the SQL for the given statement.
83 def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable',
84 extra_params: Union[Mapping[str, Any],
85 Sequence[Mapping[str, Any]], None]) -> str:
86 """ Return the comiled version of the statement.
88 compiled = cast('sa.ClauseElement', statement).compile(conn.sync_engine)
90 params = dict(compiled.params)
91 if isinstance(extra_params, Mapping):
92 for k, v in extra_params.items():
94 elif isinstance(extra_params, Sequence) and extra_params:
95 for k in extra_params[0]:
98 sqlstr = str(compiled)
100 if sa.__version__.startswith('1'):
102 sqlstr = re.sub(r'__\[POSTCOMPILE_[^]]*\]', '%s', sqlstr)
103 return sqlstr % tuple((repr(params.get(name, None))
104 for name in compiled.positiontup)) # type: ignore
108 # Fixes an odd issue with Python 3.7 where percentages are not
110 sqlstr = re.sub(r'%(?!\()', '%%', sqlstr)
111 sqlstr = re.sub(r'__\[POSTCOMPILE_([^]]*)\]', r'%(\1)s', sqlstr)
113 return sqlstr % params
115 class HTMLLogger(BaseLogger):
116 """ Logger that formats messages in HTML.
118 def __init__(self) -> None:
119 self.buffer = io.StringIO()
122 def _timestamp(self) -> None:
123 self._write(f'<p class="timestamp">[{dt.datetime.now()}]</p>')
126 def get_buffer(self) -> str:
127 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
130 def function(self, func: str, **kwargs: Any) -> None:
132 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
133 for name, value in kwargs.items():
134 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
135 self._write('</dl></p>')
138 def section(self, heading: str) -> None:
140 self._write(f"<h2>{heading}</h2>")
143 def comment(self, text: str) -> None:
145 self._write(f"<p>{text}</p>")
148 def var_dump(self, heading: str, var: Any) -> None:
153 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
156 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
160 self._write(f'<table><thead><tr><th colspan="{len(head)}">{heading}</th></tr><tr>')
162 self._write(f'<th>{cell}</th>')
163 self._write('</tr></thead><tbody>')
168 self._write(f'<td>{cell}</td>')
170 self._write('</tbody></table>')
173 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
174 """ Print a list of search results generated by the generator function.
177 def format_osm(osm_object: Optional[Tuple[str, int]]) -> str:
191 return f'<a href="https://www.openstreetmap.org/{fullt}/{i}">{t}{i}</a>'
193 self._write(f'<h5>{heading}</h5><p><dl>')
195 for rank, res in results:
196 self._write(f'<dt>[{rank:.3f}]</dt> <dd>{res.source_table.name}(')
197 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
198 self._write(f"rank={res.rank_address}, ")
199 self._write(f"osm={format_osm(res.osm_object)}, ")
200 self._write(f'cc={res.country_code}, ')
201 self._write(f'importance={res.importance or float("nan"):.5f})</dd>')
203 self._write(f'</dl><b>TOTAL:</b> {total}</p>')
206 def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
207 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
209 sqlstr = self.format_sql(conn, statement, params)
211 sqlstr = highlight(sqlstr, PostgresLexer(),
212 HtmlFormatter(nowrap=True, lineseparator='<br />'))
213 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
215 self._write(f'<code class="lang-sql">{sqlstr}</code>')
218 def _python_var(self, var: Any) -> str:
220 fmt = highlight(str(var), PythonLexer(), HtmlFormatter(nowrap=True))
221 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
223 return f'<code class="lang-python">{str(var)}</code>'
226 def _write(self, text: str) -> None:
227 """ Add the raw text to the debug output.
229 self.buffer.write(text)
232 class TextLogger(BaseLogger):
233 """ Logger creating output suitable for the console.
235 def __init__(self) -> None:
236 self.buffer = io.StringIO()
239 def get_buffer(self) -> str:
240 return self.buffer.getvalue()
243 def function(self, func: str, **kwargs: Any) -> None:
244 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
245 for name, value in kwargs.items():
246 self._write(f' {name}: {self._python_var(value)}\n')
250 def section(self, heading: str) -> None:
251 self._write(f"\n# {heading}\n\n")
254 def comment(self, text: str) -> None:
255 self._write(f"{text}\n")
258 def var_dump(self, heading: str, var: Any) -> None:
262 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
265 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
266 self._write(f'{heading}:\n')
267 data = [list(map(self._python_var, row)) if row else None for row in rows]
268 assert data[0] is not None
269 num_cols = len(data[0])
271 maxlens = [max(len(d[i]) for d in data if d) for i in range(num_cols)]
272 tablewidth = sum(maxlens) + 3 * num_cols + 1
273 row_format = '| ' +' | '.join(f'{{:<{l}}}' for l in maxlens) + ' |\n'
274 self._write('-'*tablewidth + '\n')
275 self._write(row_format.format(*data[0]))
276 self._write('-'*tablewidth + '\n')
279 self._write(row_format.format(*row))
281 self._write('-'*tablewidth + '\n')
283 self._write('-'*tablewidth + '\n')
286 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
287 self._write(f'{heading}:\n')
289 for rank, res in results:
290 self._write(f'[{rank:.3f}] {res.source_table.name}(')
291 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
292 self._write(f"rank={res.rank_address}, ")
293 self._write(f"osm={''.join(map(str, res.osm_object or []))}, ")
294 self._write(f'cc={res.country_code}, ')
295 self._write(f'importance={res.importance or -1:.5f})\n')
297 self._write(f'TOTAL: {total}\n\n')
300 def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
301 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
302 sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement, params), width=78))
303 self._write(f"| {sqlstr}\n\n")
306 def _python_var(self, var: Any) -> str:
310 def _write(self, text: str) -> None:
311 self.buffer.write(text)
314 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
317 def set_log_output(fmt: str) -> None:
318 """ Enable collecting debug information.
321 logger.set(HTMLLogger())
323 logger.set(TextLogger())
325 logger.set(BaseLogger())
328 def log() -> BaseLogger:
329 """ Return the logger for the current context.
334 def get_and_disable() -> str:
335 """ Return the current content of the debug buffer and disable logging.
337 buf = logger.get().get_buffer()
338 logger.set(BaseLogger())
342 HTML_HEADER: str = """<!DOCTYPE html>
345 <title>Nominatim - Debug</title>
348 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
350 h2 { font-size: x-large }
354 font-family: monospace
363 dt::after { content: ": "; }
376 border: solid lightgrey 0.1pt;
378 background-color: #f7f7f7
383 border: solid lightgrey 0.1pt
388 border-collapse: collapse;
391 border-right: thin solid;
399 width: calc(100% - 5pt);
410 HTML_FOOTER: str = "</body></html>"