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], Sequence[Mapping[str, Any]], None]) -> str:
84 """ Return the comiled version of the statement.
86 compiled = cast('sa.ClauseElement', statement).compile(conn.sync_engine)
88 params = dict(compiled.params)
89 if isinstance(extra_params, Mapping):
90 for k, v in extra_params.items():
92 elif isinstance(extra_params, Sequence) and extra_params:
93 for k in extra_params[0]:
96 return str(compiled) % params
99 class HTMLLogger(BaseLogger):
100 """ Logger that formats messages in HTML.
102 def __init__(self) -> None:
103 self.buffer = io.StringIO()
106 def _timestamp(self) -> None:
107 self._write(f'<p class="timestamp">[{dt.datetime.now()}]</p>')
110 def get_buffer(self) -> str:
111 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
114 def function(self, func: str, **kwargs: Any) -> None:
116 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
117 for name, value in kwargs.items():
118 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
119 self._write('</dl></p>')
122 def section(self, heading: str) -> None:
124 self._write(f"<h2>{heading}</h2>")
127 def comment(self, text: str) -> None:
129 self._write(f"<p>{text}</p>")
132 def var_dump(self, heading: str, var: Any) -> None:
137 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
140 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
144 self._write(f'<table><thead><tr><th colspan="{len(head)}">{heading}</th></tr><tr>')
146 self._write(f'<th>{cell}</th>')
147 self._write('</tr></thead><tbody>')
152 self._write(f'<td>{cell}</td>')
154 self._write('</tbody></table>')
157 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
158 """ Print a list of search results generated by the generator function.
161 def format_osm(osm_object: Optional[Tuple[str, int]]) -> str:
175 return f'<a href="https://www.openstreetmap.org/{fullt}/{i}">{t}{i}</a>'
177 self._write(f'<h5>{heading}</h5><p><dl>')
179 for rank, res in results:
180 self._write(f'<dt>[{rank:.3f}]</dt> <dd>{res.source_table.name}(')
181 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
182 self._write(f"rank={res.rank_address}, ")
183 self._write(f"osm={format_osm(res.osm_object)}, ")
184 self._write(f'cc={res.country_code}, ')
185 self._write(f'importance={res.importance or float("nan"):.5f})</dd>')
187 self._write(f'</dl><b>TOTAL:</b> {total}</p>')
190 def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
191 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
193 sqlstr = self.format_sql(conn, statement, params)
195 sqlstr = highlight(sqlstr, PostgresLexer(),
196 HtmlFormatter(nowrap=True, lineseparator='<br />'))
197 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
199 self._write(f'<code class="lang-sql">{sqlstr}</code>')
202 def _python_var(self, var: Any) -> str:
204 fmt = highlight(str(var), PythonLexer(), HtmlFormatter(nowrap=True))
205 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
207 return f'<code class="lang-python">{str(var)}</code>'
210 def _write(self, text: str) -> None:
211 """ Add the raw text to the debug output.
213 self.buffer.write(text)
216 class TextLogger(BaseLogger):
217 """ Logger creating output suitable for the console.
219 def __init__(self) -> None:
220 self.buffer = io.StringIO()
223 def get_buffer(self) -> str:
224 return self.buffer.getvalue()
227 def function(self, func: str, **kwargs: Any) -> None:
228 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
229 for name, value in kwargs.items():
230 self._write(f' {name}: {self._python_var(value)}\n')
234 def section(self, heading: str) -> None:
235 self._write(f"\n# {heading}\n\n")
238 def comment(self, text: str) -> None:
239 self._write(f"{text}\n")
242 def var_dump(self, heading: str, var: Any) -> None:
246 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
249 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
250 self._write(f'{heading}:\n')
251 data = [list(map(self._python_var, row)) if row else None for row in rows]
252 assert data[0] is not None
253 num_cols = len(data[0])
255 maxlens = [max(len(d[i]) for d in data if d) for i in range(num_cols)]
256 tablewidth = sum(maxlens) + 3 * num_cols + 1
257 row_format = '| ' +' | '.join(f'{{:<{l}}}' for l in maxlens) + ' |\n'
258 self._write('-'*tablewidth + '\n')
259 self._write(row_format.format(*data[0]))
260 self._write('-'*tablewidth + '\n')
263 self._write(row_format.format(*row))
265 self._write('-'*tablewidth + '\n')
267 self._write('-'*tablewidth + '\n')
270 def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None:
271 self._write(f'{heading}:\n')
273 for rank, res in results:
274 self._write(f'[{rank:.3f}] {res.source_table.name}(')
275 self._write(f"{_debug_name(res)}, type=({','.join(res.category)}), ")
276 self._write(f"rank={res.rank_address}, ")
277 self._write(f"osm={''.join(map(str, res.osm_object or []))}, ")
278 self._write(f'cc={res.country_code}, ')
279 self._write(f'importance={res.importance or -1:.5f})\n')
281 self._write(f'TOTAL: {total}\n\n')
284 def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
285 params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
286 sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement, params), width=78))
287 self._write(f"| {sqlstr}\n\n")
290 def _python_var(self, var: Any) -> str:
294 def _write(self, text: str) -> None:
295 self.buffer.write(text)
298 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
301 def set_log_output(fmt: str) -> None:
302 """ Enable collecting debug information.
305 logger.set(HTMLLogger())
307 logger.set(TextLogger())
309 logger.set(BaseLogger())
312 def log() -> BaseLogger:
313 """ Return the logger for the current context.
318 def get_and_disable() -> str:
319 """ Return the current content of the debug buffer and disable logging.
321 buf = logger.get().get_buffer()
322 logger.set(BaseLogger())
326 HTML_HEADER: str = """<!DOCTYPE html>
329 <title>Nominatim - Debug</title>
332 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
334 h2 { font-size: x-large }
338 font-family: monospace
347 dt::after { content: ": "; }
360 border: solid lightgrey 0.1pt;
362 background-color: #f7f7f7
367 border: solid lightgrey 0.1pt
372 border-collapse: collapse;
375 border-right: thin solid;
383 width: calc(100% - 5pt);
394 HTML_FOOTER: str = "</body></html>"