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, cast
11 from contextvars import ContextVar
15 import sqlalchemy as sa
16 from sqlalchemy.ext.asyncio import AsyncConnection
19 from pygments import highlight
20 from pygments.lexers import PythonLexer, PostgresLexer
21 from pygments.formatters import HtmlFormatter
23 except ModuleNotFoundError:
24 CODE_HIGHLIGHT = False
28 """ Interface for logging function.
30 The base implementation does nothing. Overwrite the functions
31 in derived classes which implement logging functionality.
33 def get_buffer(self) -> str:
34 """ Return the current content of the log buffer.
38 def function(self, func: str, **kwargs: Any) -> None:
39 """ Start a new debug chapter for the given function and its parameters.
43 def section(self, heading: str) -> None:
44 """ Start a new section with the given title.
48 def comment(self, text: str) -> None:
49 """ Add a simple comment to the debug output.
53 def var_dump(self, heading: str, var: Any) -> None:
54 """ Print the content of the variable to the debug output prefixed by
59 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
60 """ Print the table generated by the generator function.
64 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
65 """ Print the SQL for the given statement.
68 def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> str:
69 """ Return the comiled version of the statement.
72 return str(cast('sa.ClauseElement', statement)
73 .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
74 except sa.exc.CompileError:
76 except NotImplementedError:
79 return str(cast('sa.ClauseElement', statement).compile(conn.sync_engine))
82 class HTMLLogger(BaseLogger):
83 """ Logger that formats messages in HTML.
85 def __init__(self) -> None:
86 self.buffer = io.StringIO()
89 def get_buffer(self) -> str:
90 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
93 def function(self, func: str, **kwargs: Any) -> None:
94 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
95 for name, value in kwargs.items():
96 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
97 self._write('</dl></p>')
100 def section(self, heading: str) -> None:
101 self._write(f"<h2>{heading}</h2>")
104 def comment(self, text: str) -> None:
105 self._write(f"<p>{text}</p>")
108 def var_dump(self, heading: str, var: Any) -> None:
112 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
115 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
118 self._write(f'<table><thead><tr><th colspan="{len(head)}">{heading}</th></tr><tr>')
120 self._write(f'<th>{cell}</th>')
121 self._write('</tr></thead><tbody>')
126 self._write(f'<td>{cell}</td>')
128 self._write('</tbody></table>')
131 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
132 sqlstr = self.format_sql(conn, statement)
134 sqlstr = highlight(sqlstr, PostgresLexer(),
135 HtmlFormatter(nowrap=True, lineseparator='<br />'))
136 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
138 self._write(f'<code class="lang-sql">{sqlstr}</code>')
141 def _python_var(self, var: Any) -> str:
143 fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
144 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
146 return f'<code class="lang-python">{str(var)}</code>'
149 def _write(self, text: str) -> None:
150 """ Add the raw text to the debug output.
152 self.buffer.write(text)
155 class TextLogger(BaseLogger):
156 """ Logger creating output suitable for the console.
158 def __init__(self) -> None:
159 self.buffer = io.StringIO()
162 def get_buffer(self) -> str:
163 return self.buffer.getvalue()
166 def function(self, func: str, **kwargs: Any) -> None:
167 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
168 for name, value in kwargs.items():
169 self._write(f' {name}: {self._python_var(value)}\n')
173 def section(self, heading: str) -> None:
174 self._write(f"\n# {heading}\n\n")
177 def comment(self, text: str) -> None:
178 self._write(f"{text}\n")
181 def var_dump(self, heading: str, var: Any) -> None:
185 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
188 def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None:
189 self._write(f'{heading}:\n')
190 data = [list(map(self._python_var, row)) if row else None for row in rows]
191 assert data[0] is not None
192 num_cols = len(data[0])
194 maxlens = [max(len(d[i]) for d in data if d) for i in range(num_cols)]
195 tablewidth = sum(maxlens) + 3 * num_cols + 1
196 row_format = '| ' +' | '.join(f'{{:<{l}}}' for l in maxlens) + ' |\n'
197 self._write('-'*tablewidth + '\n')
198 self._write(row_format.format(*data[0]))
199 self._write('-'*tablewidth + '\n')
202 self._write(row_format.format(*row))
204 self._write('-'*tablewidth + '\n')
206 self._write('-'*tablewidth + '\n')
209 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
210 sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement), width=78))
211 self._write(f"| {sqlstr}\n\n")
214 def _python_var(self, var: Any) -> str:
218 def _write(self, text: str) -> None:
219 self.buffer.write(text)
222 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
225 def set_log_output(fmt: str) -> None:
226 """ Enable collecting debug information.
229 logger.set(HTMLLogger())
231 logger.set(TextLogger())
233 logger.set(BaseLogger())
236 def log() -> BaseLogger:
237 """ Return the logger for the current context.
242 def get_and_disable() -> str:
243 """ Return the current content of the debug buffer and disable logging.
245 buf = logger.get().get_buffer()
246 logger.set(BaseLogger())
250 HTML_HEADER: str = """<!DOCTYPE html>
253 <title>Nominatim - Debug</title>
256 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
258 h2 { font-size: x-large }
262 font-family: monospace
271 dt::after { content: ": "; }
284 border: solid lightgrey 0.1pt;
286 background-color: #f7f7f7
291 border: solid lightgrey 0.1pt
298 HTML_FOOTER: str = "</body></html>"