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
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',
78 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
79 """ Print the SQL for the given statement.
82 def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable',
83 extra_params: Union[Mapping[str, Any],
84 Sequence[Mapping[str, Any]], None]) -> str:
85 """ Return the comiled version of the statement.
87 compiled = cast('sa.ClauseElement', statement).compile(conn.sync_engine)
89 params = dict(compiled.params)
90 if isinstance(extra_params, Mapping):
91 for k, v in extra_params.items():
93 elif isinstance(extra_params, Sequence) and extra_params:
94 for k in extra_params[0]:
97 sqlstr = str(compiled)
99 if sa.__version__.startswith('1'):
101 return sqlstr % tuple((repr(params.get(name, None))
102 for name in compiled.positiontup)) # type: ignore
106 return sqlstr % params
109 class HTMLLogger(BaseLogger):
110 """ Logger that formats messages in HTML.
112 def __init__(self) -> None:
113 self.buffer = io.StringIO()
116 def _timestamp(self) -> None:
117 self._write(f'<p class="timestamp">[{dt.datetime.now()}]</p>')
120 def get_buffer(self) -> str:
121 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
124 def function(self, func: str, **kwargs: Any) -> None:
126 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
127 for name, value in kwargs.items():
128 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
129 self._write('</dl></p>')
132 def section(self, heading: str) -> None:
134 self._write(f"<h2>{heading}</h2>")
137 def comment(self, text: str) -> None:
139 self._write(f"<p>{text}</p>")
142 def var_dump(self, heading: str, var: Any) -> None:
147 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
150 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
154 self._write(f'<table><thead><tr><th colspan="{len(head)}">{heading}</th></tr><tr>')
156 self._write(f'<th>{cell}</th>')
157 self._write('</tr></thead><tbody>')
162 self._write(f'<td>{cell}</td>')
164 self._write('</tbody></table>')
167 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
168 """ Print a list of search results generated by the generator function.
171 def format_osm(osm_object: Optional[Tuple[str, int]]) -> str:
185 return f'<a href="https://www.openstreetmap.org/{fullt}/{i}">{t}{i}</a>'
187 self._write(f'<h5>{heading}</h5><p><dl>')
189 for rank, res in results:
190 self._write(f'<dt>[{rank:.3f}]</dt> <dd>{res.source_table.name}(')
191 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
192 self._write(f"rank={res.rank_address}, ")
193 self._write(f"osm={format_osm(res.osm_object)}, ")
194 self._write(f'cc={res.country_code}, ')
195 self._write(f'importance={res.importance or float("nan"):.5f})</dd>')
197 self._write(f'</dl><b>TOTAL:</b> {total}</p>')
200 def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
201 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
203 sqlstr = self.format_sql(conn, statement, params)
205 sqlstr = highlight(sqlstr, PostgresLexer(),
206 HtmlFormatter(nowrap=True, lineseparator='<br />'))
207 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
209 self._write(f'<code class="lang-sql">{sqlstr}</code>')
212 def _python_var(self, var: Any) -> str:
214 fmt = highlight(str(var), PythonLexer(), HtmlFormatter(nowrap=True))
215 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
217 return f'<code class="lang-python">{str(var)}</code>'
220 def _write(self, text: str) -> None:
221 """ Add the raw text to the debug output.
223 self.buffer.write(text)
226 class TextLogger(BaseLogger):
227 """ Logger creating output suitable for the console.
229 def __init__(self) -> None:
230 self.buffer = io.StringIO()
233 def get_buffer(self) -> str:
234 return self.buffer.getvalue()
237 def function(self, func: str, **kwargs: Any) -> None:
238 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
239 for name, value in kwargs.items():
240 self._write(f' {name}: {self._python_var(value)}\n')
244 def section(self, heading: str) -> None:
245 self._write(f"\n# {heading}\n\n")
248 def comment(self, text: str) -> None:
249 self._write(f"{text}\n")
252 def var_dump(self, heading: str, var: Any) -> None:
256 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
259 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
260 self._write(f'{heading}:\n')
261 data = [list(map(self._python_var, row)) if row else None for row in rows]
262 assert data[0] is not None
263 num_cols = len(data[0])
265 maxlens = [max(len(d[i]) for d in data if d) for i in range(num_cols)]
266 tablewidth = sum(maxlens) + 3 * num_cols + 1
267 row_format = '| ' +' | '.join(f'{{:<{l}}}' for l in maxlens) + ' |\n'
268 self._write('-'*tablewidth + '\n')
269 self._write(row_format.format(*data[0]))
270 self._write('-'*tablewidth + '\n')
273 self._write(row_format.format(*row))
275 self._write('-'*tablewidth + '\n')
277 self._write('-'*tablewidth + '\n')
280 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
281 self._write(f'{heading}:\n')
283 for rank, res in results:
284 self._write(f'[{rank:.3f}] {res.source_table.name}(')
285 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
286 self._write(f"rank={res.rank_address}, ")
287 self._write(f"osm={''.join(map(str, res.osm_object or []))}, ")
288 self._write(f'cc={res.country_code}, ')
289 self._write(f'importance={res.importance or -1:.5f})\n')
291 self._write(f'TOTAL: {total}\n\n')
294 def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
295 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
296 sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement, params), width=78))
297 self._write(f"| {sqlstr}\n\n")
300 def _python_var(self, var: Any) -> str:
304 def _write(self, text: str) -> None:
305 self.buffer.write(text)
308 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
311 def set_log_output(fmt: str) -> None:
312 """ Enable collecting debug information.
315 logger.set(HTMLLogger())
317 logger.set(TextLogger())
319 logger.set(BaseLogger())
322 def log() -> BaseLogger:
323 """ Return the logger for the current context.
328 def get_and_disable() -> str:
329 """ Return the current content of the debug buffer and disable logging.
331 buf = logger.get().get_buffer()
332 logger.set(BaseLogger())
336 HTML_HEADER: str = """<!DOCTYPE html>
339 <title>Nominatim - Debug</title>
342 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
344 h2 { font-size: x-large }
348 font-family: monospace
357 dt::after { content: ": "; }
370 border: solid lightgrey 0.1pt;
372 background-color: #f7f7f7
377 border: solid lightgrey 0.1pt
382 border-collapse: collapse;
385 border-right: thin solid;
393 width: calc(100% - 5pt);
404 HTML_FOOTER: str = "</body></html>"