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
11 from contextvars import ContextVar
16 import sqlalchemy as sa
17 from sqlalchemy.ext.asyncio import AsyncConnection
20 from pygments import highlight
21 from pygments.lexers import PythonLexer, PostgresLexer
22 from pygments.formatters import HtmlFormatter
24 except ModuleNotFoundError:
25 CODE_HIGHLIGHT = False
28 def _debug_name(res: Any) -> str:
30 return cast(str, res.names.get('name', next(iter(res.names.values()))))
32 return f"Hnr {res.housenumber}" if res.housenumber is not None else '[NONE]'
36 """ Interface for logging function.
38 The base implementation does nothing. Overwrite the functions
39 in derived classes which implement logging functionality.
41 def get_buffer(self) -> str:
42 """ Return the current content of the log buffer.
46 def function(self, func: str, **kwargs: Any) -> None:
47 """ Start a new debug chapter for the given function and its parameters.
51 def section(self, heading: str) -> None:
52 """ Start a new section with the given title.
56 def comment(self, text: str) -> None:
57 """ Add a simple comment to the debug output.
61 def var_dump(self, heading: str, var: Any) -> None:
62 """ Print the content of the variable to the debug output prefixed by
67 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
68 """ Print the table generated by the generator function.
72 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
73 """ Print a list of search results generated by the generator function.
77 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
78 """ Print the SQL for the given statement.
81 def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> str:
82 """ Return the comiled version of the statement.
85 return str(cast('sa.ClauseElement', statement)
86 .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
87 except sa.exc.CompileError:
89 except NotImplementedError:
92 return str(cast('sa.ClauseElement', statement).compile(conn.sync_engine))
95 class HTMLLogger(BaseLogger):
96 """ Logger that formats messages in HTML.
98 def __init__(self) -> None:
99 self.buffer = io.StringIO()
102 def _timestamp(self) -> None:
103 self._write(f'<p class="timestamp">[{dt.datetime.now()}]</p>')
106 def get_buffer(self) -> str:
107 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
110 def function(self, func: str, **kwargs: Any) -> None:
112 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
113 for name, value in kwargs.items():
114 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
115 self._write('</dl></p>')
118 def section(self, heading: str) -> None:
120 self._write(f"<h2>{heading}</h2>")
123 def comment(self, text: str) -> None:
125 self._write(f"<p>{text}</p>")
128 def var_dump(self, heading: str, var: Any) -> None:
133 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
136 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
140 self._write(f'<table><thead><tr><th colspan="{len(head)}">{heading}</th></tr><tr>')
142 self._write(f'<th>{cell}</th>')
143 self._write('</tr></thead><tbody>')
148 self._write(f'<td>{cell}</td>')
150 self._write('</tbody></table>')
153 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
154 """ Print a list of search results generated by the generator function.
157 def format_osm(osm_object: Optional[Tuple[str, int]]) -> str:
171 return f'<a href="https://www.openstreetmap.org/{fullt}/{i}">{t}{i}</a>'
173 self._write(f'<h5>{heading}</h5><p><dl>')
175 for rank, res in results:
176 self._write(f'<dt>[{rank:.3f}]</dt> <dd>{res.source_table.name}(')
177 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
178 self._write(f"rank={res.rank_address}, ")
179 self._write(f"osm={format_osm(res.osm_object)}, ")
180 self._write(f'cc={res.country_code}, ')
181 self._write(f'importance={res.importance or -1:.5f})</dd>')
183 self._write(f'</dl><b>TOTAL:</b> {total}</p>')
186 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
188 sqlstr = self.format_sql(conn, statement)
190 sqlstr = highlight(sqlstr, PostgresLexer(),
191 HtmlFormatter(nowrap=True, lineseparator='<br />'))
192 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
194 self._write(f'<code class="lang-sql">{sqlstr}</code>')
197 def _python_var(self, var: Any) -> str:
199 fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
200 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
202 return f'<code class="lang-python">{str(var)}</code>'
205 def _write(self, text: str) -> None:
206 """ Add the raw text to the debug output.
208 self.buffer.write(text)
211 class TextLogger(BaseLogger):
212 """ Logger creating output suitable for the console.
214 def __init__(self) -> None:
215 self.buffer = io.StringIO()
218 def get_buffer(self) -> str:
219 return self.buffer.getvalue()
222 def function(self, func: str, **kwargs: Any) -> None:
223 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
224 for name, value in kwargs.items():
225 self._write(f' {name}: {self._python_var(value)}\n')
229 def section(self, heading: str) -> None:
230 self._write(f"\n# {heading}\n\n")
233 def comment(self, text: str) -> None:
234 self._write(f"{text}\n")
237 def var_dump(self, heading: str, var: Any) -> None:
241 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
244 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
245 self._write(f'{heading}:\n')
246 data = [list(map(self._python_var, row)) if row else None for row in rows]
247 assert data[0] is not None
248 num_cols = len(data[0])
250 maxlens = [max(len(d[i]) for d in data if d) for i in range(num_cols)]
251 tablewidth = sum(maxlens) + 3 * num_cols + 1
252 row_format = '| ' +' | '.join(f'{{:<{l}}}' for l in maxlens) + ' |\n'
253 self._write('-'*tablewidth + '\n')
254 self._write(row_format.format(*data[0]))
255 self._write('-'*tablewidth + '\n')
258 self._write(row_format.format(*row))
260 self._write('-'*tablewidth + '\n')
262 self._write('-'*tablewidth + '\n')
265 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
266 self._write(f'{heading}:\n')
268 for rank, res in results:
269 self._write(f'[{rank:.3f}] {res.source_table.name}(')
270 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
271 self._write(f"rank={res.rank_address}, ")
272 self._write(f"osm={''.join(map(str, res.osm_object or []))}, ")
273 self._write(f'cc={res.country_code}, ')
274 self._write(f'importance={res.importance or -1:.5f})\n')
276 self._write(f'TOTAL: {total}\n\n')
279 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
280 sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement), width=78))
281 self._write(f"| {sqlstr}\n\n")
284 def _python_var(self, var: Any) -> str:
288 def _write(self, text: str) -> None:
289 self.buffer.write(text)
292 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
295 def set_log_output(fmt: str) -> None:
296 """ Enable collecting debug information.
299 logger.set(HTMLLogger())
301 logger.set(TextLogger())
303 logger.set(BaseLogger())
306 def log() -> BaseLogger:
307 """ Return the logger for the current context.
312 def get_and_disable() -> str:
313 """ Return the current content of the debug buffer and disable logging.
315 buf = logger.get().get_buffer()
316 logger.set(BaseLogger())
320 HTML_HEADER: str = """<!DOCTYPE html>
323 <title>Nominatim - Debug</title>
326 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
328 h2 { font-size: x-large }
332 font-family: monospace
341 dt::after { content: ": "; }
354 border: solid lightgrey 0.1pt;
356 background-color: #f7f7f7
361 border: solid lightgrey 0.1pt
366 border-collapse: collapse;
369 border-right: thin solid;
377 width: calc(100% - 5pt);
388 HTML_FOOTER: str = "</body></html>"